The ledger records what the agent changed, why, and how to undo it.
deepcode ledger list # recent records
deepcode ledger show <id> # one record in full
deepcode ledger export --out audit.md
deepcode ledger rollback <id> # undo one recorded changeSessions already store the message stream, and snapshots already capture file state before and after each mutation. Neither answers the question people actually ask after a run: what did it change, and how do I undo that one thing? A message log means reading a conversation to find out; snapshots are addressable but carry no intent.
The ledger is the index over both — it pairs each mutation with the request that motivated it and the checkpoint that reverses it.
| File | Contents |
|---|---|
changes.jsonl |
Workspace mutations — Write, Edit, NotebookEdit, Bash |
governance.jsonl |
Contract edits, plugin installs, trust grants, rollbacks |
Kept apart rather than as one file with a type column. Governance events are rare and high-impact; interleaved with thousands of edits, nobody would ever see them again.
~/.deepcode/projects/<project-key>/ledger/{changes,governance}.jsonl
Not in your repository. Appending to a tracked file on every edit would turn
git status into noise during exactly the activity you're reviewing. If you do
want it committed, deepcode ledger export --out <path> writes a Markdown
digest wherever you like.
intent is the request that drove the turn. paths are workspace-relative, so
records stay readable after the repo moves. rollbackHint points at the
snapshot or git checkpoint already taken for that specific call — and is
absent when no checkpoint exists, rather than guessing at one.
derivedFrom lists the files the turn read before making the change. It
answers the third question, after "what changed" and "how do I undo it": what
was this derived from — which is what you ask when a generated file is wrong and
you need to know which input to fix, or when something turns up in a commit and
you need to know what the turn had open.
deepcode ledger show chg-lz4k2p-01
# paths : src/client.ts
# from : config/gen.yaml, schema.jsonIt is observed, not declared. These are the reads the turn actually performed, so a tool that ignored its inputs shows nothing rather than a plausible list. Consequences worth knowing:
- Only
Readcounts.GrepandGlobtake a search root and return many paths; calling the root an input would claim a derivation the turn did not make, and listing every hit would drown the real inputs in whatever the search swept up. Narrow and true beats wide and approximate. - A failed read is not an input. It gave the turn nothing, and crediting it would send someone to fix a file that was never opened.
- The file being written is excluded.
Editreads its own target by construction, and including it would make every edit look self-derived. - The field is absent, not empty, when there is nothing to say. A field that is always present is a field that stops being read.
Provenance is a derivation record, not a dependency graph: it says what this one turn read, not what the file transitively depends on.
- Reads. A ledger answering "what changed" has nothing to say about a read, and the traffic would bury the mutations.
- Failed or blocked calls. A ledger of things that did not happen is worse than no ledger.
- File contents. Only paths and summaries, so the ledger never becomes a second copy of your secrets.
Bash is recorded, with no paths — a shell command's effects can't be
declared ahead of time, but "the agent ran this" is exactly what an audit needs.
The pre-Bash git checkpoint is what makes it reversible.
Ledger writes never fail a tool call. If the disk is full or the path is unwritable, the edit still succeeds and the record is dropped. An audit trail that can cost you a completed edit is worse than one with a gap.
Corrupt lines are skipped on read. Unlike a session, nothing is reconstructed from these records, so the readable ones stay useful on their own.
Newest 5000 records per file, nothing older than 90 days; trimmed automatically as records accumulate. A log that only grows is one somebody eventually deletes wholesale — which loses the recent records too.
deepcode ledger rollback <id> undoes one recorded change. It never applies
silently — every rollback goes through four steps:
- Explain — where the change came from, how the states were compared, what will physically happen, and how to get back afterwards.
- Preview — the file that will be restored or deleted.
- Confirm — accept, reject, or defer. Anything other than an explicit yes is a no; a mistyped answer must not overwrite your files.
- Apply — and record the rollback itself on the governance timeline. An audit trail with an unlogged undo is not an audit trail.
Undoing an old change is not the same operation as undoing the last one. The plan warns before you decide when:
- later changes to the same file in that session would be discarded along with
it (a
post-capture of the same call doesn't count — otherwise every single-edit rollback would warn about itself); - the file was modified outside DeepCode since the last snapshot;
- the record is a
Bashcheckpoint, which restores every tracked file that command touched.
Snapshots and ledger records age out on different schedules, so a record can outlive the checkpoint it points at. You get a reason — "snapshot 7 is no longer available", "this record has no rollback point" — rather than a stack trace. These are expected states of an audit log, not errors.
The ledger records decisions; it never influences one. Nothing in the permission path reads it. (Selfware draws the same line: memory must not become protocol authority.)
{ "id": "chg-lz4k2p-01", "timestamp": "2026-08-08T09:12:33.417Z", "actor": "agent", "threadId": "thread-lz4k1x-a3f9", "turnId": "turn-lz4k2m-77c1", "tool": "Edit", "intent": "fix the expired-token branch in auth.ts", "paths": ["src/auth.ts"], "summary": "edited src/auth.ts", "derivedFrom": ["schema.json"], "rollbackHint": { "kind": "snapshot", "ref": "7" }, }