From 58b6f73e2a5cb244e48a42eb5811cefe14a107d6 Mon Sep 17 00:00:00 2001 From: newklei Date: Mon, 23 Feb 2026 11:28:44 -0500 Subject: [PATCH 1/3] fix: expand deny rules and harden hook regex for rm and pipe-to-shell bypasses Deny rules previously only blocked `rm -rf` and `rm -fr` (lowercase, combined flags only). All other flag forms bypassed both security layers silently. Expand deny rules to cover: - All case combinations of combined short flags: -Rf, -rF, -RF, -fR, -Fr, -FR - All orderings of separated short flags: -r -f, -r -F, -R -f, -R -F and their reverses (-f -r, -f -R, -F -r, -F -R) - GNU long-form flags: --recursive, --recursive --force, --force --recursive - Pipe-to-shell via sh and zsh: * | sh, * | zsh - Process substitution: bash <(curl *) - Spaced pipe variant for existing rules: curl *| bash*, wget *| bash* Harden PreToolUse hook regex: - Old regex required combined lowercase flags only (-rf / -fr pattern) - New regex uses three chained case-insensitive grep checks: 1. Confirm rm is the actual command (not a substring of another word or arg) 2. Detect any recursive flag (-r, -R, -[flags]r, --recursive) 3. Detect any force flag (-f, -F, -[flags]f, --force) - Handles commands chained with ; && || | - Verified against 25 test cases (19 must-block, 6 must-allow) --- settings.json | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/settings.json b/settings.json index 3956051..9b11d06 100644 --- a/settings.json +++ b/settings.json @@ -13,11 +13,33 @@ "deny": [ "Bash(rm -rf *)", "Bash(rm -fr *)", + "Bash(rm -Rf *)", + "Bash(rm -rF *)", + "Bash(rm -RF *)", + "Bash(rm -fR *)", + "Bash(rm -Fr *)", + "Bash(rm -FR *)", + "Bash(rm -r -f *)", + "Bash(rm -r -F *)", + "Bash(rm -R -f *)", + "Bash(rm -R -F *)", + "Bash(rm -f -r *)", + "Bash(rm -f -R *)", + "Bash(rm -F -r *)", + "Bash(rm -F -R *)", + "Bash(rm --recursive *)", + "Bash(rm --recursive --force *)", + "Bash(rm --force --recursive *)", "Bash(sudo *)", "Bash(mkfs *)", "Bash(dd *)", "Bash(curl *|bash*)", + "Bash(curl *| bash*)", "Bash(wget *|bash*)", + "Bash(wget *| bash*)", + "Bash(* | sh)", + "Bash(* | zsh)", + "Bash(bash <(curl *))", "Bash(git push --force*)", "Bash(git push *--force*)", "Bash(git reset --hard*)", @@ -53,11 +75,11 @@ "hooks": [ { "type": "command", - "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f'; then echo 'BLOCKED: Use trash instead of rm -rf' >&2; exit 2; fi" + "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qiE '(^|;[[:space:]]*|&&[[:space:]]*|[|][|][[:space:]]*|[|][[:space:]]*)rm[[:space:]]' && echo \"$CMD\" | grep -qiE '(^|[[:space:]])-[a-zA-Z]*[rR]|--recursive' && echo \"$CMD\" | grep -qiE '(^|[[:space:]])-[a-zA-Z]*[fF]|--force'; then echo 'BLOCKED: Use trash instead of rm -rf' >&2; exit 2; fi" }, { "type": "command", - "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'git[[:space:]]+push.*(main|master)'; then echo 'BLOCKED: Use feature branches, not direct push to main' >&2; exit 2; fi" + "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qiE 'git[[:space:]]+push.*(main|master)'; then echo 'BLOCKED: Use feature branches, not direct push to main' >&2; exit 2; fi" } ] } From 33815281ec2422f00319be87c43219ff72187884 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 23 Feb 2026 15:23:33 -0800 Subject: [PATCH 2/3] Simplify: keep hook regex, drop redundant deny rule permutations The rewritten hook regex already handles all case/ordering variants of rm flags via case-insensitive chained grep checks. Remove the 16 enumerated permutations from deny rules. Drop overly broad * | sh and * | zsh rules that would block legitimate pipes. Revert unnecessary case-insensitive flag on git push hook (git is always lowercase). Co-Authored-By: Claude Opus 4.6 --- settings.json | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/settings.json b/settings.json index 9b11d06..32dc02e 100644 --- a/settings.json +++ b/settings.json @@ -13,23 +13,6 @@ "deny": [ "Bash(rm -rf *)", "Bash(rm -fr *)", - "Bash(rm -Rf *)", - "Bash(rm -rF *)", - "Bash(rm -RF *)", - "Bash(rm -fR *)", - "Bash(rm -Fr *)", - "Bash(rm -FR *)", - "Bash(rm -r -f *)", - "Bash(rm -r -F *)", - "Bash(rm -R -f *)", - "Bash(rm -R -F *)", - "Bash(rm -f -r *)", - "Bash(rm -f -R *)", - "Bash(rm -F -r *)", - "Bash(rm -F -R *)", - "Bash(rm --recursive *)", - "Bash(rm --recursive --force *)", - "Bash(rm --force --recursive *)", "Bash(sudo *)", "Bash(mkfs *)", "Bash(dd *)", @@ -37,8 +20,6 @@ "Bash(curl *| bash*)", "Bash(wget *|bash*)", "Bash(wget *| bash*)", - "Bash(* | sh)", - "Bash(* | zsh)", "Bash(bash <(curl *))", "Bash(git push --force*)", "Bash(git push *--force*)", @@ -79,7 +60,7 @@ }, { "type": "command", - "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qiE 'git[[:space:]]+push.*(main|master)'; then echo 'BLOCKED: Use feature branches, not direct push to main' >&2; exit 2; fi" + "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'git[[:space:]]+push.*(main|master)'; then echo 'BLOCKED: Use feature branches, not direct push to main' >&2; exit 2; fi" } ] } From e6ebafacae9234f4b6aba5818e325f3c83aad611 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 23 Feb 2026 15:34:40 -0800 Subject: [PATCH 3/3] =?UTF-8?q?Remove=20curl=20deny=20rules=20=E2=80=94=20?= =?UTF-8?q?curl=20is=20needed=20for=20normal=20use?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- settings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/settings.json b/settings.json index 32dc02e..2aa8d9b 100644 --- a/settings.json +++ b/settings.json @@ -16,11 +16,8 @@ "Bash(sudo *)", "Bash(mkfs *)", "Bash(dd *)", - "Bash(curl *|bash*)", - "Bash(curl *| bash*)", "Bash(wget *|bash*)", "Bash(wget *| bash*)", - "Bash(bash <(curl *))", "Bash(git push --force*)", "Bash(git push *--force*)", "Bash(git reset --hard*)",