From 54b2e3856415a6c0738157ab8454d2c21f32aa68 Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 3 Aug 2026 20:54:41 +0000 Subject: [PATCH 1/4] fix(coding-agent): allowed quoted bash pattern metacharacters - Replaced the raw character guard with quote-aware scanning while retaining command substitution and unquoted shell-control protections. - Added regression coverage for the reported Cargo benchmark filter. Fixes #7552 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/tools/bash.ts | 48 ++++++++++++++++++- .../coding-agent/test/tools/approval.test.ts | 10 ++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 40b91d9dd..ad7957f08 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,7 @@ - Fixed bug where `agent()` calls in eval cells ignored turn cancellation and continued running indefinitely - Fixed the built-in `tail` printing `tail: Broken pipe` and failing when a downstream pipeline reader exited early (e.g. `tail -c N file.jsonl | jq …` with jq aborting on a parse error); it now exits silently with 141 (128+SIGPIPE) like a real tail, in every output path including `--follow`. - Fixed the in-process ps shell builtin rejecting common procps/BSD format specifiers (`ps -o tpgid,...` failed with `unknown output format specifier`); added `tpgid`, `pri`, `flags`, real/effective user and group columns, `wchan`, fault counters, `sz`, and the STAT `+` foreground flag. +- Fixed `bash.patterns` allow rules rejecting simple commands when quoted arguments contained shell metacharacters such as Cargo benchmark regex filters ([#7552](https://github.com/can1357/oh-my-pi/issues/7552)). ## [17.2.6] - 2026-08-03 diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 776cbfcaa..f051fde50 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -58,7 +58,51 @@ export const BASH_DEFAULT_PREVIEW_LINES = DEFAULT_TERMINAL_PREVIEW_LINES; const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000; -const BASH_APPROVAL_SHELL_CONTROL_RE = /[\n\r;&|<>`$()]/u; +const BASH_APPROVAL_SHELL_CONTROL_CHARS: Record = { + "\n": true, + "\r": true, + ";": true, + "&": true, + "|": true, + "<": true, + ">": true, + "`": true, + $: true, + "(": true, + ")": true, +}; + +function hasBashApprovalShellControl(command: string): boolean { + let quote: "'" | '"' | undefined; + for (let i = 0; i < command.length; i++) { + const ch = command[i]; + if (quote === "'") { + if (ch === "'") quote = undefined; + continue; + } + if (ch === "\\") { + i++; + continue; + } + if (quote === '"') { + if (ch === '"') { + quote = undefined; + continue; + } + // Expansion remains active inside double quotes; other control-looking + // characters are literal argument text. + if (ch === "`" || ch === "$") return true; + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + continue; + } + if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) return true; + } + return false; +} + const BASH_PATTERN_APPROVAL_VALUES = new Set(["allow", "deny", "prompt"]); /** @@ -218,7 +262,7 @@ function commandSegmentMatchesBashApprovalPattern(command: string, pattern: stri // `prompt` fire on any matching segment so they mean what they appear to. function bashApprovalRuleMatches(command: string, rule: BashApprovalPatternRule): boolean { if (rule.approval === "allow") { - if (BASH_APPROVAL_SHELL_CONTROL_RE.test(command)) return false; + if (hasBashApprovalShellControl(command)) return false; return commandMatchesBashApprovalPattern(command, rule.match); } return commandSegmentMatchesBashApprovalPattern(command, rule.match); diff --git a/packages/coding-agent/test/tools/approval.test.ts b/packages/coding-agent/test/tools/approval.test.ts index c222a1713..acfbfaa0c 100644 --- a/packages/coding-agent/test/tools/approval.test.ts +++ b/packages/coding-agent/test/tools/approval.test.ts @@ -347,6 +347,16 @@ describe("tool-owned dynamic approval declarations", () => { } }); + it("allows literal shell metacharacters in quoted arguments", () => { + const settingsOverrides = { + "bash.patterns": [{ match: "cargo *", approval: "allow" }], + }; + const command = + "cargo bench --manifest-path layers/layer3/Cargo.toml --bench standardized_criterion -- --full '^layer3/write/file-wal/batch-(10|1000|10000)$'"; + + expect(bashApproval(command, settingsOverrides)).toEqual({ tier: "write", policy: "allow" }); + }); + it("honors bash pattern rules in yolo mode", () => { const tool = createBashTool({ "bash.patterns": [ From 9bcd31fcd2bd7fc015dd41c28e26f71bd45436b2 Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 3 Aug 2026 21:08:52 +0000 Subject: [PATCH 2/4] fix(coding-agent): blocked reinterpreted quoted shell payloads - Kept quoted shell controls prompt-gated when code-evaluation options can reinterpret the argument. - Covered the reported git inline shell-alias bypass while retaining Cargo regex approval. Fixes #7552 --- packages/coding-agent/src/tools/bash.ts | 12 ++++++++++-- packages/coding-agent/test/tools/approval.test.ts | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index f051fde50..1bf747195 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -71,13 +71,19 @@ const BASH_APPROVAL_SHELL_CONTROL_CHARS: Record = { "(": true, ")": true, }; +const BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE = /(?:^|[ \t])(?:-[^-]*[ce]|--(?:command|eval))(?:[= \t]|$)/u; function hasBashApprovalShellControl(command: string): boolean { let quote: "'" | '"' | undefined; + let hasQuotedShellControl = false; for (let i = 0; i < command.length; i++) { const ch = command[i]; if (quote === "'") { - if (ch === "'") quote = undefined; + if (ch === "'") { + quote = undefined; + } else if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) { + hasQuotedShellControl = true; + } continue; } if (ch === "\\") { @@ -100,7 +106,9 @@ function hasBashApprovalShellControl(command: string): boolean { } if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) return true; } - return false; + // Options such as `git -c alias.x='!...'` and `sh -c '...'` reinterpret + // otherwise literal single-quoted arguments as executable code. + return hasQuotedShellControl && BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE.test(command); } const BASH_PATTERN_APPROVAL_VALUES = new Set(["allow", "deny", "prompt"]); diff --git a/packages/coding-agent/test/tools/approval.test.ts b/packages/coding-agent/test/tools/approval.test.ts index acfbfaa0c..1edd69fac 100644 --- a/packages/coding-agent/test/tools/approval.test.ts +++ b/packages/coding-agent/test/tools/approval.test.ts @@ -329,6 +329,7 @@ describe("tool-owned dynamic approval declarations", () => { "git $(rm file.txt)", "git `rm file.txt` status", "git status > /etc/passwd", + "git -c alias.x='!touch /tmp/pwn; printf ok' x", "git status < seed", // Different binary resolution than the pattern names. "FOO=1 git status", From b621a9a637ba8208a1ea78005a797662180095fa Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 3 Aug 2026 21:13:13 +0000 Subject: [PATCH 3/4] fix(coding-agent): flagged double-quoted reinterpreted payloads - Treated double-quoted shell-control chars like single-quoted ones so a -c/-e reinterpretation option still gates them. - Covered the double-quoted git inline shell-alias bypass. Fixes #7552 --- packages/coding-agent/src/tools/bash.ts | 10 ++++++---- packages/coding-agent/test/tools/approval.test.ts | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 1bf747195..500182d93 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -95,9 +95,11 @@ function hasBashApprovalShellControl(command: string): boolean { quote = undefined; continue; } - // Expansion remains active inside double quotes; other control-looking - // characters are literal argument text. + // Expansion is active inside double quotes even in the original line. if (ch === "`" || ch === "$") return true; + // Other control characters are literal here but become executable if a + // `-c`/`-e` option reinterprets the argument through another shell. + if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) hasQuotedShellControl = true; continue; } if (ch === "'" || ch === '"') { @@ -106,8 +108,8 @@ function hasBashApprovalShellControl(command: string): boolean { } if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) return true; } - // Options such as `git -c alias.x='!...'` and `sh -c '...'` reinterpret - // otherwise literal single-quoted arguments as executable code. + // Options such as `git -c alias.x='!...'` and `sh -c "..."` reinterpret + // otherwise literal quoted arguments as executable code. return hasQuotedShellControl && BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE.test(command); } diff --git a/packages/coding-agent/test/tools/approval.test.ts b/packages/coding-agent/test/tools/approval.test.ts index 1edd69fac..8b75b3c5c 100644 --- a/packages/coding-agent/test/tools/approval.test.ts +++ b/packages/coding-agent/test/tools/approval.test.ts @@ -330,6 +330,7 @@ describe("tool-owned dynamic approval declarations", () => { "git `rm file.txt` status", "git status > /etc/passwd", "git -c alias.x='!touch /tmp/pwn; printf ok' x", + 'git -c alias.x="!touch /tmp/pwn; printf ok" x', "git status < seed", // Different binary resolution than the pattern names. "FOO=1 git status", From 98a65ffbdfc3103e47152bddd0de83e5fc35e7b2 Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 3 Aug 2026 21:18:35 +0000 Subject: [PATCH 4/4] fix(coding-agent): blocked escaped reinterpreted payloads - Preserved escaped control characters for the downstream reinterpretation safety decision. - Covered the backslash-escaped git inline shell-alias bypass. Fixes #7552 --- packages/coding-agent/src/tools/bash.ts | 14 +++++++++----- packages/coding-agent/test/tools/approval.test.ts | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 500182d93..9a4207a2b 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -75,18 +75,22 @@ const BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE = /(?:^|[ \t])(?:-[^-]*[ce]|--(?:c function hasBashApprovalShellControl(command: string): boolean { let quote: "'" | '"' | undefined; - let hasQuotedShellControl = false; + let hasReinterpretableShellControl = false; for (let i = 0; i < command.length; i++) { const ch = command[i]; if (quote === "'") { if (ch === "'") { quote = undefined; } else if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) { - hasQuotedShellControl = true; + hasReinterpretableShellControl = true; } continue; } if (ch === "\\") { + const escaped = command[i + 1]; + if (escaped && Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, escaped)) { + hasReinterpretableShellControl = true; + } i++; continue; } @@ -99,7 +103,7 @@ function hasBashApprovalShellControl(command: string): boolean { if (ch === "`" || ch === "$") return true; // Other control characters are literal here but become executable if a // `-c`/`-e` option reinterprets the argument through another shell. - if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) hasQuotedShellControl = true; + if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) hasReinterpretableShellControl = true; continue; } if (ch === "'" || ch === '"') { @@ -109,8 +113,8 @@ function hasBashApprovalShellControl(command: string): boolean { if (Object.hasOwn(BASH_APPROVAL_SHELL_CONTROL_CHARS, ch)) return true; } // Options such as `git -c alias.x='!...'` and `sh -c "..."` reinterpret - // otherwise literal quoted arguments as executable code. - return hasQuotedShellControl && BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE.test(command); + // otherwise literal quoted or escaped arguments as executable code. + return hasReinterpretableShellControl && BASH_APPROVAL_REINTERPRETED_ARGUMENT_RE.test(command); } const BASH_PATTERN_APPROVAL_VALUES = new Set(["allow", "deny", "prompt"]); diff --git a/packages/coding-agent/test/tools/approval.test.ts b/packages/coding-agent/test/tools/approval.test.ts index 8b75b3c5c..26d829b92 100644 --- a/packages/coding-agent/test/tools/approval.test.ts +++ b/packages/coding-agent/test/tools/approval.test.ts @@ -331,6 +331,7 @@ describe("tool-owned dynamic approval declarations", () => { "git status > /etc/passwd", "git -c alias.x='!touch /tmp/pwn; printf ok' x", 'git -c alias.x="!touch /tmp/pwn; printf ok" x', + "git -c alias.x=!touch\\ /tmp/pwn\\;\\ printf\\ ok x", "git status < seed", // Different binary resolution than the pattern names. "FOO=1 git status",