Golang native — the undo button for AI agent workspaces.
agent-rollback captures content-addressed snapshots of your workspace before, during, and after any AI agent run, so you can restore any file — or the entire project — in one command. It works with or without a Git repo, is 100% local, and purpose-built for AI coding agent safety.
A Go CLI (agent-rollback) and a Go library (github.com/superops-team/agent-rollback/rollback). MIT-licensed, 100% local — no telemetry, no cloud sync.
- Content-addressed storage — identical files share one blob via SHA-256, ~zero disk overhead
- Git-aware scanning — uses
git ls-filesin Git repos, falls back to filesystem walk - State deduplication — identical workspace states are not stored twice
- Atomic writes — temp file + rename for crash safety
- Unified diff — LCS-based line-level diff with context hunks; oversized, high-line-count, and binary files are summarized instead of expanded
- Selective revert — revert a single operation's files without losing later unrelated edits
- Restore preflight — validates checkpoint objects, paths, hashes, and symlink targets before destructive workspace changes
- Operation log — append-only
ops.jsonlfor full audit trail - Pin & Prune — pin known-good checkpoints, auto-prune old ones with GC
- Path safety — checkpoint ID validation prevents path traversal
go install github.com/superops-team/agent-rollback/cmd/agent-rollback@latestOr build from source:
git clone https://github.com/superops-team/agent-rollback.git
cd agent-rollback
go build -o agent-rollback ./cmd/agent-rollback/go get github.com/superops-team/agent-rollback/rollback# 1. Initialize the store in your project
agent-rollback init
# 2. Create a checkpoint before risky work
agent-rollback checkpoint "before refactor"
# 3. Let your AI agent do its thing...
# 4. Made a mess? Roll back
agent-rollback list
agent-rollback revert cp-123456-before-refactor-abcd --yes
# 5. See what changed
agent-rollback diff cp-before cp-after --patch| Command | Description |
|---|---|
init |
Initialize the rollback store |
checkpoint [label] |
Create a checkpoint |
list, ls |
List checkpoints (newest first) |
show <id> |
Show checkpoint details |
diff <from> [to] |
Diff between checkpoints |
revert <id> |
Restore workspace to checkpoint |
undo [count] |
Undo last N checkpoints |
pin <id> [name] |
Pin a checkpoint |
unpin <id> |
Unpin a checkpoint |
prune [opts] |
Delete old checkpoints |
log |
Show operation history |
search <query> |
Search checkpoints |
op revert <op-id> |
Selectively revert an operation |
gc |
Run garbage collection |
info |
Show store statistics |
--cwd <dir>— working directory--store <dir>— store directory (default:.agent-rollback/)--json— output as JSON--yes— skip confirmations
diff --patch keeps patch generation bounded. Per file, unified patch output is generated only when both sides are text, at most 1 MiB, at most 20,000 lines, and within the internal LCS complexity guard. Files above those limits, or files detected as binary, are reported with a Patch skipped ... summary while other eligible files continue to emit normal unified diffs.
go get github.com/superops-team/agent-rollback/rollbackpackage main
import (
"fmt"
"log"
"github.com/superops-team/agent-rollback/rollback"
)
func main() {
// Create a store in the current workspace
store := rollback.NewDefaultStore("/path/to/workspace")
if err := store.Initialize(); err != nil {
log.Fatal(err)
}
// Create a checkpoint before making changes
manifest, err := store.CreateCheckpoint(rollback.CheckpointOptions{
Label: "before refactor",
Source: "manual",
Dedupe: true, // skip if state is identical to latest
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Checkpoint created:", manifest.ID)
// ... make changes to your workspace ...
// Restore to the checkpoint
restored, err := store.RestoreCheckpoint(manifest.ID)
if err != nil {
log.Fatal(err)
}
fmt.Println("Restored to:", restored.ID)
}// With a descriptive label
manifest, err := store.CreateCheckpoint(rollback.CheckpointOptions{
Label: "green tests",
Source: "manual",
Dedupe: true,
})
// Without a label (auto-generated)
manifest, err := store.CreateCheckpoint(rollback.CheckpointOptions{
Source: "manual",
Dedupe: true,
})
// From an AI agent hook
manifest, err := store.CreateCheckpoint(rollback.CheckpointOptions{
Source: "hook",
Phase: "before",
ToolName: "file_edit",
Dedupe: true,
})// List all checkpoints (newest first)
checkpoints, err := store.ListCheckpoints()
for _, cp := range checkpoints {
fmt.Printf("%s %s %s\n", cp.ID, cp.CreatedAt, cp.Metadata.Label)
}
// Show a specific checkpoint
manifest, err := store.ShowCheckpoint("cp-123456-before-refactor-abcd")
fmt.Printf("Files: %d\n", len(manifest.Files))
for path, entry := range manifest.Files {
fmt.Printf(" %s %s %d bytes\n", path, entry.SHA256[:8], entry.Size)
}// Diff between two checkpoints
diffs, err := store.DiffCheckpoints("cp-before", "cp-after")
for _, d := range diffs {
fmt.Printf("%s: %s\n", d.Path, d.Change) // added, deleted, modified
}
// Diff a checkpoint against current workspace state
diffs, err := store.DiffCheckpoints("cp-before", "")
// Get a formatted summary
summary := rollback.FormatDiffSummary(diffs)
fmt.Println(summary)Store.FullPatch(from, to) applies the same patch guardrails used by the CLI: text files within 1 MiB, 20,000 lines, and the internal LCS complexity guard get context hunks; oversized, high-line-count, high-complexity, or binary files get skipped-patch summaries.
// Restore workspace to a checkpoint (restores all files)
manifest, err := store.RestoreCheckpoint("cp-123456-before-refactor-abcd")
// Undo the last N checkpoints
manifest, err := store.Undo(1) // undo last 1 checkpoint
manifest, err := store.Undo(3) // undo last 3 checkpointsRestore validates the checkpoint before deleting, overwriting, or creating workspace files. Preflight failures include categories such as missing-object, hash-mismatch, unsafe-path, and invalid-symlink; failed preflight does not append a checkpoint.restored operation.
// Pin a checkpoint so it survives pruning
err := store.PinCheckpoint("cp-good", "known good state")
// Unpin
err := store.UnpinCheckpoint("cp-good")
// Prune old checkpoints (keep last 20, protect pinned)
toDelete, err := store.PruneCheckpoints(rollback.PruneOptions{
KeepLast: 20,
KeepPinned: true,
})
for _, id := range toDelete {
store.DeleteCheckpoint(id)
}
// Run garbage collection to remove unreferenced objects
deleted, bytes, err := store.CollectGarbage()
fmt.Printf("GC: removed %d objects (%d bytes)\n", deleted, bytes)// Search by label, message, or ID
results, err := store.SearchCheckpoints("auth refactor")
for _, cp := range results {
fmt.Println(cp.ID, cp.Metadata.Label)
}// List all operations
ops, err := store.ListOps()
for _, op := range ops {
fmt.Printf("%s %s %s\n", op.ID, op.Type, op.Details.CheckpointID)
}
// Get a specific operation
op, err := store.GetOp("op-20260613000000-abcdef")// Revert only the files changed by a specific operation
result, err := store.SelectiveRevert(rollback.SelectiveRevertOptions{
OpID: "op-20260613000000-abcdef",
Force: false, // fail if conflicts detected
CreateSafety: true, // create a safety checkpoint first
})
if result.SafetyCP != nil {
fmt.Println("Safety checkpoint:", result.SafetyCP.ID)
}
fmt.Printf("Files touched: %d, Conflicts: %d\n",
len(result.FilesToTouch), len(result.Conflicts))Selective revert is supported for checkpoint.created and checkpoint.restored operations. It computes the touched files from the operation's real chronological predecessor, then refuses to apply by default if any touched file has changed or gone missing since that operation. Use Force only when overwriting those later edits is intentional.
- High-frequency automatic checkpoints are best suited to small and medium workspaces. On an Apple M3 Pro, the current benchmark suite measured small checkpoint creation at about 8.6 ms/op, medium at about 24.9 ms/op, and a 1000-file / ~100 MB workspace at about 599 ms/op.
- Treat every-operation automatic checkpointing on 1000-file / ~100 MB workspaces as unsuitable by default unless you record target-platform benchmark evidence for that workflow.
- Restore preflight reads checkpoint objects before writing the workspace. That adds I/O but prevents known-corrupt checkpoints from deleting or overwriting user files.
diff --patchis safe for normal text files, but it intentionally summarizes files above 1 MiB, above 20,000 lines, or above the internal LCS complexity guard to avoid unbounded memory growth.
// Read a file from the store (by SHA-256 hash)
data, err := store.ReadObject(manifest.Files["main.go"].SHA256)
// Read a symlink target
target, err := store.ReadSymlinkObject(manifest.Files["link"].SHA256)// Get checkpoint count
count, _ := store.CheckpointCount()
// Get total store size on disk
size, _ := store.StoreSize()
// Format for display
fmt.Printf("Checkpoints: %d, Size: %s\n", count, formatBytes(size)).agent-rollback/
├── checkpoints/<id>/manifest.json # checkpoint manifests
├── objects/<sha-prefix>/<sha> # content-addressed file blobs
└── ops.jsonl # append-only operation log
| Type | Description |
|---|---|
Store |
Storage engine — manages checkpoints, objects, and operations |
Manifest |
Checkpoint manifest — ID, timestamp, files, metadata |
FileEntry |
File or symlink entry — type, SHA-256, size, mode, target |
Metadata |
Checkpoint metadata — label, source, pinned, phase, etc. |
DiffEntry |
File difference — path, change type, before/after entries |
Operation |
Operation log entry — ID, timestamp, type, details |
CheckpointOptions |
Options for CreateCheckpoint |
PruneOptions |
Options for PruneCheckpoints |
SelectiveRevertOptions |
Options for SelectiveRevert |
SelectiveRevertResult |
Result of a selective revert |
| Method | Description |
|---|---|
NewDefaultStore(workspaceRoot) |
Create store with default .agent-rollback/ |
NewStore(workspaceRoot, storeRoot) |
Create store with custom store path |
Initialize() |
Create store directory structure |
CreateCheckpoint(opts) |
Snapshot current workspace state |
RestoreCheckpoint(id) |
Restore workspace to checkpoint state |
ListCheckpoints() |
List all checkpoints (newest first) |
ShowCheckpoint(id) |
Get a specific checkpoint manifest |
DiffCheckpoints(from, to) |
Compute differences between checkpoints |
Undo(count) |
Revert last N checkpoints |
PinCheckpoint(id, name) |
Pin a checkpoint (protect from prune) |
UnpinCheckpoint(id) |
Unpin a checkpoint |
DeleteCheckpoint(id) |
Delete a checkpoint manifest |
PruneCheckpoints(opts) |
Plan which checkpoints to delete |
CollectGarbage() |
Remove unreferenced objects, return (deleted, bytes, error) |
SearchCheckpoints(query) |
Search checkpoints by label/message/ID |
SelectiveRevert(opts) |
Revert files changed by a specific operation |
AppendOp(op) |
Append an operation to the log |
ListOps() |
List all operations (newest first) |
GetOp(id) |
Get a specific operation |
CheckpointCount() |
Get number of checkpoints |
StoreSize() |
Get total store size in bytes |
| Function | Description |
|---|---|
ComputeStateHash(files) |
Compute dedup hash from file map |
CreateCheckpointID(label) |
Generate human-readable checkpoint ID |
RequireSafeCheckpointID(id) |
Validate checkpoint ID (prevents path traversal) |
ResolveWorkspacePath(root, rel) |
Resolve relative path within workspace |
ListWorkspaceFiles(root) |
Enumerate workspace files (git-aware) |
CurrentWorkspaceFiles(root) |
Get current workspace state as file map |
DiffManifests(before, after) |
Compare two manifests, return diff entries |
FormatDiffSummary(diffs) |
Format diff entries as human-readable summary |
FormatCheckpointsList(cps, now) |
Format checkpoint list for display |
FormatOpsLog(ops, now) |
Format operation log for display |
FormatSelectiveRevertResult(r, now) |
Format selective revert result |
UnifiedDiff(oldPath, newPath, oldData, newData) |
Generate unified diff string |
ParseDuration(s) |
Parse duration string like "7d", "12h", "30m" |
go test ./rollback/ -v
go test -race ./...
go test ./benchmark -bench=. -benchmem -run '^$'MIT