From 51e7566b4ebf4168aa02b374c1e62eff844fb1df Mon Sep 17 00:00:00 2001 From: qqqys Date: Thu, 21 May 2026 10:42:23 +0800 Subject: [PATCH 1/7] fix(core): strip additional dangerous interpreter rules --- .../core/src/permissions/dangerousRules.test.ts | 13 +++++++++++++ packages/core/src/permissions/dangerousRules.ts | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/core/src/permissions/dangerousRules.test.ts b/packages/core/src/permissions/dangerousRules.test.ts index f21a43bf85..46befa7cde 100644 --- a/packages/core/src/permissions/dangerousRules.test.ts +++ b/packages/core/src/permissions/dangerousRules.test.ts @@ -158,6 +158,19 @@ describe('isDangerousBashRule', () => { expect(isDangerousBashRule(bashRule(interp))).toBe(true); }); + it.each([ + 'tsx -e *', + 'ssh prod-host -- *', + 'bunx -p dangerous-pkg *', + 'cmd /c *', + 'cmd.exe /c *', + 'bash.exe -c *', + 'powershell.exe -Command *', + 'pwsh.exe -Command *', + ])('flags Claude-aligned shell or runner wildcard %s', (s) => { + expect(isDangerousBashRule(bashRule(s))).toBe(true); + }); + it.each([ 'bun run *', 'deno run *', diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index 78d1cd4ed5..2f834a8bae 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -18,13 +18,15 @@ import type { PermissionRule } from './types.js'; /** * Tokens that, when used as the leading command of a Bash allow rule, let the * model execute arbitrary code under the AUTO classifier's nose. Covers - * shell interpreters, scripting-language interpreters, and build/package - * tools that themselves run arbitrary scripts (`cargo run`, `npm run`, …). - * Mirrors and extends ClaudeCode's `DANGEROUS_BASH_PATTERNS`. + * shell interpreters, scripting-language interpreters, remote shells, and + * build/package tools that themselves run arbitrary scripts (`cargo run`, + * `npm run`, …). Mirrors Claude Code's shell and scripting-interpreter + * checks, then extends them with additional security-relevant runners. */ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ // Shells 'bash', + 'bash.exe', 'sh', 'zsh', 'fish', @@ -32,14 +34,19 @@ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ 'tcsh', 'dash', 'ksh', + 'cmd', + 'cmd.exe', 'pwsh', + 'pwsh.exe', 'powershell', + 'powershell.exe', // Scripting-language interpreters 'python', 'python3', 'python2', 'node', 'deno', + 'tsx', 'bun', 'ruby', 'perl', @@ -69,10 +76,13 @@ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ // that without this list would be the cleanest way to bypass the // classifier in AUTO mode. 'npx', + 'bunx', 'pnpx', 'uvx', 'pipx', 'dlx', + // Remote shells + 'ssh', // Generic eval-y commands 'eval', 'exec', From 2be2e0bcf4e16e0e920a498c4650a609a859c0af Mon Sep 17 00:00:00 2001 From: qqqys Date: Thu, 21 May 2026 11:13:00 +0800 Subject: [PATCH 2/7] test(core): clarify dangerous interpreter coverage --- packages/core/src/permissions/dangerousRules.test.ts | 12 ++++++++---- packages/core/src/permissions/dangerousRules.ts | 12 +++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/core/src/permissions/dangerousRules.test.ts b/packages/core/src/permissions/dangerousRules.test.ts index 46befa7cde..52350cc037 100644 --- a/packages/core/src/permissions/dangerousRules.test.ts +++ b/packages/core/src/permissions/dangerousRules.test.ts @@ -158,16 +158,20 @@ describe('isDangerousBashRule', () => { expect(isDangerousBashRule(bashRule(interp))).toBe(true); }); + it.each(['tsx -e *', 'ssh prod-host -- *', 'bunx -p dangerous-pkg *'])( + 'flags Unix shell or runner wildcard %s', + (s) => { + expect(isDangerousBashRule(bashRule(s))).toBe(true); + }, + ); + it.each([ - 'tsx -e *', - 'ssh prod-host -- *', - 'bunx -p dangerous-pkg *', 'cmd /c *', 'cmd.exe /c *', 'bash.exe -c *', 'powershell.exe -Command *', 'pwsh.exe -Command *', - ])('flags Claude-aligned shell or runner wildcard %s', (s) => { + ])('flags Windows shell wildcard %s', (s) => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }); diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index 2f834a8bae..d58b6544d0 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -18,13 +18,14 @@ import type { PermissionRule } from './types.js'; /** * Tokens that, when used as the leading command of a Bash allow rule, let the * model execute arbitrary code under the AUTO classifier's nose. Covers - * shell interpreters, scripting-language interpreters, remote shells, and - * build/package tools that themselves run arbitrary scripts (`cargo run`, - * `npm run`, …). Mirrors Claude Code's shell and scripting-interpreter - * checks, then extends them with additional security-relevant runners. + * Unix and Windows shell interpreters, scripting-language interpreters, + * remote shells, and build/package tools that themselves run arbitrary + * scripts (`cargo run`, `npm run`, …). Mirrors Claude Code's shell and + * scripting-interpreter checks, then extends them with additional + * security-relevant runners. */ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ - // Shells + // Unix shells 'bash', 'bash.exe', 'sh', @@ -34,6 +35,7 @@ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ 'tcsh', 'dash', 'ksh', + // Windows shells 'cmd', 'cmd.exe', 'pwsh', From 9a62aa7af97c0e4de4041b88ddf76283bede152a Mon Sep 17 00:00:00 2001 From: qqqys Date: Thu, 21 May 2026 16:03:40 +0800 Subject: [PATCH 3/7] chore(core): group bash.exe with windows shells --- packages/core/src/permissions/dangerousRules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index d58b6544d0..c274761e7d 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -27,7 +27,6 @@ import type { PermissionRule } from './types.js'; const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ // Unix shells 'bash', - 'bash.exe', 'sh', 'zsh', 'fish', @@ -36,6 +35,7 @@ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ 'dash', 'ksh', // Windows shells + 'bash.exe', 'cmd', 'cmd.exe', 'pwsh', From d16cab2aaeeef1a64108c6108ff71859bd1380ec Mon Sep 17 00:00:00 2001 From: qqqys Date: Sat, 23 May 2026 19:13:56 +0800 Subject: [PATCH 4/7] fix(core): normalize dangerous interpreter tokens --- .../src/permissions/dangerousRules.test.ts | 39 ++++++++++++++++++- .../core/src/permissions/dangerousRules.ts | 25 ++++++++---- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/core/src/permissions/dangerousRules.test.ts b/packages/core/src/permissions/dangerousRules.test.ts index 52350cc037..708fc6c7e3 100644 --- a/packages/core/src/permissions/dangerousRules.test.ts +++ b/packages/core/src/permissions/dangerousRules.test.ts @@ -159,12 +159,35 @@ describe('isDangerousBashRule', () => { }); it.each(['tsx -e *', 'ssh prod-host -- *', 'bunx -p dangerous-pkg *'])( - 'flags Unix shell or runner wildcard %s', + 'flags new interpreter, remote shell, or runner wildcard %s', (s) => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }, ); + it.each([ + 'tsx', + 'ssh', + 'bunx', + 'bash.exe', + 'cmd', + 'cmd.exe', + 'pwsh.exe', + 'powershell.exe', + ])('flags new interpreter, remote shell, or runner bare name %s', (s) => { + expect(isDangerousBashRule(bashRule(s))).toBe(true); + }); + + it.each([ + 'python.exe -c *', + 'node.exe -e *', + 'tsx.exe -e *', + 'bunx.exe -p dangerous-pkg *', + 'C:\\Python\\python.exe -c *', + ])('flags Windows executable suffix wildcard %s', (s) => { + expect(isDangerousBashRule(bashRule(s))).toBe(true); + }); + it.each([ 'cmd /c *', 'cmd.exe /c *', @@ -193,6 +216,20 @@ describe('isDangerousBashRule', () => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }); + it.each([ + 'tsx script.tsx', + 'ssh prod-host -- ls', + 'cmd /c script.bat', + 'cmd.exe /c script.bat', + 'pwsh.exe -File script.ps1', + 'powershell.exe -File script.ps1', + 'python.exe script.py', + 'node.exe script.js', + 'bunx eslint .', + ])('does NOT flag concrete commands using new tokens %s', (s) => { + expect(isDangerousBashRule(bashRule(s))).toBe(false); + }); + it('flags Monitor allow rules with the same interpreter logic', () => { // Monitor is a long-running shell-command runner; broad allow rules // on it bypass the AUTO classifier just like Bash(...) ones. diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index c274761e7d..c2750ce18a 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -20,9 +20,9 @@ import type { PermissionRule } from './types.js'; * model execute arbitrary code under the AUTO classifier's nose. Covers * Unix and Windows shell interpreters, scripting-language interpreters, * remote shells, and build/package tools that themselves run arbitrary - * scripts (`cargo run`, `npm run`, …). Mirrors Claude Code's shell and - * scripting-interpreter checks, then extends them with additional - * security-relevant runners. + * scripts (`cargo run`, `npm run`, …). The exact token set is intentionally + * self-contained so AUTO-mode stripping does not depend on an external + * upstream identifier. */ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ // Unix shells @@ -108,6 +108,7 @@ const SHELL_LIKE_TOOLS: readonly string[] = Object.freeze([ * - absolute-path forms (`/usr/bin/python3` → trailing segment `python3`) * - trailing-wildcard forms (`python3*`) * - colon form (`python:`) + * - Windows executable suffixes (`python.exe`) */ function isInterpreterToken(rawToken: string): boolean { if (!rawToken) return false; @@ -120,10 +121,18 @@ function isInterpreterToken(rawToken: string): boolean { end--; } const noWildcard = rawToken.slice(0, end); - const beforeColon = noWildcard.split(':')[0]; + const beforeColon = /^[a-z]:[\\/]/i.test(noWildcard) + ? noWildcard + : noWildcard.split(':')[0]; // Last path segment so `/usr/bin/python3` → `python3` - const lastSegment = (beforeColon ?? '').split('/').pop() ?? ''; - return DANGEROUS_BASH_INTERPRETERS.includes(lastSegment); + const lastSegment = (beforeColon ?? '').split(/[\\/]/).pop() ?? ''; + const withoutExe = lastSegment.endsWith('.exe') + ? lastSegment.slice(0, -'.exe'.length) + : lastSegment; + return ( + DANGEROUS_BASH_INTERPRETERS.includes(lastSegment) || + DANGEROUS_BASH_INTERPRETERS.includes(withoutExe) + ); } /** @@ -152,12 +161,12 @@ export function isDangerousBashRule(rule: PermissionRule): boolean { // dangerous when it appears as the first token of either form // (`python -c *` or `python:*`). For colon-form, the part after `:` is // the specifier — we'll separately check whether it's concrete below. - const firstToken = content.split(/[\s:]/)[0] ?? ''; + const firstToken = content.split(/\s/)[0] ?? ''; if (!isInterpreterToken(firstToken)) return false; // Bare interpreter name (`python`, `/usr/bin/python3`) — caller decides // what to do, classifier never sees it. Dangerous. - if (firstToken === content) return true; + if (firstToken === content && !content.includes(':')) return true; // Wildcard anywhere paired with an interpreter defeats the classifier: // `python *`, `python -c *`, `bun run *`, `/usr/bin/python3 *`, From cb9661d429028164e344d88c298ca76d30937bde Mon Sep 17 00:00:00 2001 From: qqqys Date: Sat, 23 May 2026 21:23:06 +0800 Subject: [PATCH 5/7] test(core): normalize dangerous exe interpreter rules --- .../core/src/permissions/dangerousRules.test.ts | 5 +++++ packages/core/src/permissions/dangerousRules.ts | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/core/src/permissions/dangerousRules.test.ts b/packages/core/src/permissions/dangerousRules.test.ts index 708fc6c7e3..6a92e2aa98 100644 --- a/packages/core/src/permissions/dangerousRules.test.ts +++ b/packages/core/src/permissions/dangerousRules.test.ts @@ -188,6 +188,11 @@ describe('isDangerousBashRule', () => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }); + it('normalizes Windows executable suffixes in both directions', () => { + expect(isDangerousBashRule(bashRule('cmd *'))).toBe(true); + expect(isDangerousBashRule(bashRule('cmd.exe *'))).toBe(true); + }); + it.each([ 'cmd /c *', 'cmd.exe /c *', diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index c2750ce18a..38f33f5fee 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -91,6 +91,10 @@ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ 'source', ]); +function stripWindowsExecutableSuffix(token: string): string { + return token.endsWith('.exe') ? token.slice(0, -'.exe'.length) : token; +} + /** * Tools whose allow rules carry shell-like risk. `monitor` is a long-running * shell-command runner and should be treated the same as `shell` for the @@ -126,12 +130,10 @@ function isInterpreterToken(rawToken: string): boolean { : noWildcard.split(':')[0]; // Last path segment so `/usr/bin/python3` → `python3` const lastSegment = (beforeColon ?? '').split(/[\\/]/).pop() ?? ''; - const withoutExe = lastSegment.endsWith('.exe') - ? lastSegment.slice(0, -'.exe'.length) - : lastSegment; - return ( - DANGEROUS_BASH_INTERPRETERS.includes(lastSegment) || - DANGEROUS_BASH_INTERPRETERS.includes(withoutExe) + const normalizedSegment = stripWindowsExecutableSuffix(lastSegment); + return DANGEROUS_BASH_INTERPRETERS.some( + (interpreter) => + stripWindowsExecutableSuffix(interpreter) === normalizedSegment, ); } From 1359fe5276b40f265324e4368e5e547e94d1a0e3 Mon Sep 17 00:00:00 2001 From: qqqys Date: Sat, 23 May 2026 23:17:37 +0800 Subject: [PATCH 6/7] fix(core): detect windows interpreter path allows --- .../core/src/permissions/dangerousRules.test.ts | 8 ++++++++ packages/core/src/permissions/dangerousRules.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/core/src/permissions/dangerousRules.test.ts b/packages/core/src/permissions/dangerousRules.test.ts index 6a92e2aa98..ecd11a6944 100644 --- a/packages/core/src/permissions/dangerousRules.test.ts +++ b/packages/core/src/permissions/dangerousRules.test.ts @@ -188,6 +188,14 @@ describe('isDangerousBashRule', () => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }); + it.each([ + 'C:\\Python\\python.exe', + 'C:\\nodejs\\node.exe', + 'C:\\Users\\me\\bin\\tsx.exe', + ])('flags bare Windows interpreter path %s', (s) => { + expect(isDangerousBashRule(bashRule(s))).toBe(true); + }); + it('normalizes Windows executable suffixes in both directions', () => { expect(isDangerousBashRule(bashRule('cmd *'))).toBe(true); expect(isDangerousBashRule(bashRule('cmd.exe *'))).toBe(true); diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index 38f33f5fee..e62d85722e 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -35,13 +35,9 @@ const DANGEROUS_BASH_INTERPRETERS: readonly string[] = Object.freeze([ 'dash', 'ksh', // Windows shells - 'bash.exe', 'cmd', - 'cmd.exe', 'pwsh', - 'pwsh.exe', 'powershell', - 'powershell.exe', // Scripting-language interpreters 'python', 'python3', @@ -159,16 +155,20 @@ export function isDangerousBashRule(rule: PermissionRule): boolean { const content = rule.specifier.trim().toLowerCase(); if (content === '' || content === '*') return true; - // Treat both whitespace and `:` as token delimiters: an interpreter is - // dangerous when it appears as the first token of either form + // Treat whitespace as the first-token delimiter; matcher-colon form is + // handled separately below because Windows drive letters also contain `:`. + // An interpreter is dangerous when it appears as the first token of either + // form // (`python -c *` or `python:*`). For colon-form, the part after `:` is // the specifier — we'll separately check whether it's concrete below. const firstToken = content.split(/\s/)[0] ?? ''; if (!isInterpreterToken(firstToken)) return false; + const hasMatcherColon = + content.includes(':') && !/^[a-z]:[\\/]/i.test(content); // Bare interpreter name (`python`, `/usr/bin/python3`) — caller decides // what to do, classifier never sees it. Dangerous. - if (firstToken === content && !content.includes(':')) return true; + if (firstToken === content && !hasMatcherColon) return true; // Wildcard anywhere paired with an interpreter defeats the classifier: // `python *`, `python -c *`, `bun run *`, `/usr/bin/python3 *`, @@ -182,7 +182,7 @@ export function isDangerousBashRule(rule: PermissionRule): boolean { // rules — same shape as `Bash(npm run test)`, which the docstring above // commits to NOT flagging. Strip them and we'd silently disable // intentional user allow lists in AUTO. - if (content.includes(':')) { + if (hasMatcherColon) { const afterColon = content.slice(content.indexOf(':') + 1).trim(); return afterColon === ''; } From e34ee857356cf07cfa80a2fcc9bd70c83dd7d6f7 Mon Sep 17 00:00:00 2001 From: qqqys Date: Sun, 24 May 2026 00:09:28 +0800 Subject: [PATCH 7/7] fix(core): detect windows interpreter path allows --- .../src/permissions/dangerousRules.test.ts | 6 ++++ .../core/src/permissions/dangerousRules.ts | 33 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/core/src/permissions/dangerousRules.test.ts b/packages/core/src/permissions/dangerousRules.test.ts index ecd11a6944..3046f891b1 100644 --- a/packages/core/src/permissions/dangerousRules.test.ts +++ b/packages/core/src/permissions/dangerousRules.test.ts @@ -184,6 +184,9 @@ describe('isDangerousBashRule', () => { 'tsx.exe -e *', 'bunx.exe -p dangerous-pkg *', 'C:\\Python\\python.exe -c *', + 'C:\\Python\\python.exe:*', + 'C:\\Python\\python:*', + 'C:\\Program Files\\Python\\python.exe -c *', ])('flags Windows executable suffix wildcard %s', (s) => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }); @@ -192,6 +195,7 @@ describe('isDangerousBashRule', () => { 'C:\\Python\\python.exe', 'C:\\nodejs\\node.exe', 'C:\\Users\\me\\bin\\tsx.exe', + 'C:\\Program Files\\Python\\python.exe', ])('flags bare Windows interpreter path %s', (s) => { expect(isDangerousBashRule(bashRule(s))).toBe(true); }); @@ -239,6 +243,8 @@ describe('isDangerousBashRule', () => { 'python.exe script.py', 'node.exe script.js', 'bunx eslint .', + 'C:\\Python\\python.exe script.py', + 'C:\\Program Files\\Python\\python.exe script.py', ])('does NOT flag concrete commands using new tokens %s', (s) => { expect(isDangerousBashRule(bashRule(s))).toBe(false); }); diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index e62d85722e..afc8e4672f 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -91,6 +91,25 @@ function stripWindowsExecutableSuffix(token: string): string { return token.endsWith('.exe') ? token.slice(0, -'.exe'.length) : token; } +function matcherColonIndex(content: string): number { + const firstColon = content.indexOf(':'); + if (firstColon < 0) return -1; + if (/^[a-z]:[\\/]/i.test(content)) { + return content.indexOf(':', 2); + } + return firstColon; +} + +function leadingCommandToken(content: string): string { + if (/^[a-z]:[\\/]/i.test(content)) { + const exeIndex = content.indexOf('.exe'); + if (exeIndex >= 0) { + return content.slice(0, exeIndex + '.exe'.length); + } + } + return content.split(/\s/)[0] ?? ''; +} + /** * Tools whose allow rules carry shell-like risk. `monitor` is a long-running * shell-command runner and should be treated the same as `shell` for the @@ -121,9 +140,9 @@ function isInterpreterToken(rawToken: string): boolean { end--; } const noWildcard = rawToken.slice(0, end); - const beforeColon = /^[a-z]:[\\/]/i.test(noWildcard) - ? noWildcard - : noWildcard.split(':')[0]; + const colonIndex = matcherColonIndex(noWildcard); + const beforeColon = + colonIndex >= 0 ? noWildcard.slice(0, colonIndex) : noWildcard; // Last path segment so `/usr/bin/python3` → `python3` const lastSegment = (beforeColon ?? '').split(/[\\/]/).pop() ?? ''; const normalizedSegment = stripWindowsExecutableSuffix(lastSegment); @@ -161,10 +180,10 @@ export function isDangerousBashRule(rule: PermissionRule): boolean { // form // (`python -c *` or `python:*`). For colon-form, the part after `:` is // the specifier — we'll separately check whether it's concrete below. - const firstToken = content.split(/\s/)[0] ?? ''; + const firstToken = leadingCommandToken(content); if (!isInterpreterToken(firstToken)) return false; - const hasMatcherColon = - content.includes(':') && !/^[a-z]:[\\/]/i.test(content); + const colonIndex = matcherColonIndex(content); + const hasMatcherColon = colonIndex >= 0; // Bare interpreter name (`python`, `/usr/bin/python3`) — caller decides // what to do, classifier never sees it. Dangerous. @@ -183,7 +202,7 @@ export function isDangerousBashRule(rule: PermissionRule): boolean { // commits to NOT flagging. Strip them and we'd silently disable // intentional user allow lists in AUTO. if (hasMatcherColon) { - const afterColon = content.slice(content.indexOf(':') + 1).trim(); + const afterColon = content.slice(colonIndex + 1).trim(); return afterColon === ''; }