← Writing
On disk

The watcher that cried delete

Margin watches your folder so external edits reflow into the app. The hard part isn't noticing changes. It's not over-reacting to them.

Margin is a view over a folder of Markdown files, so it watches that folder. Edit a note in vim, pull a branch, drop a file in from Finder, and the app should reflow without being told.

The hard part isn't noticing that something changed. It's not over-reacting to it.

The event that isn't true

A lot of editors don't save by writing into the existing file. They write a temporary file and rename it over the top, so a reader never sees a half-written document. Vim does it. So does Margin.

The side effect is that a watcher sees a deletion followed by a creation, milliseconds apart. If you trust the event kind, your app confidently announces that the note the user is looking at has been deleted, while the replacement is already landing on disk.

The file never went anywhere. Only the inode did.

You can chase that with a re-check: when a delete arrives, stat the path again before you believe it. That works, and it's what I'd reach for first too. But it leaves you deciding how long to wait, and a genuine deletion now has to lose a race on purpose.

Not reading the event kind at all

What I do instead is refuse the question. The watcher filters to .md paths, debounces for 300ms, and emits a single event that carries no detail beyond which space changed. The front end re-reads the folder.

src-tauri/src/watcher.rsrust
let Ok(first) = rx.recv() else { break };
let mut paths = first;
let deadline = Instant::now() + DEBOUNCE;
while let Ok(more) = rx.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
    paths.extend(more);
}

A delete and a create two milliseconds apart collapse into one event, and by the time anything re-reads, the replacement is there. It costs a directory listing I don't strictly need, and in exchange I never have to decide whether a Remove was real.

There's a second suppression on top: paths the app wrote itself within the last second and a half are ignored, so Margin's own saves don't echo back through the watcher as external changes.

Identity can't be the path

The other half of the problem is what an open note is.

If a note is identified by its file path, then an atomic save has already broken it, because the path briefly pointed at nothing. Rename the file in Finder and it breaks again, permanently.

So every note carries an id in its frontmatter. The file is where the note currently lives, not what it is. A replace, a rename, a move into another folder: none of them orphan whatever is open on screen, because identity travels inside the file rather than alongside it.

That decision came out of the watcher work, but it paid for itself somewhere else entirely. Links between notes point at ids, so reorganising a folder at eleven at night doesn't break a single one.


The general shape of the lesson, which I suspect applies well beyond file watching: an event tells you that something happened, not what is now true. When those two are cheap to separate, separate them. Re-read the world instead of reconstructing it from the notifications.

Margin is the app this came out of
Local-first Markdown notes for the Mac. Free while it is alpha.
Download the alpha
More writing
Building
16 lessons from building a productivity app on weekends

Notes from the workshop rather than wisdom from the mountain. What a few months of building a notes app has changed about how I look at every app I use.

2 Aug 2026·4 min·/blog/sixteen-lessons
On disk
Margin vs Bear: Markdown notes in a database or in a folder

Bear keeps Markdown in one SQLite database. Margin keeps each note as a .md file in a folder, so it has folders and Bear doesn't. Where each is better, and how to move.

5 Oct 2026·9 min·/blog/margin-vs-bear
On disk
Your notes don't need an MCP server

Bear shipped a CLI, a Claude connector and an MCP server so agents can reach its notes. A folder of Markdown files needs none of that. What an agent gets from the folder, and what breaks when it edits it.

24 Sept 2026·6 min·/blog/no-mcp-server