Blog · 25 August 2026 · 7 min read

Never put a git repo
in iCloud Drive.

Every Ampersand vault gets a full version history backed by a real git repository. It lives outside the vault, on purpose, because the folder most people keep their notes in is the one folder git cannot survive.

Ampersand keeps a version history of every note: a timeline of saves, a line diff between any two of them, and a restore that opens the old text as an unsaved buffer rather than overwriting anything. The obvious way to build that is a real git repository, because the hard parts — content-addressed storage, deduplication, rename following, a diff engine people already trust — are solved and battle-tested.

The obvious place to put that repository is at the root of the vault, next to the notes it tracks. That is what a repository looks like everywhere else, and it is the one thing we could not do, because the default vault lives in iCloud Drive.

If you arrived here with a repository of your own that has gone strange inside a synced folder, the next section is the explanation. It is not specific to us or to Ampersand.

files the history adds to your vault
0files the history adds to your vaultthe repository lives in application support, outside the vault entirely
idle before a burst of saves becomes one commit
15 sidle before a burst of saves becomes one committhe default coalescing window; configurable in settings
destructive operation in the whole feature
1destructive operation in the whole featuredeleting a vault’s history, behind an explicit confirmation

What a sync engine does to a git repository

A .git directory does not look like documents. It is a large number of small files that are rewritten constantly, and — this is the important part — their consistency with each other is the entire design. A ref points at a commit object that must exist. The index describes the working tree it was computed against. A packfile is meaningless without its index. Git assumes that a file it wrote is the file that is there, and that everything reachable is present, right now, together.

A file-sync engine makes no such promise, and is not built to. It replicates each file independently, on its own schedule, in whatever order suits the upload queue. It resolves simultaneous edits by keeping both copies under different names. And on macOS, when disk space gets tight, it will evict a file it considers cold, leaving a tiny placeholder in its place — the file appears in a listing and is not actually there until something asks for it and waits.

None of those behaviours is a bug. All three are catastrophic for a directory whose invariant is “these files agree with one another”. And it does not announce itself as a sync problem: git reports it as corruption, because from git’s point of view that is exactly what it is.

What it looks like from git’s side
# typical symptoms, whatever app put the repository there

$ git status
error: object file .git/objects/4a/2b9c... is empty
fatal: loose object 4a2b9c... is corrupt

$ git log
fatal: bad object HEAD

$ ls -a .git
.HEAD.icloud     <- evicted to save space. a placeholder,
                    not the file. git cannot read it.
HEAD 2           <- a conflict copy of a ref
index.lock       <- left behind by an interrupted write
objects/         <- some of it uploaded, some of it not,
                    at whatever moment you looked
A file that appears in a listing and is not there. Nothing above is a git bug or an iCloud bug — eviction, conflict copies and interrupted writes are all correct behaviour for documents, applied to a directory whose whole design is that its files agree with one another.

A second machine makes it worse rather than better. Two computers with the same folder mounted are two writers to one repository, with no locking between them and a replication delay in the middle. The repository will be fine for weeks and then not be, usually on the day you needed the history.

The same argument applies to any folder your sync client owns: package directories, SQLite databases with their write-ahead logs, virtual machine images, build caches. Any of them can be described as “a folder of files”, and none of them is.

The split that makes it work

Git has always separated two things that people usually keep together: the repository (the object database, the refs, the index) and the working tree (the files you edit). They normally sit in one place, but they do not have to.

So Ampersand puts the repository in application support — machine-local, never iCloud-synced, the same neighbourhood as the session state and the crash journal — and points its working tree back at the vault root.

One vault, two locations
# the vault -- in iCloud Drive. plain files, nothing hidden.
~/Library/Mobile Documents/com~apple~CloudDocs/Ampersand/
    Roadmap.md
    Projects/
        Q4 planning.md

# the repository -- application support. never synced.
~/Library/Application Support/Ampersand/history/<vault-id>/git/
    HEAD  refs/  objects/  index

# <vault-id> is the first 16 hex characters of the SHA-256 of the
# vault's canonical path, so each vault gets its own repository.

# git has supported this split forever: the repository directory
# and the working tree are separate settings. libgit2 exposes both.
One path is the entire fix. Everything git needs to stay internally consistent sits on the machine that wrote it, and the folder your sync client owns holds nothing but markdown.
The repository and the working tree · schematic
Application Support · never synced

The repository

HEAD · refs/ · objects/ · index

Small files, rewritten constantly, meaningless unless they agree with each other.

working tree
iCloud Drive · synced

The vault

Roadmap.md · Projects/Q4 planning.md

Plain markdown, independent of each other. Exactly what a sync engine is for.

Git has separated these two things since long before anyone had a sync client; they normally sit in one place because that is convenient, not because they must. The arrow is the only connection between them — a setting, pointing one at the other.

This has a second benefit we did not initially set out for. A vault stays a plain folder of markdown files with nothing hidden in it — no dot-directory, nothing for another editor to trip over, nothing that makes the folder feel owned by us. You can open the same folder in any other tool and it is just notes.

The trade is honest and worth stating: history is per machine. It does not sync, because syncing it is the thing this entire post is about. Your notes are on every device; their history is on the device that wrote it.

libgit2, not the git binary

We drive the repository through libgit2 — the library — rather than by shelling out to git. Not on principle: a notes app cannot assume the person using it has a working git installation. On macOS, invoking git can trigger the Xcode command line tools prompt, which is an alarming thing for a text editor to do because you pressed save. There is also no subprocess to spawn, no environment to sanitise, no output to parse, and no version differences to accommodate.

We build it without HTTPS, SSH or TLS support at all. There is no remote — this repository never talks to a network, and compiling out the transports is the cheapest way to guarantee it. The security overview covers the rest of what the app does and does not reach for.

What actually gets committed

Commits are whole-tree snapshots, authored by a fixed identity so they never impersonate you, with the message set to a timestamp and the document title. Only markdown files and a small image allow-list are staged; an unchanged tree produces no commit rather than an empty one.

The history it produces
$ git log --oneline
7c1d0ae  2026-08-24T14:31:02ZQ4 planning
b48f39c  2026-08-24T14:12:47ZQ4 planning
0a52ee1  2026-08-24T09:04:19ZRoadmap

$ git show --stat b48f39c
Author: Ampersand <noreply@getampersand.app>

# one commit per editing burst, not one per keystroke.
# an unchanged tree produces no commit at all.
Ampersand autosaves aggressively, and a commit per save is not a version history — it is a keystroke log that makes the timeline useless and the repository enormous. Three entries here are hours of work.

That last point matters more than it sounds. Ampersand autosaves aggressively, and a commit per save is not a version history — it is a keystroke log that makes the timeline useless and the repository enormous. So saves are coalesced: a burst of them collapses into one commit after a configurable idle window, with a cap so a very long typing session still checkpoints. The coalescer can be flushed and awaited, which is what makes quitting safe — the pending commit lands before the window is destroyed rather than being dropped on the way out.

Recovery is deliberately conservative in the other direction. If the repository fails to open, we re-initialise it; if a transient filesystem error occurs while configuring it, we do not. Re-creating a repository is destructive, and the temptation to do it helpfully on any error is how a history disappears.

Encrypted notes are never committed

Ampersand can encrypt individual notes, and an encrypted note is excluded from version history entirely. This is not an omission we intend to fix.

The encryption format generates a fresh nonce on every write, which is exactly what you want from an authenticated cipher and exactly what makes a diff pointless: two saves of a document where you changed one word produce ciphertexts with essentially nothing in common. Every commit would report the whole file as changed, every diff would be noise, and the repository would grow by the full size of the note on each save. History would cost a great deal and tell you nothing.

The exclusion is a property of the filename

Encrypted notes carry a distinct file extension, and every part of the app that walks the vault for content — search, the link graph, the tag index, and the code that decides what to stage — already filters on extension. So the exclusion falls out of existing behaviour rather than being enforced by a check in each place, which is the version of this that cannot drift. A test asserts the staging list never learns about the encrypted extension.

Retention without rewriting the present

A history that only grows eventually has to be trimmed. The obvious approach — delete old commits — is awkward in git, because commits are immutable and every later one names its parent.

So pruning re-roots instead: the oldest commit we are keeping is rewritten as a parentless snapshot, and everything before it becomes unreachable and is eventually collected. Crucially, the tip tree stays byte-identical. The history gets shorter at the far end and the present is untouched, which is the property you actually want — nobody minds losing last spring, everybody minds their current file changing because a cleanup ran.

Deleting a vault’s history is a single explicit action behind a confirmation, and it is the only destructive path in the whole feature. Turning version history off stops new commits and leaves everything already recorded exactly where it is.

What we took from it

  • Decide which directories your sync engine owns, and never hand it one whose consistency you depend on. Sync operates on files; git, databases and package caches operate on relationships between files. The mismatch is structural, not a tuning problem.
  • Machine-local state belongs in a machine-local place. Application support exists for this. Anything under a synced folder is, sooner or later, going to be synced.
  • A library beats a subprocess for anything on a save path. No installation to assume, no environment to sanitise, no output format to parse, no system prompt appearing because someone pressed save.
  • Do not version data whose representation is deliberately unstable. A good cipher produces different bytes for identical input. That is the point of it, and it makes diffing meaningless — so we do not pretend otherwise.

The vaults documentation covers where your notes live and how to move them, and the technical specifications list the rest. If you like postmortems, a window that could not paint and the bug that argued for its own dismissal are two we published on the same day.

Get started

Your notes are files.
Their history is yours too.

Ampersand keeps every save in a real git repository on your own machine — no account, no server, and nothing hidden inside the folder your notes live in.

Free to start · Mac, Windows & Linux · No account required