session adopt: refuse a session that is not this command's caller
Commit
session adopt moves the named session into the current worktree and resets its checkpoint bookkeeping โ StepCount, CheckpointTranscriptStart, LastCheckpointID, FilesTouched. Naming the wrong session therefore mutates a third party's running session and silently detaches their next commit from it. That is the damage in the report this series came from: an agent read a session ID out of entire session current, which used to answer with a foreign worktree's live session, and adopted it.
strategy.ResolveCallerSession (#2322) made the caller knowable and SessionResolution.IsCaller() was added as the gate for commands that act on a session rather than display one. This wires the one command that acts.
None of the pre-existing checks could have caught it, which is worth stating because they look like they should. sessionBelongsToSourceWorktree asks only whether the ID and the worktree agree with each other โ and any pair read out of session current satisfies that by construction, since they were reported together. isAdoptableSourceSession asks only whether the session is still live, and being live is precisely what makes hijacking it harmful. Neither asks "is this mine".
Three outcomes. A session the resolver confirms is the caller's own is adopted silently, because that is what the command is for: an agent whose work moved to another worktree adopting its own session. A session positively identified as someone else's, or one whose ownership cannot be confirmed at all, is refused โ a terminal gets a confirmation naming both sessions, and without one the command fails. Auto-selection goes through the same gate, which matters because adopt --from <path> needs no session ID at all and was reachable without session current ever being run.
No terminal means refuse rather than proceed. The incident was an agent running this non-interactively, so a prompt nobody can see must not count as consent. Unconfirmable also covers every cross-machine --from, where process ancestry cannot speak to another host.
--allow-foreign-session is the escape hatch and is deliberately not folded into --force/--yes. Those already carry two meanings (replace local state; confirm same-store adoption), and --force is the flag an agent reaches for on any refusal โ so waiving a safety check on someone else's live session gets a name that says that, pinned by a test.
The existing adoption tests now declare ownership through adoptAsCaller rather than the override, so they exercise the supported path; go test inherits the developer's real agent variable, which names a session that is emphatically not the fixture.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Entire-Checkpoint: 01M2389BRDDWK3DT2913TCPBNR