# Workspace state
How `workspace.activate` (alias `activate_project`) switches the codescout
MCP server's active project, and what state is shared across every tool
call — including across subagents that share the parent's MCP server.
## What `activate_project` does
A single call to `activate_project(path=...)` flips the server's active
project to the given root. Implementation: `src/tools/config/mod.rs`
(`ActivateProject` tool). The call has these side effects, in order:
1. **Clears `ctx.guide_hints_emitted`** — the per-session set tracking
which `get_guide(topic)` topics the model has been hinted about. A
fresh activation re-triggers first-call hints for the new project.
2. **Resolves the path.** Bare project IDs (no `/`) inside a workspace
are focus-switches; absolute paths trigger full activation. Path
must be an existing directory or you get a `RecoverableError`
(`isError: false`, sibling calls survive).
3. **Prewarms LSP** for the project's languages (background — does not
block the response).
4. **Auto-registers dependencies** for cross-project navigation
(`crate::library::auto_register::auto_register_deps`).
5. **Resets `path_note_emitted_since_activation`** on
`CodeScoutServer` — so the next non-`run_command` tool response
that contains the new root re-emits `[codescout] paths are
relative to <root>`.
The response includes `project_hints` (primary language, manifest,
entry points, build commands) so the model has orientation context
even without running `onboarding`.
## The home/foreign distinction
The **first** project activated in an MCP session is the **home**
project. Every subsequent activation to a different path is a **foreign**
activation. The `read_only` param defaults differ:
- **home**: `read_only = false` (mutations allowed by default)
- **foreign**: `read_only = true` (read-only by default; pass
`read_only=false` explicitly to enable writes)
This matters because the MCP server is shared state across the session.
Activating a foreign project leaves the server pointed at it until you
explicitly restore home or end the session.
## Per-session state reset
Activation clears these per-session sets:
| State | Owner | Behavior |
|---|---|---|
| `guide_hints_emitted` | `CodeScoutServer` | Cleared on every activation **and on `workspace(post_compact=true)`** (compaction re-arm); **persisted** per `CLAUDE_CODE_SESSION_ID` (`.codescout/guide_hints/<id>.json`), so it survives `/mcp` restarts within one conversation instead of re-injecting guide bodies the conversation already holds. Written by **both** an explicit `get_guide(topic)` fetch and the first-touch auto-inject of a tool with `relevant_guide_topic()` — one shared keyspace, so either path suppresses the other's re-emit. After a clear, the next of either re-emits. |
| `path_note_emitted_since_activation` | `CodeScoutServer` | Cleared on every activation. Next stripped response re-emits the path-relative banner. |
| `section_coverage` | `CodeScoutServer` | NOT cleared. Section-read tracking persists across activations. |
| Output buffers (`@tool_*`, `@cmd_*`) | `OutputBuffer` | NOT cleared. Buffers from before the switch remain readable. |
## Path-relative annotation
Every non-`run_command` tool response that contains paths under the
active project root is stripped to project-relative form. The first
stripped response since activation carries a trailing note:
```
[codescout] paths are relative to /home/user/<project>
```
Subsequent stripped responses in the same activation window do NOT
re-emit the note (novelty-gated). `run_command` output is exempt —
raw shell bytes, stripping would corrupt path literals. See
`docs/issues/2026-05-28-path-annotation-spam.md` for the rationale.
## Cross-project workflow pattern
When you need to work in a sibling project briefly:
```
1. workspace(activate, path="/home/user/other-project")
2. <do the work — any number of tool calls>
3. workspace(activate, path="/home/user/code-explorer") # restore home
```
Skip step 3 and the server stays pointed at the foreign project. The
next session inherits the foreign root as "active." This is the
**workspace gate** from `server_instructions` — restore home before
the turn ends.
For one-off reads, prefer `read_only=true`:
```
workspace(activate, path="/sibling", read_only=true)
```
Read-only mode blocks writes at the agent layer regardless of the
caller's intent — defense against accidental edits while scouting.
## Subagent semantics
Subagents that share the parent's MCP server share:
- The same active project (no per-subagent override)
- The same `guide_hints_emitted` set (parent-triggered hints don't
re-fire for subagents)
- The same `path_note_emitted_since_activation` flag
A subagent that needs the workspace pointed at a different root must
itself call `activate_project` — and then restore the parent's home
before returning. This is dangerous: the parent's next call after the
subagent returns will see whatever workspace state the subagent left.
Prefer not to switch workspace inside subagents; if you must, document
in the subagent's spawn prompt that it will restore home before exit.
## Per-call workspace pinning
`activate_project` flips the *one* shared active-project slot, so parallel
subagents that activate different workspaces race — last writer wins, and a
subagent can silently read another's worktree. The fix is to **pin per call**
instead of activating: pass `workspace=<absolute path>` on each tool call.
- The pin resolves that single call against the named workspace, regardless of
the server's active project. Concurrent pins never collide — each call
carries its own target.
- Single-workspace work omits the param; the server's active project is used.
- Pinning is the recommended path for parallel multi-workspace fan-out. Prefer
it over calling `activate` inside a subagent (see Subagent semantics above),
which leaves the shared slot pointed wherever the subagent last set it.
## Anti-patterns
- **Forgetting to restore home.** Iron-Law-grade. Server is shared
state; the next session sees your foreign activation as the active
project. Symptoms: tools operate on the wrong codebase, semantic
search returns unrelated results.
- **Switching workspaces inside a subagent without restoration.**
Parent's next tool call lands in the subagent's workspace. Caller
has no way to detect this without an extra `workspace(status)` call.
- **Relying on `guide_hints_emitted` to survive activation or compaction.** (It now *does* survive `/mcp` restarts — persisted per `CLAUDE_CODE_SESSION_ID`.) Every
`activate_project` resets it. If a hint was useful, capture the
guide content in the parent's prompt or call `get_guide(topic)`
again after activation.
- **Treating `read_only=true` as a no-op.** It blocks mutations at
the agent layer; tools that try to write will fail with
`RecoverableError`. Use it deliberately for scout-only work.
## Related
- `get_guide("error-handling")` — `RecoverableError` routing for
invalid paths and read-only violations
- `get_guide("progressive-disclosure")` — `[codescout] paths are
relative to <root>` mechanics, path stripping, buffer behavior
- `docs/issues/2026-05-28-path-annotation-spam.md` — full rationale
for the novelty-gated path annotation
- Iron Law 6 in `server_instructions` — subagent dispatch discipline
(parent must brief subagents about workspace state, among other
context)