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.