You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
boost graph session-start is installed as an async: true SessionStart hook. It spawns node .../boostgraph.js session-start, which runs a full indexAll() of the project root. Nothing in that path watches the host: no PPID watchdog, no stdin lifeline (the hook reads stdin for 500 ms and then unrefs it), no time, file-count, or memory budget. When the Claude Code session that started the hook exits, the whole tree (sh -c → boost → node) is reparented to PID 1 and keeps indexing at full CPU until it finishes or someone kills it.
On this machine one such orphan ran for 15 minutes at 115 to 130 % CPU with 906 to 930 MB resident (82 GB virtual) after its session had been closed for 12 of those minutes. It held boostgraph.lock, so the next session's daemon disabled its file watcher, and that session's own session-start raced it on the same database, which grew from 565 MB to 1.65 GB plus a 631 MB WAL before both were killed. Combined resident memory of the three BoostGraph node processes for one directory was about 2.4 GB.
Environment
Boost v0.13.5 at the time (the orphan's node binary was already (deleted), so an auto-update had replaced the version directory beneath it; now v0.13.11), bundled BoostGraph package v1.5.0, bundled node v24.16.0. Ubuntu under WSL2, kernel 6.18.33.2-microsoft-standard-WSL2, 36 GB RAM. Hook as installed by boost init in ~/.claude/settings.json:
Timeline (UTC, 6 September; process snapshots recorded by Boost's own command history)
Time
Event
03:57:01
Session A starts in /tmp. Hook spawns pid 1460064 sh -c boost graph session-start → 1460065 boost graph session-start → 1460152 node boostgraph.js session-start
03:57:49
Snapshot: 1460152 at 906 MB RSS, top consumer on the machine
04:00:28
Session A exits (/exit)
04:02:12
Session B starts in /tmp. Its daemon (pid 1466602) logs Listening; ten seconds later: File watcher disabled … BoostGraph file lock held by another process past the retry budget; auto-sync disabled
04:02:52
Snapshot: 1460064 has PPID 1; 1460152 is at 130 % CPU, 930 MB RSS, exe -> node (deleted). Session B's own indexer 1466617 is at 101 % CPU, 720 MB. Daemon 1466602 at 820 MB
04:08:25
Snapshot: 1460152 still running, 11 min 24 s elapsed, 115 % CPU, 913 MB. 1466617 at 758 MB
04:11
Database at 1.65 GB + 631 MB WAL, still growing
04:12:34
Both indexers killed by hand; /tmp/.boost deleted at 04:14
An empty directory /tmp/.git existed (created 3 September, contents: nothing). findEnsureProjectRoot accepts any directory with a .git entry (directory.js:259, fs.existsSync(path.join(dir, '.git'))), and unsafeIndexRootReason refuses only the filesystem root, $HOME, and ancestors of $HOME. /tmp on this machine held about 194,000 files, most of them Python virtual environments left by earlier tooling. The indexer walked all of them. That is the reason for the memory figure, but the same lifecycle gap applies to any large project: the process outlives its session regardless of what it is indexing.
Where the lifecycle gap is (lib/dist, v0.13.11 bundle)
bin/boostgraph.jssession-start action: readStdinWithTimeout(500) then sessionStartEnsureIndexed → cg.indexAll(). The comment above it notes that Claude keeps the hook's stdin open until the command exits, which is exactly the lifeline the MCP server uses (mcp/stdin-teardown.js, "an MCP stdio server's lifeline is its stdin"). The hook discards it: readStdinWithTimeout pauses and unref()s stdin after the timeout, so the later end/error when the host dies is never observed.
session-start.js has no reference to process.ppid, BOOSTGRAPH_HOST_PPID, or the watchdog modules. mcp/index.jsinstallPpidWatchdog runs only for the MCP server in direct mode.
BOOSTGRAPH_HOST_PPID is set to the boost launcher's pid, not the agent's, so even a watchdog keyed on it would see a live parent: the launcher is orphaned along with the node process.
No budget of any kind: indexAll() runs to completion or until killed.
Impact
Every closed session leaves its cold-start index running to completion. On a large tree that is minutes of a saturated core and up to a gigabyte of RSS per abandoned session, invisible unless the user looks at top (where it appears as MainThread, since that is the thread name node sets).
The orphan holds boostgraph.lock, so the next session in the same project gets a daemon with auto-sync disabled and a second indexer racing the first on the same SQLite file.
The daemon and MCP proxy do the right thing here; only the hook path lacks supervision.
Suggested fixes
Keep stdin as the lifeline after the 500 ms read: leave an end/error listener attached (as treatStdinFailureAsShutdown does for the server) and abort the index when it fires. Claude Code holds the pipe open for the hook's lifetime, so host exit is observable.
Add the PPID watchdog to session-start (poll process.ppid divergence, plus the sh/launcher chain: if the launcher's own parent becomes 1, stop). Thread the agent's pid, not the launcher's, into BOOSTGRAPH_HOST_PPID.
Bound the cold-start index: a file-count or byte ceiling above which the hook declines and prints a one-line hint (boost graph init to index deliberately), and add the OS temp directory to unsafeIndexRootReason.
Require a real repository, not a .git entry: git rev-parse --git-dir or a check for HEAD inside .git.
BOOSTGRAPH_NO_SESSION_START=1 in the environment disables the hook; index projects deliberately with boost graph init. Remove any stray .git entries in directories you launch agents from. Check for leftovers with ps -eo pid,ppid,etime,pcpu,rss,args | grep '[b]oostgraph.js session-start' and kill them.
Method
Process snapshots are the ps outputs Boost's history database recorded for the sessions in question (the commands table in Boost's local history database keeps each command's original output), cross-checked against the Claude Code transcripts for session start and exit times. Code behaviour is from the shipped lib/dist/*.js in the v0.13.11 bundle. The /tmp/.boost index and the orphan were deleted at the time, so the numbers above are from those snapshots, not a live reproduction.
Summary
boost graph session-startis installed as anasync: trueSessionStart hook. It spawnsnode .../boostgraph.js session-start, which runs a fullindexAll()of the project root. Nothing in that path watches the host: no PPID watchdog, no stdin lifeline (the hook reads stdin for 500 ms and then unrefs it), no time, file-count, or memory budget. When the Claude Code session that started the hook exits, the whole tree (sh -c→boost→node) is reparented to PID 1 and keeps indexing at full CPU until it finishes or someone kills it.On this machine one such orphan ran for 15 minutes at 115 to 130 % CPU with 906 to 930 MB resident (82 GB virtual) after its session had been closed for 12 of those minutes. It held
boostgraph.lock, so the next session's daemon disabled its file watcher, and that session's ownsession-startraced it on the same database, which grew from 565 MB to 1.65 GB plus a 631 MB WAL before both were killed. Combined resident memory of the three BoostGraph node processes for one directory was about 2.4 GB.Environment
Boost v0.13.5 at the time (the orphan's
nodebinary was already(deleted), so an auto-update had replaced the version directory beneath it; now v0.13.11), bundled BoostGraph package v1.5.0, bundled node v24.16.0. Ubuntu under WSL2, kernel6.18.33.2-microsoft-standard-WSL2, 36 GB RAM. Hook as installed byboost initin~/.claude/settings.json:Timeline (UTC, 6 September; process snapshots recorded by Boost's own command history)
/tmp. Hook spawns pid 1460064sh -c boost graph session-start→ 1460065boost graph session-start→ 1460152node boostgraph.js session-start/exit)/tmp. Its daemon (pid 1466602) logsListening; ten seconds later:File watcher disabled … BoostGraph file lock held by another process past the retry budget; auto-sync disabledexe -> node (deleted). Session B's own indexer 1466617 is at 101 % CPU, 720 MB. Daemon 1466602 at 820 MB/tmp/.boostdeleted at 04:14Snapshot at 04:02:52, verbatim apart from
$HOME:Why
/tmpwas indexed at allAn empty directory
/tmp/.gitexisted (created 3 September, contents: nothing).findEnsureProjectRootaccepts any directory with a.gitentry (directory.js:259,fs.existsSync(path.join(dir, '.git'))), andunsafeIndexRootReasonrefuses only the filesystem root,$HOME, and ancestors of$HOME./tmpon this machine held about 194,000 files, most of them Python virtual environments left by earlier tooling. The indexer walked all of them. That is the reason for the memory figure, but the same lifecycle gap applies to any large project: the process outlives its session regardless of what it is indexing.Where the lifecycle gap is (
lib/dist, v0.13.11 bundle)bin/boostgraph.jssession-startaction:readStdinWithTimeout(500)thensessionStartEnsureIndexed→cg.indexAll(). The comment above it notes that Claude keeps the hook's stdin open until the command exits, which is exactly the lifeline the MCP server uses (mcp/stdin-teardown.js, "an MCP stdio server's lifeline is its stdin"). The hook discards it:readStdinWithTimeoutpauses andunref()s stdin after the timeout, so the laterend/errorwhen the host dies is never observed.session-start.jshas no reference toprocess.ppid,BOOSTGRAPH_HOST_PPID, or the watchdog modules.mcp/index.jsinstallPpidWatchdogruns only for the MCP server in direct mode.BOOSTGRAPH_HOST_PPIDis set to theboostlauncher's pid, not the agent's, so even a watchdog keyed on it would see a live parent: the launcher is orphaned along with the node process.indexAll()runs to completion or until killed.Impact
top(where it appears asMainThread, since that is the thread name node sets).boostgraph.lock, so the next session in the same project gets a daemon with auto-sync disabled and a second indexer racing the first on the same SQLite file.Suggested fixes
end/errorlistener attached (astreatStdinFailureAsShutdowndoes for the server) and abort the index when it fires. Claude Code holds the pipe open for the hook's lifetime, so host exit is observable.session-start(pollprocess.ppiddivergence, plus thesh/launcher chain: if the launcher's own parent becomes 1, stop). Thread the agent's pid, not the launcher's, intoBOOSTGRAPH_HOST_PPID.boost graph initto index deliberately), and add the OS temp directory tounsafeIndexRootReason..gitentry:git rev-parse --git-diror a check forHEADinside.git.(deleted)case), or haveboost updateskip removing a version directory that has live processes (the POSIX counterpart of boost init re-extracts BoostGraph over a running server on Windows, then every boost graph fails with unlinkat ...\node.exe: Access is denied (v0.13.7) #71).Workaround
BOOSTGRAPH_NO_SESSION_START=1in the environment disables the hook; index projects deliberately withboost graph init. Remove any stray.gitentries in directories you launch agents from. Check for leftovers withps -eo pid,ppid,etime,pcpu,rss,args | grep '[b]oostgraph.js session-start'and kill them.Method
Process snapshots are the
psoutputs Boost's history database recorded for the sessions in question (thecommandstable in Boost's local history database keeps each command's original output), cross-checked against the Claude Code transcripts for session start and exit times. Code behaviour is from the shippedlib/dist/*.jsin the v0.13.11 bundle. The/tmp/.boostindex and the orphan were deleted at the time, so the numbers above are from those snapshots, not a live reproduction.