⏱ Hermes Chronos
Time-travel debugging and speculative branching for Hermes Agent.
Chronos takes an isolated filesystem snapshot before every modifying tool call.
When a later step breaks the build, corrupts a config, or deletes a directory,
you can inspect the diff and put the workspace back. The snapshots live in a
private shadow git repository. The project's own .git, its branches, commits,
stashes, and hooks are left alone.
Current release: 1.0.2. Tested on Hermes 0.19.0 and the September 2026 source
tree.
Demo
- 88-second launch film — Hermes Chronos — Never Restart a Workspace Again (YouTube)
- Short cut — @MossAIStudio on X
The film was recorded against 1.0.0, so two moments are out of date: it shows
the model calling chronos_rollback with no approval step, and it shows
/rewind in the CLI. In 1.0.1 and later every model rollback stops at Hermes'
approval prompt first, and the command is /chronos (rewind is a Hermes
built-in). Everything else in the film matches the shipped plugin.
write_file / patch / terminal / execute_code
│
▼
┌───────────────────┐
│ pre_tool_call │──► snapshot ──► .hermes/chronos/repo
└───────────────────┘ (private git objects)
│
▼
tool runs
│
┌───────────────────┐
│ post_tool_call │──► failure? ──► transform_tool_result
└───────────────────┘ "rollback with abc12345"
│
▼
chronos_rollback / /chronos / Desktop pane
│
▼
safety checkpoint ──► restore files ──► workspace
What it does
- Shadow checkpoints. A private git object database under
.hermes/chronos/repouses the workspace as its work tree. It never opens,
creates, or rewrites the user's.git, and it works in projects that are not
git repositories at all. - Automatic snapshots.
pre_tool_callsnapshots beforewrite_file,
patch,terminal,execute_code,skill_manage, and compatible aliases.
The checkpoint records the tool, turn, session, timestamp, and a short
description. - Recovery hints. When a tool result looks like a failure,
transform_tool_resultappends a short notice naming the checkpoint and the
exactchronos_diff/chronos_rollbackcalls. The model can recover without
a human in the loop. - Reversible rollback. Restoring a checkpoint first creates a safety
checkpoint of the current state, so you can restore the pre-rollback state as
well. - Desktop timeline. A native Hermes Desktop pane (
desktop/plugin.js)
lists checkpoints, previews the file diff, and restores with a two-step
button. The pane polls its scoped backend every five seconds. - Interactive
/chronos. List, inspect, dry-run, or restore from any Hermes
surface: CLI, TUI, Desktop, or messaging gateway. The model's
chronos_rollbackand the slash command both stop at Hermes' approval prompt
before overwriting workspace files. - Guard rails. Root and home directories are refused. Ignored paths such as
node_modulesand.venvare never touched. A snapshot is bounded by an 8 s
budget, well insidepre_tool_call's 30 s window: if a workspace is too large
to snapshot in time, Chronos logs it and the tool runs without a checkpoint. - Approval before rollback. The model's
chronos_rollbackreturns Hermes'
{"action": "approve"}directive, so a human confirms every restore (and a
denial comes back as a blocked tool result). The model tool also has no
no_safetyflag: it always writes a safety checkpoint first.
Install
From the published repository, pinned to a reviewed commit:
hermes plugins install DevTarlow/hermes-chronos --ref <40-character-sha>
hermes plugins enable hermes-chronos
From a local checkout:
git clone https://github.com/DevTarlow/hermes-chronos.git ~/.hermes/plugins/hermes-chronos
hermes plugins enable hermes-chronos
Hermes installs the declared Python dependencies (gitpython, pydantic) into
the plugin environment. git must be on PATH. The engine calls git directly
with an explicit git directory and work tree.
Enable the Desktop half separately in Capabilities → Plugins → Chronos Time
Travel. The pane is off until you turn it on, like every third-party Desktop
extension.
Configuration
Settings live under plugins.entries.hermes-chronos.settings in
~/.hermes/config.yaml. Current Hermes releases also show them as a form in
Settings → Plugins. Hermes 0.19 has no settings form; the plugin reads the
same keys from config.yaml directly.
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| auto_snapshot | bool | true | Snapshot before modifying tool calls. |
| max_checkpoints | int | 50 | Recent checkpoints kept before the shadow repository is compacted with git gc. |
| ignored_patterns | list | .git, .hermes, and common dependency/cache directories | Extra gitignore-style patterns. .git and .hermes are always excluded. |
| clean_untracked_on_rollback | bool | true | Remove untracked, non-ignored files to match the snapshot. Set false to restore them from the safety checkpoint. |
ignored_patterns defaults to .git, .hermes, node_modules, venv,
.venv, __pycache__, and common cache/tool directories.
Example:
plugins:
entries:
hermes-chronos:
settings:
auto_snapshot: true
max_checkpoints: 80
ignored_patterns:
- .git
- .hermes
- node_modules
- dist
clean_untracked_on_rollback: true
Tools
Every tool returns a JSON string. Success envelopes contain "status": "ok".
Failures contain "status": "error" and "error".
| Tool | What it does |
| --- | --- |
| chronos_snapshot | Take a checkpoint now. Args: description (required), tool_name. |
| chronos_rollback | Restore a checkpoint. Args: checkpoint_id, clean. Always asks the user to approve first, and always writes a safety checkpoint. |
| chronos_diff | Diff between checkpoints or the working tree. Args: from_checkpoint, to_checkpoint, checkpoint_id, max_chars. |
| chronos_list_checkpoints | Recent checkpoints with ids, tools, turns, and timestamps. Arg: limit. |
checkpoint_id accepts an 8-character id, a full hash, a 1-based index, or
latest. With no arguments, chronos_diff shows uncommitted changes when the
tree is dirty and the latest checkpoint when the tree is clean.
A rollback response includes the restored id, the safety checkpoint id, the
number of files changed, and the untracked-file counts:
{
"status": "ok",
"restored_checkpoint": "1a2b3c4d",
"safety_checkpoint": "9f8e7d6c",
"files_changed": 3,
"restored_files": 41,
"untracked_removed": 2,
"untracked_preserved": 0,
"clean": true
}
untracked_removed counts files deleted by a clean: true restore.
untracked_preserved counts files put back by clean: false.
Slash command
/chronos list the newest checkpoints
/chronos --list 15 list 15
/chronos 3 restore the 3rd newest checkpoint
/chronos latest restore the newest checkpoint
/chronos 1a2b3c4d restore by id
/chronos --diff 1a2b3c4d show the files changed by a checkpoint
/chronos --dry-run 3 preview what restoring would undo
/chronos --no-clean 1a2b3c4d restore tracked files, keep untracked files
/chronos --no-safety 1a2b3c4d skip the pre-restore safety checkpoint
/chronos --compact repack the shadow repository now
Rollback always takes a safety checkpoint first. --dry-run compares the
selected checkpoint with the current worktree, including changes made after a
previous rollback.
Desktop pane
desktop/plugin.js default-exports a Hermes Desktop plugin and registers a
right-side Time Travel pane. It reads the active session's working directory
from host.state.cwd, calls the plugin's scoped backend through ctx.rest, and
uses ctx.setInterval / ctx.setTimeout so timers are cleaned up when the
plugin unloads. It imports only @hermes/plugin-sdk, react, and React hooks.
Selecting a checkpoint loads its diff. Restore this checkpoint arms on the
first click and restores on the second. The button disarms after four seconds.
Backend API
Hermes mounts dashboard/plugin_api.py at /api/plugins/hermes-chronos/:
| Method | Path | Purpose |
| --- | --- | --- |
| GET | / | Plugin metadata and endpoint list. |
| GET/POST | /health | Shadow repo health and statistics. |
| GET/POST | /timeline | Checkpoints plus effective config. |
| GET/POST | /diff | Diff by checkpoint id or between refs. |
| GET | /checkpoint/{id} | Metadata plus the checkpoint's diff. |
| POST | /rollback | Restore {checkpoint_id, cwd?, clean?, create_safety?}. |
The Desktop half uses the POST forms so the workspace path travels in a JSON
body and stays out of the URL. All endpoints are scoped to this plugin's namespace and
sit behind Hermes' normal dashboard authentication. Long git operations run in
a worker thread so the gateway event loop is not blocked.
api.py builds the router. dashboard/plugin_api.py loads the engine and route
builder by path and exposes router. Hermes imports a dashboard backend by file
path, so the backend cannot rely on package-relative imports.
How the shadow repository works
git init --bare .hermes/chronos/repocreates the object database.- A private git config sets
core.worktreeto the workspace, disables hooks,
signing, fsmonitor, and automatic gc, and pointscore.excludesFileat
.hermes/chronos/exclude. - Every git command runs with
--git-dir <shadow> --work-tree <workspace>,
GIT_CONFIG_GLOBALpointed at the private config,GIT_CONFIG_NOSYSTEM=1,
and a fixed author identity. User aliases and hooks cannot run. git add -Astages the workspace.git diff --cached --quiet HEADdetects
no-op snapshots.git commitwrites aCHRONOS_CP: {json}message..hermes/chronos/.gitignorecontains*, so the snapshot database never
appears in the project's owngit status.- Rollback takes a safety checkpoint, then runs
git read-tree --reset -u <checkpoint>.HEADdoes not move, so the next
checkpoint is a normal forward commit and no checkpoint becomes unreachable. - With
clean: true, untracked, non-ignored files are removed so the tree
matches the snapshot. Withclean: false, those files are restored from the
safety commit and left untracked.
Safety and limitations
- Not a sandbox. Chronos runs with the user's permissions, like every
in-process Hermes plugin. It only snapshots and restores files inside the
session workspace. - Ignored files are not snapshotted or restored.
.gitignorerules and
ignored_patternsapply. This keepsnode_modules, virtualenvs, and caches
out of the shadow repo, but a rollback cannot recreate an ignored file. - Empty directories are not tracked by git and are not recreated.
- A clean rollback deletes untracked, non-ignored files to match the
snapshot exactly. The safety checkpoint captures those files first, so the
restore can be reversed. Setclean: falseor use--no-cleanto keep them. .hermes/chronos/never shows up in your owngit status. Chronos writes
a.gitignorecontaining*inside that directory (the same trick
git init --bareuses), so it does not touch your.gitignoreor your other
.hermes/content.max_checkpointsbounds the listing and triggers compaction. Once history
passesmax_checkpoints + 10, a background thread expires reflogs and repacks
the shadow repo;git gcnever runs inside a tool-call hook. Git stores
content-addressed deltas, so a quiet workspace stays small even with a long
checkpoint list.- Git is required. Snapshot failures are logged and never block a tool.
- Broad roots are refused.
/and the current user's home directory are
rejected to avoid snapshotting an entire machine. - Linux only so far. The engine uses
pathlib,subprocess, and a POSIX
file lock with a thread-lock fallback. macOS and Windows have not been run. - Desktop rendering is tested outside Electron. The pane's registration and
initial render were exercised with React and a stubbed SDK. A visual pass in
the Desktop app is still worthwhile.
Disclosures (catalog rule 13)
- Reads files in the active workspace to build checkpoints.
- Writes only under
<workspace>/.hermes/chronos/and restores files in the
workspace during an explicit rollback. - Spawns local
gitsubprocesses. No network calls, no telemetry, no remote
repositories, no shell. - Compacts the shadow repository (reflog expire +
git gc) in a background
thread once history exceedsmax_checkpoints + 10; never inside a tool call. - Does not read credentials,
.envfiles, or Hermes auth stores. - Does not update itself. Updates go through Hermes and the catalog pin.
Validation
# Same admission gate the catalog CI runs
hermes plugins validate . --install-deps
# Real runtime discovery, manifest parsing, register(ctx), and drift checks
hermes plugins doctor . --ci
# Engine, tool, hook, command, and API tests
python -m unittest discover -s tests -v
Expected: validate reports every check green with no warnings, doctor
reports 4 tools and 3 hooks, and all 29 tests pass.
Acceptance results
These checks ran against the published repository at the accepted commit, not
against a working tree.
| Check | Result |
| --- | --- |
| Catalog validation | 16/16 checks, 0 warnings |
| Doctor | 4 tools, 3 hooks, no load error |
| Test suite | 29/29 passed |
| Real hook/tool pipeline | 33/33 checks |
| Config behavior | 8/8 checks |
| Dashboard mount and API | 14/14 checks |
| Hermes 0.19.0 compatibility | 11/11 checks |
| Desktop registration and render | 16/16 checks |
The runtime checks cover real write_file and patch calls through Hermes,
failure-hint injection, rollback, safety-checkpoint undo, /chronos, user
.git isolation, ignored paths, and JSON error envelopes. Regression tests also
pin the review fixes: no no_safety on the model tool, an approve directive
for chronos_rollback, a .gitignore over the shadow database, a bounded
checkpoint budget, and background-only compaction.
Catalog submission
The plugin is published at
https://github.com/DevTarlow/hermes-chronos.
The catalog entry is maintained with the submission files as
catalog/hermes-chronos.yaml; it is submitted to NousResearch/hermes-agent
under plugin-catalog/, not shipped inside the plugin directory.
- Push the exact commit whose tree is the plugin. The public repository above
is already at that commit. - Set the catalog entry's
shato the full 40-character commit. Any later
change needs a new commit, a new pin, and a re-run of the validation gate. - Copy the catalog file to
plugin-catalog/hermes-chronos.yamlin a fork of
NousResearch/hermes-agentand open a PR. - In the PR description, note the shadow-git design, the local git subprocesses,
the workspace writes, and that the Desktop pane is opt-in.
The catalog entry uses the current schema (repo, sha, maintainer, tier,
category, capabilities) and declares the same requires_hermes floor as
plugin.yaml.
Project layout
hermes-chronos/
├── plugin.yaml # manifest (tools, hooks, deps, config schema)
├── __init__.py # real Hermes entrypoint: register(ctx)
├── init.py # compatibility alias for tooling that expects init.py
├── engine.py # isolated shadow-git engine
├── tools.py # chronos_* tool handlers + JSON contracts
├── hooks.py # pre/post/transform lifecycle hooks
├── commands.py # /chronos
├── api.py # FastAPI router builder
├── dashboard/
│ ├── manifest.json # backend mount declaration
│ └── plugin_api.py # Hermes mounts this file and its router
├── desktop/
│ └── plugin.js # native Desktop timeline pane
├── tests/
│ ├── test_chronos.py # engine, tools, hooks, command
│ └── test_api.py # dashboard router
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── README.md
└── LICENSE
Implementation notes
The plugin uses the current Hermes surfaces. A few choices differ from the
original sketch:
- Directory plugins load from
__init__.pyand call
ctx.register_tool(name=..., toolset=..., schema=..., handler=...). Handlers
receive(args, **kwargs).init.pyis only a compatibility alias. post_tool_callis an observer. The failure hint is added through
transform_tool_result, andprovides_hooksdeclares all three hooks.- Dashboard backends mount from
dashboard/plugin_api.py.api.pybuilds the
router. There is noctx.register_rest_routeAPI. - The catalog schema uses
shafor the exact commit. It does not use the older
commitkey. - Mutations use subprocess git because GitPython derives the work tree from the
repository location, which is wrong for a separate shadow git directory.
GitPython is still declared and used for optional read-only introspection. - Hermes 0.19 passes structured tool results to
transform_tool_result, and it
has noctx.get_config. The plugin handles both: structured results get a
JSONchronos_notice, and settings fall back to the active profile's
config.yaml.
License
MIT. See LICENSE.
README from the reviewed commit 1abeae1; it updates when the author re-pins.

DevTarlow