A plan often links supporting documents beside it, like [evidence](foo-evidence.md). Clicking one in plan review returns 404. The link resolves against the review root, which is the working directory, while the plan itself lives in ~/.claude/plans/.
Cause
Claude Code's ExitPlanMode input carries both plan and planFilePath, and planFilePath is an absolute path under ~/.claude/plans/. The file is written about a second before the hook fires and matches the inline plan exactly. apps/hook/server/index.ts reads only Gemini's plan_filename and plan_path and drops the Claude Code field.
Proposal
Thread planFilePath into ServerOptions, trusted only when the file on disk holds the plan under review. The path is model-generated tool input. Without that content check an injected agent could name ~/.ssh/plan.md and pick its own containment root.
Then let getTrustedBaseDir accept the attested plan directory as a base without promoting it to a root. Sibling documents the plan names become readable. The rest of ~/.claude/plans/ stays unreachable.
#1437 moves containment into isPathAllowed in packages/shared/doc-resolve.ts. Part of the intent there was to enable this feature by consolidating duplicate path authorization checks.
A plan often links supporting documents beside it, like
[evidence](foo-evidence.md). Clicking one in plan review returns 404. The link resolves against the review root, which is the working directory, while the plan itself lives in~/.claude/plans/.Cause
Claude Code's
ExitPlanModeinput carries bothplanandplanFilePath, andplanFilePathis an absolute path under~/.claude/plans/. The file is written about a second before the hook fires and matches the inline plan exactly.apps/hook/server/index.tsreads only Gemini'splan_filenameandplan_pathand drops the Claude Code field.Proposal
Thread
planFilePathintoServerOptions, trusted only when the file on disk holds the plan under review. The path is model-generated tool input. Without that content check an injected agent could name~/.ssh/plan.mdand pick its own containment root.Then let
getTrustedBaseDiraccept the attested plan directory as abasewithout promoting it to a root. Sibling documents the plan names become readable. The rest of~/.claude/plans/stays unreachable.#1437 moves containment into
isPathAllowedinpackages/shared/doc-resolve.ts. Part of the intent there was to enable this feature by consolidating duplicate path authorization checks.