Oops! Made a mistake? No worries - you can always go back!
A single binary with zero runtime dependencies. No Git installation required.
go install github.com/iyulab/oops@latestOr download from GitHub Releases.
oops save essay.txt "first draft" # 📸 Save a version (starts versioning the file)
# ... write something ...
oops save essay.txt "added conclusion" # 📸 Save another
oops history essay.txt # 📜 View all versions
oops changes essay.txt # 🔍 What changed since the last save?
oops back essay.txt 1 # ⏪ Go back to version #1
oops oops! essay.txt # ↩️ Made a mistake? Back to the last saveGoing back never loses work: if the file has changes that were not saved, oops keeps them as an automatic version first, so you can return to them too.
| Command | Also | Description |
|---|---|---|
oops save <file> [message] |
start, track, commit, snap |
📸 Save a version (the first save starts versioning) |
oops history <file> |
log, list |
📜 List versions |
oops changes <file> [from] [to] |
diff |
🔍 Compare the file with the latest version, with #from, or #from with #to |
oops back <file> <version> |
checkout, restore |
⏪ Go back to a version |
oops oops! <file> |
undo |
↩️ Go back to the latest version |
oops cat <file> <version> |
📄 Print a version (--out <path> writes it to a file) |
|
oops now <file> |
status |
ℹ️ Does the file match a saved version? |
oops files |
ls |
📁 List versioned files |
oops mv <from> <to> |
🚚 Move a file and keep its versions | |
oops done <file> |
untrack |
🗑️ Stop versioning a file and delete its versions |
oops prune |
🧹 Remove old automatic versions | |
oops gc |
🧹 Remove the versions of files that no longer exist | |
oops config |
⚙️ Default storage mode | |
oops update |
🔄 Update oops |
A version is either saved (manual, the default) or automatic (auto):
oops save notes.md "before the big edit" # saved - kept until you delete it
oops save notes.md --auto # automatic - may be removed by pruneVersions made by back to keep unsaved changes are saved versions. prune only ever removes automatic
versions, and never a file's newest version by age.
oops prune --max-age 30d # automatic versions older than 30 days
oops prune --max-size 500MB # keep the store under 500 MB
oops prune --max-age 30d --dry-run # see what would go
oops gc # versions of files that were deletedWith --max-size, oops removes the oldest automatic version of the file that has the most versions
first, and keeps each file's newest version until nothing else is left to remove. prune without
options removes nothing.
| Mode | Location |
|---|---|
| Local (default) | .oops/ beside the file - added to an existing .gitignore |
Global (-g) |
~/.oops/ |
| Any directory | --store <dir> or the OOPS_STORE environment variable |
oops config --default-global makes global the default; -l overrides it.
A store holds any number of files:
<store>/
└── files/
└── <key>/ ← one directory per file (key = hash of its path; relative to
the folder for a local store, so renaming the folder keeps its versions)
├── index.json ← versions: number, time, kind, label, actor, metadata
└── blobs/ ← content, one file per distinct version (gzip when useful)
Every command accepts --json and then prints exactly one JSON object on stdout, success or error.
oops --json save report.md "draft" --actor sync --meta batch=42{ "saved": true, "path": "/work/report.md",
"version": { "n": 3, "hash": "…", "size": 1204, "time": "2026-01-01T09:00:00Z",
"kind": "manual", "actor": "sync", "label": "draft", "meta": { "batch": "42" } } }Saving content equal to the latest version succeeds without a new version:
{"saved": false, "reason": "unchanged", …}.
| Command | JSON |
|---|---|
save |
{saved, reason?, path, version} |
history |
{path, versions: [version…]} — filter with --kind manual|auto and --where key=value |
now |
{path, exists, latest, current, changed} — current is 0 when no version matches |
back, oops! |
{path, restored, savedBefore: version|null} |
changes |
{path, from, to|null, binary, diff} |
files |
{store, files: [{path, versions, latest}], unreadable: [{dir, error}]} |
prune |
{dryRun, removed: [{path, n, reason}], freedBytes, totalBytes, overCapBytes, unreadable: [{dir, error}]} — removed lists only what was applied |
gc |
{orphans, removed, dryRun} — a history whose index cannot be read is never an orphan |
--version |
{version} |
| error | {"error": {"code": "<code>", "message": "<text>"}} |
Exit codes
| Code | Meaning | error.code |
|---|---|---|
| 0 | success (including "unchanged") | |
| 1 | other failure | error |
| 2 | wrong arguments or flags | usage |
| 3 | the file is not versioned | not_tracked |
| 4 | no such version | version_not_found |
| 5 | another oops process kept the lock | lock_timeout |
| 6 | the file does not exist | file_not_found |
| 7 | the target already has versions (mv) |
already_tracked |
--actor <text>and--meta key=value(repeatable) are stored on the version as given.- With
--json, oops never asks a question:doneandgcneed--yes. - Several oops processes may work on one store at once; each waits up to
--lock-timeout(10s). - An application that ships oops can set
OOPS_NO_UPDATE=1sooops updaterefuses to replace it.
0.4 uses a new store format and does not read 0.3 stores (.oops/<name>.git). Restore anything you
need with oops 0.3 first. Commands now take the file: oops save notes.md "message" instead of
oops save "message", and one folder can hold any number of versioned files.
- 📝 Writers - essays, articles, manuscripts
- 📊 Researchers - notes, data files
- ⚙️ Config files - when you need quick rollback
- 🤖 Tools that change files for you - save an automatic version first, go back if the change was wrong
For software projects: use Git.
Because everyone makes mistakes when editing files. With Oops, you can simply say "oops!" and go back to a safe state. No complex commands, no fear of losing work.
MIT