Skip to content

Instantly share code, notes, and snippets.

@ProxiBlue
Created August 30, 2026 02:24
Show Gist options
  • Select an option

  • Save ProxiBlue/69b6c60a578aa3814f0d9788116e47a2 to your computer and use it in GitHub Desktop.

Select an option

Save ProxiBlue/69b6c60a578aa3814f0d9788116e47a2 to your computer and use it in GitHub Desktop.
make rm saver with ai
#!/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