Summary
The "show each advisory type at most once per session" throttle in hooks/core/routing.mjs (guidanceOnce) never throttles under Claude Code: the marker directory is keyed on process.ppid, but Claude Code spawns every hook invocation under a fresh parent process, so the key changes on every call.
Measured impact (Claude Code on WSL2, plugin 1.0.65)
- 2,149 directories
/tmp/context-mode-guidance-<pid> accumulated, one per hook invocation (each containing a single marker file that no later invocation ever reads).
- The
context_guidance tip (~90 tokens) was injected on every Bash/Read/Grep call in every session, instead of once per session per type. On a working session with 100+ tool calls, that is thousands of tokens of repeated identical guidance, which is the opposite of what context-mode is for.
- Nothing ever cleans these directories up (sessionstart's lazy cleanup only handles plugin cache version dirs).
Root cause
// hooks/core/routing.mjs
const _guidanceId = process.env.VITEST_WORKER_ID
? `${process.ppid}-w${process.env.VITEST_WORKER_ID}`
: String(process.ppid);
const _guidanceDir = resolve(tmpdir(), `context-mode-guidance-${_guidanceId}`);
process.ppid is only stable when the host process invokes hooks directly and stays alive. Claude Code (and likely other hosts that shell out per hook) gives each invocation a different parent PID, so O_CREAT | O_EXCL always succeeds and the guidance is returned every time.
Suggested fix
Key the marker directory on the session rather than the parent process. The hook stdin payload already carries session_id:
- in
pretooluse.mjs, after JSON.parse(raw): if (input.session_id) globalThis.__ctxGuidanceSessionId = String(input.session_id);
- in
routing.mjs, resolve the marker dir lazily: session_id (via the global) → process.env.CLAUDE_SESSION_ID → process.ppid as the last-resort fallback (preserves current behavior for in-process hosts like the OpenCode ts-plugin, and the VITEST_WORKER_ID suffix logic).
Running with this patch locally: first call per type gets the tip, subsequent calls are quiet, one marker dir per session. Happy to send a PR if useful.
A cleanup pass for stale context-mode-guidance-* dirs (age-gated, like the #181 cache cleanup) would also help hosts that accumulated thousands of them.
Two smaller observations from the same audit
<output_constraints> in the SessionStart routing block (word_limit 500 words, "Return only: file path + 1-line description") reads like subagent instructions, but it is injected into the main session too, where it fights the host's own response conventions. Consider emitting it only on the Agent/Task prompt-append path.
- On this setup,
ctx_fetch_and_index failed 60% and ctx_search 58% of calls over 7 days (147 log files), while ctx_execute was at 0% over 919 calls, yet the injected hierarchy routes to batch_execute/search first. A "fall back to ctx_execute on error" hint in the guidance would keep agents from retrying the failing path. I can open a separate issue with the failure details if useful.
Summary
The "show each advisory type at most once per session" throttle in
hooks/core/routing.mjs(guidanceOnce) never throttles under Claude Code: the marker directory is keyed onprocess.ppid, but Claude Code spawns every hook invocation under a fresh parent process, so the key changes on every call.Measured impact (Claude Code on WSL2, plugin 1.0.65)
/tmp/context-mode-guidance-<pid>accumulated, one per hook invocation (each containing a single marker file that no later invocation ever reads).context_guidancetip (~90 tokens) was injected on every Bash/Read/Grep call in every session, instead of once per session per type. On a working session with 100+ tool calls, that is thousands of tokens of repeated identical guidance, which is the opposite of what context-mode is for.Root cause
process.ppidis only stable when the host process invokes hooks directly and stays alive. Claude Code (and likely other hosts that shell out per hook) gives each invocation a different parent PID, soO_CREAT | O_EXCLalways succeeds and the guidance is returned every time.Suggested fix
Key the marker directory on the session rather than the parent process. The hook stdin payload already carries
session_id:pretooluse.mjs, afterJSON.parse(raw):if (input.session_id) globalThis.__ctxGuidanceSessionId = String(input.session_id);routing.mjs, resolve the marker dir lazily:session_id(via the global) →process.env.CLAUDE_SESSION_ID→process.ppidas the last-resort fallback (preserves current behavior for in-process hosts like the OpenCode ts-plugin, and theVITEST_WORKER_IDsuffix logic).Running with this patch locally: first call per type gets the tip, subsequent calls are quiet, one marker dir per session. Happy to send a PR if useful.
A cleanup pass for stale
context-mode-guidance-*dirs (age-gated, like the #181 cache cleanup) would also help hosts that accumulated thousands of them.Two smaller observations from the same audit
<output_constraints>in the SessionStart routing block (word_limit500 words, "Return only: file path + 1-line description") reads like subagent instructions, but it is injected into the main session too, where it fights the host's own response conventions. Consider emitting it only on the Agent/Task prompt-append path.ctx_fetch_and_indexfailed 60% andctx_search58% of calls over 7 days (147 log files), whilectx_executewas at 0% over 919 calls, yet the injected hierarchy routes tobatch_execute/searchfirst. A "fall back to ctx_execute on error" hint in the guidance would keep agents from retrying the failing path. I can open a separate issue with the failure details if useful.