Aphasia58 downloadsPer-note version control with meaningful history. Save snapshots with names, diffs, and all attachments.
Another per-note version-control plugin for Obsidian.
Built around one idea: keep a meaningful snapshot history.
Important notes:
- Tested against a sandbox vault on both desktop and mobile, but can't cover every scenario.
- The plugin has known limitations, worth a look for the cases it can't fully handle.
- No feature changes are planned — this was written to solve my own problems. Bugs and Obsidian-version breaks will be fixed.
Folder-wide snapshot and restore are out of scope by design (see Out of scope).

In Settings → Community plugins → Browse, search for Note Snapshots, then Install and Enable.
Build the plugin and copy it into your vault:
pnpm install
pnpm build
This writes dist/. Copy it to <vault>/.obsidian/plugins/note-snapshots/, then
enable Note Snapshots under Settings → Community plugins.
Requires Obsidian 1.5.0 or newer. Works on desktop and mobile — see the mobile picture-refresh lag under Limitations.
Open the sidebar from the ribbon's history icon (Snapshot history), or run Open snapshot history from the command palette.
| Command | Description |
|---|---|
| Save a named snapshot of the current note | Prompts for a name (optionally prefilled) and a note, then snapshots |
| Save a snapshot of the current note | Snapshots without prompting |
| Open snapshot history | Opens the sidebar |
| Clean up history of deleted notes | Permanently removes orphaned histories older than the configured window (run manually; never automatic) |
The sidebar shows whether the note matches a saved snapshot, has unsaved changes, or
is untracked. Each row shows one snapshot's details and has a ⋮ menu of actions for
it; the header's ⋮ menu holds the note-wide ones. Click a row to diff it against the
current note, or double-click it to restore that snapshot.
| Setting | Default | Notes |
|---|---|---|
| Snapshot store folder | .note-snapshots |
Vault-relative. Changing it doesn't move existing history. |
| Keep history of deleted notes | 30 days | Age an orphaned history must reach before Clean up removes it. 0 keeps it indefinitely. Cleanup is always manual. |
| Suggested name for new snapshots | none | Prefills the name prompt: date, datetime, or a custom template. |
| Confirm before restoring | only when a restore would overwrite unsaved work | Always / Only when a restore would overwrite unsaved work / Never. Attachments modified-in-place are also considered unsaved work. |
| Confirm before deleting a snapshot | on |
The codebase started largely vibe-coded with Claude; docs/implementation-plan.md records the design rationale and where the initial output was simplified and hardened during review.
Requires Node ≥ 20.19.6 and pnpm 10.25.0 (both provided by the dev container).
pnpm install
pnpm dev # rebuild on change
pnpm build # tsc + esbuild production bundle
pnpm typecheck # tsc over src and test
pnpm test # core checks against an in-memory vault
pnpm test runs the core against a stubbed obsidian module and an in-memory
vault, so the invariants above — the no-growth property, copy-vs-move
discrimination, attachment recovery — are checked without launching Obsidian.
Build output folder: OBSIDIAN_PLUGIN_DIR if set, else
<OBSIDIAN_VAULT>/.obsidian/plugins/note-snapshots/, else dist/ (flat — the
location Obsidian's community-plugin build verification expects; the installed cases
above use the id from manifest.json instead). Pair OBSIDIAN_VAULT with pnpm dev
for a rebuild-into-vault loop; see
.devcontainer/devcontainer.json.
Snapshots are plain files in the vault — syncable, greppable, recoverable by hand:
.note-snapshots/
central.json # note index: id → path, for rename tracking and orphan cleanup
<noteId>/
manifest.json # this note's snapshot pointer and per-snapshot metadata
<snapshotId>.md # snapshot content, uncompressed
attachments/<hash>.bin # embedded attachments, deduplicated by content hash
A note's noteId is a ns-id key written into its frontmatter on the first snapshot.
Identity is resolved by that id, not by path, so renaming A.md to B.md keeps the
history intact. The path recorded in central.json is separate bookkeeping: it's
repaired lazily as the note moves, and used to find a deleted note's history so it
can be marked orphaned for cleanup.
A note's snapshot pointer marks which snapshot its working file currently
matches, or none. The V1, V2, … labels in the sidebar are display only —
sorted by capture time.
"Unsaved work" means the note text or any embedded attachment is not in stored snapshots.
This is only for flagging. You can always save a snapshot.
Restoring writes the stored content back and moves the snapshot pointer to the target snapshot. No new snapshot is created, so flipping between snapshots never grows the history.
When the note has unsaved changes, restoring asks first: Snapshot & restore (the default) captures that work as a snapshot so it stays recoverable, while Restore only discards it and checks the snapshot out straight away.
Only the content is restored; the note's name and folder are left alone.
Embedded attachments in the target snapshot are reconciled alongside the text:
The plugin works on a per-note basis, so take care if the attachment is shared with another note (see Limitations).
Whether a still-present attachment has changed is checked by size, then by hash if below the "Large attachment threshold" setting (25 MB by default), so a large attachment can be mistakenly judged unchanged (see When attachment recovery falls short). Lower the threshold if that risk matters more to you than the cost of hashing large files, or set it to 0 to always hash regardless of size.
Setting "Confirm before restoring" to Never skips this prompt: the restore proceeds
immediately, still recreating any missing attachment but leaving a changed-in-place one
untouched for safety.
Attachment recovery is best-effort. Two questions about what you did cover where it can miss.
1. Did you move the note to another folder since the snapshot?
If not, every embed resolves — the attachment comes back exactly where it was. If you did, whether it still resolves depends on the link style:
![[image.png]]:,
::):This is because restore never rewrites embed links, so a recreated attachment only resolves where its link already points. The plugin decides where to place an attachment purely by comparing recorded paths from the snapshot:
2. Did you modify the attachment in place (same name, different content)?
Almost always fine: restore notices the change and asks before touching the attachment.
The one exception is an edit to an attachment at or above the "Large attachment threshold" setting (25 MB by default) that leaves it the exact same size — restore checks by size alone, takes it for unchanged, and leaves it.
ns-id key in its frontmatter. Strip or clear that field — a "clear frontmatter"
command, a format conversion, manual editing — and the note silently detaches from
its history; the next snapshot just starts a new one, with no error.A snapshot is about one note and its embedded attachments. Links to other
notes ([[Other note]]) and embeds of other notes (![[Other note]]) are stored
as plain text; the plugin never snapshots, restores, or otherwise touches the
notes they point at. Version-controlling complex relationships between notes and
attachments is out of scope. If you need whole-vault, commit-style history across
many files at once, use obsidian-git
instead — or alongside this plugin, for the coarse-grained layer.
No folder operations, by design. A folder-wide snapshot could only ever be a batch over independent per-file histories:
Snapshot the files you care about, one history each.