fix(careful): warn on chained rm even when the last target is safe

The safe-exception block whitelisted rm -rf of build artifacts by
extracting targets with a single greedy match (.*rm ...), which only ever
inspects the LAST rm in the command. A chain like 'rm -rf /; rm -rf
node_modules' was therefore judged solely by its trailing safe target and
allowed without warning, waving through the destructive 'rm -rf /'.

Gate the shortcut to single rm invocations: when any shell separator
(; | & newline, incl. JSON-escaped \n/\r from the grep extraction path)
is present, fall through to the destructive-pattern check, which warns on
any recursive rm. Single-command artifact cleanups still allow.

Adds 3 regression tests covering semicolon and && chains in both orders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jayesh Betala
2026-07-09 18:55:54 -07:00
committed by Garry Tan
co-authored by Claude Opus 4.8
parent 11de390be1
commit b22f7a66e1
2 changed files with 38 additions and 1 deletions
+13 -1
View File
@@ -26,7 +26,19 @@ fi
CMD_LOWER=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]')
# --- Check for safe exceptions (rm -rf of build artifacts) ---
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then
# Only whitelist when the command is a SINGLE rm invocation. The target
# extraction below greedily matches the LAST `rm ` in the string, so a chained
# command like `rm -rf /; rm -rf node_modules` would be judged solely by its
# final (safe) targets and wave through the destructive earlier `rm -rf /`.
# When any shell separator is present, skip the shortcut and fall through to the
# destructive-pattern check, which warns on any recursive rm.
HAS_SEPARATOR=false
case "$CMD" in
# Real separators, plus JSON-escaped newlines (\n / \r) which survive the
# grep extraction path as literal two-char sequences and still mark a chain.
*';'*|*'|'*|*'&'*|*$'\n'*|*$'\r'*|*'\n'*|*'\r'*) HAS_SEPARATOR=true ;;
esac
if [ "$HAS_SEPARATOR" = false ] && printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then
SAFE_ONLY=true
RM_ARGS=$(printf '%s' "$CMD" | sed -E 's/.*rm[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*//;s/--recursive[[:space:]]*//')
for target in $RM_ARGS; do