Install
Python >=3.10, standard library only. Tested on Python 3.15 on macOS. Not on PyPI yet, so install from the repository:
$ pipx install git+https://github.com/Johnbjoseph-cybersec/rewindagents
# or: pip install git+https://...
$ rewindagents --version
The last command prints the version it installed:
$ rewindagents --version
rewindagents 0.0.7
Or from a clone, pip install .. Without installing, PYTHONPATH=src python3 -m rewindagents runs the same CLI from a clone; then the undo hint says python3 -m rewindagents instead of rewindagents.
Add the hook
$ pipx install git+https://github.com/Johnbjoseph-cybersec/rewindagents && cd your-project && rewindagents init
init looks for the agents it supports (Claude Code, Codex, Cursor, Gemini CLI, Copilot CLI, Droid, Qwen Code): their config directory in the project or your home directory, or their command on PATH. It prints the plan, asks [y/N], and adds the hook to each one it found, one line per agent: installed, already there, replaced or not found. It never adds a second copy and keeps every hook already in the file. --yes skips the question. --global writes each agent's user config instead, so every project is recorded. Without --yes and without a terminal to ask on, it only prints the plan.
Or add it to one agent at a time:
$ cd /path/to/your/project
# adds the hook to .claude/settings.json in this project
$ rewindagents install
# or for every project
$ rewindagents install --settings ~/.claude/settings.json
install adds a PreToolUse and a PostToolUse hook to the settings file, keeps everything already in it, and does nothing if the hook is already there. The command it writes names the Python that rewindagents was installed into by absolute path, so it runs whether or not a virtualenv is active. To remove the hook, run rewindagents uninstall (see Remove it).
# adds the hook to .codex/hooks.json in this project
$ rewindagents install --agent codex
# adds the hook to .cursor/hooks.json in this project
$ rewindagents install --agent cursor
# or for every project
$ rewindagents install --agent cursor --settings ~/.cursor/hooks.json
For Codex the file is .codex/hooks.json, in the same shape as Claude Code's settings; Codex asks you to trust the hook once (/hooks) and remembers the trust by the hook's hash. For Cursor the file is .cursor/hooks.json with a preToolUse and a postToolUse entry; because Cursor's preToolUse is a permission hook that treats an empty answer as a failure, the hook prints {"permission": "allow"} before a call and {} after one, which changes nothing (a deny from another hook still wins). The Codex and Cursor hooks are tested with the payloads in the vendors' documentation and through the installed command, not yet in a live session.
The other agents with a hook, each built from the vendor's hook documentation and tested with the documented payloads, not yet in a live session:
# adds the hook to .gemini/settings.json in this project
$ rewindagents install --agent gemini
# adds the hook to .github/hooks/rewindagents.json in this repository
$ rewindagents install --agent copilot
# adds the hook to .factory/settings.json (or .factory/hooks.json if it exists)
$ rewindagents install --agent droid
# adds the hook to .qwen/settings.json in this project
$ rewindagents install --agent qwen
# same flags as install, for every agent
$ rewindagents uninstall --agent qwen
A first session
The session behind the clip on the front page, as it ran. Every line below is what the command printed; tests/test_demo.py runs the same session and fails if a line changes.
Ask Claude Code for anything in a project with the hook installed; here, 12 steps that add a timeout to a small service, then two steps that delete the tests and overwrite the config.
replaylists every step with its number.# a coding agent has been working in this repo; rewindagents recorded every step $ rewindagents replay 1 Read README.md 2 Read app/config.py 3 Edit app/config.py [1 checkpointed] 4 Read app/server.py 5 Edit app/server.py [1 checkpointed] 6 Write tests/test_timeout.py [1 checkpointed] 7 Bash python3 -m unittest discover -s tests -t . -q 8 Edit app/server.py [1 checkpointed] 9 Bash python3 -m unittest discover -s tests -t . -q 10 Edit README.md [1 checkpointed] 11 Bash git status --short 12 Read tests/test_server.py 13 Bash rm -rf tests [3 checkpointed] 14 Write app/config.py [1 checkpointed]git shows the damage: three files changed, two deleted.
# its last two steps deleted the tests and overwrote the config $ git status --short M README.md M app/config.py M app/server.py D tests/test_config.py D tests/test_server.pyRewind to the last good step. Every restored file is named, and the rewind is itself a step, so the last line says how to undo it.
# put the files back the way they were right after step 12 $ rewindagents rewind 12 session demo: rewind to step 12 of 14 (did change 4 files) restore tests/test_config.py restore tests/test_server.py restore tests/test_timeout.py restore app/config.py this rewind is step 15; undo it with: rewindagents rewind 14 --session demogit shows the first 12 steps kept and the tests back.
# the tests are back; the first 12 steps stay $ git status --short M README.md M app/config.py M app/server.py ?? tests/test_timeout.py
What gets recorded
In each project the agent works in, with the hook installed:
- every tool call gets a
prerecord (tool and input) and apostrecord (tool and response) in.rewindagents/log.jsonl, in one hash chain. A response longer than 4,000 characters is cut, with the full length noted. - in Claude Code, before
Write,Edit,MultiEdit,NotebookEdit, the current bytes of the target file are copied to.rewindagents/objects/<sha256>and the hash goes into theprerecord. A file that does not exist yet is recorded as absent. Files over 20 MB are noted and not copied. - around every
Bashcall, the project's files are saved before the command and scanned again after it; thepostrecord checkpoints only the files that were changed, deleted or created. Copies are stored by SHA-256, and a stat cache in.rewindagents/index.jsonmeans only files whose size or modification time changed are read again. - in Codex and Cursor, that same scan runs around every call except the tools their documentation names that cannot change files (Cursor:
Grep,Read; Codex:update_plan), because the input of their edit tools is not documented and the hook never reads a file path out of those payloads. .rewindagents/gets its own.gitignore, so the log and the copies stay out of your commits.
The hook never stops a call and never changes one. It prints nothing and always exits 0; any problem goes to stderr only.
Replay and rewind
Run these from the project directory. They work on the latest session unless you pass --session ID.
# one line per step: number, tool, file or command
$ rewindagents replay
# step 12 in full: input, checkpoints, result
$ rewindagents replay --step 12
# what would change
$ rewindagents rewind 12 --dry-run
# the same files, as a unified diff: now -> step 12
$ rewindagents diff 12
# only that file (a directory means every file under it)
$ rewindagents diff 12 src/db.py
# files back to how they were right after step 12
$ rewindagents rewind 12
# put back only these
$ rewindagents rewind 12 --only src/db.py --only tests/
# keep the current files, then rewind to step 12
$ rewindagents branch 12 --name try2
replay prints one line per step: number, tool, file or command, and how many files the step checkpointed. replay --step N shows one step in full. rewind N puts back every file that a step after N changed and deletes the files those steps created; rewind 0 undoes the whole session. Before writing anything it checks the hash chain and the SHA-256 of every stored copy: if the chain is broken it changes nothing, and a copy that no longer matches is reported and left as it is.
A rewind is undoable. It first saves the current bytes of every file it will change and records itself as a new step, then prints undo it with: rewindagents rewind <step> --session <id>. In the session above, the rewind to step 12 of 14 became step 15, and undoing it is rewind 14.
Branch
branch N --name X does what rewind N does and, in a git repository, first commits the whole current working tree to the branch rewindagents/X. It does this through a temporary index, so your HEAD, index and current branch stay as they were. Outside git, the current files are kept in the log only.
Fork and compare
# pick the step to start from, say 12
$ rewindagents replay
# ../<project>-try2 with step 12's files
$ rewindagents fork 12 --name try2
# run any agent with a hook here, or edit by hand
$ cd ../your-project-try2
$ cd - && rewindagents compare --check "python3 -m pytest -q"
# how two runs differ, as a git diff
$ rewindagents compare --diff your-project try2
Rewind to any step, fork it, let another agent take it from there, compare. A recorded step is a starting point you can hand to another agent, another model, another prompt or a person, and the runs that follow can be compared on what they actually did to the files and on your own check. rewindagents fork 12 --name try2 makes a new directory next to the project (<project>-try2; --dir picks another place outside it) holding the project's files as rewind 12 would leave them: the project as it is now, with every change that session made after step 12 taken back, untracked files included. In a git repository it is a git worktree on the branch rewindagents/run/try2, so your HEAD, index and files stay as they were; elsewhere it is a plain directory. Run any agent there, or edit by hand: with a hook in place (the project's agent config comes along when git does not ignore it, or a --global hook), its steps go into the run's own log, and replay, rewind and diff work in the run as in any project.
Back in the project, rewindagents compare lines up the project and every run forked from the same step: per run, the agents and their number of tool calls since the start, the files changed, created or deleted and the lines added and removed; per file, which runs ended with the same bytes (the same letter). rewindagents compare --check "python3 -m pytest -q" runs your command in each run and reports pass or the exit code, with the last lines of a failing one. rewindagents compare --diff proj try2 prints how two runs differ as a git diff (git apply in the first turns its files into the second's).
From ux-review/try-runs.sh, against an installed build: the agent's run shop fails the check, a second attempt clamp made in the fork passes, and this is how the two differ.
$ rewindagents fork 2 --name clamp
2 files as they were right after step 2 of session demo-0001; git worktree on branch rewindagents/run/clamp
$ rewindagents compare --check "python3 test_cart.py"
runs from session demo-000 step 2 of shop (2 files at the start)
run agents steps files lines check
shop claude 1 1 +1 -1 FAIL (exit 1)
clamp - 0 1 +1 -1 pass
file shop clamp
cart.py changed A changed B
$ rewindagents compare --diff shop clamp
diff --git a/cart.py b/cart.py
--- a/cart.py
+++ b/cart.py
@@ -1,2 +1,2 @@
def total(prices, discount=0):
- return sum(prices) - discount # TODO clamp
+ return max(0, sum(prices) - discount)
forkrefuses a step it cannot rebuild exactly (a stored copy missing, a command whose scan did not finish, a log that does not verify) and leaves nothing behind when it fails half-way.comparewrites nothing; the command--checkruns may write in the run it runs in.- Runs from different steps are not mixed.
Timeline in a browser
# opens the timeline in your browser; Ctrl+C stops it
$ rewindagents ui
$ rewindagents ui --session abc --no-open --port 8765
A local page over the log. Click a step, or move with j / k, to see each file's diff and the step's input and result (d hides or shows the diffs). It listens on 127.0.0.1 only, on a free port unless you pass --port. The link it prints carries a random token made for this run; every request without it gets a 403, and so does any request whose Host header is not 127.0.0.1 or localhost on that port. Rewinds are POST only and must come from the page itself. No fonts, scripts or images are fetched from anywhere, so it works offline.
Export
# writes .rewindagents/replay-<session>.html
$ rewindagents export
# a given session, to a given file
$ rewindagents export --session abc --out replay.html
# steps, tools and paths only: no inputs, results or diffs
$ rewindagents export --no-contents
# keep secrets as recorded (they are replaced by default)
$ rewindagents export --no-redact
One HTML file with the whole session: every step with its tool, input and result, and for every file a step checkpointed, the change that step made as a unified diff (the file before the step against the file after it: its next checkpoint in the session, or the file as it is now). Inline CSS only, no scripts, nothing fetched. Paths are shown relative to the project; where the project lives on disk is not in the page.
- A file over 256 KB on either side is described, not diffed. A diff longer than 400 lines is cut, with the count of lines left out. Binary files show their size before and after.
--no-contentsdrops inputs, results and diffs and keeps the one-line summary of each step.- If the hash chain does not verify, the page is still written, with a notice naming the first bad record, and the command exits 1.
Find a session
# every session in the log: id, first and last time, steps, files touched
$ rewindagents sessions
# only sessions with a step that mentions it, and those steps
$ rewindagents sessions --grep config.py
One line per session, in the order the sessions first wrote to the log: the session id, the local time of its first and last record, the number of steps (with how many of them are rewinds or branches) and the files it touched; a second line names the first 5 files and counts the rest. The session that wrote the last record is marked (latest).
--grep TEXT keeps the sessions with a step whose replay line or checkpointed path contains TEXT, in any case, and lists those steps with their numbers. Exit code 1 if no session matches.
Move a session
# the latest session, and the file copies it names, in one file
$ rewindagents bundle --out s1.bundle
# a given session
$ rewindagents bundle --session abc --out abc.bundle
# carry secrets as recorded (they are replaced by default)
$ rewindagents bundle --no-redact --out s1.bundle
# in another clone: check the file, then add the session to its log
$ rewindagents import s1.bundle
Then replay, rewind and branch work on that session in the other clone, with --session <id>. By default, secrets are replaced in the records and the stored copies, as export replaces them. import writes nothing until everything checks out: the file's own SHA-256, every record's hash, that every record belongs to the session and is in log order, every stored copy against its SHA-256, that no copy a record names is missing, that this project's log verifies, and that it does not already hold the session. Nothing goes over a network; carry the file however you like.
Attach to a PR
# the session behind this change, secrets replaced
$ rewindagents bundle --out .rewindagents-pr.bundle
$ git add .rewindagents-pr.bundle
The .rewindagents/ store is git-ignored, so the checkout holds no log of its own; commit a bundle.
# .github/workflows/replay.yml
on: pull_request
permissions:
contents: read
pull-requests: write # for the one comment; nothing else writes
jobs:
replay:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Johnbjoseph-cybersec/rewindagents/attach@<commit sha>
with:
bundle: .rewindagents-pr.bundle
The action in attach/ imports the bundle, checks the log's hash chain (against head: too, if you pin one), writes the session's export page and uploads it as a workflow artifact, then posts one comment on the pull request with the chain result, the number of secrets replaced and the download link. A later push updates that comment instead of adding another. Secrets are always replaced in the page; the action has no input to turn that off. It installs rewindagents from the same commit as the action, so pinning the action's SHA pins the tool. The token is handed to the comment step only. If the log does not verify, the page is uploaded with its notice, and then the job fails. It has not run on GitHub yet.
Verify
$ rewindagents verify --head <pinned-hash>
verify checks every record against the one before it and names the first bad record. A hash chain cannot tell on its own that records were cut off the end or that the whole file was rewritten and re-hashed; rewindagents head prints the current head hash, and verify --head checks against a copy you pinned somewhere the agent cannot write.
The recorder also works on its own, without the hook:
$ echo '{"tool": "Bash", "args": {"command": "ls"}}' | rewindagents record --session s1
# OK: 1 records, chain intact
$ rewindagents verify
# list recorded calls
$ rewindagents show
# current head hash
$ rewindagents head
Is it recording?
# per agent config file: ok, STALE, partial or no hook; then the log: records, last write, chain
$ rewindagents status
The last line of status is this project's log: how many records it holds, when the last one was written, and whether the chain verifies. status writes nothing. A hook is stale when its interpreter path or its PYTHONPATH directory no longer exists; status then exits 1.
Free space
# what would be deleted, and how many bytes
$ rewindagents gc --dry-run
# delete it
$ rewindagents gc
# first drop scans whose command never finished
$ rewindagents gc --drop-pending-older-than 7
gc deletes every stored copy that no log record (in any session) and no pending scan (a command whose after-scan has not run) names, and prints how many it deleted and the bytes those files took on disk. Every step of every session in the log can still be rewound afterwards; the copies a rewind needs are the ones the log names. It refuses to run when the log does not verify, and it never writes to the log. A scan whose after-scan never ran (the agent was killed, a crash) stays pending and keeps its copies; --drop-pending-older-than DAYS removes the scans older than DAYS first, so their copies are collected too. Scans younger than 1 day are never dropped.
Overhead
Time the hook adds to one tool call (both hook processes, before and after the call), median of 15 calls, measured by bench/hook_overhead.py on macOS 27.0.1, Apple M6, 12 CPUs, Python 3.15.0, on 2026-11-12 (commit b11b369, load average 22.2 at the start). Each hook event is a new Python process; starting one and importing the package alone takes 108 ms (median), and every call starts two.
- 100 files:
Read228 ms,Edit210 ms,Bash281 ms (90th percentile 273 ms, 281 ms, 438 ms); the firstBashcall, which copies every file into the empty store, 410 ms. - 1,000 files:
Read224 ms,Edit220 ms,Bash351 ms (90th percentile 241 ms, 277 ms, 495 ms); the firstBashcall, which copies every file into the empty store, 605 ms. - 10,000 files:
Read226 ms,Edit223 ms,Bash1,003 ms (90th percentile 292 ms, 351 ms, 1,180 ms); the firstBashcall, which copies every file into the empty store, 4,554 ms.
Other machines, disks and projects will differ; python3 bench/hook_overhead.py measures yours. The raw numbers are in bench/results.json.
Limits
- Only the project directory is scanned, saved and restored. A command that changes files elsewhere is recorded, not saved, and files outside the project are reported and left alone.
- In a git repository the scan follows
git ls-files --cached --others --exclude-standard, so git-ignored files are not saved. Outside git, every file under the project except under.git,.rewindagents,.venv,__pycache__,node_modules,venv. - Symlinks are not saved. Files over 20 MB are not copied. A project with more than 20,000 files is not scanned around
Bashcommands; the call is still recorded andrewindreports the step as not scanned. - Directories created by later steps stay, empty, after a rewind. If anything could not be restored,
rewindlists it and exits 1. - Needs a Unix for now: the log lock has a Windows path (
msvcrt), but the tests have not run on Windows yet. The Claude Code hook has run in real sessions; the Codex and Cursor hooks are tested with the documented payloads, not yet in a live session.
Remove it
# removes the hook from .claude/settings.json in this project
$ rewindagents uninstall
# same flags as install
$ rewindagents uninstall --agent cursor --settings ~/.cursor/hooks.json
uninstall removes only the hook entries install wrote, so every other hook and setting stays, and it never touches .rewindagents/: recorded sessions can still be replayed and rewound. To remove everything, also delete .rewindagents/ in the project and uninstall the package (pipx uninstall rewindagents or pip uninstall rewindagents). Nothing else was created.
Tests
$ python3 -m unittest discover -s tests -v
That runs against the source tree and skips the tests of the installed command. .fire-test-cmd in the repository holds the full command: it builds a venv, installs the package into it and runs every test, including the ones that drive the installed rewindagents binary, replay the session above, and rebuild this site byte for byte.