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.
# 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 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.
# 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.
The repository
HEAD · refs/ · objects/ · index
Small files, rewritten constantly, meaningless unless they agree with each other.
The vault
Roadmap.md · Projects/Q4 planning.md
Plain markdown, independent of each other. Exactly what a sync engine is for.
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.
$ git log --oneline
7c1d0ae 2026-08-24T14:31:02Z — Q4 planning
b48f39c 2026-08-24T14:12:47Z — Q4 planning
0a52ee1 2026-08-24T09:04:19Z — Roadmap
$ 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.
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
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.
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