One command destroyed it. One command brought it back.

undo reverts what the last shell command did to the filesystem. No daemon, no snapshots, no new habits. This is a real session, line by line:

~/work $ rm -rf thesis/

The mistake. By the time you feel it, rm already returned.

~/work $ undo

No flags to remember. It targets the last command that changed the filesystem.

$ rm -rf thesis/  (14:02:11, 4 changes)
  deleted   thesis/draft.md
  deleted   thesis/refs.bib
  deleted   thesis/notes/
  deleted   thesis/

undo shows exactly what that command did before touching anything. The four files were saved by hardlink at the moment rm deleted them, so the backup cost nothing.

revert this? [y/N] y
restored 4 change(s)

It asks first. Restores refuse to clobber files you changed since, unless you force them.

~/work $ ls thesis
draft.md  notes/  refs.bib

Back, with permissions and timestamps intact. And undo redo exists if you change your mind again.

rm ran normally. It was just being watched.

A shell hook (zsh, bash, fish) arms a small LD_PRELOAD library around each command. When the command calls libc to destroy something, the library saves the file first, then lets the call through. This is the trace of that rm:

rm -rf thesis/  unlinkat("thesis/draft.md")   -> hardlink to session store  journaled  unlinkat("thesis/refs.bib")   -> hardlink to session store  journaled  unlinkat("thesis/notes", DIR) -> mode 755 recorded          journaled  unlinkat("thesis", DIR)       -> mode 755 recorded          journaled # hardlinks share the inode: no data copied, no matter the size.# the journal is a plain text file. undo replays it bottom-up.

The same interception covers rename (mv over a file you needed), open with write flags (the > that truncated your config), mkdir, chmod, and friends. Between your commands, nothing runs at all.

Every command that changed something is one entry.

Not just the last one. undo list is the filesystem history of your session, newest first, one command per row:

$ undo list  17847195022114  Jul 25 16:41:07    1 changes  > notes.md  17847194815520  Jul 25 16:37:41  132 changes  rm -rf node_modules/.cacheu 17847191203348  Jul 25 15:58:12    4 changes  rm -rf thesis/  17847189947761  Jul 25 15:37:19    2 changes  mv report.pdf ~/archive/  17847186671203  Jul 25 14:42:55    7 changes  ./cleanup.sh

Any row is a target: undo apply 178471899 reverts the mv from an hour ago without touching anything after it. The u marks a session that is currently undone, waiting for a possible undo redo. The last 30 sessions are kept, within a 1 GiB budget, and undo purge wipes the store.

Three lines. That is the entire setup.

$ brew install edaywalid/tap/undo
$ echo 'source ~/.local/share/undo/undo.zsh' >> ~/.zshrc   # bash + fish ship too
$ exec zsh   # done. try it:  touch x && rm x && undo

No account, no daemon to enable, no config file to write. Not a brew person? Every other door is open too:

Debian / Ubuntu
sudo dpkg -i undo_*.deb
Fedora / openSUSE
sudo rpm -i undo_*.rpm
Arch
sudo pacman -U undo_*.pkg.tar.zst
Nix
nix run github:edaywalid/undo
Any distro, no root
curl -fsSL undo.edaywalid.com/install.sh | sh
From source
make install (gcc + go)

Packages live on the releases page. MIT licensed.

You are right to be skeptical.

No. Trash tools replace rm with a different command and only cover deletion. undo keeps your habits and covers the rest of the accident surface: mv over a file you needed, > truncating a config, chmod -R gone wrong, a script that cleaned too much. And it works when the deletion happens three processes deep inside a build tool, where no alias can save you.