November 202610 min read

I built a Google Docs for me and Claude

I was marking up a printed draft with a pen and filming the pages for Claude. So we built Margin: a private, git-backed web app where I leave notes on Markdown files and Claude answers with suggested edits.

I've been writing a fantasy novel with Claude.

Well, it started as a novel. It's turned into seven novels and an encyclopedia.

Under the prose there's a SQLite database (obviously) with tens of thousands of facts about the world: family lineages, what happened when, and in which region. All verifiably internally correct.

It's there so the story can span 2,000 years without contradicting itself. Some of those facts are marked private, and not even the novels are allowed to reveal them. It is my favorite thing to spend tokens on. I talked about it on the podcast if you want the long version.

Several months in I had a full draft of the first book. I had it typeset into a PDF, got it printed (on paper!) at FedEx, and marked it up with a pen.

The printed draft on a wooden table: a spiral-bound book with a frosted cover and a pen hiding the title.

Seven chapters in, I stopped. The story was good, but man, I kept marking the same mistakes over and over, and it was getting harder to read.

Too many similes. Obscure words I didn't know. Sentences that ran too long.

A printed page marked up in pen: underlines, double question marks, a circled word, a triple check mark, and "Very confused here" written in the margin. Character names are blurred.

The next problem was getting those notes back to Claude. They were in ink, on paper. No way I'm gonna go type them all out. So I filmed a video of me flipping through the pages and just... gave Claude the video and said good luck; have fun.

It worked pretty well.

Claude pulled the frames, found 42 sharp two-page spreads, read my handwriting, and came back with a catalog of every mark. Then it asked me about the ones it couldn't read, one at a time, each with a cropped screenshot. "Is this a frown or a smile?" (It was a :/. It means "meh." I don't hate it, it's just lame.)

A printed line of dialogue underlined in pen, with a small hand-drawn meh face beside it. A character name is blurred.

It worked, but it's a silly loop. Print, mark, film, decode, discuss. I wanted something closer to Google Docs, but just for me and Claude.

So we built one. It's called Margin.

(But it doesn't matter what it's called! It's for me, and me alone. More on that later.)

What it does

I open a chapter on my iPad (or any browser really.) I drag across some words and a little toolbar pops up beside them, with one button for each of my paper marks:

  • ❤️ love it
  • 🤩 exciting
  • 🤔 confused
  • 😐 meh
  • 👎 don't like it
  • ✂️ cut this
  • 📝 a plain note
Margin on an iPad: a phrase selected in the chapter with a floating toolbar under it offering love, exciting, confused, meh, don’t like, cut and note. Character names are blurred.

I tap one, maybe add a few words, and keep reading. I can even write the comment with my Apple Pencil, straight into the text box.

A few seconds later a Claude session on my local machine (!) wakes up, reads my notes, and answers each one. Usually the answer is a suggested edit: the old text struck through, the new text beside it, and a line about why. I tap accept or reject. Accept rewrites the file and makes a git commit. Reject with a comment sends it back for another try.

There's also a History tab, which is git log for the chapter I'm reading, and a Chat tab, which I'll get to.

Margin on an iPad: a chapter on the left with a suggested edit in red and green, and on the right the note thread with my comment, the editor’s reply, a word-by-word diff, and Accept and Reject buttons. Character names are blurred.

It's all files

There is no database. The chapters are Markdown files. The notes live in a JSON file next to each chapter, in a .margin folder:

chapters/
  01-first-chapter.md
  .margin/
    01-first-chapter.json

Each note is a thread. It stores the text I selected, the mark, the messages, and at most one suggestion. Here's the shape, with a made-up sentence:

{
  "id": "t_8f2c",
  "author": "aaron",
  "quote": "the rope ran out like a thought he had lost",
  "mark": "dislike",
  "status": "open",
  "messages": [
    { "author": "aaron", "text": "another simile" }
  ],
  "suggestion": {
    "author": "editor",
    "old": "the rope ran out like a thought he had lost",
    "new": "the rope ran out",
    "note": "Cut the comparison. The action is enough.",
    "status": "pending"
  }
}

A thread is anchored by the quoted text, not by a line number or a character offset. If the paragraph changes around it, the note still finds its place.

This is the part I like most. Claude doesn't need an MCP server or a special API to work with my notes. They're files on disk. It can read them, grep them, and reason about them like anything else in the repo.

Git is the history

The folder of chapters is its own git repository. Two kinds of commits land in it.

Text changes commit right away. When I accept a suggestion, the server finds the old text in the file, swaps in the new text, and commits with a trailer that points back to the note:

Cut the comparison

Thread: t_8f2c

So every change to the book traces back to the note that caused it.

Comments and chat commit in the background, about every 45 seconds, as one "Notes: comments and chat" commit. I don't need a commit per thumbs-down.

Accepting has a few rules that came from real bugs. The old text has to match exactly once in the file, or the accept is refused. Matching ignores line wrapping and straight versus curly quotes, because those differences are never what I meant. And after a cut, the server tidies the punctuation. The first cut I tested left me with "before noon,." in the middle of a sentence.

Claude suggests, I decide

Claude never edits a chapter directly. That's a hard rule, written into the project's instructions. Every change is a suggestion I accept or reject.

Claude makes suggestions with a small command line tool that talks to the same server I do:

python3 tools/margin/cli.py suggest chapters/01-first-chapter.md \
  --thread t_8f2c \
  --old "the rope ran out like a thought he had lost" \
  --new "the rope ran out" \
  --note "Cut the comparison. The action is enough."

The browser polls every 2.5 seconds, so the suggestion shows up on my iPad while I'm still reading the next page.

Waking Claude up

This was the part I wasn't sure would work. Here's what I typed when I asked for it:

lets talk about the web app. i think it could be cool! I'm almost picturing a new type of interface where humans and agents work together with comments, notes, edits, all backed by git so we have a rich history. so likely all backed by markdwon too? so you dont have to use an mcp, you could use the filesystem? but i think i'd want you to run here, in solo, and maybe use a background monitor to watch for comments and stuff so I can "chat" with you from the web ui? kinda like a google docs but just for you and me? i dunno man! ask me questions using your askuserquestion tool to help me refine it

Claude runs in a terminal. How does a web page tell it there's something to do? I guessed at a background monitor, which would have meant Claude polling for new comments. It turned out there was a better way.

I run my agents in Solo, a desktop app I've developed for running dev processes and coding agents side by side. Solo has a local HTTP API, and one of its endpoints types a line into a running agent's terminal. So the server does this:

req = urllib.request.Request(
    f"{origin}/api/processes/{pid}/input",
    data=json.dumps({"input": text}).encode(),
    headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
    method="POST",
)

That's the whole trick. When I leave a note, the server waits for about 12 quiet seconds, so a run of quick marks becomes one wake-up instead of ten. Then it types one line into the editor's terminal:

[Margin] Aaron left notes: 3 thread(s) are waiting...

The editor runs cli.py inbox, works through the threads, and goes back to sleep. No polling loop on Claude's side. No tokens spent while I'm not reading.

The Chat tab uses the same trick. Here's what that looks like in Solo. Each project has its own agents and commands in the sidebar. The editor is the margin-editor agent, sitting under my main Claude session, and the Margin server is one of the project's commands.

The Solo window: a sidebar of projects with their agents and commands, and a Claude session in the main pane reporting on the editor’s work, with my chat message from Margin arriving at the bottom. Private names are blurred.

And here's one of my chat messages landing in that terminal, zoomed in:

A line arriving in Claude’s terminal inside Solo, labeled as unverified Solo HTTP API input, carrying my chat message from Margin: "so is this working pretty well?"

Two Claudes

There are two sessions wired in, and they have different jobs.

The editor is a dedicated Claude with a standing brief: the rules of the book, what each mark means, and the list of mistakes I keep flagging. It only answers notes on the text.

The other one is the main session I've been working with all along, the one that knows the whole project. That's who the Chat tab talks to. If I'm reading on the couch and want to ask "is this working?" or talk about something bigger than one sentence, the message goes straight to that session, along with which chapter I have open. The reply shows up in the same tab.

Margin on an iPad: a chapter with two suggested edits shown as red strikethrough and green replacement text, and the chat panel on the right. Character names are blurred.

I asked for the chat late, almost as an afterthought. It turned out to matter. A line edit and a conversation are different things, and they shouldn't share a context.

The boring stack

  • Python standard library only. One server.py, 708 lines. No framework, no build step.
  • One HTML file, 542 lines.
  • A 113-line command line tool for Claude.
  • The server listens on localhost only. Tailscale Serve puts it on my tailnet over HTTPS, so my iPad can reach it and nobody else can:
tailscale serve --bg --https=8443 http://127.0.0.1:4760

There's no sign-in. The tailnet is the access control.

One more thing: the paper notes came along. We wrote a small importer that turned the catalog from the video into 137 threads, each anchored to its text. When I first opened Margin, my pen marks from chapters one through seven were already there. The editor worked through them and had 88 suggestions waiting for me.

What's not done

  • A note lives inside one paragraph. Select across two and it clips to the first.
  • A suggestion can't be only whitespace, so Claude can't propose a bare paragraph break. It has to ask.
  • Chat messages reach Claude marked as unverified, since anyone on my tailnet could have typed them. For anything hard to undo, Claude confirms with me in the terminal first.

Why I think this matters

I didn't set out to build a product. I wanted to see something come into the world. To get something out of my head. To make something. Not the software! The novel. Software is but a means to the end.

The shape feels right to me, and not just for novels. A human reads and reacts. An agent proposes. The human decides. Everything is a plain file, and git remembers all of it.

Blog posts are next. I want to edit these the same way.

The whole thing was built in 23 minutes by Fable 5.2, in the same session that decoded the video.

Malleable software

This has me thinking about the nature of software itself.

The software I'm using can change right under my fingertips. I could ask Claude for a new toolbar from the chat box inside the app, refresh the page, and it would be there. That chat box talks to the same Claude that wrote the app.

The tool I'm using includes the thing that can rebuild the tool I'm using. Woah.

Margin has exactly one user, so it can fit me exactly. If I don't like something, I ask, and then it's different. There's no roadmap, no feature request, and no settings page. There's no god dang pressure.

It also changes what's worth building. I would never have built a custom editing app for one side project novel. I have a job. I have 4 kids.

But now, a tool only has to be worth 23 minutes.

I see a lot of talk about the death of software. Maybe big box software is dying, I dunno.

But I'm not trying to sell Margin to anyone. It makes my life easier, and it gets me closer to my dreams.

Software like this is filling in the gaps that traditional software never could, and I think that's a reason to be optimistic.

Quick video overview

AI & agents

More on AI & agents

Everything on AI & agents
Recently

Other recent writing

All writing

Get the next one by email

I’ll send you some more. Not too many.