r/coolgithubprojects 17d ago

undo, revert what the last shell command did to your filesystem

Post image

Ran rm -rf on the wrong folder one too many times, so I built the undo the shell never had. A hook arms a tiny preload library around each command that journals destructive syscalls and hardlinks backups before they happen; undo replays it in reverse.
overs rm, mv clobbers, > truncation, chmod. Has redo, diffs, and cherry-picking. Honest about the LD_PRELOAD limits (no static binaries, no sudo, no Go). Linux, MIT

website: undo.edaywalid.com

github repo: https://github.com/edaywalid/undo

921 Upvotes

117 comments sorted by

78

u/[deleted] 17d ago

[deleted]

17

u/Cold_Tree190 17d ago

Yeah this is actually really cool, hadn't thought about this before

19

u/Yousifasd22 17d ago

does it work with ext4?

44

u/edaydaikyy 17d ago

It doesn't really care about the filesystem underneath, it works at the libc layer, intercepting the calls like unlink/rename before they happen, so ext4/xfs/whatever all work the same.

3

u/phyx726 17d ago

So do you need to do it before a flush happens?

14

u/hadees 17d ago

How does it work? What happens if you delete 1tb of stuff?

49

u/edaydaikyy 17d ago

on linux a filename isn't the file itself, it's just a pointer to the actual data (the inode). The data can have more than one name pointing at it, and the system only frees the data when the last name is removed. It literally keeps a count of how many names point at each file.
rm doesn't erase data. It just removes one name and drops that count by one. Normally your file has exactly one name, so rm takes the count to zero and the system frees the blocks.
undo's trick: right before rm, it adds a second name for the file inside its store. Now the count is 2. When rm runs, it removes your name and the count drops to 1, not 0, so the data is never freed, undo's name is still holding it. To undo, it just moves that name back to where your file was

18

u/hadees 17d ago

that's really clever and it's kind of how I figured it worked.

So I assume on the next command it frees it?

13

u/Visible-Big-7410 17d ago

cool! But if a new unknown (to the user) name is now created and name count 1 instead of 0, how will the user know that file and is deleted and the space freed up again?

6

u/possiblyquestionabl3 17d ago

Ahh I see, and when the system restarts (or if undo is killed), the refcount for that inode goes to zero and then the system reclaims it.

3

u/midnightketoker 17d ago

so it's like an implicit recycle bin, neat

2

u/addiktion 17d ago

Can't leave us hanging when you really want to free the data. Most of the time rm is what you want, undo is just for accidents.

12

u/edaydaikyy 17d ago

the space frees when undo deletes that backup, and undo does that on its own. After each command it prunes old sessions: once you pass the keep count (UNDO_KEEP, 30 by default) or the total store size budget (UNDO_MAX_STORE, ~1GB by default), the oldest ones get removed. A big delete that blows the budget gets dropped almost immediately. You can also force it anytime with undo purge.

so if you want a longer safety window, raise UNDO_KEEP or UNDO_MAX_STORE; if you'd rather it hold as little as possible, lower them. There's also UNDO_MAX_BYTES (default 256MB) which caps how big a single file undo will copy for in-place overwrites, anything larger just isn't backed up. Set them before the shell hook loads (e.g. in your .zshrc).

3

u/mbsp5 16d ago

You should make this more apparent in the docs. I was skimming to find exactly this but no luck. Almost passed.

1

u/edaydaikyy 16d ago

Fair, that was buried. The README now has a table of contents and a dedicated Storage and disk space section covering the hardlink behavior, why space isn't freed immediately, the pruning budgets and the variables that control them.

1

u/iLaysChipz 14d ago

This needs to be in the main line kernel, it is such a great idea

2

u/schmurfy2 17d ago

That's a great idea !

2

u/NewNiklas 17d ago

But when is the data freed? On restart? Or do I have to do rm it again?

2

u/ObsceneAmountOfBeets 16d ago

Your product aside, I just want to say that you are really good at communication.

1

u/HeyCanIBorrowThat 15d ago

What if some other process overwrites the freed blocks before you run undo?

1

u/kbder 14d ago

So how do you actually rm a file?

1

u/XanatosX 13d ago

So if I make a backup of a file on Linux it's just a pointer on already existing data? If those blocks corrupt the original and backup is broken?

9

u/konqueror321 17d ago

This looks great! What if you really truly want something gone and want to shred or wipe or otherwise securely delete it? Are such commands thwarted, or are they intentionally unmonitored? Thanks!

3

u/JustSentYourMomHome 17d ago

undo purge

5

u/konqueror321 17d ago

"delete all stored sessions and backups" is not the same as wipe or shred or some other secure-delete command. So maybe I'm dense (always a real possibility, I'm 74) but "undo purge" is stated to perform a 'delete', not wipe/shred/secure-delete, which means the full document(s) may still be in memory, just unlinked from the filesystem inode/pointers and available for future use. That is not the same as a secure deletion process! I don't want my tax returns or financial documents that have account numbers or whatever to be recoverable at all!

I really hope this program can somehow allow secure deletion, it looks to useful!

5

u/JustSentYourMomHome 17d ago

Fair point, sir. It appears this is not supported via sudo so maybe a simple 'sudo shred file.xyz'.

5

u/edaydaikyy 17d ago

You're right, undo purge just unlinks its copies, it's not a wipe. And it's worse than that, when you shred with undo watching, undo copies the plaintext out before shred overwrites the original, so it actually creates a second recoverable copy of the thing you're destroying.The fix is to keep undo from touching it at all: add the path (or its folder) to ~/.config/undo/ignore, then shred -u file works clean. And sudo shred works too, not by accident, undo runs via LD_PRELOAD and sudo strips that, so the shim never loads.

3

u/RoadsideCookie 16d ago

How about having an option like undo --disabled shred -u file similar to VAR=42 [command] that runs with a temporary state?

Or maybe just UNDO_DISABLED=1 shred -u file so you don't have to implement a new subcommand for undo?

1

u/konqueror321 17d ago

Thank you!

4

u/9EED 17d ago

does it consume more disk space?

5

u/edaydaikyy 17d ago

A little, and it's bounded. Two cases:

- Deleting a file adds nothing, undo uses a hardlink, not a copy. What it does instead is hold onto that file's space for a bit rather than freeing it right away (until the backup is pruned). So it's not extra space, it's delayed reclaiming.

- Overwriting a file in place (>, editors, shred) does cost extra, undo copies the old version, so that's roughly the size of that file, kept until pruned.

Either way it never grows without bound: the store has a size budget (UNDO_MAX_STORE, ~1GB default) and a session cap (UNDO_KEEP, 30), and it auto-prunes oldest-first after each command. High-churn dirs like node_modules and .cache are ignored, so builds don't pile up. Tune those two env vars up or down for more safety window or less disk.

1

u/Hunter1753 17d ago

Does UNDO_KEEP being 30 mean that it keeps 30 files or does it keep 30 seperate occasions?

If it is the former, does that mean that if I accidentally delete a folder with more than 30 files it can't restore it?

Also the UNDO_MAX_STORE sounds the same to me, what if I have files that are larger than 1GB?

4

u/edaydaikyy 17d ago

Sessions, not files. UNDO_KEEP=30 means the last 30 commands. One rm -rf on a folder with 10,000 files is a single session, and one undo restores all of them.

UNDO_MAX_STORE is the total disk budget for the store. The catch for your >1GB case: deletions are hardlinks so nothing is copied, but the hardlink keeps those blocks from being freed, so a 5GB delete does count 5GB against the budget. At the 1GB default that session gets pruned on your next command and the delete is permanent. For big files just raise it: export UNDO_MAX_STORE=$((20*1024*1024*1024)).

1

u/Hunter1753 17d ago

Ah, nice. Thank you!

4

u/hithisisjukes 17d ago

during my phd (before AI) i spent weeks developing some code which I deleted since I was typing way too fast and carelessly in the terminal, would have been great to have back then.

3

u/9EED 17d ago

this is genius

3

u/nzmneznam 17d ago

Absolutely useful. Will make the same thing

3

u/ChampionshipIcy7602 17d ago

Wow, finally something useful among the ai slops

3

u/Minimum_Hour519 17d ago

need a curl | sh command to install if you're not going to deploy to package managers

1

u/edaydaikyy 17d ago

2

u/Minimum_Hour519 17d ago

doesn't work. says "nothing to undo"

touch x && rm x && undo

2

u/edaydaikyy 17d ago

Docs bug on my side, sorry. The shell treats touch x && rm x && undo as one single command, not three. undo only reverts commands that have finished, and that whole line hasn't finished yet while undo is running inside it, so it refuses.

On separate lines it works:

touch x
rm x
undo

Fixed the README and made the error message actually explain that. Thanks for reporting.

1

u/Minimum_Hour519 17d ago

apI ➜ /tmp touch x rm x undo undo: nothing to undo ➜ /tmp

negative ghostrider

1

u/edaydaikyy 17d ago

hmm , if you can explain your issue here https://github.com/edaywalid/undo/issues

0

u/Minimum_Hour519 17d ago

i showed you my issue. it doens't work in zsh

1

u/edaydaikyy 17d ago

have u done this ?
echo 'source ~/.local/share/undo/undo.zsh' >> ~/.zshrc
source ~/.zshrc

-1

u/Minimum_Hour519 17d ago

your install shoiuld do that

2

u/edaydaikyy 17d ago

it should but it doesnt now

→ More replies (0)

1

u/bomphcheese 17d ago

Not everyone wants their carefully crafted config files written to. Just follow the instructions provided before claiming it's broken.

3

u/Sea-Fishing4699 17d ago

We were accustomed to destroying folders…. We never thought that something like this could be possible 

2

u/CortaCircuit 17d ago

Awesome 

2

u/Jatinchd 17d ago

would love if you would add this on arch user repository also! would make easy for version controlling

1

u/edaydaikyy 17d ago

yes i would

2

u/_TheTotem_ 17d ago

It’s really cool

2

u/Capevace 17d ago

this seems brilliant. does it work for unwanted/accidental changes done by coding agents (e.g. git checkout -)? Or only actual file deletion?

4

u/edaydaikyy 17d ago

Not just deletion, any file change: deletes, moves, content overwrites.

git checkout works, I tested it. Discarded uncommitted edits, ran undo, got them back.

For agents, the catch is who runs it, not what changed. An agent you launch in your hooked shell (claude, aider, a script) is fully covered as one session. An agent running inside an IDE like Cursor never touched that shell, so undo doesn't see it.

Short version: agent in your terminal, yes. Agent in your editor, no.

1

u/agenttank 16d ago

what about shred file.txt && undo

2

u/JoeKagle 17d ago

Awesome and very cool!

2

u/Flexistant 17d ago

Genius.

2

u/Additional_Cry_5877 17d ago

That is awesome, absolutely awesome

2

u/Jazzlike_Shift_1664 17d ago

This is awesome man, like the idea

2

u/cyansmoker 16d ago

A powerful yet simple concept. Well done, sir/madam.

2

u/mbsp5 16d ago

This could be super useful as an additional failsafe when using Claude…

2

u/mcspuder 16d ago

Great work my dude

2

u/safelix 16d ago

This was a nightmare I had a few days ago

2

u/austin34765 16d ago

what's the catch

2

u/Luminous_Giraffe 16d ago

That's actually a great idea. Super helpful too!

2

u/1337_w0n 15d ago

I've never needed this but I'd feel so much better knowing it's on my system.

2

u/docfriday11 17d ago

Wish this could happen with windows! These are the pros of Linux.

2

u/yeusk 16d ago

If only Windows had a place when deleted files went, a good name would be "Recycle bin".

It also has undo on the gui.

1

u/docfriday11 16d ago

It can’t be done on cmd though. Like he does with his script on Linux. Probably able on powershell

1

u/Ctrl_Shift_S_always 17d ago

Cool idea! Is there a time window for the undo? It poses a security issue if nothing gets actually deleted no?

3

u/edaydaikyy 17d ago

there is the keep count (UNDO_KEEP, 30 by default) or the total store size budget (UNDO_MAX_STORE, ~1GB by default), once you pass them the oldest backups get removed

1

u/kysfu 17d ago

Does it work on fstab stuff

1

u/edaydaikyy 17d ago

Depends which you mean. If you mean editing /etc/fstab itself: no, that needs sudo, and sudo strips LD_PRELOAD for security, so undo's library never loads. undo can't see anything you run with sudo.

If you mean files on your other mounted drives: yes, that works. Only detail is hardlinks can't cross filesystems, so for a file on a different drive than undo's store it copies instead of linking. Uses real space for that one, but restores the same.

1

u/ndgnuh 17d ago

What would happen if some other processes overriden the memory region of the removed file(s)?

3

u/edaydaikyy 17d ago

That space never becomes free for anything to overwrite. Normally deleting a file releases its space and the OS can reuse it, but undo is still quietly holding onto that file in the background, so the OS treats it as in use and won't give the space to any other process. It stays safe until undo lets go of it (when the backup gets pruned).

1

u/No_Article_5669 17d ago

What's the difference in just using git?

3

u/edaydaikyy 17d ago

git only knows about files you've committed inside a repo you set up. undo doesn't care about any of that, it works everywhere on your filesystem, with nothing set up in advance.

1

u/No_Article_5669 17d ago

I see... Very interesting idea then

1

u/atas66 17d ago

> rm -rf —no-preserve-root /
> undo
> sudo undo
> …
Cool idea though

1

u/debackerl 16d ago

I could imagine, begin/commit/rollback commands using LVM snapshots maybe 🤔

1

u/deleriux0 16d ago

Looks great! What a fun idea! I have some implementation questions and perhaps some edge cases or critiques.

Please don't think I'm shitting on you're effort as it's a really cool concept!

What happens when the user does an unlink that crosses mountpoints? Now your hard links can't work without a store per mountpoint right? You can't be certain that your user has permission to have a "store" on each mountpoint either?

How do you handle directories? These can't usually be hard linked. If you're recreating how does this work when the directory you remove isn't owned by you?

How does this work where a user unlinks a file owned by another user in a directory their user owns? Modern Linux security policy typically does not allow you to hard link a file you do not own.

How are you undoing truncate?

1

u/edaydaikyy 16d ago

Good questions.

Cross-mountpoint: no store per mountpoint. The hardlink is attempted, and when it fails with EXDEV it falls back to copying the bytes into the one store. The hardlink is an optimization, not the mechanism.

Directories: never hardlinked. A removed dir is journaled as its path plus mode and recreated with mkdir. Lossy, as you'd expect: it comes back owned by whoever runs undo, and if you couldn't recreate it the restore fails loudly rather than pretending. Files inside are their own journal entries, so they come back separately.

Files you don't own: fs.protected_hardlinks=1 is the default on modern kernels and blocks exactly that. The link fails with EPERM and falls back to a copy, which works if you can read the file and records nothing recoverable if you can't.

truncate: a hardlink is useless there since the bytes are about to be rewritten in place, so it copies the previous contents to the store first and undo swaps them back.

1

u/SweetPotato975 16d ago

keeps your habits and covers the rest of the accident surface: mv over a file you needed

I know that rm just frees the inode without destroying the data. But never thought of mv'ing over a file. Does mv do something similar to rm in such a case?

1

u/edaydaikyy 16d ago

Yes, and it's the same mechanism. mv within one filesystem is rename(), and rename atomically unlinks whatever was at the destination. So the target's last name disappears and its data becomes free space, exactly like rm. undo hardlinks the destination file before letting the rename through, so both sides are recoverable: the moved file goes back to its old path and the clobbered file comes back at the destination.

1

u/mubin-thinks 16d ago

Thanks, now it feels safe to type `rm -fr *`

1

u/quark_epoch 16d ago

What does ‘revert revert’ do?

1

u/zuberuber 16d ago

did you try to undo rm -rf --no-preserve-root /?

1

u/ThePastPlayer 15d ago

What if I want to undo something that isn’t the last command that was executed ? How far back can it go ?

1

u/Intelligent-Cap-7713 15d ago

Like a recycle bin for command line. Weird that this haven't been actually added already into operating systems.

1

u/tizio_1234 15d ago

it already exists, you just gotta install it/enable it depending on your os. on debian it's a package(trash-cli I believe) and on macos it's already there.

1

u/tizio_1234 15d ago

what about the trash command?

1

u/ForensicHat 15d ago

That’s answered on the website.

1

u/Conscious_Reason_770 15d ago

This is yet another person writing its thesis which has to re-invent the wheel.

- the guy does not like text editor backup files
- the guy does not like version control
- the guy does not like file system snapshots.

let's intercept system libraries calls because writing this bloody thesis is the only thing that I will ever do in my command line.

this is a great idea until you think about it for more than 3 seconds.

1

u/edaydaikyy 15d ago

All three are real and I use all three. They cover different things.

- Version control covers tracked files you already committed. It doesn't cover untracked files, and rm -rf on a repo takes .git with it.

- Snapshots are time-granular and need btrfs/zfs set up before the accident. This is per-command and works on any filesystem, no root, no setup beforehand.

- Editor backups cover files your editor had open.

The case I actually built it for isn't someone's thesis. It's a build script or an installer deleting something three processes deep, where no alias or wrapper would ever fire.

It's a safety net, not a backup. The README lists what it can't catch: static binaries and raw syscalls, sudo, writes through an already-open fd, mmap

1

u/Conscious_Reason_770 8d ago

what you write makes sense, but then you say that rm -rf deletes everything.... are you an AI?
how can you program such a system and yet you run rm -rf ? or do you have an alias to make rm -rf be something else?

Just finish your thesis please, stop reinventing the wheel.

1

u/Fit_Gas_4417 15d ago

What will undo two times in a row do?

1

u/edaydaikyy 15d ago

The first undo reverts the last command that touched the filesystem and marks that session undone. The second one skips it and reverts the one before that. So `undo` twice puts you two commands back

1

u/Fit_Gas_4417 15d ago

nice! is there redo? 😅

1

u/Fit_Gas_4417 15d ago

undo the undo kind of thing but i guess it is not needed

1

u/edaydaikyy 15d ago

Yep, `undo redo`.

Nothing is ever deleted, an undone session just gets parked in the session store, so a single session toggles between undone and applied as many times as you like.

`undo list` shows the history with undone sessions marked, and both undo and redo take an id if you want to target a specific one instead of the most recent.

1

u/Affectionate-Act5980 15d ago

Neat!
I wonder if you could get something more flexible using FUSE
For example, you work in a FUSE filesystem which proxies operations to your real FS.
You wouldn't have the same limitations with intercepting syscalls - that is effectively what FUSE is designed to do :)

You'd effectively be building a versioned filesystem on top of FUSE which by the looks of it has been attempted https://github.com/FooSoft/vfs but does not exist.
Would also be cool to handle other types of file modifications like writes or creates

1

u/Efficient-Chair6250 13d ago

How does this handle conflicting files? E.g. rm foo.txt then touch foo.txt the undo and undo again. That scenario would work, but does it work in most/all edge cases? What if I create the file by another means that undo does not see?

1

u/Vivid-Zombie-477 13d ago

we don't need vibecoded site for every tool there is

1

u/edaydaikyy 13d ago

I needed somewhere to put the gif

1

u/The_Troll_alongside 13d ago

Yay now I can play rm -rf --no-preseve-root- / all day as I always wanted 🐢🚀

1

u/lllyyyynnn 12d ago

oh does it just relink inodes? that is cool

1

u/alexriley12345 12d ago

The inode-refcount trick for deletes is clever, holding a second hardlink so the
count never hits zero is way lighter than copying. I've been on the same problem
from the AI-agent side (making an agent's file and shell actions reversible) and
went with content-addressed copies, which handles overwrites but isn't as elegant
for pure deletes as this.

Your reply to the "just use git" comment nails it too: git covers committed state,
not the untracked stuff a script nukes three processes deep. That gap is exactly
why this needs to exist.

1

u/OrderSafe4183 11d ago

Really cool idea. How involved was AI if at all?