Context. A session’s state belongs to the project the session is running in: Claude Code’s SessionStart hook writes .claude/wiki-knowledge/sessions/<session_id>.json, OpenCode’s session-tracker plugin writes the same shape under .opencode/. That project is never the vault — in query-from-anywhere mode the vault is somewhere else entirely (ADR-0004) — so “which project is this session’s?” is its own question with its own answer, and until now it had two: findSessionsDir (enchiridion-ts/src/sessionstate.ts) consulted $CLAUDE_PROJECT_DIR and stopped its .claude/ walk at $HOME, while openCodeSessionsDir (enchiridion-ts/src/transcriptcapture.ts) had no env override and no $HOME stop, ending in a cwd fallback. Same question, two answers, and the divergence was in the terminal rule — the part that has nothing to do with host layout (#493).
That divergence is not cosmetic. A cwd fallback is the exact mechanism of #485: a hook that resolved a root from wherever it happened to be standing scattered .claude/wiki-knowledge/sessions/ trees through content folders and split one session’s state across two directories. The rule that survived that fix — resolve the project, or write nothing — is the one worth having, and one of the two walks was still not applying it.
Decision — one order for the session root, with the host supplying only its layout. The session root resolves, highest priority first:
$CLAUDE_PROJECT_DIR for Claude Code, bound at session start so it does not move when the session runs cd or enters a worktree. OpenCode exports no equivalent, so this rule is skipped for it..claude/ or .opencode/. Writer and reader agree on a root even when cwd is a subdirectory.$HOME — ~/.claude and ~/.opencode are a host’s global configuration, not markers for a project — and returning “no project” is the answer, never a cwd fallback.Only two things differ per host: the marker directory and the env override. The walk, the $HOME stop and the refusal to guess are one shared rule (findProjectRoot), so a second host cannot grow a second answer to the same question.
Decision — this is not ADR-0004’s order, and the difference is the terminal rule. The vault root resolves $WIKI_ROOT → nearest ancestor with a vault marker (wiki/ or .wiki-root) → cwd itself. Same first two levels, opposite last one: a cwd fallback where this has a $HOME stop. So they are not one order, and the difference is deliberate rather than an oversight to be unified later.
The vault root’s fallback is load-bearing because of what its callers do: the commands that resolve it exist to operate on a vault, query-from-anywhere requires an answer for “which vault” even when cwd is not one, and enchiridion init turns that answer into a vault. The session root’s callers are hooks and skills, installed anywhere and running unattended; they ask not “which vault” but whose state is this, and there is exactly one project a session belongs to — one the host either states in the environment or marks on disk. When neither holds, there is no project to belong to, and inventing one is #485. A $HOME stop fits that shape and a fallback does not; a fallback fits the vault’s shape and a $HOME stop would break init.
sessionstate.ts owns the rule: HostLayout (marker, projectDirEnv, stateDir), findProjectRoot(cwd, layout, lookupEnv) — the order above — and the two forms over it, findProjectSessionsDir (undefined when nothing identifies a project) and projectSessionsDir (falling back to a cwd-relative path, so a path is always returned). Claude Code is the module’s own host: findSessionsDir/sessionsDir are that layout applied, unchanged in name and behaviour.transcriptcapture.ts’s OpenCode support is an adapter over it: it declares .opencode/, no env override, and .opencode/wiki-knowledge/sessions under the root, and findOpenCodeSessionsDir is the shared rule applied to that. isOpenCodeSessionTracked — the tie-break evidence when both host session-id variables are set — reads through it.findProjectSessionsDir is what a writer resolves through, and projectSessionsDir is what a reader that must name the directory it looked in uses (the Claude Code reader does: it stats the directory and reports it in its failure message). The OpenCode adapter has no reader form at all — its one caller never surfaces a path, and “no project” is its answer.$HOME, and it no longer falls back to cwd. The fallback was reachable only in a directory holding the state tree but not the .opencode/ directory above it, which the tracker plugin cannot produce (it creates .opencode/ to write into). The $HOME stop does change a real case: an OpenCode session run from $HOME no longer reads its tracker state, so when both host ids are set the tie-break falls to Claude Code — the documented behaviour for an untracked session, and the capture itself never needed tracker state (#402).wiki-plugin/wiring/opencode/plugins/session-tracker.ts) writes whatever directory OpenCode hands it, and it runs inside the host rather than in this bundle, so it cannot import this rule: an OpenCode session whose directory is $HOME still records state there, and this rule simply never reads it. Giving the writer the same boundary is follow-up work, not something this ADR can do from the reader’s side.captureOpenCodeSession no longer takes a cwd. It fetches the transcript by shelling out to opencode export <id> and writes into the vault; it resolves no root, so the parameter existed only for signature parity with the Claude Code path, where cwd is genuinely used.$HOME stop costs a project rooted at the home directory — a dotfiles-style repo — which resolves as no project for either host. That is the same trade the Claude Code walk already made: a state tree in $HOME is indistinguishable from global configuration, and misfiling a session’s state is the failure #485 recorded.initwiki’s generated gitignore still spells both state trees by hand. That is a pattern, not a resolution: it must keep covering trees that no current writer would create, because the #323 conversion path runs over directories written by older versions.$OPENCODE_SESSION_ID’s producers are about session identity, not the project root; and tool-call-stats’s Claude-only PostToolUse registration is about the log, not where the log lives.$HOME to be the project. Rule 3 would have to become “no session root, state lives elsewhere” rather than a boundary this order draws, and the $HOME stop is the part that would move.