Skip to content

Issue: MCP Health Check Hook Fails on Windows for npm-installed Binary Wrappers #350

@GZBLCC

Description

@GZBLCC

Summary

The PreToolUse MCP health check hook (mcp-health-check.js) incorrectly marks codegraph (and potentially other
npm-installed stdio-type MCP servers) as unhealthy on Windows, because probeCommandServer() spawns the command without
shell: true. On Windows, npm packages install a #!/bin/sh wrapper script whose companion .cmd wrapper requires shell
interpretation to execute.

Environment

┌──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Item │ Value │
├──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ OS │ Windows 11 Pro (win32) │
├──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Node.js │ v24.15.0 │
├──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ codegrap │ v0.9.3 (npm global install) │
│ h │ │
├──────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Hook │ ~/.claude/plugins/cache/everything-claude-code/everything-claude-code/1.9.0/scripts/hooks/mcp-health │
│ file │ -check.js │
└──────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┘

MCP Server Config

{
"type": "stdio",
"command": "codegraph",
"args": ["serve", "--mcp"]
}

Root Cause

File: mcp-health-check.js, line 299, inside probeCommandServer():

child = spawn(command, args, {
env: mergedEnv,
cwd: process.cwd(),
stdio: ['pipe', 'ignore', 'pipe']
});

shell is not set, so it defaults to false.

On Windows, npm install -g @colbymchenry/codegraph installs two entry points:

┌───────────────┬────────────────────────────────┬─────────────────────────────┐
│ File │ Type │ Size │
├───────────────┼────────────────────────────────┼─────────────────────────────┤
│ codegraph │ POSIX shell script (#!/bin/sh) │ 439 bytes │
├───────────────┼────────────────────────────────┼─────────────────────────────┤
│ codegraph.cmd │ Windows batch wrapper │ (calls node on npm-shim.js) │
└───────────────┴────────────────────────────────┴─────────────────────────────┘

When spawn('codegraph', ...) is called without shell: true on Windows:

  1. Node.js resolves codegraph via PATH and finds the extension-less sh script first
  2. CreateProcess cannot execute a #!/bin/sh script — it's not a PE executable or a recognized batch file
  3. Node.js reports ENOENT (even though the file exists, it is not executable by the OS)

The .cmd wrapper exists and would work, but spawn() without shell: true cannot launch .cmd files either — it throws
EINVAL on Node.js v24.

Only spawn('codegraph', args, { shell: true }) works, because cmd.exe can interpret the .cmd wrapper correctly.

Reproduction

// FAILS — same code path as the health check hook
const p = spawn('codegraph', ['status']);
// Error: spawn codegraph ENOENT

// ALSO FAILS — even with explicit .cmd extension
const p = spawn('codegraph.cmd', ['status']);
// Error: spawn EINVAL

// WORKS — shell: true lets cmd.exe handle the .cmd wrapper
const p = spawn('codegraph', ['status'], { shell: true });
// stdout: CodeGraph Status ... [OK] Index is up to date

Impact

  • The health check hook marks codegraph as unhealthy in ~/.claude/mcp-health-cache.json
  • All PreToolUse calls for mcp__codegraph__* tools are blocked (exit code 2)
  • This persists across sessions because state is cached on disk
  • The actual MCP server (spawned by the Claude Code harness, which likely uses shell: true) works fine — the health
    check is a false negative
  • Any npm-installed stdio MCP server on Windows is potentially affected

Affected State File

~/.claude/mcp-health-cache.json:
{
"servers": {
"codegraph": {
"status": "unhealthy",
"lastError": "spawn codegraph ENOENT",
"failureCount": 2
}
}
}

Proposed Fix

In mcp-health-check.js, line 299, add shell: true on Windows:

child = spawn(command, args, {
env: mergedEnv,
cwd: process.cwd(),
stdio: ['pipe', 'ignore', 'pipe'],
shell: process.platform === 'win32' // ← add this
});

Or more defensively, set it unconditionally — shell: true is harmless on Unix.

Workaround

Set ECC_MCP_HEALTH_FAIL_OPEN=true to bypass the health check, or delete ~/.claude/mcp-health-cache.json and restart
the session after the fix is applied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions