{"id":"6649f92a-ec30-40f9-b6c4-2da6cfa7e4c8","slug":"preimage","kind":"mcp","name":"preimage","summary":"The undo layer for AI agents. Checkpoint files and SQLite tables before the agent breaks them, restore them after.","owner":"muraa-p","repoUrl":"https://github.com/muraa-p/preimage","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-01T07:31:58.000Z","addedAt":"2026-10-21T06:52:54.426Z","logoUrl":"https://avatars.githubusercontent.com/u/85794781?u=3476ff5f2495399e0a629a5c6c6c830f0588d027&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["databases","developer-tools","files-documents"],"tags":["ai-agents","checkpoint","claude-code","cli","data-loss","developer-tools","nodejs","opencode"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-21T06:53:28.114Z","sample":false,"url":"https://conduits.dev/tools/preimage","language":"JavaScript","transports":["stdio"],"install":{"npm":"@muraa-p/preimage"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# preimage\n\n[![ci](https://github.com/muraa-p/preimage/actions/workflows/ci.yml/badge.svg)](https://github.com/muraa-p/preimage/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/@muraa-p/preimage.svg)](https://www.npmjs.com/package/@muraa-p/preimage)\n[![Node.js](https://img.shields.io/badge/node-%3E%3D22.5.0-brightgreen.svg)](https://nodejs.org/)\n[![License](https://img.shields.io/npm/l/preimage.svg)](./LICENSE)\n[![Tests](https://img.shields.io/badge/tests-153%20passing-brightgreen)](#development)\n\n**The undo layer for AI agents.**\n\nYour agent is one confident `rm -rf` away from deleting a table, rewriting a\nconfig, or \"helpfully\" migrating a database. git can't save you: those changes\nnever entered a commit, and they often aren't files at all.\n\n`preimage` writes a checkpoint before the work starts, then puts everything back\nbyte-for-byte — including database rows git has never heard of.\n\n![preimage rolling back an agent's file edits](docs/demo-files.gif)\n\n```\n$ preimage checkpoint \"before the agent gets creative\"\ncheckpoint 0001 created\n  4 files, 893 B\n\n$ # the agent quietly changes the port and the greeting, and leaves a stray file\n│ config.js rewritten, DEBUG.md added\n\n$ # the project's own tests notice\n✖ 2 failing, 0 passing\n\n$ preimage diff 1\ndiff against checkpoint 0001\n  1 added\n  1 modified\n  0 deleted\n  3 unchanged\n    config.js\n    DEBUG.md\n\n--- /dev/null\n+++ b/DEBUG.md\n@@ -0,0 +1 @@\n+# scratch notes, will delete later\n\n--- a/config.js\n+++ b/config.js\n@@ -1,5 +1,5 @@\n export const config = {\n \tname: \"hello-service\",\n-\tport: 3000,\n-\tgreeting: \"Hello\",\n+\tport: Number(process.env.PORT) || 8080,\n+\tgreeting: \"Hi\",\n };\n\n$ preimage restore 1 --purge --yes\nrestored checkpoint 0001 (before the agent gets creative)\n  1 files written\n  3 already identical\n  1 new files removed\n\n$ # tests again, after the rollback\n✔ 2 passing, 0 failing\n\n$ and the stray file\n│ DEBUG.md: gone\n```\n\nThe project's own tests go from failing to passing, and the scratch file the\nagent left behind is gone.\n\nNow the part git can never do:\n\n![preimage rolling back a destructive database migration](docs/demo-database.gif)\n\n```\n$ preimage checkpoint \"before migration\" --db app.db\ncheckpoint 0001 created\n  1 files, 8.0 KB\n  2 rows across 1 database(s)\n\n$ # a migration drops a user, promotes another, invents a third\n$ preimage diff 1\ndiff against checkpoint 0001\n  0 added\n  0 modified\n  0 deleted\n  0 unchanged\n  rows: 1 missing, 1 changed, 1 extra\n\n$ preimage restore 1 --remove-extra\nrestored checkpoint 0001 (before migration)\n  0 files written\n  0 already identical\n  2 rows written\n  1 rows removed\n\n$ sqlite3 app.db \"SELECT * FROM users\"\n[{\"id\":1,\"email\":\"ada@org.org\",\"role\":\"admin\"},\n {\"id\":2,\"email\":\"bob@org.org\",\"role\":\"staff\"}]\n```\n\n`ada` is an admin again. `bob` is back. The ghost row is gone. No commit, no\n`git checkout`, no hand-written `UPDATE`.\n\nAnd when the agent drops a whole table, the schema comes back with it:\n\n```\n$ preimage checkpoint \"before the schema change\" --db app.db\ncheckpoint 0001 created\n  1 files, 12 KB\n  2 rows across 1 database(s)\n\n$ # the agent decides users is no longer needed\n$ preimage diff 1\ndiff against checkpoint 0001\n  0 added\n  0 modified\n  0 deleted\n  0 unchanged\n  rows: 1 missing, 0 changed, 0 extra\n  table dropped: app.db:users\n\n$ preimage restore 1 --yes\nrestored checkpoint 0001 (before the schema change)\n  0 files written\n  0 already identical\n  1 tables recreated\n  2 rows written\n\n$ sqlite3 app.db \".tables\"\ninvoices users\n```\n\n`DROP TABLE users` is undoable. preimage stored the `CREATE` statement next to\nthe rows, so recreating the table is part of the restore rather than a separate\nrecovery ritual.\n\nRun the whole demo yourself:\n\n```bash\ngit clone https://github.com/muraa-p/preimage && cd preimage\nnode scripts/demo.mjs\n```\n\n## Install\n\n```bash\nnpm install -g @muraa-p/preimage\n```\n\nZero dependencies. Node 22.5+ (it uses the built-in `node:sqlite`, so there is\nno native build step and no supply chain to audit).\n\nThen wire it to your agent — see [Agent integration](#agent-integration).\n\n### If you're on Windows\n\nTwo things that are not preimage's fault, and that will otherwise look like it is:\n\n**PowerShell blocks the command.** npm installs a `.ps1` shim, and the default\n`ExecutionPolicy` refuses to run it:\n\n```\npreimage : File ...\\npm\\preimage.ps1 cannot be loaded because running scripts is\ndisabled on this system.\n```\n\nEither call `preimage.cmd` instead (`preimage.cmd checkpoint`), or widen the\npolicy once:\n\n```powershell\nSet-ExecutionPolicy -Scope CurrentUser RemoteSigned\n```\n\n**Node cannot spawn the `.cmd` without a shell.** If you are calling preimage\nfrom a script rather than a terminal, `execFile(\"preimage\", ...)` fails with\n`EINVAL`. Use `shell: true`, or point at the entry point directly:\n\n```js\nconst bin = \"C:/Users/You/AppData/Roaming/npm/node_modules/@muraa-p/preimage/bin/preimage.js\";\nexecFileSync(process.execPath, [bin, \"checkpoint\", \"before the refactor\"]);\n```\n\nMCP is unaffected by all of this — the client launches the server, not your\nshell.\n\n## Why this exists\n\nThe tooling around coding agents is very good at *watching* them. Session\nlogs, traces, prompt caches, token dashboards, eval harnesses — all of it\nobserves what the agent did. Almost nothing helps you *undo* it.\n\n| | git | preimage |\n|---|---|---|\n| Untracked files | No | Yes |\n| `.gitignore`d files | No | Yes |\n| Database rows | No | Yes |\n| Works outside a repo | No | Yes |\n| Needs `git add` + commit discipline | Yes | No |\n| Reverts agent-created junk | No | Yes, with `--purge` |\n\nPreimage is not a backup tool and not a VCS. It is the write-ahead log for the\none window where a non-human is editing your machine.\n\n## Usage\n\n```bash\npreimage init                          # create the journal\npreimage checkpoint \"before refactor\"  # snapshot now\npreimage list                          # see checkpoints\npreimage diff                          # what changed since the latest\npreimage diff 3                        # ...since a specific one\npreimage restore 3                     # put it back\n```\n\n`diff` doesn't just name the files. It prints a real unified diff, byte for byte\nwhat `git diff` would print, so you can read the damage instead of opening each\nfile to find it:\n\n```\n$ preimage diff\ndiff against checkpoint 0001\n  1 added\n  1 modified\n  0 deleted\n  2 unchanged\n    app.js\n    SCRATCH.js\n\n--- a/app.js\n+++ b/app.js\n@@ -1,4 +1,8 @@\n-export function greet(name) {\n-  return `hello ${name}`;\n+export function greet(name, greeting = 'hi') {\n+  const msg = `${greeting} ${name}`;\n+  console.log(msg);\n+  return msg;\n+}\n+\n+export function farewell(name) {\n+  return 'bye';\n }\n```\n\n`--no-hunks` gives you just the file list. Binary files and files too large to\ndiff line-by-line are reported with the reason rather than skipped silently.\n\n`diff` and `show` default to the most recent checkpoint. `restore` always wants\nan explicit id, because guessing wrong there is expensive.\n\nEvery command takes `--json` for machine-readable output. `diff --json` puts the\npatches in a `patches` array, each with `path`, `added`, `removed` and the\n`diff -u` text.\n\n### Options that matter\n\n```\n--root <dir>       Project root (default: cwd)\n--db <path>        Capture a SQLite database too (repeatable)\n--table <name>     Limit to specific tables (repeatable)\n--max-bytes <n>    Skip files larger than this (default 10 MB)\n--no-hunks         diff: file names only, no line-level patches\n--dry-run          Report what restore would do, change nothing\n--purge            Also delete files created after the checkpoint\n--remove-extra     Also delete rows created after the checkpoint\n--yes              Skip the confirmation prompt\n```\n\n## Agent integration\n\n### MCP — this is the path, and it is agent-agnostic\n\n`preimage mcp` speaks the Model Context Protocol over stdio, so it works with\n**every** MCP client, not just Claude. That includes OpenAI Codex and ChatGPT,\nCursor, VS Code and Copilot, Windsurf, Gemini CLI, Zed, Cline, Roo Code, Kilo\nCode, Amazon Q and Claude Desktop. If your agent speaks MCP, preimage plugs in.\n\nMost clients take the same JSON:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"preimage\": { \"command\": \"preimage\", \"args\": [\"mcp\"] }\n  }\n}\n```\n\nWhere each one wants it:\n\n| Agent | How to add it |\n| --- | --- |\n| **Claude Code** | `claude mcp add preimage -- preimage mcp` |\n| **OpenAI Codex** | `codex mcp add preimage -- preimage mcp`, or a `[mcp_servers.preimage]` table in `~/.codex/config.toml` |\n| **Gemini CLI** | `gemini mcp add preimage preimage mcp`, or `mcpServers` in `~/.gemini/settings.json` |\n| **Cursor** | `.cursor/mcp.json`, using the `mcpServers` key above |\n| **VS Code / Copilot** | `.vscode/mcp.json` — the key is `servers`, not `mcpServers` |\n| **Windsurf** | `~/.codeium/windsurf/mcp_config.json` |\n| **Zed** | `.zed/settings.json`, under `context_servers` |\n| **Claude Desktop** | `claude_desktop_config.json` |\n\nClients that launch the server in the wrong working directory will snapshot the\nwrong tree, so pass the path explicitly if that happens:\n\n```jsonc\n\"preimage\": { \"command\": \"preimage\", \"args\": [\"mcp\", \"--root\", \"/path/to/project\"] }\n```\n\nFour tools:\n\n- `preimage_checkpoint` — snapshot before risky work\n- `preimage_diff` — what changed since a checkpoint, with a unified diff per file\n- `preimage_restore` — roll back\n- `preimage_list` — what's recoverable\n\n`preimage_restore` **requires an explicit `confirm: true`**, and refuses\notherwise. An agent mid-task should not be able to undo your working state by\naccident; it has to ask.\n\n`preimage_diff` returns the patches, so the agent sees the lines it changed\nrather than being told a file changed and having to go read it. The patch text is\ncapped at a budget; if a refactor exceeds it, the response says how many files\nwere left out rather than letting the agent conclude they were untouched. Pass\n`includePatches: false` when you only want the file names.\n\n### Hooks (automatic)\n\nSnapshotting before every single edit is correct but wasteful, so the bundled\nhooks debounce: one checkpoint per 120 seconds. The first write in a burst\ncaptures the state you actually want to return to.\n\n```bash\npreimage hook install claude-code    # prints a settings.json fragment\npreimage hook install opencode       # prints a plugin command\n```\n\nMerge the printed fragment into `~/.claude/settings.json`. The hook never fails\nyour session — if the checkpoint can't be taken, the edit proceeds anyway.\n\nSet `PREIMAGE_SESSION_WINDOW=0` to snapshot on literally every write.\n\nThis is the one part that is **not** agent-agnostic: hooks are wired for Claude\nCode and OpenCode only. Every other agent is covered by MCP above — the\ndifference is that a hook snapshots *without the model asking*, so on the other\nagents you either rely on the model calling `preimage_checkpoint` or wrap your\nown command chain.\n\nThe bundled hook scripts are bash. On Windows they need WSL or Git Bash on your\nPATH; **MCP has no such requirement** and is the path to use there. If you want\nhook behaviour on Windows, the script is four lines — call `preimage checkpoint\n--root <dir>` from whatever your harness runs before a write.\n\n> **The GIFs above were recorded before `diff` printed line-level hunks**, so\n> they show the file list without the patch. The transcripts below them are\n> regenerated from real runs and are current. Re-recording the GIFs needs\n> `vhs`, `ffmpeg` and `ttyd`; the tapes are in `tape/`.\n\n## Safety design\n\nThis tool deletes things, so the defaults are conservative.\n\n- **Nothing destructive without a flag.** `restore` only rewrites files it\n  recorded. Deleting agent-created files requires `--purge`; deleting\n  agent-created rows requires `--remove-extra`.\n- **Confirmation before you lose data.** Interactive restores prompt first.\n  Agent restores need `confirm: true`.\n- **Big files are skipped, never blanked.** Files over `--max-bytes` are\n  recorded as existing but their content is not stored. On restore preimage\n  leaves them completely alone rather than overwriting them with nothing.\n- **Writes are atomic.** Every restore goes through a temp file and a rename,\n  so a crash mid-restore cannot leave a half-written file.\n- **A checkpoint is all-or-nothing.** The row, the file entries and the database\n  rows are written in one transaction. A checkpoint that dies partway through\n  leaves nothing behind rather than an empty row that looks restorable. Anything\n  unfinished is refused outright, because a restore that reports success without\n  restoring anything is the one failure this tool cannot have.\n- **Parallel agents do not collide.** Several `preimage checkpoint` processes can\n  run at once; they queue on the journal rather than failing on a lock.\n- **Path traversal is blocked.** A tampered journal record cannot write outside\n  the project root.\n- **Table scope is explicit.** preimage never snapshots an entire database\n  silently. You name the tables, or you opt in per database. And a database you\n  captured with `--db` is restored table by table, never as a file — that's the\n  only way `--table users` can leave `audit_log` alone.\n- **A dropped table comes back.** The `CREATE` statement is stored alongside the\n  rows, so `DROP TABLE users` is undoable, schema included.\n\n## How it works\n\nA checkpoint is a content-addressed snapshot in a SQLite journal at\n`<root>/.preimage/journal.db`.\n\n```\ncheckpoints   id, created_at, label, source, status\nfiles         checkpoint_id, path, kind, mode, size, sha\nblobs         sha, bytes            -- deduplicated by content hash\ndb_tables     checkpoint_id, db_path, table_name, ddl\ndb_rows       checkpoint_id, db_path, table_name, pk, row_json\n```\n\nBlobs are keyed by sha256, so a file that appears in twenty checkpoints is\nstored once. Restore looks up the blob and writes it back; identical files are\nskipped, which makes restore idempotent and fast. `preimage gc` drops blobs no\nlonger referenced.\n\nDatabase rows are stored as JSON keyed by primary key (falling back to `rowid`),\nwith BigInt and BLOB columns tagged so they round-trip exactly. Each table's\n`CREATE` statement is stored too, which is what lets restore rebuild a table the\nagent dropped. Restores run inside a transaction per table.\n\nA database captured with `--db` is handed to the table adapter and removed from\nthe file layer's work list. Without that, restoring `--table users` would\nrewrite the whole `.db` file and silently revert every table you never asked\nabout.\n\nAdd `.preimage/` to `.gitignore`.\n\n## Limitations\n\nWorth knowing before you rely on it:\n\n- **SQLite only** for database capture. Postgres and MySQL adapters are the\n  obvious next step; the `dbadapter.js` interface is small.\n- **Not a backup.** The journal lives next to your project. It protects you\n  from the agent's mistakes, not from your disk failing.\n- **Restore is a snapshot, not a merge.** You get the state at the checkpoint,\n  not a three-way diff. If you changed something by hand after the checkpoint\n  and want to keep it, restoring loses it.\n- **First 10 MB per file.** Larger files are tracked but not restorable.\n- **Single-writer.** The journal is not designed for concurrent restores from\n  several processes at once.\n\n## Development\n\n```bash\nnpm test          # 153 tests, node:test\nnpm run check     # syntax check every entrypoint\nnode scripts/demo.mjs             # all three stories, instant\nnode scripts/demo.mjs --story=1   # just one story\n```\n\nCI runs the suite on Linux, macOS and Windows across Node 22 and 24, and\nadditionally installs the packed tarball into a clean project to check that the\nreal binary works from a real install.\n\n### Re-recording the demos\n\nThe GIFs in this README come from [`tape/`](./tape), recorded with\n[vhs](https://github.com/charmbracelet/vhs). Needs `vhs`, `ffmpeg` and `ttyd` on\n`PATH`.\n\n```bash\nvhs tape/files.tape        # -> docs/demo-files.gif\nvhs tape/database.tape     # -> docs/demo-database.gif\n```\n\nBoth record `scripts/demo.mjs` with `--paced`, so the text in the GIFs is\ngenerated by the same code path quoted above. If you change the demo, re-record\nand update the transcripts here in the same commit.\n\n## Contributing\n\nIssues and PRs welcome. Keep it dependency-free — that constraint is the whole\nsecurity argument for a tool that sits in the agent's hot path. Destructive\nbehaviour stays behind a flag, and every bug fix ships with the test that fails\nwithout it.\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). Vulnerabilities go to\n[SECURITY.md](SECURITY.md), not the issue tracker.\n\n## License\n\nMIT"}