Part of #6244
Problem
Long-running sessions keep firing hooks whose scripts no longer exist. Claude Code loads hook config at session start. For plugins in a marketplace added from a local path, it loads them in place from the source directory, and "edits to the source directory take effect at the next session start or /reload-plugins" (code.claude.com/docs/en/plugins/loading.md:185). So when the checkout behind a local-directory marketplace moves to a commit that deletes a hook script, every running session keeps calling the deleted path until it reloads. Each call starts node and bash and fails. Until the session reloads, the guard that path implemented enforces nothing.
This is documented upstream behavior, not a Claude Code defect, so no plugin can stop it. What the plugin can fix is the detector's message. harness-ops' Stop hook hook-failure-audit.sh is the decoupled detector for exactly this (#2577; header :15-18). Today it handles the case poorly:
- A missing registered script reaches the hook as bash's
<path>: No such file or directory with exit 127. That has no exec-failure signature, so the record is classed ambiguous (plugins/harness-ops/hooks/hook-failure-audit.sh:223-242,400-404).
- The message then gives both remedies, including "read the hook's own logic for a command it could not run" (
:506). For this case that is a false lead: the registered script itself is gone.
- The remedy line says "restart the session to load the fix" (
:513). /reload-plugins reloads plugin hooks without a restart: "Reloaded: N plugins · N skills · N agents · N hooks ..." (code.claude.com/docs/en/plugins/cli-reference.md:830). It requires Claude Code 2.1.260+ (commands.md:127). Restarting a 24-hour orchestrator session to clear this is far more costly than a reload.
Evidence
Verified this pass (origin/main 26f98e156; docs fetched 2026-10-04):
From the 2026-10-04 Windows audit (Claude Code 2.1.289; E1 attributed measurement):
- Transcript hook records with "No such file" under
C:\Projects\melodic\claude-code-plugins\plugins\...:
- context-guard "Checking the context-zone gate..." PreToolUse: 119 of 119 fires failed;
- source-control "ready-for-review flip evidence gate" PreToolUse: 292 of 292 failed;
- context-guard PostToolBatch "Checking for a context-zone crossing...": 541 of 867 failed (E1).
- 21 of 28 running claude/node/bun processes started before the last settings change (process-name heuristic, E2).
Not verified: the exact stderr and exitCode Claude Code writes for these records. The audit reported "No such file" text. bash's own wording for a missing script operand is <path>: No such file or directory with exit 127, but no record was read in this pass.
Separate case, not this issue: "Plugin directory does not exist: ...\cache\melodic-software\harness-ops\1.1.0 / 2.5.0, context-guard\0.8.5" is the cache-directory case tracked in #6073. The new occurrence is posted there.
Proposed approach
- Add a fourth class,
stale-config, decided before ambiguous. The record's stderr carries No such file or directory on a path whose basename (for example zone-gate.sh) is the basename of a script argument in the record's registered command or args. Compare basenames, not whole paths: the same file can appear as C:\..., C:/..., mixed separators (this session's own hook errors print ...\plugins\guardrails/hooks/exec-bash.mjs) or MSYS /c/..., and whether the record carries ${CLAUDE_PLUGIN_ROOT} expanded is unverified. This is narrow in the same way the launch class is. A launched hook that prints the phrase about some other file of its own does not match, because the basename must be the registered script's.
- For
stale-config, the message says:
- the hook's own script is missing from disk;
- this session is running hook config loaded before the plugin changed;
- run
/reload-plugins (Claude Code 2.1.260+), or restart, to load the current config;
- the guard enforced nothing for the listed calls.
- Change the launch-class remedy line at
:513 to name /reload-plugins first and restart second. It applies to both plugin updates and in-place edits.
- Tests: a fixture record per new branch, and a stay-quiet case where a launched hook reports a missing child file that is not its registered script. The repo's hook-precision rule requires a stay-quiet test for any classifier change.
Alternatives considered:
- The item's proposal, a SessionStart or ConfigChange notice telling the user to restart after a plugin update. ConfigChange fires for settings and skill files only (
hooks.md:316,2746), not plugin hook files. SessionStart fires on /clear and compaction, but a stale session's SessionStart rows run the old config, and nothing records which version the session loaded. A new per-Stop version comparison would add work to every turn, which is the cost this audit is trying to cut. Not recommended. Improving the detector that already runs gives the notice on the first failure at no added per-turn cost.
- Upstream: nothing to file. In-place loading and
/reload-plugins are the documented contract.
Acceptance criteria
Constraints and gotchas
Context
Source: local handoff item 20261004-150216-claude-perf-audit-plugin-fixes.md, fix 9. Related: #6073 (cache directories vanishing under live sessions; comment posted), #2577 and #2849 (detector and classification history), #6152 and #4465 (the deletions), #6046 (testing briefs breaking on mid-session plugin updates).
Part of #6244
Problem
Long-running sessions keep firing hooks whose scripts no longer exist. Claude Code loads hook config at session start. For plugins in a marketplace added from a local path, it loads them in place from the source directory, and "edits to the source directory take effect at the next session start or
/reload-plugins" (code.claude.com/docs/en/plugins/loading.md:185). So when the checkout behind a local-directory marketplace moves to a commit that deletes a hook script, every running session keeps calling the deleted path until it reloads. Each call starts node and bash and fails. Until the session reloads, the guard that path implemented enforces nothing.This is documented upstream behavior, not a Claude Code defect, so no plugin can stop it. What the plugin can fix is the detector's message. harness-ops' Stop hook
hook-failure-audit.shis the decoupled detector for exactly this (#2577; header:15-18). Today it handles the case poorly:<path>: No such file or directorywith exit 127. That has no exec-failure signature, so the record is classedambiguous(plugins/harness-ops/hooks/hook-failure-audit.sh:223-242,400-404).:506). For this case that is a false lead: the registered script itself is gone.:513)./reload-pluginsreloads plugin hooks without a restart: "Reloaded: N plugins · N skills · N agents · N hooks ..." (code.claude.com/docs/en/plugins/cli-reference.md:830). It requires Claude Code 2.1.260+ (commands.md:127). Restarting a 24-hour orchestrator session to clear this is far more costly than a reload.Evidence
Verified this pass (origin/main
26f98e156; docs fetched 2026-10-04):plugins/context-guard/hooks/zone-gate.shandzone-crossing-inject.shwere deleted on 2026-10-04 by feat(mods)!: adopt Claude Code mods and move context-guard and rate-limit-guard onto them #6152 (69a808a), when the zone gate moved into the context-guard mod.plugins/source-control/hooks/pr-ready-evidence-gate.shwas deleted on 2026-09-24 by refactor(source-control)!: remove the skill-evidence system and the review-lane guards #4465 (aaf4c44). Both found withgit log --diff-filter=D.claude-code-pluginscheckout (audit config lane,known_marketplaces.json). That is the in-place case inloading.md:185.:244-253):command not foundand similar are excluded because a launched hook can print them about its own child.From the 2026-10-04 Windows audit (Claude Code 2.1.289; E1 attributed measurement):
C:\Projects\melodic\claude-code-plugins\plugins\...:Not verified: the exact stderr and exitCode Claude Code writes for these records. The audit reported "No such file" text. bash's own wording for a missing script operand is
<path>: No such file or directorywith exit 127, but no record was read in this pass.Separate case, not this issue: "Plugin directory does not exist: ...\cache\melodic-software\harness-ops\1.1.0 / 2.5.0, context-guard\0.8.5" is the cache-directory case tracked in #6073. The new occurrence is posted there.
Proposed approach
stale-config, decided beforeambiguous. The record's stderr carriesNo such file or directoryon a path whose basename (for examplezone-gate.sh) is the basename of a script argument in the record's registeredcommandor args. Compare basenames, not whole paths: the same file can appear asC:\...,C:/..., mixed separators (this session's own hook errors print...\plugins\guardrails/hooks/exec-bash.mjs) or MSYS/c/..., and whether the record carries${CLAUDE_PLUGIN_ROOT}expanded is unverified. This is narrow in the same way the launch class is. A launched hook that prints the phrase about some other file of its own does not match, because the basename must be the registered script's.stale-config, the message says:/reload-plugins(Claude Code 2.1.260+), or restart, to load the current config;:513to name/reload-pluginsfirst and restart second. It applies to both plugin updates and in-place edits.Alternatives considered:
hooks.md:316,2746), not plugin hook files. SessionStart fires on/clearand compaction, but a stale session's SessionStart rows run the old config, and nothing records which version the session loaded. A new per-Stop version comparison would add work to every turn, which is the cost this audit is trying to cut. Not recommended. Improving the detector that already runs gives the notice on the first failure at no added per-turn cost./reload-pluginsare the documented contract.Acceptance criteria
hook_non_blocking_errorstderr is<registered script path>: No such file or directorywith exit 127 is classedstale-config, and the emitted message names/reload-plugins./c/...form, and for a registered command with${CLAUDE_PLUGIN_ROOT}both literal and expanded.No such file or directoryabout a path that is not in its registered command staysambiguous(orcompleted), unchanged from today.launch,ambiguousandcompletedfixtures produce byte-identical messages, except the remedy sentence at:513, which now names/reload-pluginsbefore restart.Constraints and gotchas
bash ".../hook-failure-audit.sh",plugins/harness-ops/hooks/hooks.json:253), so it survives a missing node; keep it so.:251-253explains why)./reload-pluginsdoes not connect or disconnect plugin MCP servers in non-interactive sessions (cli-reference.md:846). That does not affect hooks.Context
Source: local handoff item 20261004-150216-claude-perf-audit-plugin-fixes.md, fix 9. Related: #6073 (cache directories vanishing under live sessions; comment posted), #2577 and #2849 (detector and classification history), #6152 and #4465 (the deletions), #6046 (testing briefs breaking on mid-session plugin updates).