acpmux: a new remote Claude session needs a checkout or scratch folder
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 fromnew_session_with_idon 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
.gititself 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~/.cacheis 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.
- A git work tree is any folder that has
- The refusal is
invalid_paramswithreason: 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 (
.gitfile) are fine; - a temp subfolder is fine; the temp root, a remote chain's own
acpmux-remote-*folder and~/.cache/<lane>-tmpare 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