Edit your Markdown notes by hand on a Supernote e-ink tablet, and have the changes flow back into the source files automatically — checkboxes ticked, lines struck, margin notes applied — read by Claude from your actual handwriting.
Your Markdown is the source of truth. The tool renders it to PDFs on the device, you annotate in ink, and on check-in it extracts your strokes, has Claude interpret them, and 3-way-merges the result back into the source. Write in any pen colour; counts and tallies stay correct on their own; every automated edit is a revertible git commit.
render (mirror) annotate by hand read ink (.mark layer)
Markdown ──────────► PDF on Supernote ──────────► your ink ──────────► Claude reads it
▲ source = truth │ (Google Drive sync) │ │
│ ▼ ▼ ▼
└────────── 3-way merge into source ◄── recompute derived counts ◄── interpret + apply
(auto-commit, revertible)
This tool is one piece of a larger idea: a knowledge architecture for human + Claude collaboration — a structure where your thoughts, your docs, and project state are organized so that the right context surfaces at the right moment with minimal friction, both for you to find things and for Claude to recall them. Handwriting on the tablet is one input; the real product is retrieval and shared memory. Everything below is in service of that: plain Markdown, in git, that a person and an agent can both read and trust.
- Markdown is canonical. The PDF on the device is a disposable view; edits are
folded back into the
.mdand a fresh PDF is re-rendered. - Ink comes from the
.marklayer, not pixels. A Supernote annotation sidecar (<doc>.pdf.mark) is a native Supernote document whose strokes live in a layer separate from the page — so we extract them directly withsupernote-tool. This is colour-independent (black pen works as well as red) and exact, with none of the fragility of trying to colour-filter a flattened export. - The LLM interprets; code derives. Claude only does the genuinely fuzzy part —
reading handwriting and deciding what a mark means. Anything that's a pure function
of the document (e.g. a "Quick stats" tally of
[x]/[ ]/[!]items) is recomputed deterministically in code, so it can't drift or become a merge conflict. - Conflict-safe + revertible. Device edits and desktop edits are reconciled with a real 3-way merge (true collisions surface as conflict markers rather than auto- applying). Source docs live in git, and every automated edit is its own commit.
- Python 3.11+ (uses
tomllib) - poppler — provides
pdftoppm(brew install poppler/apt install poppler-utils) - Claude access for the handwriting read — by default your Claude subscription via the
claudeCLI (claude -p; no API key, no metered tokens). Or switch to the Anthropic API backend (backend = "api"in config) with anANTHROPIC_API_KEY. - A Supernote tablet synced to a cloud folder your computer can see (the reference setup is Google Drive for Desktop; any path works via config)
git clone <this-repo> NoteSync && cd NoteSync
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
# Config: copy the example and edit it (config.toml is gitignored).
cp config.example.toml config.toml # set source_base, scan_roots, supernote_root
# Handwriting reader: your Claude subscription (the `claude` CLI) is the default — no key.
# Only if you set backend = "api" in config.toml, add your Anthropic key:
cp .env.example .env # ANTHROPIC_API_KEY (api backend only)
# Put your notes under version control so every auto-edit is revertible:
git -C "$(python config.py source_base)" init # if not already a repoIt renders out of the box with the bundled DejaVu Sans. To use your own font, point
font_regular / font_bold / font_italic in config.toml at your TTFs.
Every setting can also be supplied via environment (handy for headless/cloud runs),
which overrides config.toml: SUPERNOTE_SOURCE_BASE, SUPERNOTE_SCAN_ROOTS
(comma-separated), SUPERNOTE_ROOT, SUPERNOTE_MODEL, SUPERNOTE_CHECKLIST.
./sync.sh mirror # render the whole notes tree -> PDFs on the device (+ prune renamed/moved orphans)
# ... annotate a doc by hand on the tablet, then "Export with annotations" ...
./sync.sh in # extract your ink, route it, report any drift
./sync.sh process <rel> --apply # Claude reads the ink -> 3-way merge -> apply -> re-render
./sync.sh status # show the device Document/ and EXPORT/ folders<rel>is the source path relative tosource_base, e.g.notes/todo.md.- Run
processwithout--applyfirst to preview; it only writes with--apply. - A clean merge applies and re-renders; a real collision writes conflict markers to
merge_out/for you to resolve, then re-mirror.
mirrorwalkssource_base(limited toscan_roots), renders each Markdown doc to a tablet-shaped PDF at the matching path under the Supernote sync folder, snapshots the exact source bytes tobase/, and writesmanifest.json(PDF → source + hash). It also recomputes any self-labelled count lines so tallies stay honest, and prunes device files orphaned by a renamed/moved/deleted source (see below).- You annotate the PDF on the device in any pen, then Export with annotations — the explicit "I'm done" signal.
inpulls your strokes straight from the.marklayer (marklayer.py), composites them over the source page for context, and routes the doc back to its source, flagging whether the source also changed on the desktop (route.py).processhas Claude read the ink against the page + a handwriting-quirks profile (read_ink.py), then 3-way-merges its interpretation into the live source with the original render as the common ancestor (reconcile.py). Counts are re-derived, the edit is committed, and the doc re-renders to the device.
Once a .mark has been read it's logged by content hash (marks.py), so the device
re-uploading the same ink never reprocesses or freezes the doc.
The mirror only adds PDFs, so renaming or moving a source folder once stranded its whole
old tree on the device. mirror now prunes: after writing the manifest it deletes any
device PDF (plus its base/ snapshot and already-read .mark) that's no longer in the
manifest — the orphans a rename/move/delete leaves behind. It only ever touches the tool's
own namespace (Document/Library + base/), never your tablet's Document/, EXPORT/, or
Note/ content.
One hard rule: if an orphan still has unread handwriting (an unprocessed .mark on a doc
whose source moved), the whole prune aborts and tells you to reconcile it first — so
cleanup never deletes around ink you haven't applied yet. mirror.py --force-prune overrides
to prune the rest, and still never deletes a pending mark.
Pruning is on for ./sync.sh mirror and the unattended heartbeat (run.py), and off
for a bare python mirror.py (safe to run anywhere). Deletions go to the Drive mount
(recoverable from Drive trash ~30 days). Preview anytime with ./sync.sh mirror --dry-run.
You don't tag anything. Claude treats every mark as if it were prefixed with @claude: it
carries out what the note intends rather than transcribing your words into the page, and it
rebuilds the sections you touch so the doc comes back finished, not annotated. It reads the rest
of the repo for context whenever a note needs it, and leaves sections you didn't touch (and
derived counts) alone.
- A check / strike / status marker → done / remove, applied as-is.
- A margin note or directive ("tighten this", "reconcile with the punchlist", "these are the decisions") → carried out, with whole-repo awareness when the change depends on information beyond this page.
- New lines in a blank area → folded in as new content.
(@claude is still honored as optional emphasis but is no longer needed. Repo-awareness uses the
claude_code backend — the default.)
It syncs any Markdown tree — folders carry no special meaning. Two light conventions:
- Checkboxes style themselves —
- [ ]/[~]/[x]/[!]/[-]render as real boxes on the device, in any doc, no setup. - Optional frontmatter — a
---block at the top (parsed and stripped, never shown on the PDF) tunes a doc:format: checklistadds a status legend,format: bookgives a clean prose layout. Most docs need none.
See docs/05-conventions.md for the details and
RECIPES.md for worked structures (a punch-list, a book, reviewing a repo's
docs) with runnable examples/.
| File | Role |
|---|---|
config.py |
All environment/user settings (env > config.toml > auto-detect) |
mirror.py |
Render the notes tree → device PDFs + manifest |
marklayer.py |
Extract ink from the .mark layer, composite over the page |
read_ink.py |
Claude reads the handwriting → interpreted Markdown |
reconcile.py |
3-way merge device edits into the source |
derive.py |
Recompute derived counts/totals deterministically |
marks.py |
Ledger of already-read annotations (by content hash) |
vcs.py |
Auto-commit each applied edit (revertible restore points) |
pending.py / route.py |
Find pending annotations / route back to source |
render.py / export.py |
Markdown → tablet-shaped PDF |
sync.sh |
One-command orchestration of the above |
Design notes and decisions live in docs/.
The local check-out/check-in loop is complete and battle-tested. Running it unattended
(so it works with the laptop shut) is designed but not yet deployed — a persistent
container that rclone-mounts Drive and runs the loop; see
deploy/HOSTING.md and docs/04-status.md.
MIT — see LICENSE. The bundled DejaVu Sans font is under its own permissive license (assets/fonts/DejaVu-LICENSE.txt).