-
-
Save ProxiBlue/69b6c60a578aa3814f0d9788116e47a2 to your computer and use it in GitHub Desktop.
make rm saver with ai
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # /bin/rm + /usr/bin/rm + /usr/local/bin/rm — wrapper that trashes instead of deleting. | |
| # | |
| # Why: 2026-06-29 incident — gitnexus-rebuild cron rm -rf'd .git on | |
| # 3 projects. Moving to trash instead makes those mistakes recoverable. | |
| # | |
| # Installed via dpkg-divert: real coreutils rm moved to /bin/rm.real, | |
| # this wrapper at /bin/rm (and /usr/bin/rm via the Ubuntu usrmerge symlink). | |
| # That means EVERY caller — interactive, scripts, cron, even direct | |
| # /bin/rm — hits this wrapper. No bypass. | |
| # | |
| # Restores: | |
| # trash-list # see what's in trash | |
| # trash-restore # interactive picker to put back | |
| # trash-empty 7 # purge entries >7 days old | |
| # | |
| # Escape hatches for "really delete": | |
| # /bin/rm.real <args> # the real coreutils binary (post-dpkg-divert) | |
| # command rm <args> # does NOT bypass — shell builtin escape | |
| # # only avoids alias resolution, not PATH | |
| # \rm <args> # same — only escapes aliases | |
| # | |
| # Exit semantics: mirrors rm. | |
| # -f: silently skip missing files (don't error) | |
| # otherwise: error on missing files (rm-compatible) | |
| set -e | |
| # Refuse obvious system bombs even when caller passes --no-preserve-root. | |
| # These paths are huge and trashing them would fill the trash bucket and | |
| # break the system. Caller wants real rm? Use /bin/rm.real. | |
| for arg in "$@"; do | |
| case "$arg" in | |
| /|/bin|/boot|/dev|/etc|/home|/lib|/lib32|/lib64|/lost+found|/media|/mnt|/opt|/proc|/root|/run|/sbin|/srv|/sys|/tmp|/usr|/var) | |
| echo "rm-wrapper: REFUSED to trash protected system path: $arg" >&2 | |
| echo "rm-wrapper: if you really mean it, use: /bin/rm.real $*" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| done | |
| # Refuse any path whose basename is ".git" — these are project git repos. | |
| # 2026-06-29 incident: gitnexus-rebuild rm -rf'd $project/.git on 3 projects | |
| # via nightly cron. NEVER AGAIN. Categorical block, no exceptions. Caller | |
| # really means it? They can use /bin/rm.real directly and own the consequence. | |
| for arg in "$@"; do | |
| case "$arg" in -*) continue ;; esac # skip flag args | |
| case "$arg" in | |
| .git|*/.git|*/.git/) refusal="$arg" ;; | |
| *) | |
| abs_check=$(readlink -m "$arg" 2>/dev/null || true) | |
| case "$abs_check" in | |
| */.git|*/.git/) refusal="$arg" ;; | |
| *) refusal="" ;; | |
| esac | |
| ;; | |
| esac | |
| if [ -n "${refusal:-}" ]; then | |
| echo "rm-wrapper: REFUSED to touch git repo path: $refusal" >&2 | |
| echo "rm-wrapper: deleting .git destroys the repo. If you really mean it, use: /bin/rm.real -rf $refusal" >&2 | |
| echo "rm-wrapper: (this guard exists because 3 projects lost .git to a cron rm-rf on 2026-06-29.)" >&2 | |
| exit 3 | |
| fi | |
| done | |
| force=0 | |
| end_flags=0 | |
| paths=() | |
| for arg in "$@"; do | |
| if [ "$end_flags" = "0" ]; then | |
| case "$arg" in | |
| --) end_flags=1; continue ;; | |
| --force) force=1; continue ;; | |
| --recursive|--dir|--verbose|--interactive|--interactive=*|--preserve-root|--no-preserve-root|--one-file-system|--help|--version|-r|-R|-d|-v|-i|-I) | |
| continue | |
| ;; | |
| -*) | |
| # Combined short flags like -rf, -rfv. Check if 'f' is in there. | |
| case "$arg" in *f*) force=1 ;; esac | |
| continue | |
| ;; | |
| esac | |
| fi | |
| paths+=("$arg") | |
| done | |
| # Apply -f semantics: drop missing paths silently. Without -f, surface a | |
| # real error for the first missing one (rm-compatible). | |
| real_paths=() | |
| for p in "${paths[@]}"; do | |
| if [ -e "$p" ] || [ -L "$p" ]; then | |
| real_paths+=("$p") | |
| elif [ "$force" = "0" ]; then | |
| echo "rm-wrapper: cannot remove '$p': No such file or directory" >&2 | |
| exit 1 | |
| fi | |
| done | |
| # Nothing left to trash — exit 0 (matches rm -f behavior on all-missing). | |
| [ "${#real_paths[@]}" = "0" ] && exit 0 | |
| # Split paths into "trash-worthy" (real files we want recoverable) and | |
| # "ephemeral" (anything under /tmp, /var/tmp — those dirs ARE meant to be | |
| # wiped, and the trash daemon can't cross volumes to them anyway). | |
| trash_paths=() | |
| ephemeral_paths=() | |
| for p in "${real_paths[@]}"; do | |
| abs=$(readlink -m "$p") | |
| case "$abs" in | |
| /tmp/*|/var/tmp/*) ephemeral_paths+=("$p") ;; | |
| *) trash_paths+=("$p") ;; | |
| esac | |
| done | |
| real_rm_rc=0 | |
| if [ "${#ephemeral_paths[@]}" -gt 0 ]; then | |
| /bin/rm.real -rf -- "${ephemeral_paths[@]}" || real_rm_rc=$? | |
| fi | |
| trash_rc=0 | |
| if [ "${#trash_paths[@]}" -gt 0 ]; then | |
| # trash-put is the binary from trash-cli package (apt install trash-cli). | |
| trash-put -- "${trash_paths[@]}" || trash_rc=$? | |
| fi | |
| # Surface the first non-zero exit. Caller scripts that rely on rm's exit | |
| # code (e.g. `rm foo || handle_error`) still see failures. | |
| [ "$real_rm_rc" != "0" ] && exit "$real_rm_rc" | |
| exit "$trash_rc" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment