Getting My Files Back From Btrfs After a Shift+Delete (With discard=async On)
I nuked a folder full of local-only git repos, everything said TRIM had already wiped it, and btrfs restore got it back anyway.
Varun Agnihotri
Python, LLMs & Cybersecurity
I deleted a folder out of /pythonic/codes with Shift+Delete. Not a copy, not a build artifact, the actual working folder with a bunch of small projects in it. Every one of them was a git repo, and every one of them was local only. No remote, no clone anywhere, nothing pushed.
Realised about ten seconds later. Sat there for a bit.
Here's what I did next and what actually got the files back, including the part where I was fairly sure it was hopeless.
First, the boring checks
Shift+Delete skips the trash entirely, it just calls unlink, so ~/.local/share/Trash was empty of anything useful. Fine, expected that.
Next thing worth trying is whether any process still had the files open. If something's holding an open handle, the inode sticks around even with zero links and you can pull the data straight out of /proc:
sudo lsof +L1Got a huge dump of Chrome and Spotify shared memory junk, plus some stale handles into an old Next.js out/ directory from a different project. Nothing from the folder I actually cared about. No luck there.
Git was the other obvious one, except the .git directories were inside the deleted folders, so they went too. Nothing to restore from.
Then the part that made me think it was over
Checked what I was actually dealing with:
findmnt -no FSTYPE,SOURCE /Btrfs, which is the Fedora default. /pythonic turned out to be its own filesystem on /dev/nvme0n1p4, separate from root. That's actually good news, because nothing the system does in the background writes to it, so no journald or dnf or thumbnailer quietly trampling my blocks while I panicked.
Then I checked the mount options and my stomach dropped:
findmnt -no OPTIONS /pythonicrw,relatime,seclabel,ssd,discard=async,space_cache=v2,subvolid=5,subvol=/discard=async. That's on by default for Btrfs on modern kernels. It means seconds after the delete, the kernel queued up TRIM commands and told the SSD those blocks were free. On most NVMe drives TRIMmed blocks come back as zeros, immediately, because the controller just drops the mapping. There's no residual data to carve out. That's the whole reason photorec and friends are useless here.
Everything I could find on this said the same thing: with TRIM on an SSD, it's gone, don't bother. I was working through it with GeminiGemini 3.6 Flash, Google's model, as of August 2026 at the time and it agreed, put my chances in the low single digits, told me the blocks had most likely already been zeroed and that carving tools wouldn't help.
I bothered anyway. Turns out that call was wrong, and I'm glad I didn't take it as final.
Stopping the bleeding
Killed the scheduled trim so nothing new got discarded while I worked:
sudo systemctl stop fstrim.timerThen unmounted the filesystem. I skipped this the first time and got told off:
ERROR: /dev/nvme0n1p4 is currently mounted, cannot continuebtrfs restore reads the raw device and flatly refuses to touch a mounted one. There were also a couple of processes sitting with their working directory inside /pythonic, so:
cd /
sudo fuser -km /pythonic
sudo umount /pythonicBecause /pythonic is a separate partition I didn't need a live USB for any of this, which saved a lot of time. If the folder had been on /, I'd have had to boot off a USB stick instead.
Walking backwards through tree roots
Here's the bit that isn't explained well anywhere, and the reason I'm writing this post at all.
btrfs restore reads a filesystem tree and copies files out of it. The current tree doesn't have your folder in it anymore, that's the whole problem. So you have to point it at an older tree root, from before the delete. You get the list of candidates like this:
sudo btrfs-find-root /dev/nvme0n1p4Superblock thinks the generation is 18850
Superblock thinks the level is 0
Found tree root at 109559808 gen 18850 level 0
Well block 108691456(gen: 18849 level: 0) seems good, but generation/level doesn't match, want gen: 18850 level: 0
Well block 107216896(gen: 18848 level: 0) seems good, but generation/level doesn't match, want gen: 18850 level: 0
Well block 106381312(gen: 18847 level: 0) seems good, but generation/level doesn't match, want gen: 18850 level: 0
Well block 82526208(gen: 18846 level: 0) seems good, but generation/level doesn't match, want gen: 18850 level: 0
...That screenshot also catches me forgetting to unmount first, which is the error from the last section.
Those "doesn't match" lines look like errors but they're exactly what you want. Each one is an older snapshot of the tree, newest first. Take the byte number at the front and feed it to restore with -t, with -D for a dry run so it just lists what it would pull out without writing anything:
sudo btrfs restore -t 108691456 -D -v -i /dev/nvme0n1p4 /home/pythonicvarun/recover-i says ignore errors and keep going, which you need, because a half-stale tree throws a lot of noise. Expect screenfuls of this:
parent transid verify failed on 115441664 wanted 18850 found 18843
Ignoring transid failureAnd sometimes a root just won't open at all:
ERROR: root [5 0] level 0 does not match 2
Could not open root, trying backup super
ERROR: superblock bytenr 274877906944 is larger than device size 107374182400That one's not fatal either, it just means that particular root is no good. Move to the next number in the list and try again.
Bottom of that one shows a run taking 2m31s, which is about what each pass cost me.
So that's the loop. Take a byte number, dry run it, look at what it lists, move down to an older generation, repeat. Each pass took a couple of minutes. I went through several before one finally listed my folder with real file sizes next to the names instead of nothing.
Then the same command without -D:
sudo btrfs restore -t <THE_ONE_THAT_WORKED> -v -i /dev/nvme0n1p4 /home/pythonicvarun/recoverFiles came out into ~/recover. Restore to a different filesystem than the one you're recovering from, always. /home is on a separate partition here so it was fine.
One thing worth checking after: open a few of the recovered files and make sure they've actually got content in them. TRIM damage shows up as files with the right names and the right sizes that are full of zeros inside. Mine were fine.
What I'm actually taking away from this
Not "Btrfs is great," and definitely not "TRIM doesn't matter."
The real problem was never the delete. It's that I had a folder of git repos with no remote on any of them. git init and then never pushing gives you all the ceremony of version control and none of the actual safety, the entire history sits in one directory that one rm can take out. Pushing to a private repo, or even a bare repo on a box I already pay for, is one command and would have made this a non-event instead of an hour of terminal archaeology.
Snapshots would've covered it too. It's a CoW filesystem, btrfs subvolume snapshot is nearly free, and Fedora doesn't set up Snapper for you by default. I'd just never bothered on that partition.
Both of those cost about five minutes to set up. I've now done both.