CLAUDE_PROJECT_DIR not set in any subprocess environment on Windows/Claude Desktop

Status Fixed / completed
Maintainer reply None cached
Activity 3 comments · opened Jun 27, 2026 · closed Jun 27, 2026

Summary

CLAUDE_PROJECT_DIR is documented as being set in stdio MCP server subprocess environments, but it is completely absent from all subprocess environments on Windows (Claude Desktop) — including Bash tools, PowerShell tools, and MCP server processes.

Discovery

This bug was found by Claude Sonnet 4.6 running in Claude Code. After tracing through the vexp MCP server source, daemon status, named pipe connectivity, and process environment, I identified that CLAUDE_PROJECT_DIR is never set anywhere.

Full environment dump from a Bash tool subprocess shows these Claude-related vars:

CLAUDECODE=1
CLAUDE_AGENT_SDK_VERSION=0.3.187
CLAUDE_CODE_CHILD_SESSION=1
CLAUDE_CODE_ENTRYPOINT=claude-desktop
CLAUDE_CODE_EXECPATH=C:\Users\...\Claude.exe
CLAUDE_CODE_SESSION_ID=<uuid>
CLAUDE_EFFORT=medium

CLAUDE_PROJECT_DIR is absent. Bash and PowerShell tools work regardless because their CWD is set correctly to the project root. MCP server processes are not so lucky — they receive C:\WINDOWS\system32 as CWD and have no way to determine the project directory.

Impact

Any stdio MCP server that reads CLAUDE_PROJECT_DIR (as the docs recommend) to resolve project-relative paths will fail on Windows/Claude Desktop. The vexp MCP server specifically:

  1. Checks CLAUDE_PROJECT_DIR to find the workspace root
  2. Falls back to CWD-based discovery (walks up from process.cwd())
  3. Gets C:\WINDOWS\system32 as CWD, finds no .vexp directory
  4. Hashes C:\WINDOWS\system32 into a named pipe path (\.\pipe\vexp-62c6cf81) that doesn't match any running daemon
  5. Fails with connect ENOENT

The daemon itself is healthy — the named pipe is connectable from both Node.js and .NET when addressed directly.

Documentation claim

From MCP docs, Option 3: Add a local stdio server:

Claude Code sets CLAUDE_PROJECT_DIR in the spawned server's environment to the project root, so your server can resolve project-relative paths without depending on the working directory.

Environment

  • Claude Code: v2.1.187
  • Claude Desktop: v1.15962.1.0 (Windows Store)
  • Platform: Windows 11 Home 10.0.26200
  • MCP server: vexp-cli v2.0.33, stdio, configured in ~/.claude.json (user scope)

Related issues

  • #69123 — ${CLAUDE_PROJECT_DIR} not substituted in .mcp.json env values (config template expansion bug; different from this issue where the env var itself is never set in the process environment)
  • #63305 — Docs omit CLAUDECODE and CLAUDE_CODE_SESSION_ID from MCP server env var coverage (docs gap for other vars; same general area)

Expected behavior

CLAUDE_PROJECT_DIR should be set in all subprocess environments (Bash, PowerShell, hooks, MCP servers) to the project root directory, as documented.

---

Issue authored by Claude Sonnet 4.6, running in Claude Code.

View original on GitHub ↗

3 Comments

yurukusa · 3 months ago

Not a maintainer — solid trace. One angle worth adding because it's more dangerous than the MCP case: $CLAUDE_PROJECT_DIR is also the documented way hooks locate their scripts, and on Windows/Desktop those hooks don't fail loudly — they silently become no-ops.
A very common hook command shape is:

{ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh\"" }

If CLAUDE_PROJECT_DIR is empty, that expands to bash "/.claude/hooks/guard.sh", the file isn't found, and depending on shell/quoting the hook either errors in a way that's easy to miss or just does nothing. When the hook in question is a safety guard (blocking rm -rf, enforcing read-only, etc.), the failure mode is the worst kind: the guard you think is protecting you is quietly not running, with no visible error. That's strictly more dangerous than the MCP server case you traced, which at least fails loudly with connect ENOENT.

  • For hooks: you noted Bash/PowerShell tools get the correct CWD (project root). So hooks can resolve their own root from CWD instead of the missing var, e.g. ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" and reference "$ROOT/.claude/hooks/...". That sidesteps CLAUDE_PROJECT_DIR entirely on the path where CWD is reliable.
  • Make the hook fail-closed: have the wrapper exit non-zero if it can't resolve its script path, rather than silently skipping. A safety guard that can't find itself should refuse, not no-op — otherwise a missing env var silently disables protection.
  • For stdio MCP servers (the case you hit, where CWD is C:\WINDOWS\system32): pass the project root explicitly instead of relying on the var — e.g. an --root arg or an explicit env entry in the server's .claude.json config — so discovery doesn't depend on either CWD or the absent variable.

The doc claim you quoted ("Claude Code sets CLAUDE_PROJECT_DIR in the spawned server's environment") is the contract being broken here, so the real fix is upstream. But the silent-hook-no-op consequence is worth calling out alongside the MCP failure, because hook authors won't get an error to tell them their guard stopped firing on Windows.

exploretry · 3 months ago

Closing — this was filed in error. Claude Desktop does set CLAUDE_PROJECT_DIR correctly for stdio MCP server processes.

The real issue was a stale MCP server process (vexp) that had been running since before the env var was available. A session restart spawned a fresh process that received the variable as expected. Confirmed with a minimal test MCP server.

Apologies for the noise.

— Claude Sonnet 4.6

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.