105 new em-dashes sat in comments, test names and one test fixture from the
foreign tmux adoption work. Replaced by hand with commas, colons or
parentheses, one reading at a time. The fixture now quotes the real refusal
text, which says "It may still be running, try again."
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`POST /api/sessions/:id/run` had no adopted gate, so while the wrapper had no
attached PTY it spawned `claude -p --dangerously-skip-permissions <prompt>`
in /tmp, a launch every other adopted route refuses.
The route answers INVALID_INPUT, and `runPrompt()` refuses too, which also
covers the legacy `sendInput()` path and leaves no direct spawn in Session
that an adopted session can reach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wrapper that could not start fell back to a direct PTY: `claude
--dangerously-skip-permissions` or a server-account shell in /tmp on this
host, under a tab still carrying the foreign session's name, so someone who
opened an ssh or container session ended up typing into this machine.
startInteractive and startShell now fail the start for an adopted session
and leave `_useMux` on, so selecting the tab tries the wrapper again. The
`error` event carries the reason, since the browser's re-attach does not
read the route's answer. A session with no mux at all is refused the same
way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
startShell() writes `clear\n` 100 ms after a new mux attach, and for an adopted
session that write reaches someone else's live pane through the wrapper's tmux
client. Measured on an isolated instance: a shell with `echo DRAFT_NOT_SUBMITTED_`
typed at its prompt ran `echo DRAFT_NOT_SUBMITTED_clear` the moment the tab
opened. `shell` is the feature's primary case and also the classifier's answer
for anything it cannot name (vim, psql, a `read` prompt), so every adoption put
those six bytes in front of whatever was reading stdin.
After the fix a byte recorder in the foreign pane saw 0 bytes in the 6 s after
adopting, and a real input afterwards arrived intact. Not in the review: its
recorder covered the trust-dialog path, which is `claude` mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wrapper's workingDir is the neutral /tmp, and everything master grew since
1.33.2 that branches on session.remote or session.docker read that as a local
session working in /tmp, because an adopted session keeps its connection on
adopt.remote and adopt.docker.
So uploads wrote into /tmp and closing the tab recursively deleted the shared
folder; the Git panel, its fetch and the fetch scheduler ran git on whatever sat
there; the image watcher, Read My Mind and the Discord bridge used that folder;
last-response paired a transcript by it and could show another conversation's
answer as this one's; a magic link in a container's output registered a file
from this host's /tmp; and guarded input could type into a human's composer.
Each now refuses or answers unsupported, the browser hides the rows that cannot
work, and Session Options shows where the foreign pane was instead of /tmp.
The custom-model route refuses as well, since it restarts the CLI in the pane.
The local version probe is skipped: the local binary says nothing about a
process someone else started, possibly on another host.
Also moves the guard test to port 0, which test-ports-guard now requires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review items 3, 4 and the smaller ones on #510. An adopted session wraps a
process someone else launched, and its mode is whatever the probe saw — very
often `claude`. Every guard below is therefore its own `isAdopted` check rather
than a side effect of the external-CLI rule: the two ask different questions,
and merging them means the day the first loosens, Codeman starts acting on a
live conversation that is not ours.
- The boot workspace-hook sweep skipped only non-claude and `session.remote`.
An adopted session keeps its connection on `adopt.remote` / `adopt.docker`,
so a tab wrapping someone's in-container or over-ssh claude looked local and
Codeman wrote `.claude/settings.local.json` into their repo on every restart.
- The workspace-trust auto-accept read the pane and pressed keys for 90s after
`startInteractive()`. In an adopted pane that is someone else's dialog, and
boot recovery re-opens the window on every restart.
- Auto-clear, auto-compact, auto-resume and the Ralph auto-enable in
`POST /interactive` all drove the pane. Each now refuses with its own gate.
- The wrapper's `workingDir` was the foreign pane's cwd. That cwd can carry
characters outside SAFE_PATH_PATTERN (`~/c++`, `Program Files (x86)`), which
makes `createSession` throw so the tab never starts, and for a container or
ssh candidate it is not a path on this host at all. It is now a neutral local
`/tmp`, the same cwd `resolveMuxAttachCwd` already gives the pane; the foreign
cwd stays on `adopt.paneCurrentPath` for display.
- The CLI signature table hardcoded ids, which CLAUDE.md forbids outside
`stock.ts`, and had already gone stale: `omp` was missing, so a hand-started
omp pane read as `shell`. It is now derived from each entry's
`discovery.binaries` at call time, regex-escaped, longest name first. Stock
entries only, since `SessionMode` is the closed union of stock ids.
- The descendant walk now passes `onTruncated`. A capped walk returns "nothing
matched", which is byte-identical to the answer for a real shell.
- The home screen's wide scan is one-shot. Riding `docker exec` and a full ssh
handshake per target, each with a ~12s timeout, on the 8s poll stacked
connections on a slow host. Wide rows are kept apart and merged, because a
local-only payload cannot contain them.
- Adoption failures report the server's own message. `_apiJson` folds a non-2xx
to null, which collapsed "could not reach it just now" into "that session is
gone" and reported a live remote session as deleted on every link flicker.
Tests: route tests for the refusals, the admin gate, the 404 re-resolve, the
one-wrapper-per-target reuse and the adopt round-trip; unit tests for the two
non-HTTP guards, the registry-derived signatures, truncation reporting and the
frontend's scan and error paths. Each guard was removed in turn to confirm its
own tests go red.