bak icon

Undo for the files git never sees.

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

~/configs — zsh

a real session — every line above is output bak actually prints

10commands, and you only need three of them
zstdcompressed on the way in, transparently
SHA-256on every version — identical content is never stored twice in a row
1third-party dependency in the whole binary

The problem

Everyone has done this.

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.

🕐

Versions, not guesses

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.

📂

Out of your way

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.

↩︎

A restore that cannot lose work

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

The whole history of one file.

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.

bak list nginx.conf
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

Only the lines that moved.

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.

bak diff nginx.conf v2
--- 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

Deliberately impossible to regret.

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.

bak restore nginx.conf v2
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

Every command, with its output.

Pick one on the left. The panel shows what you type and what bak prints back.


        

Flags

Nine of them, and sensible without any.

FlagWhat it does
-m, --note TEXTAttach a note to this version. It shows up in bak list forever.bak nginx.conf -m "before tls"
--keep NAuto-prune while snapshotting, keeping only the newest N versions of this file.
--yesSkip every confirmation prompt. Also what lets a file above --max-size through.
--jsonMachine-readable output, where the command supports it. Good for scripts and shell prompts.
-f, --fullFull-file diff including unchanged lines, instead of the default compact hunks.
--no-compressStore the snapshot raw instead of zstd-compressed. Written as vN.raw.
--no-derefSnapshot the symlink itself rather than the content it points at.
--max-size SIZEGuard rail, default 100MB. Anything larger needs --yes. Accepts 512k, 100MB, 2g.
--allFor bak list: every tracked file rather than one.

Where it all goes

One directory, and it is just files.

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

Six minutes where this saves you an hour.

🖥️

System config edits

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.

🐚

Dotfile tinkering

Snapshot ~/.zshrc, add the plugin, and if shell startup crawls, bak diff tells you precisely what you added before you revert it.

🔁

Iterating on one file

A query, a config, a prompt. Snapshot each attempt with -m, then diff the one that finally worked against the three that didn't.

⚠️

Before a codemod

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.

🔑

Local .env history

Keep 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.

🗑️

Recovering a deleted file

If it was ever snapshotted, it still exists. bak restore <file> recreates it at the original path with its original mode bits.

Getting started

Two commands, then you already know how to use it.

Tap the repository

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

Install

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

Snapshot something

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

Upgrading and uninstalling

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

Four things it is not.

A backup tool that oversells itself is worse than none. Here is exactly where bak stops.

✕
It is not a backup system.

Everything is local. If the disk dies, the history dies with it. This is scratch undo — keep your 3-2-1 backups regardless.

✕
It is not git.

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.

✕
It is not encrypted at rest.

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.

✕
It is not automatic.

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.

Stop making .bak2 files.

Free, open source, and small enough to read in an afternoon.

macOS & Linux · MIT licensed · v0.1.1