diff --git a/src/core/auto-approval/__tests__/commands.spec.ts b/src/core/auto-approval/__tests__/commands.spec.ts new file mode 100644 index 0000000000..d7264f7062 --- /dev/null +++ b/src/core/auto-approval/__tests__/commands.spec.ts @@ -0,0 +1,32 @@ +import { containsDangerousSubstitution, getCommandDecision } from "../commands" + +describe("containsDangerousSubstitution()", () => { + it("does not flag Python assignment grouping '=(' inside heredoc content", () => { + const cmd = [ + "python - <<'PY'", + "import json, pathlib", + "data = {'notes': ''}", + "data['notes']=(data.get('notes','')+'; policy set to linked-only by user').lstrip('; ')", + "print(data['notes'])", + "PY", + ].join("\n") + + expect(containsDangerousSubstitution(cmd)).toBe(false) + }) + + it("does not flag simple assignment grouping pattern x=(...)", () => { + expect(containsDangerousSubstitution("x=(1+2)")).toBe(false) + }) + + it("does flag zsh process substitution when used as an argument", () => { + expect(containsDangerousSubstitution("cat =(echo hi)")).toBe(true) + }) +}) + +describe("getCommandDecision()", () => { + it("auto-approves allowed heredoc command when no dangerous substitution is present", () => { + const cmd = ["python - <<'PY'", "x=(1+2)", "print(x)", "PY"].join("\n") + + expect(getCommandDecision(cmd, ["python"], [])).toBe("auto_approve") + }) +}) diff --git a/src/core/auto-approval/commands.ts b/src/core/auto-approval/commands.ts index 83a80cab0f..f22fc52f32 100644 --- a/src/core/auto-approval/commands.ts +++ b/src/core/auto-approval/commands.ts @@ -46,7 +46,9 @@ export function containsDangerousSubstitution(source: string): boolean { // Check for zsh process substitution =(...) which executes commands // =(...) creates a temporary file containing the output of the command, but executes it - const zshProcessSubstitution = /=\([^)]+\)/.test(source) + // Only match when preceded by whitespace or command separators (argument position), + // not when preceded by identifiers/brackets (assignment position like x=(...) or data['key']=(...)) + const zshProcessSubstitution = /(^|[\s&|;])=\([^)]+\)/.test(source) // Check for zsh glob qualifiers with code execution (e:...:) // Patterns like *(e:whoami:) or ?(e:rm -rf /:) execute commands during glob expansion diff --git a/src/shared/parse-command.ts b/src/shared/parse-command.ts index a8d87b76ac..06ab305ea2 100644 --- a/src/shared/parse-command.ts +++ b/src/shared/parse-command.ts @@ -2,6 +2,30 @@ import { parse } from "shell-quote" export type ShellToken = string | { op: string } | { command: string } +type HeredocStart = { + delimiter: string + stripLeadingTabs: boolean +} + +function parseHeredocStart(line: string): HeredocStart | null { + // Matches: + // - <