Skip to content

fix(agent-core): keep Bash cwd applied to commands containing & - #2957

Open
pucedoteth wants to merge 1 commit into
MoonshotAI:mainfrom
pucedoteth:fix-bash-cwd-background-operator
Open

fix(agent-core): keep Bash cwd applied to commands containing &#2957
pucedoteth wants to merge 1 commit into
MoonshotAI:mainfrom
pucedoteth:fix-bash-cwd-background-operator

Conversation

@pucedoteth

Copy link
Copy Markdown

Closes #2890.

Problem

Both Bash tools build the shell invocation by string concatenation:

`cd ${shellQuote(shellCwd)} && ${command}`

In POSIX shell grammar && binds tighter than &, so the cd gets swallowed into the command's first AND-list:

cd /d && a & b     parses as     { cd /d && a } & b

{ cd /d && a } runs in a background subshell, so the parent shell's directory never changes and b runs wherever the session already was — not in the requested cwd. Verified against bash:

$ bash -c "cd '/tmp/cwdtest/target' && pwd & pwd"
/tmp/cwdtest             <- wrong: original directory
/tmp/cwdtest/target

; and a trailing & are unaffected — cd mutates the shell's own directory, and a whole-command background job still contains the cd. It is specifically an & between segments that loses it.

Fix

Terminate the cd on its own line so it stays a top-level command and the requested directory applies to everything that follows:

`cd ${shellQuote(shellCwd)} || exit 1\n${command}`
$ bash -c "cd '/tmp/cwdtest/target' || exit 1
pwd & pwd"
/tmp/cwdtest/target
/tmp/cwdtest/target

Everything else is byte-identical in behaviour. I checked each case against a real shell:

case before after
a & b b in wrong dir both in requested dir
failing cd bash: cd: …: No such file or directory, exit 1 same message, exit 1
trailing & correct correct
a ; b correct correct
heredoc correct correct
comment-only correct correct

Scope

Both engines built the string identically, so both are fixed — agent-core (v1) and agent-core-v2.

acp-server references the old shape in three doc comments, which I updated. Its actual matching logic is unaffected: isBashToolInvocation keys off args.length === 3 && args[1] === '-c' plus the noninteractive env, and terminal correlation uses event.shellCommand.endsWith(command) — the invocation still ends with the model's command under the new form.

I considered passing cwd through ProcessExecOptions (the runner already supports it) instead of a shell cd. I did not, because it moves the failure mode for a nonexistent directory from a shell-level message + exit 1 to a spawn-level ENOENT, and needs the native Windows path rather than the windowsPathToPosixPath form used inside the shell. That felt like a bigger change than this bug warrants — happy to switch if you'd prefer it.

Tests

Added keeps cwd applied to commands containing a background operator to the v2 suite. It fails on main:

AssertionError: expected 'cd \'/workspace\' && sleep 1 & pwd'
                to be 'cd \'/workspace\' || exit 1\nsleep 1 & pwd'

13 existing assertions pinned the old literal string across three test files; those are updated to the new form.

  • vitest run on the three bash/shell test files: 133 passed
  • oxlint --type-aware on the five touched files: 0 warnings, 0 errors
  • Full vitest run: 1110 passed | 1 failed — the failure is kap-server test/prompts.test.ts > compresses inline base64 image prompts into session media-originals, which is unrelated to this change and flaky under full-suite parallelism: it passes in isolation both with my change and on a clean checkout (28/28 both ways), and a second full run failed a different set of tests.

Changeset included (patch on @moonshot-ai/kimi-code and @moonshot-ai/kimi-code-sdk — both bundle the affected internal packages).

The shell invocation was built as `cd <cwd> && <command>`. In POSIX shell
grammar `&&` binds tighter than `&`, so the cd is swallowed into the
command's first AND-list:

  cd /d && a & b   parses as   { cd /d && a } & b

`{ cd /d && a }` runs in a background subshell, leaving the parent
shell's directory unchanged, so `b` runs wherever the session already
was rather than in the requested cwd. Verified against bash:

  $ bash -c "cd '/tmp/cwdtest/target' && pwd & pwd"
  /tmp/cwdtest            <- wrong
  /tmp/cwdtest/target

Terminating the cd on its own line keeps it a top-level command, so the
requested directory applies to the whole command:

  $ bash -c "cd '/tmp/cwdtest/target' || exit 1
  pwd & pwd"
  /tmp/cwdtest/target
  /tmp/cwdtest/target

Behaviour is otherwise unchanged: a failing cd still writes the same
message to stderr and exits 1, and `;`, trailing `&`, heredocs and
comment-only commands all behave as before.

Both engines built the string the same way, so both are fixed. The three
acp-server references to the old shape are doc comments only — the
matching logic there keys off `args[1] === '-c'` and
`shellCommand.endsWith(args.command)`, both of which still hold.
@changeset-bot

changeset-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 02a61eb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/kimi-code Patch
@moonshot-ai/kimi-code-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02a61eb3d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +197 to +201
// `cd … && <command>` binds the cd into the command's first AND-list, so a
// command containing `&` runs everything after it in the shell's original
// directory (`cd /d && a & b` parses as `{ cd /d && a } & b`). Terminating
// the cd on its own line keeps the requested cwd applied to the whole
// command; a failing cd still prints to stderr and exits non-zero.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move the explanation to the file header

In this v2 file, the nearest AGENTS guidance requires comments to live only in the top-of-file block and never beside statements. This newly added statement comment therefore violates the local convention; please either fold the rationale into the existing header or remove it, keeping the executable line self-contained.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L36-L38

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bash tool cwd is silently ignored when the command starts with cmd &

1 participant