diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index fc5a17ede..f5d5c088d 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -484,8 +484,10 @@ export class BashTool implements AgentTool { const env = normalizeBashEnv(rawEnv); // Extract leading `cd && ...` into cwd when the model ignores the cwd parameter. + // Constrained to a single line so a `&&` that sits on a later line of a multiline + // script can't pull the entire script into the "cwd" capture. if (!cwd) { - const cdMatch = command.match(/^cd\s+((?:[^&\\]|\\.)+?)\s*&&\s*/); + const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/); if (cdMatch) { cwd = cdMatch[1].trim().replace(/^["']|["']$/g, ""); command = command.slice(cdMatch[0].length); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 8d0da09d4..ea641d1fc 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -1288,6 +1288,23 @@ function b() { ); }); + it("should not pull cwd from a later-line `&&` when the command is multiline", async () => { + // Regression for #?: the `^cd ... && ...` extractor used `\s` and `[^&\\]`, + // which let the lazy match cross newlines and capture the whole script as the + // "cwd" when any later line contained `&&`. The model intended `cd` to run as + // part of a multiline script, not to relocate the entire command. + const command = [ + "cd /this/directory/definitely/does/not/exist/12345", + "echo first-line", + "echo second && echo third", + ].join("\n"); + const result = await bashTool.execute("test-call-multiline-cd", { command }); + const output = getTextOutput(result); + expect(output).toContain("first-line"); + expect(output).toContain("second"); + expect(output).toContain("third"); + }); + it("should expose background-job tools when bash auto-background is enabled", () => { const autoBackgroundSession = createTestToolSession( testDir,