Summary
On Windows, the dashboard never starts under the detached VS Code SessionStart hook because plugin/scripts/dashboard-build.sh fails and retries forever. There are three independent root causes that stack; with all three the dashboard build never completes and ~/.claude-smart/dashboard-unavailable is rewritten every session. All three are present on main at v0.2.49 (the v0.2.49 Windows work in #99 touched dashboard-service.sh/_lib.sh/smart-install.sh, not dashboard-build.sh, which is unchanged since #65 / 2026-06-07).
Environment: Windows 11, Git-Bash (msys2), system Node v26.4.0 / npm 11.18.0 on PATH (so the private-node install is intentionally skipped by install_private_node). Claude Code VS Code extension (CLAUDE_CODE_ENTRYPOINT=claude-vscode).
The symptom is masked by --silent (see cause 2), so the failing log initially only shows a repeating:
[claude-smart] dashboard build: npm ci failed; removed partial node_modules
[claude-smart] dashboard: .next missing — starting background build
... (every session)
Root cause 1 — concurrent-build race clobbers node_modules
dashboard-build.sh is spawned near-simultaneously by two paths on session start (smart-install.sh Setup and dashboard-service.sh SessionStart). The single-flight lock has a TOCTOU hole: the pid file is written only after mkdir acquires the lock, so a peer arriving in that window sees the lock dir but no pid file. claude_smart_pid_alive_file returns false on a missing file, so the peer judges the lock stale, rm -rfs it and steals it. Two npm ci then run in the same dir, and each failure path does rm -rf node_modules, clobbering the other → the "npm ci failed" loop, .next never builds.
Fix: disambiguate "missing pid" by the lock dir's mtime — a young lock means a peer is mid-acquire (back off), an aged lock with no pid means the holder crashed before writing its pid (reclaim). Closes the race without reintroducing a permanent deadlock on a crashed holder.
Root cause 2 — npm ci aborts: a dependency postinstall can't resolve bare node
Once the race is fixed, npm ci still fails deterministically:
npm error command C:\WINDOWS\system32\cmd.exe /d /s /c node -e "import('./config/scripts/postinstall.js').catch(() => void 0)"
npm error Der Befehl "node" ist ... konnte nicht gefunden werden. # 'node' not found
msw's postinstall shells out to bare node. npm.cmd itself runs (it locates node via its own %~dp0), but the cmd.exe grandchild npm spawns for the lifecycle script resolves node only through the inherited Windows %PATH%, which doesn't reliably carry the nodejs dir in the detached hook. npm 7+ no longer prepends node's execPath dir to a script's PATH, so the script fails and the whole npm ci aborts. dashboard-build.sh:113 also passes --silent, which hid this error entirely.
Fix: drop --silent (so failures are diagnosable) and install with --ignore-scripts. The dashboard builds and runs correctly without dependency lifecycle scripts (sharp etc. resolve via their prebuilt platform packages); npm's own newer default already blocks unlisted install scripts, so this just makes it explicit and portable.
Root cause 3 — oversized PATH truncated for cmd.exe children: next build → "next not found"
Same class, now at the build step:
> next build
Der Befehl "next" ist ... konnte nicht gefunden werden.
The detached hook's PATH is ~7–9 KB: claude_smart_prepend_node_bins pushes a (nonexistent, when a system node is used) private-node dir to the front, and repeated login-shell sourcing duplicates the whole list 3–4×. When npm's node process spawns a cmd.exe child for next build, that oversized environment is truncated for the child, dropping the entries the script needs (nodejs and npm-appended node_modules/.bin), so bare next isn't resolvable. Measured: the hook PATH reaches ~9 KB; an interactive build with the same content deduped to ~3 KB succeeds.
Fix: order-preservingly deduplicate PATH (anchoring the resolved nodejs dir at the front) before invoking npm, shrinking it back under the truncation threshold — the same small PATH under which an interactive build succeeds.
Reference implementation
All three are fixed and verified end-to-end (fresh install from a vendored copy → clean npm ci, next build completes, dashboard serves 200 on http://localhost:3001, race lock logs "peer acquiring lock; backing off") in these downstream patches against the vendored copy (path there is plugins/claude-smart/scripts/dashboard-build.sh):
- smoochy/claude-marketplace#94 — race fix + drop
--silent
- smoochy/claude-marketplace#95 —
--ignore-scripts + PATH dedup
Happy to open a PR porting these to plugin/scripts/dashboard-build.sh (+ optionally hardening claude_smart_prepend_node_bins) if useful.
Summary
On Windows, the dashboard never starts under the detached VS Code SessionStart hook because
plugin/scripts/dashboard-build.shfails and retries forever. There are three independent root causes that stack; with all three the dashboard build never completes and~/.claude-smart/dashboard-unavailableis rewritten every session. All three are present onmainat v0.2.49 (the v0.2.49 Windows work in #99 toucheddashboard-service.sh/_lib.sh/smart-install.sh, notdashboard-build.sh, which is unchanged since #65 / 2026-06-07).Environment: Windows 11, Git-Bash (msys2), system Node v26.4.0 / npm 11.18.0 on
PATH(so the private-node install is intentionally skipped byinstall_private_node). Claude Code VS Code extension (CLAUDE_CODE_ENTRYPOINT=claude-vscode).The symptom is masked by
--silent(see cause 2), so the failing log initially only shows a repeating:Root cause 1 — concurrent-build race clobbers
node_modulesdashboard-build.shis spawned near-simultaneously by two paths on session start (smart-install.shSetup anddashboard-service.shSessionStart). The single-flight lock has a TOCTOU hole: the pid file is written only aftermkdiracquires the lock, so a peer arriving in that window sees the lock dir but no pid file.claude_smart_pid_alive_filereturns false on a missing file, so the peer judges the lock stale,rm -rfs it and steals it. Twonpm cithen run in the same dir, and each failure path doesrm -rf node_modules, clobbering the other → the "npm ci failed" loop,.nextnever builds.Fix: disambiguate "missing pid" by the lock dir's mtime — a young lock means a peer is mid-acquire (back off), an aged lock with no pid means the holder crashed before writing its pid (reclaim). Closes the race without reintroducing a permanent deadlock on a crashed holder.
Root cause 2 —
npm ciaborts: a dependency postinstall can't resolve barenodeOnce the race is fixed,
npm cistill fails deterministically:msw's postinstall shells out to barenode.npm.cmditself runs (it locates node via its own%~dp0), but thecmd.exegrandchild npm spawns for the lifecycle script resolvesnodeonly through the inherited Windows%PATH%, which doesn't reliably carry the nodejs dir in the detached hook. npm 7+ no longer prepends node's execPath dir to a script's PATH, so the script fails and the wholenpm ciaborts.dashboard-build.sh:113also passes--silent, which hid this error entirely.Fix: drop
--silent(so failures are diagnosable) and install with--ignore-scripts. The dashboard builds and runs correctly without dependency lifecycle scripts (sharp etc. resolve via their prebuilt platform packages); npm's own newer default already blocks unlisted install scripts, so this just makes it explicit and portable.Root cause 3 — oversized
PATHtruncated for cmd.exe children:next build→ "next not found"Same class, now at the build step:
The detached hook's
PATHis ~7–9 KB:claude_smart_prepend_node_binspushes a (nonexistent, when a system node is used) private-node dir to the front, and repeated login-shell sourcing duplicates the whole list 3–4×. When npm's node process spawns acmd.exechild fornext build, that oversized environment is truncated for the child, dropping the entries the script needs (nodejs and npm-appendednode_modules/.bin), so barenextisn't resolvable. Measured: the hookPATHreaches ~9 KB; an interactive build with the same content deduped to ~3 KB succeeds.Fix: order-preservingly deduplicate
PATH(anchoring the resolved nodejs dir at the front) before invoking npm, shrinking it back under the truncation threshold — the same smallPATHunder which an interactive build succeeds.Reference implementation
All three are fixed and verified end-to-end (fresh install from a vendored copy → clean
npm ci,next buildcompletes, dashboard serves 200 onhttp://localhost:3001, race lock logs "peer acquiring lock; backing off") in these downstream patches against the vendored copy (path there isplugins/claude-smart/scripts/dashboard-build.sh):--silent--ignore-scripts+ PATH dedupHappy to open a PR porting these to
plugin/scripts/dashboard-build.sh(+ optionally hardeningclaude_smart_prepend_node_bins) if useful.