rinode is a Linux utility that preserves deleted files and directories in constant time without copying data blocks, allowing instant restoration.
Standard Linux deletion tools have two main drawbacks:
- Immediate extent tree deallocation: When you delete a file with
rm, the kernel drops the inode link count to zero. Filesystems like ext4 immediately clear the inode extent tree and mark blocks as free. Once that happens, undeleting requires scanning raw disk blocks with tools likeext4magicorphotorec, which often fails for fragmented or large files. - Slow cross-filesystem copies: Desktop trash specifications (FreeDesktop Trash) copy files byte-by-byte when moving across mount points or subvolumes. Deleting a 50GB directory tree or large virtual machine image can take minutes and wastes write cycles.
rinode keeps inodes alive at the filesystem level:
- Mount-boundary discovery: It walks up the directory tree using
statx(2)withSTATX_MNT_IDto find the exact mount point or Btrfs subvolume root. - Constant-time move: It relocates deleted entries into a permissions-locked storage directory (
.rinode-storage, mode0700) on the same filesystem usingrenameat2(2). The inode number, extent tree, permissions, and timestamps stay intact on disk. - Audit indexing: Metadata (original path, inode, device ID, owner, permissions, and a 64KB xxHash fingerprint) is recorded into an embedded SQLite database (
rinode.db) in Write-Ahead Log (WAL) mode. - Instant restoration:
- Move back (default): Recreates any missing parent directories (
mkdir -p) and moves the inode back to its original location viarename(). - Snapshot copy (
--keep-copy): On filesystems that support Copy-on-Write (Btrfs, XFS), it issues anioctl(FICLONE)system call to point a new directory entry to the existing data blocks with zero duplication. On ext4, it copies the file.
- Move back (default): Recreates any missing parent directories (
For an in-depth systems document analyzing the Linux inode lifecycle, extent tree deallocation, VFS link count semantics, and atomic directory transaction flows:
rinode relies directly on modern Linux VFS system calls and requires Linux kernel 5.8 or newer:
statx(2)withSTATX_MNT_ID(Linux 5.8+): Required to discover filesystem mount and Btrfs subvolume boundaries without crossing into adjacent mount points. Kernels prior to 5.8 do not supportSTATX_MNT_ID.renameat2(2)withRENAME_NOREPLACE(Linux 3.15+): Enforces kernel-level atomic moves without overwrite races (eliminating TOCTOU bugs).ioctl(2)withFICLONE(Linux 4.5+): Enables zero-copy reflink snapshots during restore on Copy-on-Write filesystems (Btrfs, XFS).- Rust 1.75+: Required only when compiling from source.
makeSystem-wide install. Puts the binary in /usr/local/bin (already on
PATH on most distributions), the man page in /usr/local/share/man/man1,
and shell completions for Fish, Bash, and Zsh:
sudo make installSingle-user install without sudo. Puts the binary in ~/.local/bin:
make install-userThis requires ~/.local/bin on your PATH (Debian-based shells add it
automatically at next login):
# Bash (~/.bashrc)
export PATH="$HOME/.local/bin:$PATH"
# Fish (~/.config/fish/config.fish)
set -gx PATH "$HOME/.local/bin" $PATHIf both locations contain a copy, the one earlier on PATH wins
(~/.local/bin usually comes first). sudo make install warns when a
shadowing copy exists. To keep only the system installation:
rm -i ~/.local/bin/rinodeTo uninstall (removes binaries, completions, configurations, databases, and storage):
sudo make uninstallRunning rinode without arguments opens a split-screen terminal interface:
rinode- Left pane: Table of deleted files (
DELETED, ID, filename, size, deletion time, inode number). - Right pane:
- Upper section (
DETAILS): Inode metadata, permissions, ownership, deletion timestamp, 64KB content fingerprint, and storage path. - Lower section (
HISTORY): Audit table of previously restored, purged, and excluded files, with status and local timestamps.
- Upper section (
- Navigation & focus:
Tab/BackTaborh/l(or arrow keys) toggle focus between Deleted Files and History.j/knavigate rows within the active pane.
- Actions:
- Press
Enteron a deleted entry to open the action menu (Restore, Restore (keep copy), Metadata, Delete permanently, Copy path). - Press
Enteron a file in history to inspect its metadata. - Quick keys:
rto restore,xto delete permanently,tto cycle themes,eto view active exclusion rules,qto exit.
- Press
rinode provides built-in color schemes matching modern terminal palettes. Press t in the dashboard to cycle through themes live, or set theme in your configuration file:
| Catppuccin Mocha | Solarized Dark |
|---|---|
![]() |
![]() |
Available themes:
default: Preserves your terminal's transparent background with cyan/green accents.catppuccin: Catppuccin Mocha palette with mauve borders and sky-blue highlights.solarized: Solarized Dark palette with cyan borders and amber accents.dracula: Dracula theme with purple borders and green status indicators.gruvbox: Gruvbox Dark theme with bright orange and aqua accents.tokyo_night: Tokyo Night storm theme with magenta and cyan accents.nord: Nord arctic palette with frost cyan and teal accents.rose_pine: Rosé Pine palette with rose, foam, and gold highlights.one_dark: One Dark editor palette with blue, purple, and green accents.monokai: Monokai Pro high-contrast palette with vibrant pink, cyan, and yellow.kanagawa: Kanagawa Japanese art palette with wave aqua, crystal blue, and autumn red.cyberpunk: Cyberpunk Neon high-contrast palette with neon pink, cyan, and lime.forest: Deep forest emerald palette with green borders and amber highlights.sunset: Vibrant dusk gradient with amber, coral, and violet tones.moonlight: Indigo night sky palette with cool blue and silver accents.high_contrast: Pure black background with bold yellow, cyan, and magenta.
rinode rm moves items into local storage. It accepts standard POSIX rm flags (-r, -R, -f, -v, -i, -d) for drop-in compatibility, plus safety override flags:
# Delete a single file
rinode rm report.pdf
# Delete a directory hierarchy
rinode rm -rf ./build_output/
# Delete a protected system path (prompts in TTY, or pass --allow-protected)
rinode rm --allow-protected /etc/nginx/sites-available/old.conf
# Permanently delete without preserving (unlinks directly from filesystem)
rinode rm --no-storage unwanted_cache.tar
# Note: -p, --permanent, and --no-vault are supported aliasesrinode lsShows a formatted table of active entries in storage:
╭────┬──────────────┬────────┬─────────────────────┬─────────┬─────────────────────────────────────╮
│ ID ┆ Name ┆ Size ┆ Deleted At ┆ Inode ┆ Original Path │
╞════╪══════════════╪════════╪═════════════════════╪═════════╪═════════════════════════════════════╡
│ 1 ┆ report.pdf ┆ 2.4 MB ┆ 2026-09-10 17:55:20 ┆ 1982182 ┆ /home/user/documents/report.pdf │
╰────┴──────────────┴────────┴─────────────────────┴─────────┴─────────────────────────────────────╯
Restore by ID or filename:
# Restore by entry ID (moves file back to original location)
rinode restore 1
# Restore by name
rinode restore report.pdf
# Overwrite if a file already exists at the destination path
rinode restore --force report.pdf
# Restore a copy while retaining the snapshot in storage
# Restored copy keeps the original permission bits and owner, then syncs to disk
rinode restore --keep-copy 1
# Note: --keep-vault is supported as an aliasrinode inspect 1Displays complete inode provenance:
Inode Metadata for Entry #1
-----------------------------------------
Filename: report.pdf
Original Path: /home/user/documents/report.pdf
Inode Number: 1982182
Mount/Device: 252:0 (mnt_id: 47)
File Size: 2.4 MB (2516582 bytes)
Permissions (Oct): 100644
Owner UID / GID: 1000 / 1000
Link / Move Type: RENAME_MOVE
Status: DELETED
Deleted At: 2026-09-10T17:55:20.171592361+00:00
Fast Fingerprint: dc1025ce6c498bd0
Storage Location: /home/user/.rinode-storage/report.pdf__1982182_0_1789062920171503320_cac8891a2b3c4d5e
You can prevent temporary build artifacts, log files, or specific paths from being preserved:
# Open the interactive menu
rinode exclude
# Add a glob pattern (converted to a filename regex)
rinode exclude "*.log"
# Add a folder name (converted to a path regex)
rinode exclude "node_modules"
# Add an absolute system path prefix
rinode exclude "/var/cache"
# List active rules
rinode exclude --list
# Test whether a candidate path would be preserved or unlinked directly
rinode exclude --test /home/user/repo/node_modules/pkg/index.js
# Remove a rule
rinode exclude --remove "*.log"Permanently unlinks files from storage and reclaims disk space.
Every rinode rm also auto-purges entries older than retention_days
and prunes oldest-first when total size exceeds max_storage_bytes:
# Purge specific files by ID or filename
rinode purge 1
rinode purge 1 3 5
rinode purge notes.txt
# Purge entries older than the configured retention period (default: 30 days)
rinode purge
# Purge entries older than N days
rinode purge --days 7
# Purge all preserved files immediately
rinode purge --allrinode init generates a shell function named r:
rwithout arguments launches the terminal UI.r <files...>preserves files usingrinode rm.r ls,r restore <id>,r inspect <id>, andr purgeforward to their respective subcommands.
To load it, add the following to your shell configuration file:
rinode init fish | sourceeval "$(rinode init bash)"eval "$(rinode init zsh)"The default shortcut command name is r. If r conflicts with an existing tool (such as GNU R or ranger), pass --alias <NAME>:
rinode init fish --alias ri | sourceTo disable the shortcut function entirely and keep only completions or standard rm redirection:
rinode init fish --alias none --alias-rm | sourceTo replace standard rm with rinode rm, add the --alias-rm flag:
rinode init fish --alias-rm | sourceFiles moved to storage are kept in a hidden directory on the matching mount point (.rinode-storage, mode 0700) or in the user directory (~/.local/share/rinode/storage/).
To avoid name collisions when multiple files with the same name are deleted over time across different directories, rinode renames each entry using this format:
<sanitized_filename>__<inode>_<dev_minor>_<timestamp_nanos>_<random_hex>
Example:
report.pdf__1982182_0_1789062920171503320_cac889
This format provides several properties:
- Collision immunity: Nanosecond timestamps combined with 6 hex characters of random entropy ensure that rapid deletions of files with identical names never collide.
- Provenance on disk: In the event that the SQLite index (
rinode.db) is removed or corrupted, the entry's original inode number and filesystem device minor ID remain recoverable directly from the storage filename.
rinode enforces an active VFS-aware safety policy to protect against catastrophic deletion commands, symlink traversal tricks, accidental root removal, and credential leakage:
- Tier 0: Refuse (Critical System Roots & Storage):
/,/proc,/sys,/dev,/run, internal storage (.rinode-storage), metadata database (rinode.db), and configuration files.- Overriding requires all three flags:
--no-preserve-root --allow-protected --force. - Permanent direct unlinking (
--no-storage/-p) is strictly forbidden; items can only be preserved into storage.
- Overriding requires all three flags:
- Tier 1: Protected Roots & Mount Boundaries:
/etc,/usr,/bin,/sbin,/lib*,/boot,/root,/var, and active filesystem mount roots (detected viastatx(STATX_MNT_ID)).- Single file: Prompts
[y/N]in interactive sessions; requires--allow-protectedin automated scripts. - Bulk deletion (>10 files or >1 GiB): Prompts
[y/N]showing count and size; requires--allow-protected --forcein scripts. - Permanent direct unlinking is strictly forbidden.
- Single file: Prompts
- Tier 2: Sensitive Material & Credentials:
~/.ssh,~/.gnupg,~/.pki,~/.aws,id_*,*.pem,*.key,.env,/etc/shadow,/etc/sudoers.- Prompts
[y/N]in interactive sessions;-f/--forceconfirms in scripts. - Permanent direct unlinking is permitted with confirmation or
-fso private keys are not forced to linger in storage.
- Prompts
- Tier 3: Semi-Protected:
/opt,/srv.- Prompts
[y/N]in interactive sessions;-for--allow-protectedconfirms.
- Prompts
- Lexical dot rejection: Immediately rejects
.,..,./,../,foo/.,foo/..,/./, and/.before filesystem resolution. - Symlink double-checking: Inspects both the symlink itself and its canonical target path; the strictest tier between them applies.
- Temporary directory carve-out: The root directories of
/tmpand/var/tmpare protected as Tier 0, while temporary child files inside them are treated as standard unlinked exclusions. - Exclusion rule hardening: The
rinode excludecommand rejects attempts to whitelist Tier 0 or Tier 1 system paths, preventing exclusion-inversion vulnerabilities.
All safety overrides and denials are automatically logged to the safety_overrides table in SQLite (rinode.db), recording timestamp, UID, EUID, PID, PPID, working directory, canonical path, safety tier, flags passed, and the decision result.
When deleting a symbolic link, rinode does not traverse or alter the link target. It reads the target destination path via readlink and saves the raw string in the audit index. During restoration, symlinkat recreates the symbolic link with its original target path intact, preserving relative and absolute link destinations.
Under Linux VFS semantics, moving an open file to another directory on the same filesystem does not invalidate open file descriptors. If a background process or service is actively writing to a file when rinode rm is executed, renameat2(2) relocates the directory entry into .rinode-storage without dropping the inode's link count to zero. The active process continues reading and writing to its descriptor without EBADF or write errors.
If the entry is subsequently purged from storage, the kernel unlinks the directory entry, and the physical disk extents are released once all processes holding the descriptor close it.
Configuration files are resolved in this order:
./rinode.toml(Current directory)~/.config/rinode/config.toml(User configuration)/etc/rinode/config.toml(System fallback)
Default configuration:
# Theme selection (default, catppuccin, solarized, dracula, gruvbox, tokyo_night, nord, rose_pine, one_dark, monokai, kanagawa, cyberpunk, forest, sunset, moonlight, high_contrast)
theme = "default"
[storage]
retention_days = 30
# Max bytes kept in PRESERVED status, oldest pruned first on rm. 0 means unlimited.
max_storage_bytes = 21474836480 # 20 GiB
[safety]
preserve_root = true
bulk_count = 10
extra_protected = []
[exclusions]
system_paths = []
path_regex = [
".*/node_modules/.*",
".*/\\.git/.*",
".*/target/(debug|release)/.*",
".*/build/.*",
".*/\\.cache/.*",
".*/__pycache__/.*",
]
filename_regex = [
"^\\..*\\.swp$",
".*~$",
".*\\.tmp$",
"^core(\\.\\d+)?$",
]| Filesystem | Deletion mechanism | Restore mechanism | Snapshot copy (--keep-copy) |
|---|---|---|---|
| ext4 | renameat2 |
rename |
Kernel file copy |
| Btrfs | renameat2 within subvolume |
rename |
ioctl(FICLONE) (reflink CoW) |
| XFS | renameat2 |
rename |
ioctl(FICLONE) (reflink CoW) |
Integration tests run against a local test hierarchy:
make test



