v1.0.113 — Hotfix: /ctx-upgrade no longer poisons project dir
A focused hotfix on top of v1.0.112. One bug, one fix, two TDD slices, defense-in-depth.
What broke
After running /ctx-upgrade, Claude Code killed and respawned the MCP server with cwd set to the plugin install directory (~/.claude/plugins/cache/context-mode/context-mode/<version>/). The legacy start.mjs then unconditionally set CLAUDE_PROJECT_DIR = originalCwd, poisoning every downstream ctx_stats, SessionDB, hash, and event-attribution computation. Sessions silently re-rooted under the plugin install dir instead of the user's actual project. ctx_stats showed "This conversation started in ~/.claude/plugins/cache/context-mode/context-mode/<version>" — wrong path, wrong DB, wrong stats.
This affected every user who upgraded mid-session via /ctx-upgrade since v1.0.107 introduced the in-product upgrade flow.
What changed
Defense-in-depth fix across two layers:
-
start.mjs— no env poisoning at the source. NewisPluginInstallPath()guard checks iforiginalCwdmatches.claude/plugins/(cache|marketplaces)/...(cross-OS regex, POSIX + Windows separators). When true, the env auto-set is skipped —CLAUDE_PROJECT_DIRandCONTEXT_MODE_PROJECT_DIRstay unset rather than carrying a wrong value forward. -
server.ts getProjectDir()— defensive resolver. Delegates to a new pureresolveProjectDir({env, cwd, pwd})helper insrc/util/project-dir.ts. The env-var chain (CLAUDE_PROJECT_DIR→GEMINI_PROJECT_DIR→VSCODE_CWD→OPENCODE_PROJECT_DIR→PI_PROJECT_DIR→IDEA_INITIAL_DIRECTORY→CONTEXT_MODE_PROJECT_DIR) now rejects any value matchingisPluginInstallPathand falls through to the next source. Before falling back toprocess.cwd()(whichstart.mjschdir'd to the plugin dir), the resolver triesprocess.env.PWD— shell-set, NOT updated byprocess.chdir(), so it survives the chdir and points at the user's real session cwd.
The resolver stays total — if PWD is also missing or also a plugin path, it returns cwd rather than throwing, so project-independent tools (sandbox execute, fetch-and-index) keep working. Only project-dependent tools see the degenerate state, and they render gracefully.
Verification
- Reproducer in pure subprocess form: pre-fix returned a path containing
/.claude/plugins/cache/. Post-fix returns the user's project dir when PWD is set (the typical Claude Code MCP restart case). - Diagnose discipline: hypotheses ranked before patching, both fix layers verified independently, full repro chain re-run after fix.
Tests
Two new vertical TDD test files:
tests/util/project-dir.test.ts— 11 tests forisPluginInstallPath(cross-OS) andresolveProjectDirenv-chain semantics.tests/util/start-mjs-no-poison.test.ts— 3 subprocess integration tests verifyingstart.mjsbootstrap behavior with plugin vs project cwd.
tests/core/server.test.ts updated to pin the new contract: server.ts getProjectDir delegates to resolveProjectDir; the env chain itself is asserted in the shared resolver.
Full suite: 2,787 pass, 24 skipped, zero new regressions. Typecheck clean.
Compatibility
15 adapters, 3 OS (macOS / Linux / Windows), no breaking changes, MCP surface unchanged. The fix is OS-agnostic — the regex matches both / and \ separators.
Upgrade
ctx-upgrade # plugin
npm install -g context-mode # standalone
After upgrade, ctx_stats will correctly show your actual project path even if the previous MCP session had a poisoned env.