Skip to content

fix(mcp): resolve ${CLAUDE_PROJECT_DIR} in overlay paths at runtime (#57, #58) - #60

Merged
basicScandal merged 1 commit into
mainfrom
fix/57-runtime-overlay-resolution
Jul 13, 2026
Merged

fix(mcp): resolve ${CLAUDE_PROJECT_DIR} in overlay paths at runtime (#57, #58)#60
basicScandal merged 1 commit into
mainfrom
fix/57-runtime-overlay-resolution

Conversation

@basicScandal

Copy link
Copy Markdown
Member

Fixes the root-cause bug where private overlays wired with ${CLAUDE_PROJECT_DIR} silently returned public-only results, plus the doctor blind spot that couldn't catch it. Closes #57 and #58.

Root cause (#57)

starlog init bakes ${CLAUDE_PROJECT_DIR}/.starlog/{private-facts,private-corpus,policy}.json into the MCP server's env block. But Claude Code does not expand ${CLAUDE_PROJECT_DIR} there${VAR} expansion runs at config-parse time from Claude Code's own environment, where CLAUDE_PROJECT_DIR is unset, so the token reaches the server literal. The loaders then failed readFileSync, warned only to stderr (which the MCP stdio channel discards), and degraded to public-only. Completely silent; search just returned public packages.

Verified against the live MCP server processes on this machine — every one carries the literal token, e.g.:

CLAUDE_PROJECT_DIR=/Users/scandal/ai/whoisgenius      ← injected into process.env at runtime
STARLOG_PRIVATE_FACTS=${CLAUDE_PROJECT_DIR}/.starlog/private-facts.json   ← literal, unexpanded

So facts and policy are affected too, not just corpus.

Fix

CLAUDE_PROJECT_DIR is injected into the spawned server's process.env (the documented pattern), so the server can resolve the token itself. New src/engine/overlay-path.tsresolveOverlayPath():

  • expands ${VAR} / ${VAR:-default} from process.env (CLAUDE_PROJECT_DIR falls back to cwd, never a bogus /.starlog/…)
  • expands a leading ~
  • resolves project-relative paths against CLAUDE_PROJECT_DIR || cwd
  • idempotent on absolute paths (existing loader tests untouched)

loadPrivateCorpus, loadPrivateFacts, and loadPolicy call it at read time. This repairs already-wired installs with no re-init — the init env format is unchanged, so there's no migration. The misleading init.ts comment (claimed parse-time expansion) is corrected.

doctor (#58)

checkPrivateOverlays reported Private overlays wired: [ok] on mere presence of the env keys — it never checked they resolved, so a stale absolute path or cross-project leak passed as healthy. It's the tool you'd run to catch #57, and it structurally couldn't. It now resolves each wired path exactly as the server will (resolveOverlayPath(value, projectDir)) and warns when it doesn't land on this project's .starlog/.

Tests

TDD throughout (RED verified before each GREEN):

  • overlay-path.test.ts — 8 cases (token expansion, cwd fallback, ~, relative, :-default, idempotence, explicit projectDir override)
  • token-resolution cases added to private-corpus.test.ts and facts/service.test.ts
  • doctor.test.ts — misrouted/cross-project path now warns

Full suite green: 577/577, typecheck clean.

Notes for the reviewer

🤖 Generated with Claude Code

Claude Code does not expand ${CLAUDE_PROJECT_DIR} inside an mcpServers env
value -- ${VAR} expansion runs at config-parse time from Claude Code's own
environment, where the variable is unset, so the token baked by `starlog
init` reached the server literal. Every private overlay (corpus, facts,
policy) then failed its readFileSync, warned only to stderr (which the MCP
stdio channel discards), and silently degraded to public-only.

Claude Code does inject CLAUDE_PROJECT_DIR into the spawned server's
process.env, so the fix is a runtime resolver: new resolveOverlayPath()
expands ${VAR}/${VAR:-default} (CLAUDE_PROJECT_DIR falling back to cwd),
a leading ~, and project-relative paths, and is idempotent on absolute
paths. loadPrivateCorpus / loadPrivateFacts / loadPolicy call it at read
time, repairing already-wired installs with no re-init. The misleading
init.ts comment that claimed parse-time expansion is corrected.

doctor's "Private overlays wired" check reported ok on mere presence of
the env keys, so a stale absolute path or cross-project leak passed as
healthy -- it could not catch this class of bug. It now resolves each
wired path exactly as the server will (via resolveOverlayPath against the
known project root) and warns when it does not land on this project's
.starlog/.

Full suite green (577 tests); no package.json version bump (left for the
release step per RELEASING.md).

Closes #57
Closes #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@basicScandal
basicScandal merged commit bf2188b into main Jul 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

init bakes unexpandable ${CLAUDE_PROJECT_DIR} into MCP env → private corpus silently degrades to public-only

1 participant