cp file.bak upgraded — timestamps, as many versions as you like,
list, diff and restore, and history kept out of the directory
you are working in. For /etc/hosts, ~/.zshrc,
.env, and every other loose file too small for a repo.
macOS & Linux · one dependency · MIT licensed · v0.1.1
a real session — every line above is output bak actually prints
The problem
cp nginx.conf nginx.conf.bak. Then .bak2. Then
.bak.old. Three days later you have four files, no idea which
one worked, no dates, no notes, and no diff — and they are
all cluttering the directory you are trying to work in.
Every snapshot is numbered, timestamped, sized, and can carry a note. bak list shows you the whole history of one file in one screen — no more opening four .bak files to work out which is which.
History lives in ~/.bak, not next to the file. Your directory listing stays clean, your git status stays clean, and nothing accidentally ships to production because it was sitting in the folder.
If the live file has changes that exist in no snapshot, bak saves it as a new version before it overwrites anything — and prints the exact command to undo the restore.
bak list
Version number, timestamp, size, and whichever note you left at the time.
The newest is marked current. Every version number you see
here is a valid argument to diff, show and
restore.
bak list --all widens it to every file you have ever
snapshotted, with a version count and how long ago you last touched it —
useful when you have forgotten what you were mid-way through changing.
v5 2026-05-23 14:40 1.2 KB (current, "auto before restore") v4 2026-05-23 14:02 1.2 KB "before tls" v3 2026-05-22 09:10 1.1 KB v2 2026-05-20 16:44 1.0 KB v1 2026-05-18 11:30 0.9 KB $ bak list --all ~/Pictures/note.txt 3 versions last 2h ago /etc/hosts 7 versions last 1d ago ~/.zshrc 12 versions last 3d ago
bak diff
The default diff is compact — changed hunks with three
lines of context either side, so a one-line change in a 400-line config
prints four lines, not four hundred. Pass -f when you want
the whole file.
Compare any two points in time: latest against the live file, a specific version against the live file, or two old versions against each other.
--- nginx.conf v2 (2026-05-20 16:44) +++ nginx.conf current @@ -12,3 +12,4 @@ listen 80; + listen 443 ssl; server_name example.com; $ bak diff nginx.conf v2 v4 # two old versions $ bak diff -f nginx.conf v2 # whole file
bak restore
Before overwriting anything, bak hashes the live file and
checks it against every version in history. If those bytes exist nowhere
else, it snapshots them first — and then tells you the
command that puts them back.
Run it without a version and you get an interactive picker instead. Deleted the file entirely? If it was ever snapshotted, restore recreates it, mode bits and all.
current nginx.conf differs from all snapshots. -> saving current as v5 first restored nginx.conf <- v2 (undo: bak restore nginx.conf v5) # nothing was lost — v5 is the file you just replaced $ bak restore nginx.conf v5 restored nginx.conf <- v5
Reference
Pick one on the left. The panel shows what you type and what bak prints back.
Flags
| Flag | What it does |
|---|---|
| -m, --note TEXT | Attach a note to this version. It shows up in bak list forever.bak nginx.conf -m "before tls" |
| --keep N | Auto-prune while snapshotting, keeping only the newest N versions of this file. |
| --yes | Skip every confirmation prompt. Also what lets a file above --max-size through. |
| --json | Machine-readable output, where the command supports it. Good for scripts and shell prompts. |
| -f, --full | Full-file diff including unchanged lines, instead of the default compact hunks. |
| --no-compress | Store the snapshot raw instead of zstd-compressed. Written as vN.raw. |
| --no-deref | Snapshot the symlink itself rather than the content it points at. |
| --max-size SIZE | Guard rail, default 100MB. Anything larger needs --yes. Accepts 512k, 100MB, 2g. |
| --all | For bak list: every tracked file rather than one. |
Where it all goes
History lives in ~/.bak. Each tracked file gets a directory
named for the SHA-256 of its absolute path, so
/etc/hosts and ~/hosts can never collide, and
renaming a folder never silently merges two histories.
Inside, meta.json records every version's time, size, hash,
note and mode bits; the snapshots sit beside it as compressed blobs. No
database, no daemon, nothing to corrupt — and every write is atomic.
Point BAK_DIR somewhere else and the whole store moves with it.
# relocate the store — iCloud, an external disk, anywhere
export BAK_DIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs/bak"
~/.bak/
├── index.json
└── objects/
└── <sha256(abspath)>/
├── meta.json
├── v1.zst
├── v2.zst
└── v3.zst
In practice
bak /etc/hosts before you touch DNS or a local override. When the machine stops resolving anything, the way back is one command instead of an afternoon.
Snapshot ~/.zshrc, add the plugin, and if shell startup crawls, bak diff tells you precisely what you added before you revert it.
A query, a config, a prompt. Snapshot each attempt with -m, then diff the one that finally worked against the three that didn't.
Snapshot the file before sed, a formatter, or a migration script rewrites it in place. One line of insurance against a regex you got slightly wrong.
.env historyKeep a trail of your local environment file without ever committing it. It is gitignored for a reason — that reason should not also mean no undo.
If it was ever snapshotted, it still exists. bak restore <file> recreates it at the original path with its original mode bits.
Getting started
Homebrew needs to know where the formula lives. The explicit URL form works whatever the repository is called.
brew tap rohitshidid/bak https://github.com/rohitshidid/bak
The formula builds from the v0.1.1 tag with Go, which Homebrew pulls in as a build-only dependency. Nothing is left behind afterwards.
brew install rohitshidid/bak/bak
bak --version
That is the whole workflow. Snapshot before you edit, and list, diff or restore whenever you need to look back.
bak ~/.zshrc -m "before adding the plugin"
bak list ~/.zshrc
Or build it from source — Go 1.22 and one module, nothing else:
git clone https://github.com/rohitshidid/bak.git
cd bak
go test ./...
go build -o bak .
./bak --help
brew upgrade rohitshidid/bak/bak pulls the newest tag.
brew uninstall bak removes the binary but leaves
~/.bak exactly where it is — your history is yours,
and no package manager is going to delete it on your behalf. Remove it by
hand if you actually want it gone.
Honestly
A backup tool that oversells itself is worse than none. Here is exactly where bak stops.
Everything is local. If the disk dies, the history dies with it. This is scratch undo — keep your 3-2-1 backups regardless.
One file at a time, with no atomic multi-file commits, no branches and no merges. For a project, use the tool built for projects.
Snapshots are compressed, not encrypted. Your .env history sits in ~/.bak as recoverable plaintext — so do not point BAK_DIR at storage you would not hand the file to directly.
Nothing is watching the filesystem. You run bak before you edit — which is the same habit as cp file.bak, just with something useful at the other end.
.bak2 files.Free, open source, and small enough to read in an afternoon.
macOS & Linux · MIT licensed · v0.1.1