# Planning Session MCP via Ephemeral Worktree **Date:** 2026-04-24 **Status:** Design approved, pending implementation plan **Scope:** `ClaudeDo.Worker` — planning session launch, MCP config delivery ## Problem When a user starts a planning session, `claude` is spawned in the list's working directory via Windows Terminal and passed `--mcp-config ` pointing at a session-local `mcp.json`. In practice, the spawned `claude` session does **not** pick up the ClaudeDo MCP server: `mcp__claudedo__*` tools are not available, and no trust prompt is shown. The user has to fall back to the built-in `TaskCreate` tool, which writes nothing to ClaudeDo. The `--mcp-config` flag is documented for headless (`-p`) invocations; in interactive TUI mode it appears to be either ignored or silently dropped on at least some CLI versions. The JSON payload itself is already correct (verified against Claude Code docs — `type: "http"` + `Authorization` header is the documented form). The reliable path per Claude Code docs is project-root `.mcp.json` auto-discovery plus a one-time trust approval (or `enableAllProjectMcpServers: true`). ## Goal Spawn planning sessions so that `mcp__claudedo__*` tools are available immediately, without modifying any file in the user's working directory and without requiring a trust prompt. ## Non-goals - Installer-time MCP registration (rejected — loses per-session token isolation; pollutes every `claude` invocation on the machine). - Changing how task execution (non-planning) spawns `claude`. - Supporting planning on a working directory that is not a git repository. ## Approach: ephemeral planning worktree Each planning session runs inside its own short-lived git worktree, created from `HEAD` of the list's working directory. The worktree is the isolated surface where we write `.mcp.json` and the settings override. The worktree is force-removed on `FinalizeAsync` / `DiscardAsync`. ### Files changed - `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` - `src/ClaudeDo.Worker/Planning/PlanningSessionContext.cs` (extend to carry worktree path + branch name) - `src/ClaudeDo.Worker/Planning/PlanningSessionFiles.cs` (may drop `McpConfigPath` if no longer used) - `src/ClaudeDo.Worker/Planning/WindowsTerminalPlanningLauncher.cs` - `src/ClaudeDo.Worker/Runner/WorktreeMaintenanceService.cs` (optional — defensive startup prune) - DI registration in `src/ClaudeDo.Worker/Program.cs` (inject `GitService`, `WorkerConfig`, `IDbContextFactory` into `PlanningSessionManager`) ### Data flow on `StartAsync` 1. Resolve `list.WorkingDir`; hard-error if `null`, not a directory, or not a git repo (`GitService.IsGitRepoAsync`). 2. Resolve `HEAD` via `GitService.RevParseHeadAsync`. 3. Resolve worktree strategy from `AppSettingsRepository.GetAsync` (same resolution as `WorktreeManager.CreateAsync`): - `sibling` → `\.claudedo-worktrees\planning\` - `central` → `\planning\` Normalize with `Path.GetFullPath`. 4. Branch name: `claudedo/planning/`. 5. `GitService.WorktreeAddAsync(list.WorkingDir, branchName, worktreePath, baseCommit, ct)`. On `"already exists"` failure, run the same self-heal pattern as `WorktreeManager.CreateAsync` (list worktrees for branch → force-remove stale → prune → delete branch → retry once). 6. Write into the worktree: - `\.mcp.json` — JSON with env-var expansion for the token (see below). - `\.claude\settings.local.json` — `{ "enableAllProjectMcpServers": true }` (create `.claude` dir if missing). 7. Write session artifacts in the session directory (unchanged from today): `system-prompt.md`, `initial-prompt.txt`. The session-local `mcp.json` is no longer written — drop that write. 8. Return `PlanningSessionStartContext` with `WorkingDir = worktreePath` and a new `WorktreePath` field (redundant with `WorkingDir` for now, but explicit for cleanup). Also carry `BranchName` so finalize/discard can delete it. ### MCP JSON payload ```json { "mcpServers": { "claudedo": { "type": "http", "url": "http://127.0.0.1:47821/mcp", "headers": { "Authorization": "Bearer ${CLAUDEDO_PLANNING_TOKEN}" } } } } ``` The token never lives on disk in literal form — `${CLAUDEDO_PLANNING_TOKEN}` is expanded by Claude Code at load time from the spawned process's environment. ### `.claude/settings.local.json` payload ```json { "enableAllProjectMcpServers": true } ``` Since the worktree is always empty of user customizations (fresh checkout), we write this file unconditionally. No merge / backup logic needed. ### Launcher changes (`WindowsTerminalPlanningLauncher`) - `LaunchStartAsync`: - Set `psi.Environment["CLAUDEDO_PLANNING_TOKEN"] = ctx.Token` (new field on `PlanningSessionStartContext`). - `-d` now points at the worktree path (already handled by `ctx.WorkingDir` change). - **Remove** `--mcp-config` and its path argument. - Keep `--allowedTools mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill` — `enableAllProjectMcpServers` only handles trust, not per-tool pre-approval. - Keep `--append-system-prompt-file` as the "single-value flag buffer" before the positional prompt (the existing arg-order concern is unchanged). - `LaunchResumeAsync`: - Same env-var setup. - Same `-d `. - **Remove** `--mcp-config` (the worktree's `.mcp.json` is discovered automatically). - Keep `--resume `. ### Finalize / Discard `PlanningSessionManager.FinalizeAsync` and `DiscardAsync` gain: 1. Look up the worktree path + branch name (deterministic from `taskId` → reuse the same resolution code as `StartAsync`). 2. `GitService.WorktreeRemoveAsync(list.WorkingDir, worktreePath, force: true, ct)` — `--force` because claude may have created scratch files. 3. `GitService.BranchDeleteAsync(list.WorkingDir, branchName, force: true, ct)`. 4. Delete the session dir as today. All three steps are best-effort in `DiscardAsync` (log warnings, don't throw — the user explicitly asked to discard). `FinalizeAsync` should propagate failures, since a failed cleanup leaves resources we care about. ### Resume Resume already looks up `list.WorkingDir` from the list; the worktree path is deterministic from `taskId`. `ResumeAsync` must: 1. Verify the worktree directory exists; if not, hard-error ("planning session was discarded or lost — cannot resume"). 2. Return `PlanningSessionResumeContext` with `WorkingDir = worktreePath` and the token (re-read from session state — see Token persistence below). ### Token persistence The token today is generated in `StartAsync` and embedded in `mcp.json` at creation time — never read again. With env-var expansion, the token must be available on **resume**. Options: - **A) Persist token to session dir** (`\token`) with `FileOptions.WriteAllBytes`, restrict file ACL to current user. Read on resume. - **B) Store token hash in DB, raw token in memory only** — breaks across Worker restarts → no resume possible. **Chosen: A.** Token file sits inside the existing session directory (`\\token`), restricted to the current user via Windows ACLs (`File.SetAccessControl` with an explicit DACL granting `FullControl` to `WindowsIdentity.GetCurrent()` only). Cleaned up in `DiscardAsync`/`FinalizeAsync` with the rest of the session dir. ### Defensive startup cleanup `WorktreeMaintenanceService` already prunes worktrees tracked in the DB. Planning worktrees are **not** in the DB (they're purely filesystem-backed, keyed by `taskId` via path convention). Add a lightweight pass: - Enumerate directories matching `\.claudedo-worktrees\planning\*` (for each strategy / central root we know about). - For each, check whether a corresponding session dir exists under `~/.todo-app/sessions/`. - If no session dir: `git worktree remove --force` + `git branch -D claudedo/planning/`. This is a small addition; if scoped too large, defer to a follow-up and accept that a crashed Worker leaves orphaned worktrees until manual cleanup. ## Edge cases | Case | Behavior | |------|----------| | `list.WorkingDir` not a git repo | Hard-error on `StartAsync`. Surface message in UI. | | Worktree branch already exists from a prior crashed session | Self-heal: force-remove matching worktrees, prune, delete branch, retry once. (Same pattern as `WorktreeManager.CreateAsync`.) | | User closes Windows Terminal without clicking Finalize/Discard | Session dir + worktree remain. `ResumeAsync` works. Startup cleanup handles abandoned sessions whose session dir the user manually deletes. | | Claude creates/edits files in the planning worktree | Discarded with the worktree. No impact on user's real working dir. | | User deletes the session dir out from under the Worker | `ResumeAsync` hard-errors. Startup cleanup GCs the orphaned worktree. | | Two simultaneous planning sessions on the same task | Already prevented by task status transition (`Planning` is exclusive). No new consideration. | | `HEAD` is on a detached commit | `git worktree add` handles this fine — base commit is explicit. | ## Testing Extend `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (or a new file) with integration tests using the real-SQLite + real-git pattern the project already uses: - **Start happy path:** worktree dir exists after `StartAsync`, contains `.mcp.json` with `${CLAUDEDO_PLANNING_TOKEN}` literal, contains `.claude/settings.local.json` with `enableAllProjectMcpServers: true`. - **Finalize cleanup:** worktree dir is gone, branch is gone, session dir is gone. - **Discard cleanup:** same as finalize. - **Self-heal:** pre-create a stale branch `claudedo/planning/`, then `StartAsync` must succeed. - **Non-git working dir:** `StartAsync` throws a specific error type. - **Resume after Worker restart:** seed session dir + token file, recreate `PlanningSessionManager`, `ResumeAsync` returns context pointing at the still-existing worktree. Mock `IPlanningTerminalLauncher` (already an interface) so tests don't actually spawn `wt.exe`. ## Trade-offs and alternatives considered 1. **Write `.mcp.json` into the user's working dir with backup/restore.** Rejected — clobber risk, file-noise on crash, user's `.gitignore` may not cover it, exposes token alongside source even with env-var expansion (because expansion is on claude's side, the raw `${VAR}` string still lives in the user's repo). 2. **User-scope registration via installer** (`claude mcp add --scope user`). Rejected — requires a static secret baked into the Worker, loses per-session isolation, every `claude` session on the machine sees claudedo tools. 3. **Keep `--mcp-config` and debug why it's not honored.** Rejected — even if it works on the maintainer's machine, the behavior is undocumented for interactive TUI mode, and we'd need a fallback anyway. Fixing to the documented path eliminates the uncertainty. ## Open questions resolved - **WorkingDir must be a git repo?** Yes — hard-error. - **Worktree path strategy?** Follow the same `sibling`/`central` setting as task execution. - **HEAD snapshot vs WIP?** HEAD snapshot is fine — planning proposes subtasks, doesn't edit files. ## Implementation sequencing A separate implementation plan (via `superpowers:writing-plans`) will break this into test-first steps.