acpmux: a new remote Claude session needs a checkout or scratch folder

TE
1d ago
+209-0

A remote chain's Claude Code runs with CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 (hub/remote_sandbox.rs). In that mode Claude creates empty placeholder files in its working folder at every start: package.json, the npm/yarn/pnpm lockfiles, bunfig.toml, .npmrc, .yarnrc*, .gitmodules, the .env* files and node_modules/.bin.

A session created with acpmux new --machine X --cwd ~/Projects (a folder that holds checkouts) therefore leaves an empty ~/Projects/package.json above every checkout. Workspace-aware tools in those checkouts (vp fmt, package managers) then treat it as the parent workspace and fail with Failed to parse JSON file at .../package.json: EOF while parsing a value.

Change

  • New hub/remote_cwd.rs, called from new_session_with_id on the receiving daemon, which is the machine that can read the folder.
  • It refuses to create a remote-origin Claude Code session (HarnessKind::ClaudeStdio) whose folder is neither inside a git work tree nor a scratch folder.
    • A git work tree is any folder that has .git itself or above it. Linked worktrees and submodules count.
    • A scratch folder is one strictly below the system temp dir, never the temp dir itself and never a remote chain's own acpmux-remote-* folder. That is the scratch the remote sandbox (remote_sandbox::refused_folder) accepts too; it refuses top-level dot folders in the home directory, so ~/.cache is not scratch here (a session admitted there would fail at its first spawn).
    • Symlinks are resolved first, so a link is judged by where it leads.
  • The refusal is invalid_params with reason: remote.cwd_not_checkout. It names the cause and both ways out, for example: "a remote Claude session needs a git checkout or a scratch folder as its working folder: Claude creates empty placeholder files (package.json, lockfiles, .env*) in its working folder, and /home/u/Projects isn't a checkout. Start it in a checkout, or a scratch folder named after the lane such as /tmp/-tmp."
  • Creation only. An existing session's respawn, resume and transfer never pass through this check, so sessions already created in such a folder keep working.
  • Unchanged:
    • local sessions;
    • other harnesses (ACP, terminal);
    • the trigger bridge's fixed-id chats (server/trigger_chat.rs), which keep their own folder rule.

Tests

Unit tests live in hub/remote_cwd.rs. The integration-test HOME sits inside the repository checkout, so every integration-test folder already counts as a checkout. The gate is therefore one pure function (admit_new) that the call site invokes, and the tests cover:

  • a folder above the checkouts (and a home) is refused with the cause, the suggestion and the reason;
  • a checkout, a folder inside one, and a linked worktree (.git file) are fine;
  • a temp subfolder is fine; the temp root, a remote chain's own acpmux-remote-* folder and ~/.cache/<lane>-tmp are refused;
  • a symlink into a checkout is fine; a link in the temp dir pointing to a non-checkout is refused;
  • only a new remote Claude session is checked: local, trigger and ACP/terminal sessions are unchanged.

Red and green, focused hosted verification (remote_cwd):

  • At 0e77673d7f4b (the gate stubbed to admit everything): only_a_new_remote_claude_session_is_checked ... FAILED; the other four pass.
  • At the head: see the run linked below.

Observed / Not run

  • Observed: the hosted focused runs above.
  • Not run: the full cmux-tui suite locally. The change adds one check on the creation path and no other behaviour.

Changelog

Changed: acpmux refuses to create a remote Claude Code session in a folder that is neither a git checkout nor a scratch folder, naming why.

🤖 Generated with Claude Code

Agent session: agent session