Hermes Agent Deep Cuts: The Shadow Git Store Behind `/rollback`
Every file-mutating tool call your agent makes is an uncommitted transaction with no rollback — until you turn on Hermes checkpoints. Then, silently, before write_file touches disk or sed -i rewrites a config, the working directory is committed to a shadow git repository the model cannot call. The module’s own docstring is explicit: “This is NOT a tool — the LLM never sees it. It’s transparent infrastructure controlled by the checkpoints config flag or --checkpoints CLI flag.”
That separation is the systems-level point. Hermes did not hand the agent an undo button — it made undo an infrastructure property of the execution loop, invisible to the thing being undone. This post is about how that shadow store works, why it is built as one shared git repository instead of many, and the boundary at which the happy path stops being a transaction.
The feature hiding behind a flag
Filesystem checkpoints are opt-in. The Checkpoints & /rollback guide is blunt about why: “Checkpoints are opt-in as of v2 — most users never use /rollback, and the shadow-store storage is non-trivial over time, so the default is off.”
Enable per session:
hermes chat --checkpoints
Or globally in ~/.hermes/config.yaml:
checkpoints:
enabled: true
When enabled, the Checkpoint Manager snapshots the project before mutations:
- File tools —
write_fileandpatch - Destructive terminal commands —
rm,rmdir,cp,install,mv,sed -i,truncate,dd,shred, output redirects (>), andgit reset/clean/checkout
…at most one snapshot per directory per turn, so long sessions don’t spam commits. The snapshot reasons are recorded verbatim (“before write_file”, “before terminal: sed -i …”), which makes the /rollback listing read like an audit trail of the agent’s mutations.
One store to rule the worktrees
The design that makes this cheap is the v2 store. Instead of a shadow repo per project (the v1 design), all projects share one bare-ish git repository at ~/.hermes/checkpoints/store/, and isolation lives in per-project refs and indexes:
~/.hermes/checkpoints/
├── store/ # single shared git repo
│ ├── objects/ # git internals, deduplicated across projects
│ ├── refs/hermes/<hash> # per-project branch tip
│ ├── indexes/<hash> # per-project git index
│ ├── projects/<hash>.json # workdir + created_at + last_touch
│ └── info/exclude # default excludes
├── .last_prune # auto-prune idempotency marker
└── legacy-<ts>/ # archived pre-v2 shadow repos
Each <hash> is sha256(absolute_workdir)[:16]. The module’s header explains the economics: the pre-v2 layout “burned ~40 MB each (~500 MB total)” for a dozen worktrees of the same repo because each re-stored the same blobs; a shared store lets git’s content-addressable object DB deduplicate across projects and across turns, so “adding a new worktree costs near-zero.”
Three implementation details in checkpoint_manager.py are worth calling out because each one prevents a real failure:
- Git is environment-isolated. Every subprocess runs with
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEpinned to the store, andGIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEMpointed at/dev/nullwithGIT_CONFIG_NOSYSTEM=1. Your~/.gitconfig—commit.gpgsign = true, signing hooks, credential helpers — cannot break a background snapshot or spawn a pinentry window mid-session. The store’s own config setscommit.gpgsign falseandgc.auto 0. The test suite proves snapshots still commit with a global gpgsign config and a deliberately broken gpg binary. - Input validation is security-shaped. Restore accepts only 4–64 hex-character commit hashes and rejects anything starting with
-(git flag injection), and single-file restore rejects absolute paths and..traversal outside the working directory. The tests assertrestore("--patch"),restore("abc; rm -rf /"), andrestore(…, "/etc/passwd")all fail. - Failure is non-fatal by design. If git is missing, checkpoints are transparently disabled; every error inside the manager is logged at debug and the tool call proceeds. Your agent’s work is never blocked by its own safety net.
The command surface
In-session (works in both CLI and messaging gateway, per the slash-commands reference):
/rollback list checkpoints with change stats
/rollback <N> restore to checkpoint N (also undoes the last chat turn)
/rollback diff <N> preview the diff since checkpoint N
/rollback <N> <file> restore a single file from checkpoint N
Out-of-session, hermes checkpoints inspects and manages the store without the agent running:
hermes checkpoints # status: total size, per-project breakdown
hermes checkpoints prune --retention-days 3 --max-size-mb 200
hermes checkpoints clear -f # nukes ALL rollback history (asks first)
hermes checkpoints clear-legacy # deletes only v1-migration archives
A bonus you get for free once checkpoints exist: /diff session, the cumulative diff of everything Hermes changed in a directory, computed against the earliest retained checkpoint — the source notes it is “an approximation of what Hermes changed, not an exact per-session ledger.”
A practical scenario
You give the agent a “refactor configs” task on a project without a committed baseline. It runs sed -i across five files, and one substitution is wrong: a value you need is now silently different. Your real .git has nothing to help — the working tree was dirty before the session started.
/rollback
📸 Checkpoints for /path/to/project:
1. 4270a8c 2026-03-16 04:36 before patch (1 file, +1/-0)
2. eaf4c1f 2026-03-16 04:35 before write_file
3. b3f9d2e 2026-03-16 04:34 before terminal: sed -i s/old/new/ config.py (1 file, +1/-1)
/rollback diff 1 shows exactly what the last turn changed before you commit to it. /rollback 1 restores the files and rewinds the agent’s context so the next turn sees a filesystem that matches what the model believes is there — the part that makes it an agent-level undo, not a file-level one. And because restore first takes a pre-rollback snapshot, you can undo the undo. Single-file restore (/rollback 3 config.py) lets you salvage one file without touching the rest.
The docs’ best-practice combination: checkpoints plus git worktrees — one worktree per agent session, with the shadow store as the second layer.
The gotcha: it’s a safety net, not a transaction
The happy path fails in several distinct ways, all documented in the issue tracker:
1. It is scoped to directories, not turns. Issue #69173 (open, “needs-decision”) makes the boundary explicit: the checkpoint scope is derived from the terminal call’s working directory, and a local terminal command “is not restricted to that directory and runs with the same OS identity as Hermes.” A command like rm -rf "$HERMES_HOME/checkpoints" deletes the shared store itself — “rollback history is destroyed.” Deleting a file in another project by absolute path, deleting under /tmp, or deleting a Docker volume: none of those are recoverable by /rollback. The issue’s proposed contracts (isolated execution, a privileged checkpoint service, or an explicit “best-effort local mode”) are still awaiting a maintainer decision. Treat /rollback as covering eligible files under checkpointed working directories, not as a transaction over the turn.
2. The directory you’re in matters. Issue #10505: /rollback from a directory with no direct checkpoints returns “No checkpoints found for /home/admin” even though subdirectories have rich histories. Resolution matches the resolved project root of the current directory exactly. Run /rollback from the project root, not from a nested shell.
3. Gateway deployments had a real gap. Issue #11409 documented that the gateway never called ensure_checkpoint() for file tools — the hooks existed in the CLI’s run_agent.py but not gateway/run.py, so “even with checkpoints.enabled: true … no snapshots are created.” Multiple fix PRs followed (#11428, #11555, #18843, #22841), and a comment as recent as 2026-07-19 reported it still broken in 0.18.2. If you run Hermes through the gateway, verify snapshots actually appear in ~/.hermes/checkpoints/ after a file write — don’t assume the config flag propagated.
4. Your history window is bounded and decaying. max_snapshots (default 20) is enforced by rewriting the per-project ref and git gc --prune=now; max_total_size_mb (default 500) drops the oldest commits round-robin; files over max_file_size_mb (default 10 MB) are excluded from snapshots entirely, as are directories over 50,000 files, /, and $HOME. Auto-prune (on by default, retention_days: 7) is deliberately conservative: it records the parent directory’s (st_dev, st_ino) at snapshot time and only classifies a project as an orphan when deletion is observable — an unmounted external volume or a downed VPN must not cost you your restore points. Interactive hermes checkpoints prune shows a preview and binds deletion to exactly what you approved, so a workdir that vanishes while you’re answering the y/N prompt survives the sweep. The docstring for HERMES_CHECKPOINT_TIMEOUT is the other knob: git subprocess timeouts are configurable (default 30s, clamped 10–60), the fix for the old hardcoded-timeout reports.
5. It is a recovery control, not a prevention control. Checkpoints sit below the approval layer in the trust stack. They do not block rm -rf — the dangerous-command approval, the hardline blocklist, and approvals.deny do. And they don’t catch everything the terminal regex misses: #69171 notes the trigger regex itself is bypassable. Layers, not guarantees.
6. “Checkpoint” means something else in batch mode. The batch runner has its own checkpoint.json resume state for interrupted trajectory runs. Different feature, same word — don’t go looking for /rollback history in data/<run_name>/.
How to verify it without risking anything
hermes checkpoints status # empty base before you enable anything
cd /tmp && mkdir ckpt-demo && cd ckpt-demo
hermes chat --checkpoints # session with checkpoints on
Then, in the session: write a file, run a sed -i, and type /rollback — you should see snapshots named “before write_file” and “before terminal: …”. Try /rollback diff 1, then /rollback 1 <file> on a scratch file. Confirm your real .git was never touched (ls -a shows no .git), and that ~/.hermes/checkpoints/ exists only because you enabled it. Clean up with hermes checkpoints clear — which asks first, because it erases every rollback history you have.
The verification questions that matter:
- Did a snapshot appear before the mutation, not after?
- Does
/rollbackfrom a subdirectory find the project root’s history? - Is the store capped (size, snapshot count), and does pruning refuse to delete ambiguous orphans?
- Does restore produce a pre-rollback snapshot so the undo is itself undoable?
Facts, inference, and the open edge
Observed (docs and main-branch source/tests, all linked): checkpoints are opt-in with enabled: false as the default; one shared bare git store under ~/.hermes/checkpoints/store/ with per-project refs/indexes/metadata; snapshots fire before write_file/patch and a listed set of destructive terminal patterns, at most one per directory per turn; git subprocesses are env-isolated from the user’s git config; restore validates hashes and confines file paths; max_snapshots/max_total_size_mb/max_file_size_mb are enforced with ref rewriting and gc --prune=now; auto-prune is evidence-gated via recorded parent (st_dev, st_ino); the /rollback command family and the hermes checkpoints CLI family exist as documented; and the issue tracker contains the scope-escape (#69173), gateway-gap (#11409), and current-directory (#10505) failure reports.
Inference: Hermes deliberately keeps the checkpoint machinery invisible to the model — giving the agent a tool to manipulate its own undo journal would let it game or expand the store, and the deduplicated single-store design exists specifically to make the feature cheap enough to be invisible.
Open questions: whether the installed gateway build on any given deployment actually snapshots (issue history says verify); the exact startup-sweep orphan policy (the docs comment says startup never deletes orphans, while the test docstring says orphan pruning “runs unattended at startup” — evidence-gated either way); and the unresolved authority boundary from #69173 — whether local terminal should be sandboxed, shielded by a privileged checkpoint service, or honestly documented as best-effort.
The interesting part is not that Hermes added an undo command. It is that the project encoded undo as an infrastructure property of the mutation path — a content-addressed journal the agent can’t touch, isolated from the user’s own git configuration, capped and pruned like a real storage system. In agent systems, the difference between “the model can revert its work” and “the runtime journals the work before it happens” is where recoverability actually lives.
Sources
- Checkpoints and /rollback — user guide
- Checkpoint Manager implementation (
tools/checkpoint_manager.py) - Checkpoint Manager test suite (
tests/tools/test_checkpoint_manager.py) hermes checkpointsCLI (hermes_cli/checkpoints.py)- Slash command registry,
/rollbackdefinition (hermes_cli/commands.py) - Slash Commands reference
- CLI Commands reference
- Security guide — approval layers and the hardline blocklist
- Issue #69173 — local terminal side effects can escape checkpoint scope and delete the rollback store
- Issue #11409 — Gateway mode: CheckpointManager snapshots not created
- Issue #10505 — /rollback lists only current directory checkpoints
- Related: #69171 (bypassable trigger regex), #13104 (shadow-git vs CoW backend), #68877 (turn vs task scope), #65763 (opt-out default discussion)
- Batch processing checkpointing (a different “checkpoint”)
- Hermes Agent repository