Skip to content

fix(harness-ops): name stale hook config and suggest /reload-plugins #6247

Description

@kyle-sexton

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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  • A transcript fixture whose hook_non_blocking_error stderr is <registered script path>: No such file or directory with exit 127 is classed stale-config, and the emitted message names /reload-plugins.
  • The same classification holds for fixtures whose stderr path is in backslash Windows form, forward-slash Windows form, mixed separators, and MSYS /c/... form, and for a registered command with ${CLAUDE_PLUGIN_ROOT} both literal and expanded.
  • A fixture where a launched hook prints No such file or directory about a path that is not in its registered command stays ambiguous (or completed), unchanged from today.
  • Existing launch, ambiguous and completed fixtures produce byte-identical messages, except the remedy sentence at :513, which now names /reload-plugins before restart.
  • The once-per-registration latch and the cursor behave as before (existing tests pass).
  • The harness-ops README section for the failure audit lists the new class.

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).

Activity

  1. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    on Oct 4, 2026
  2. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

    Thanks for the detailed write-up. I understand the ask: harness-ops' hook-failure-audit.sh Stop hook currently classes a missing registered hook script as ambiguous, giving a misleading remedy (pointing at the hook's own logic rather than naming the real cause: stale in-place-loaded plugin config). The proposal adds a stale-config classification, matched by basename on the stderr path against the registered command/args, with a message pointing at /reload-plugins first and restart second, plus updated fixtures/tests and a README update.

    This looks well scoped with clear acceptance criteria and evidence already gathered. I didn't find an existing open issue tracking this specific classifier change; #6073 (cache directories vanishing) and #6046 (testing briefs breaking on plugin updates) are related but distinct. Nothing here needs a maintainer decision beyond normal review of the approach when picked up.

    Automated triage. A maintainer reviews it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: mediumReal value, no hard deadline; normal backlog flow.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions