fix: block critical recursive delete paths#524
Conversation
📝 WalkthroughWalkthrough
ChangesRecursive-delete policy
Possibly related issues
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Automated code review started - full review. Results will be posted here. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
__tests__/hooks/builtin-policies.test.ts (1)
621-630: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd test coverage for
$HOME/relative paths and multiple slashes.To ensure the edge-case bypasses identified in the implementation review are prevented, consider adding these permutations to the parameterized tests.
♻️ Proposed test additions
it.each([ "rm -rf $HOME", "rm -rf ${HOME}", + "rm -rf $HOME/Documents", "rm -rf ~/Documents", + "rm -rf ~//Documents", "rm -rf /home/chetan", "rm -rf /var/lib", + "rm -rf //var/lib", ])("blocks critical recursive-delete target %s", async (command) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/hooks/builtin-policies.test.ts` around lines 621 - 630, Extend the parameterized cases in the “blocks critical recursive-delete target” test to cover recursive-delete commands using the relative `$HOME/` form and paths containing multiple slashes. Keep the existing context setup and deny assertion unchanged, ensuring both added permutations are rejected by policy.fn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/hooks/builtin-policies.ts`:
- Around line 649-653: Replace the raw-command regex in the find deletion check
with parsed-token analysis: identify an actual find executable invocation,
require a -delete argument, and validate its target paths using the same
critical-path logic as rm handling (including paths such as // and /var/lib).
Preserve the existing deny response while avoiding matches inside quoted
arguments or unrelated commands.
- Around line 634-644: Update isCriticalRecursiveDeletePath to collapse
consecutive slashes in the normalized path before applying the protected-path
checks, so inputs such as //usr and /var//lib match the existing rules. Extend
the home-relative checks to recognize paths beginning with $HOME/ and ${HOME}/
in addition to ~/ while preserving the existing exact home-token handling.
---
Nitpick comments:
In `@__tests__/hooks/builtin-policies.test.ts`:
- Around line 621-630: Extend the parameterized cases in the “blocks critical
recursive-delete target” test to cover recursive-delete commands using the
relative `$HOME/` form and paths containing multiple slashes. Keep the existing
context setup and deny assertion unchanged, ensuring both added permutations are
rejected by policy.fn.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c3323c33-ff33-4b23-a793-c18560c08d85
📒 Files selected for processing (2)
__tests__/hooks/builtin-policies.test.tssrc/hooks/builtin-policies.ts
| function isCriticalRecursiveDeletePath(token: string): boolean { | ||
| const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); | ||
| return normalized === "/" | ||
| || normalized === "~" | ||
| || normalized === "$HOME" | ||
| || normalized === "${HOME}" | ||
| || normalized.startsWith("~/") | ||
| || /^\/[A-Za-z_][\w.-]*$/.test(normalized) | ||
| || /^\/(?:home|Users)\/[^/]+$/.test(normalized) | ||
| || /^\/(?:var\/(?:lib|log)|usr\/(?:lib|local))$/.test(normalized); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Fix path normalization to prevent bypasses and include $HOME relative paths.
- Multiple slashes bypass: Paths like
//usr,//var/lib, or/var//libwill bypass the exact regex matches because multiple consecutive slashes are not collapsed (e.g.,//usris perfectly valid in Bash but fails/^\/[A-Za-z_]/). - Missing
$HOME/coverage: The logic correctly covers~/withnormalized.startsWith("~/"), but misses$HOME/and${HOME}/. This leavesrm -rf $HOME/Documentsexposed, contradicting the PR objective to protect home-relative paths.
Normalize paths by collapsing consecutive slashes, and add checks for $HOME/ and ${HOME}/.
🛡️ Proposed fix
function isCriticalRecursiveDeletePath(token: string): boolean {
- const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
+ const normalized = token.replace(/\/{2,}/g, "/").replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
return normalized === "/"
|| normalized === "~"
|| normalized === "$HOME"
|| normalized === "${HOME}"
|| normalized.startsWith("~/")
+ || normalized.startsWith("$HOME/")
+ || normalized.startsWith("${HOME}/")
|| /^\/[A-Za-z_][\w.-]*$/.test(normalized)
|| /^\/(?:home|Users)\/[^/]+$/.test(normalized)
|| /^\/(?:var\/(?:lib|log)|usr\/(?:lib|local))$/.test(normalized);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function isCriticalRecursiveDeletePath(token: string): boolean { | |
| const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); | |
| return normalized === "/" | |
| || normalized === "~" | |
| || normalized === "$HOME" | |
| || normalized === "${HOME}" | |
| || normalized.startsWith("~/") | |
| || /^\/[A-Za-z_][\w.-]*$/.test(normalized) | |
| || /^\/(?:home|Users)\/[^/]+$/.test(normalized) | |
| || /^\/(?:var\/(?:lib|log)|usr\/(?:lib|local))$/.test(normalized); | |
| } | |
| function isCriticalRecursiveDeletePath(token: string): boolean { | |
| const normalized = token.replace(/\/{2,}/g, "/").replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); | |
| return normalized === "/" | |
| || normalized === "~" | |
| || normalized === "$HOME" | |
| || normalized === "${HOME}" | |
| || normalized.startsWith("~/") | |
| || normalized.startsWith("$HOME/") | |
| || normalized.startsWith("${HOME}/") | |
| || /^\/[A-Za-z_][\w.-]*$/.test(normalized) | |
| || /^\/(?:home|Users)\/[^/]+$/.test(normalized) | |
| || /^\/(?:var\/(?:lib|log)|usr\/(?:lib|local))$/.test(normalized); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/builtin-policies.ts` around lines 634 - 644, Update
isCriticalRecursiveDeletePath to collapse consecutive slashes in the normalized
path before applying the protected-path checks, so inputs such as //usr and
/var//lib match the existing rules. Extend the home-relative checks to recognize
paths beginning with $HOME/ and ${HOME}/ in addition to ~/ while preserving the
existing exact home-token handling.
| const hasDestructivePath = parseArgvTokens(cmd).some(isCriticalRecursiveDeletePath); | ||
|
|
||
| if (/\bfind\s+(?:\/|~|\$HOME|\$\{HOME\})(?=\s|$)[\s\S]*?\s-delete\b/.test(cmd)) { | ||
| return deny("Catastrophic deletion blocked"); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Refine find -delete detection to avoid false positives and bypasses.
The current regex for find operates on the raw cmd string:
- False positives: Commands like
echo "find / -delete"orgit commit -m "find / -delete"will incorrectly trigger a block because token boundaries and quotes are ignored. - Bypasses:
find // -deletebypasses the exact string match. - Inconsistent coverage: Hardcoding
(?:\/|~|\$HOME|\$\{HOME\})misses other critical paths (like/var/lib) that are correctly protected againstrm -rf.
You can fix all of these by leveraging the parsed tokens to verify that find and -delete are actual executable tokens, holding find target paths to the exact same standard as rm.
🛡️ Proposed fix
- const hasDestructivePath = parseArgvTokens(cmd).some(isCriticalRecursiveDeletePath);
-
- if (/\bfind\s+(?:\/|~|\$HOME|\$\{HOME\})(?=\s|$)[\s\S]*?\s-delete\b/.test(cmd)) {
- return deny("Catastrophic deletion blocked");
- }
+ const tokens = parseArgvTokens(cmd);
+ const hasDestructivePath = tokens.some(isCriticalRecursiveDeletePath);
+
+ const findIdx = tokens.indexOf("find");
+ const deleteIdx = tokens.indexOf("-delete");
+ if (findIdx !== -1 && deleteIdx > findIdx) {
+ const findArgs = tokens.slice(findIdx + 1, deleteIdx);
+ if (findArgs.some(isCriticalRecursiveDeletePath)) {
+ return deny("Catastrophic deletion blocked");
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hasDestructivePath = parseArgvTokens(cmd).some(isCriticalRecursiveDeletePath); | |
| if (/\bfind\s+(?:\/|~|\$HOME|\$\{HOME\})(?=\s|$)[\s\S]*?\s-delete\b/.test(cmd)) { | |
| return deny("Catastrophic deletion blocked"); | |
| } | |
| const tokens = parseArgvTokens(cmd); | |
| const hasDestructivePath = tokens.some(isCriticalRecursiveDeletePath); | |
| const findIdx = tokens.indexOf("find"); | |
| const deleteIdx = tokens.indexOf("-delete"); | |
| if (findIdx !== -1 && deleteIdx > findIdx) { | |
| const findArgs = tokens.slice(findIdx + 1, deleteIdx); | |
| if (findArgs.some(isCriticalRecursiveDeletePath)) { | |
| return deny("Catastrophic deletion blocked"); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/builtin-policies.ts` around lines 649 - 653, Replace the
raw-command regex in the find deletion check with parsed-token analysis:
identify an actual find executable invocation, require a -delete argument, and
validate its target paths using the same critical-path logic as rm handling
(including paths such as // and /var/lib). Preserve the existing deny response
while avoiding matches inside quoted arguments or unrelated commands.
|
Review failed: Could not clone repository. |
|
Your PR is awaiting review by a moderator. Till then you can join the Discord for conversation: https://discord.gg/qaFM2uYFb |
|
Your PR is awaiting review by a moderator. Till then you can join the Discord for conversation: https://discord.befailproof.ai |
|
Your PR is awaiting review by a reviewer. Till then you can join the Discord for conversation: https://discord.befailproof.ai |
|
Automated code review started - full review. Results will be posted here. |
|
Phase 2 in progress - deep code analysis. Detected a focused security-focused PR: extends block-rm-rf with critical path protection. Continuing to Phase 3 (build + test) next. |
| } | ||
|
|
||
| function isCriticalRecursiveDeletePath(token: string): boolean { | ||
| const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); |
There was a problem hiding this comment.
Path normalization bypass via multiple slashes
The isCriticalRecursiveDeletePath function at line 635 normalizes trailing slashes but does not collapse multiple consecutive slashes in the middle. Paths like //usr, /var//lib, or //home/user bypass all the pattern checks.
Fix: Add .replace(/\/{2,}/g, '/') before existing normalizations. Also add startsWith('$HOME/') and startsWith('${HOME}/') checks.
| }); | ||
| const hasDestructivePath = parseArgvTokens(cmd).some(isCriticalRecursiveDeletePath); | ||
|
|
||
| if (/\bfind\s+(?:\/|~|\$HOME|\$\{HOME\})(?=\s|$)[\s\S]*?\s-delete\b/.test(cmd)) { |
There was a problem hiding this comment.
find with -delete flag: false positives and bypasses
The regex at line 651 checks raw command text: /\bfind\s+(?:\/|~|\/home/failproofai|\$\{HOME\})[\s\S]*?\s-delete\b/. This has two issues:
- False positives: Commands like
echo "find /" && echo "-delete"orgit commit -m "find / with cleanup to delete"will incorrectly trigger a block because the regex doesn't respect token boundaries or quotes. - Bypasses:
find // --delete,find / -exec rm -rf {} \;, and other delete-equivalent forms slip through. - Incomplete coverage: Only checks for
/ | ~ | $HOMEpaths but misses/var/lib,/usr/local,/home/user, and other critical paths thatisCriticalRecursiveDeletePathalready handles.
Fix: Use parsed tokens instead of raw regex. Check whether find and -delete (or --delete) are actual argv tokens, and whether any target paths between them match isCriticalRecursiveDeletePath. This aligns the find check with the rm check and eliminates both false positives and bypasses.
| || normalized === "~" | ||
| || normalized === "$HOME" | ||
| || normalized === "${HOME}" | ||
| || normalized.startsWith("~/") |
There was a problem hiding this comment.
Missing
The function checks normalized === "$HOME" and normalized === "${HOME}" as exact tokens, and checks normalized.startsWith("~/") for home-relative paths. But it does NOT check normalized.startsWith("$HOME/") or normalized.startsWith("${HOME}/").
This means rm -rf $HOME/Documents is blocked (because parseArgvTokens splits on whitespace, so $HOME/Documents is one token that fails startsWith("~/") and ===$HOME), but rm -rf "$HOME/Documents" (quoted single token) is treated differently — wait, actually it IS blocked because isCriticalRecursiveDeletePath receives the raw token $HOME/Documents which matches none of the patterns. After parseArgvTokens splits on whitespace and strips quotes, the token becomes just $HOME/Documents, which does not trigger any critical path check.
Fix: Add normalized.startsWith("$HOME/") and normalized.startsWith("${HOME}/") alongside the existing tilde check to ensure consistent protection for all home-relative path notations.
🔍 Automated Code Review📋 Executive SummaryThis PR extends the 📊 Change Architecturegraph TD
A["blockRmRf() policy"] -->|"extracts"| B["isCriticalRecursiveDeletePath()"]
B --> C["Exact matches: / , ~ , $HOME , ${HOME}"]
B --> D["Prefix check: ~/ paths"]
B --> E["Regex: single-segment /X paths"]
B --> F["Regex: /home/user, /Users/user"]
B --> G["Regex: /var/lib, /var/log, /usr/lib, /usr/local"]
A --> H["Raw regex: find PATH -delete"]
A --> I["Existing: rm flag detection (combined + separated)"]
style B fill:#90EE90
style H fill:#FFD700
Legend: 🟢 New | 🟡 Risk (regex-based, no token parsing) 🔴 Breaking Changes✅ No breaking changes detected. This is a pure additive extension to an existing policy — nothing removed, no API changed, no config modified.
|
hermes-exosphere
left a comment
There was a problem hiding this comment.
Automated review: Approved with suggestions. See inline comments and summary for details. ✅
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline src/hooks/builtin-policies.ts --items all --match 'blockRmRf|isCriticalRecursiveDeletePath|parseArgvTokens' --view expanded
printf '\n--- Relevant policy definitions and call sites ---\n'
rg -n -C 12 'function (isCriticalRecursiveDeletePath|blockRmRf|parseArgvTokens)|const (isCriticalRecursiveDeletePath|blockRmRf|parseArgvTokens)|\b(parseArgvTokens|isCriticalRecursiveDeletePath)\s*\(' src/hooks/builtin-policies.ts
printf '\n--- Focused block-rm-rf tests ---\n'
rg -n -C 8 'block-rm-rf|find .*-(delete|exec)|\$HOME|~/' __tests__/hooks/builtin-policies.test.ts
printf '\n--- parseArgvTokens definitions outside the policy file ---\n'
rg -n -C 6 '\b(parseArgvTokens)\s*[=:<(]' -g '*.{ts,tsx,js,mjs,cjs}' .Length of output: 19031
The suggested The targeted repairs should canonicalize repeated separators, recognize |
hermes-exosphere
left a comment
There was a problem hiding this comment.
Automated review: Approved. ✅
chhhee10
left a comment
There was a problem hiding this comment.
Thanks @Vasyl198 — all the #520 cases close properly and the baselines hold, nice to see tests with it.
One blocker: startsWith("~/") matches at any depth, so rm -rf ~/dev/myapp/node_modules now denies (allowed on main) — that's normal cleanup. Same root: ~/Documents denies but
/home//Documents allows, and find /home/ -delete slips through.
A depth-based rule should fix all three — deny the home dir + its immediate children, allow anything deeper, same for both notations, and reuse it for find.
Non-blocking: $(echo /) still gets through, can be its own issue. Ping me when it's updated.
hermes-exosphere
left a comment
There was a problem hiding this comment.
Hermes Agent Code Review
Verdict: APPROVED ✅
Summary
This PR extends the block-rm-rf policy to cover additional critical recursive-delete paths and find ... -delete variants targeting root or home. The changes are clean, well-factored, and thoroughly tested.
What Changed
-
src/hooks/builtin-policies.ts: ExtractedisCriticalRecursiveDeletePath()from the inline logic, expanding it to match$HOME,${HOME},~/-prefixed paths,/home/<user>,/Users/<user>,/var/lib,/var/log,/usr/lib,/usr/local. Added a separatefind ... -deletecheck forfind /,find ~,find $HOME,find ${HOME}variants. -
__tests__/hooks/builtin-policies.test.ts: Added tests for the new blocked paths, thefind / -deletecase, and theallowPathsoverride for~/scratch.
What Works Well
- ✅ Clean factoring: The
isCriticalRecursiveDeletePathfunction isolates path classification from the delete-detection logic, making both easier to read and maintain. - ✅ Comprehensive regex patterns: Each critical path category is covered by a specific regex, avoiding over-broad matches (e.g.,
/usr/local/binis correctly not flagged as critical). - ✅
allowPathsintegration: The~/scratchtest confirms that configured safe paths override the new restrictions. ThermTargetIsAllowedfunction handles this correctly via path-boundary comparison. - ✅
find ... -deletecatch: Early return before therm-specific checks correctly blocksfind / -deleteeven though it has nothing to do withrm. - ✅ All CI green: 10/10 checks passing (build, test, e2e, lint, type-check, docs, quality, CodeRabbit, OSV-Scanner, Socket Security).
- ✅ 30
block-rm-rftests pass locally (npx vitest run -t "block-rm-rf").
Edge Cases Verified
The following patterns were evaluated against the new logic:
| Command | Expected Result | Actual |
|---|---|---|
rm -rf /var/lib |
Deny | ✅ Blocked |
rm -rf /usr/local |
Deny | ✅ Blocked |
rm -rf /usr/local/bin |
Allow (not critical) | ✅ Allowed |
rm -rf ~/Documents |
Deny | ✅ Blocked |
rm -rf ~/scratch (with allowPaths) |
Allow | ✅ Allowed |
rm -rf /home/user/project |
Allow (depth 2+) | ✅ Allowed |
find / -delete |
Deny | ✅ Blocked |
find $HOME -delete |
Deny | ✅ Blocked |
rm -rf /var/lib/mysql (subdirectory) |
Allow | ✅ Allowed |
rm -rf /tmp |
Deny | ✅ Blocked |
| Quoted paths (single/double) | Deny | ✅ Blocked |
Chained via && |
Deny | ✅ Blocked |
Observations (Non-Blocking)
-
$HOME/some/pathis not flagged: A command likerm -rf $HOME/Documentspasses through becauseisCriticalRecursiveDeletePathonly matches the bare$HOMEstring, not variable-prefixed paths. This is a fundamental limitation of string-based policy analysis before shell expansion and is out of scope for this PR. -
find /var/lib -deleteis not caught: Thefindcheck only coversfind /,find ~,find $HOME, andfind ${HOME}roots. Other critical paths like/var/libare only checked in therm-specific logic. If future iterations want to expand thefindcheck to allisCriticalRecursiveDeletePathtokens, it would be straightforward — the function is already available. -
allowPathsfor thefindcheck: Thefind ... -deleteearly return does not consultallowPaths. This is fine becausefind / -deletein any form is catastrophic, but worth noting if an allow-path override forfindis ever needed.
New Tests Added
✅ blocks critical recursive-delete target: rm -rf $HOME
✅ blocks critical recursive-delete target: rm -rf ${HOME}
✅ blocks critical recursive-delete target: rm -rf ~/Documents
✅ blocks critical recursive-delete target: rm -rf /home/chetan
✅ blocks critical recursive-delete target: rm -rf /var/lib
✅ blocks find / -delete
✅ allows a configured home-relative path (~/scratch with allowPaths)
Reviewed by Hermes Agent
What
Extends
block-rm-rfcoverage to deny recursive deletion of critical paths:$HOME,${HOME}, and home-relative paths such as~/Documents/home/<user>and/Users/<user>/var/lib,/var/log,/usr/lib, and/usr/localfind / -deleteand equivalent home-root targetsAn explicit
allowPathsentry still permits the configured home-relative path.Why
The policy previously recognized only the root,
~, and single-segment absolute paths. Common destructive forms could bypass it while ordinary project cleanup should remain allowed.Addresses #520.
Validation
npx vitest run __tests__/hooks/builtin-policies.test.ts -t "block-rm-rf"— 37 passednpx eslint src/hooks/builtin-policies.ts __tests__/hooks/builtin-policies.test.tsnpx tsc --noEmitThe full policy test file still has an unrelated pre-existing failure in
block-read-outside-cwdallow-path handling. Docker smoke testing was unavailable because Docker Desktop's local API did not become ready.Summary by CodeRabbit
find ... -deletecommands targeting critical directories.~/scratch, remain permitted.