Repository navigation
v1.1.3 Codex MCP on WSL /mnt can open same repo via upper/lowercase paths, corrupting .codegraph despite CODEGRAPH_NO_DAEMON #1057
Description
Summary
On @colbymchenry/codegraph@1.1.3, Codex MCP still becomes unstable on WSL2 /mnt/d even with the recommended workaround from #974:
[mcp_servers.codegraph]
command = "env"
args = ["CODEGRAPH_NO_DAEMON=1", "codegraph", "serve", "--mcp", "--no-watch"]The new distinguishing signal is that a single Codex parent process spawned two CodeGraph MCP server processes for the same physical repository with different path casing:
Codex parent:
3608352 .../bin/codex
parent cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
CodeGraph MCP children of the same parent:
3608757 cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
3609744 cwd=/mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal
Both paths point to the same .codegraph/codegraph.db inode on DrvFs:
upper=70:6192449487701361 /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal/.codegraph/codegraph.db
lower=70:6192449487701361 /mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal/.codegraph/codegraph.db
After this, CLI codegraph status reported:
Failed to get status: database disk image is malformed
Killing the repo-matching CodeGraph MCP processes made CLI status/sync recover, but the Codex MCP tool call then failed with:
Error: tool call error: tool call failed for `codegraph/codegraph_explore`
Caused by:
Transport closed
Environment
- CodeGraph:
1.1.3 - Node:
v22.19.0 - Client: OpenAI Codex CLI
- OS: WSL2 Ubuntu on Windows, project on
/mnt/dDrvFs - Project path used by shell / parent process:
/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal - MCP config:
[mcp_servers.codegraph]
command = "env"
args = ["CODEGRAPH_NO_DAEMON=1", "codegraph", "serve", "--mcp", "--no-watch"]Observed process state
pwd and pwd -P from the active shell both showed the uppercase canonical-looking path:
/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
The Codex parent process also had the uppercase cwd:
3608352 3608345 Mon Jun 29 15:39:36 2026 .../bin/codex
parent cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
But the same parent spawned two CodeGraph MCP server processes:
PID 3608757
ppid=3608352
cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
env=CODEGRAPH_NO_DAEMON=1
cmdline=node .../bin/codegraph serve --mcp --no-watch
PID 3609744
ppid=3608352
cwd=/mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal
env=CODEGRAPH_NO_DAEMON=1
cmdline=node .../bin/codegraph serve --mcp --no-watch
The Codex MCP tool call also carried the lower-case project path:
codegraph.codegraph_explore({
"projectPath": "/mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal",
"query": "RunCurrentVersionSwitch CurrentVersionSwitchService ExecuteBatch ValidateBatchPreconditions RecordBatchNotExecuted DrawingTransferManagerForm CanExecuteBatchVersionSwitch DrawingTransferPlanRecord IsExecutableRecord",
"maxFiles": 5
})
Daemon-related observation despite CODEGRAPH_NO_DAEMON=1
Even with CODEGRAPH_NO_DAEMON=1 in the MCP server environment, .codegraph/daemon.pid contained a relocated daemon:
{
"pid": 3607983,
"version": "1.1.3",
"socketPath": "/tmp/codegraph-befc5052950cc73f.sock",
"startedAt": 1782718772179
}Process:
PID 3607983
cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
env=CODEGRAPH_DAEMON_INTERNAL=1
cmdline=.../codegraph.js serve --mcp --path /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
.codegraph/daemon.log included:
[CodeGraph daemon] Socket /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal/.codegraph/daemon.sock unusable (ENOTSUP); relocating to /tmp/codegraph-befc5052950cc73f.sock.
[CodeGraph daemon] Listening on /tmp/codegraph-befc5052950cc73f.sock (pid 3607983, v1.1.3). Idle timeout 300000ms.
[CodeGraph MCP] File watcher disabled — project is on a WSL2 /mnt/ drive, where recursive fs.watch is too slow to be reliable. The graph will not auto-update; run `codegraph sync` (or install the git sync hooks via `codegraph init`) to refresh.
This may be expected under some path, but it was surprising because the user-facing MCP process had CODEGRAPH_NO_DAEMON=1.
Symptoms
codegraph syncwas run.codegraph statusthen failed:
Failed to get status: database disk image is malformed
- Multiple CodeGraph MCP processes existed, including two for the same physical repo via upper/lower casing.
- After killing repo-matching processes, CLI
statusrecovered andsynccompleted. - The subsequent Codex MCP call failed:
Error: tool call error: tool call failed for `codegraph/codegraph_explore`
Caused by:
Transport closed
Why this seems distinct from #974
#974 was on v1.1.0 and the recommended workaround was CODEGRAPH_NO_DAEMON=1 to skip the shared daemon on WSL2 /mnt.
In this case:
- CodeGraph is
1.1.3. - The MCP server process has
CODEGRAPH_NO_DAEMON=1. - The MCP server also has
--no-watch. - The same Codex parent process spawns two CodeGraph MCP children for the same physical repo with different path casing.
- The same SQLite database inode is shared by those two path spellings.
- A relocated internal daemon still appears in
.codegraph/daemon.pid. - The index can become malformed, then CLI recovers after killing repo-matching MCP processes, while MCP still returns
Transport closed.
Expected behavior
On WSL2 /mnt / DrvFs, CodeGraph should either:
- canonicalize/case-fold project roots that point to the same physical directory before deciding project identity and
.codegraphownership, or - lock the SQLite database robustly across equivalent path spellings, or
- fail fast with an actionable error when the same physical
.codegraphdirectory is opened through multiple path spellings.
It should not allow two MCP processes from the same client parent to treat upper/lowercase spellings of the same DrvFs path as separate project roots while writing the same SQLite database.
Questions
- Is
CODEGRAPH_NO_DAEMON=1expected to fully prevent creation/use of a shared daemon in v1.1.3, or can a relocatedCODEGRAPH_DAEMON_INTERNAL=1process still appear? - Does CodeGraph currently canonicalize WSL DrvFs paths by physical identity, or only by path string/realpath?
- Would you prefer this be fixed in CodeGraph project-root identity handling, or should MCP clients like Codex avoid sending
projectPathvalues that differ in casing from the server process cwd?
I can provide additional command transcripts if useful.
Activity
inth3shadows commentedon Jun 30, 2026
Opened #1082 with a fix for this.
Short version: the MCP server caches each open SQLite connection keyed by the resolved-root path string, so two spellings of one repo (an upper/lowercase
/mntpath on WSL, or a symlinked checkout) become two cache keys → two connections to the same.codegraph/codegraph.db→ corruption. The PR keys the cache on(dev, ino)filesystem identity instead, which is identical for every spelling. Worth noting:realpathSyncalone does not fix the case-variant half — on a case-insensitive, case-preserving filesystem it returns the caller's casing — which I verified on a real WSL DrvFs mount. Reproduced before/after on WSL2; added a deterministic symlink-based regression test that runs on case-sensitive CI too.One process question, not specific to this issue: for WSL /
/mnt-specific bugs like this, do you prefer a PR (as here) or an issue comment with the repro + diagnosis? Happy to match whichever keeps your review queue lightest.inth3shadows commentedon Sep 8, 2026
Still reproduces on
main@ee83636. Flagging one thing before this gets folded into the new writer lock — it isn't covered by it.src/mcp/writer-lock.ts(#1744) is a cross-process pidfile, acquired once per server at startup (daemon.ts:210,engine.ts:294,index.ts:373) to decide who owns live auto-sync. #1057 is one process opening two SQLite connections to one database through two spellings of the path, insideToolHandler's cache. The lock cannot see that — a second acquire from the same pid returns{kind:'acquired'}by design ("Same process already holds it").The keying that causes it is untouched on
main:findNearestCodeGraphRootreturns a cased path string, so on a case-insensitive mount two spellings of one repo become two cache entries, and a second connection to the same.codegraph/codegraph.db.A separate note on the writer lock itself
Found while checking the above.
getWriterPidPathnormalizes withfs.realpathSync, and that does not converge on a case-insensitive mount. Probed today on WSL2 DrvFs:So two direct-mode servers started at two spellings of one repo each write their own
writer.pidunder a different.codegraphspelling, and both acquire — the gap #1740 set out to close, reappearing one layer up. Happy to open that as its own issue rather than leave it buried here; say the word and I will.#1082 collapses every spelling onto a single
dev:inokey, which is the identity that actually converges. It has been open since 2026-06-30 and GitHub currently reports itDIRTY/CONFLICTING— I'll refresh it againstmainif it's still wanted.merge: upstream/main, keeping the dev:ino cache key
inth3shadows commentedon Sep 8, 2026
Correcting myself: the writer-lock half of my earlier comment is wrong. Please disregard it.
I claimed that because
getWriterPidPathnormalizes withfs.realpathSync, and realpath does not converge on a case-insensitive mount, two direct-mode servers started at two spellings of one repo would each write their ownwriter.pidand both acquire. I inferred that from the realpath behaviour without testing the acquire itself. Tested now, againstmain@b8d46d1, two separate processes on WSL2 DrvFs/mnt/c:The lock holds. The reason is that the pidfile path being a different string does not make it a different file: on a case-insensitive mount both spellings resolve to one inode, so the
linkSync/O_EXCLcreate hitsEEXISTwhichever spelling asks.The filesystem does the deduplication that realpath doesn't, and for a symlinked checkout realpath converges anyway. So there is no writer-lock defect here and nothing to file — my apologies for the noise.
What this does not change: everything else in that comment stands, and I re-verified it at
b8d46d1rather than assuming it survived the day's commits:projectCacheis still keyed by the path string, two spellings still produce two entries, and the measured3 connections for 2 projectsthrough the realToolHandleris unaffected — that path never touches the writer lock.fix(mcp): share one connection across two spellings of one root (colb…
fix(mcp): keep root aliases out of the connection cache (colbymchenry…
fix(mcp): deduplicate equivalent project paths (#1057) (#2020)
colbymchenry commentedon Sep 27, 2026
Thanks for the detailed report, @yjtdkj, and thanks for using CodeGraph! This is fixed on
mainby #2020, which builds on @inth3shadows's #1082 (thank you!): the MCP server now recognises two spellings of the same repository (a symlink, or different casing on a case-insensitive drive like WSL/mnt/c) as one index and shares a single database connection, instead of opening it twice. The fix will be in the next release.