claude-code - 💡(How to fix) Fix [BUG] Plugin-declared PostToolUse hook silently dropped on CLI v2.1.133 despite plugin enabled and hook syntactically valid [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

A plugin-declared PostToolUse hook at ~/.claude/plugins/marketplaces/<org>/<plugin>/skills/hooks/hooks.json was not invoked by the Claude Code CLI during a real customer-facing skill run, despite the plugin being enabled and the hook declaration being syntactically valid. The same hook command, when registered in ~/.claude/settings.json, fires reliably on every tool call.

Because the hook silently does not fire, downstream observability artifacts (a structured run-trace.json of every tool call) are missing entries that other artifacts in the same bundle reference. There is no .hook-errors.log, no stderr output, and no other diagnostic surface to detect the failure other than counting events at end-of-run.

Error Message

Without diagnostic surfaces (no error log, no stderr, no exit-code change), the failure is invisible until an engineer manually audits a bundle and notices the cross-reference gap. We caught this on a customer run. Other plugin authors are likely silently affected and not aware.

Root Cause

Because the hook silently does not fire, downstream observability artifacts (a structured run-trace.json of every tool call) are missing entries that other artifacts in the same bundle reference. There is no .hook-errors.log, no stderr output, and no other diagnostic surface to detect the failure other than counting events at end-of-run.

Fix Action

Fix / Workaround

Workaround we shipped

We shipped a layered workaround in our codebase (PR #59). The relevant parts:

  • Related VS Code extension issue (different surface, same shape): #18547
  • Closest closed regression report (settings.json side): #55644
  • Our workaround: https://github.com/ChalkTalk/claude/pull/59 (squash commit c3b6f3a on main)
  • Specific files in our patch:
    • skills/references/run-trace/init-trace.py — auto-install to user settings
    • skills/references/run-trace/verify-phase.py--check-hook-firing halt
    • skills/references/run-trace/README.md — Platform note section documenting the workaround for future engineers
    • skills/references/adoption-template-for-customer-facing-skills.md — Block 8 (output self-test) spec
RAW_BUFFERClick to expand / collapse

Preflight

  • Searched existing issues. The closest match is #18547 (VS Code extension, plugin hooks not firing), but that report explicitly notes "CLI loads the plugin hook correctly, without copying to settings.json." This issue is the inverse: a Claude Code CLI session in v2.1.133 silently dropped a plugin-declared PostToolUse hook, while the same hook fired reliably when copied into ~/.claude/settings.json. Filing as a separate issue since the surface (CLI) is different from #18547 (VS Code extension).
  • Single-bug report.
  • Latest version of Claude Code at time of repro (2.1.133).

Summary

A plugin-declared PostToolUse hook at ~/.claude/plugins/marketplaces/<org>/<plugin>/skills/hooks/hooks.json was not invoked by the Claude Code CLI during a real customer-facing skill run, despite the plugin being enabled and the hook declaration being syntactically valid. The same hook command, when registered in ~/.claude/settings.json, fires reliably on every tool call.

Because the hook silently does not fire, downstream observability artifacts (a structured run-trace.json of every tool call) are missing entries that other artifacts in the same bundle reference. There is no .hook-errors.log, no stderr output, and no other diagnostic surface to detect the failure other than counting events at end-of-run.

Environment

  • Claude Code: 2.1.133 (CLI, not VS Code extension)
  • OS: macOS, Darwin 24.6.0 arm64
  • Python: 3.13.12
  • Plugin source: marketplace plugin enabled via chalktalk@chalktalk: true in ~/.claude/settings.json
  • Plugin hooks file: ~/.claude/plugins/marketplaces/chalktalk/skills/hooks/hooks.json
  • Hook command path (resolved via ${CLAUDE_PLUGIN_ROOT}): ~/.claude/plugins/marketplaces/chalktalk/skills/references/run-trace/hooks/trace_emit.py

Expected behavior

When a plugin is enabled and declares a PostToolUse hook in hooks.json, the hook script should be invoked once per matching tool call (Bash, Edit, Write, MultiEdit, Read, mcp__* for our matcher), receive the standard hook envelope on stdin, and have its stdout/stderr handled per the documented contract.

Actual behavior

The hook script did not run at all during the failed session. Specifically:

  1. Plugin was enabled (chalktalk@chalktalk: true in ~/.claude/settings.json).
  2. The hooks.json declaration was syntactically valid (verified by python3 -c "import json; json.load(open(...))").
  3. The hook command path (resolved via ${CLAUDE_PLUGIN_ROOT}) pointed to an executable Python script.
  4. Across a multi-hour skill run with hundreds of Bash, Read, Edit, and mcp__* tool calls, zero hook events were emitted into the journal the script writes to. The journal file existed (the skill creates it at startup), but contained only events emitted by an in-skill helper, none from the hook.
  5. No .hook-errors.log was written. No stderr surfaced. No CC diagnostic indicated a hook had been registered or rejected.

Reproduction (in-session, observed during a brainstorming session before filing this report)

This is the empirical reproduction we ran inside a Claude Code session to isolate the failure mode. The reproduction was performed inside the same plugin context that exhibited the production failure.

  1. Wrote ~/.claude/.run-trace-active pointing at a temp report directory (the file the hook walks up to discover it should fire).
  2. Triggered a Read tool call on a markdown file. Read matches the hook's matcher predicate, so we expected one hook event in the journal.
  3. Observed: the journal file stayed empty. No .hook-errors.log was written. No stderr surfaced from the agent session.
  4. Manually piped a synthetic PostToolUse envelope (matching CC's documented JSON shape) into python3 ~/.claude/plugins/marketplaces/chalktalk/skills/references/run-trace/hooks/trace_emit.py. The hook ran cleanly and wrote the event to the journal as expected.

This rules out the hook script itself being broken, the .run-trace-active discovery logic being broken, the marker location being wrong, and the matcher predicate being wrong. The remaining unverified surface is the plugin-loader's hook invocation path inside Claude Code itself.

Hypotheses ruled out

  • Hook script broken: ruled out (manual stdin pipe ran the script cleanly and produced the expected journal entry).
  • Marker discovery broken: ruled out (helper-side events in the same journal were emitted using the same marker).
  • Permissions: ruled out (script is chmod +x, owned by the user, plugin dir readable).
  • Matcher predicate: ruled out (the manual pipe used a synthetic envelope matching the same matcher; production session also performed Read calls which match).
  • Plugin not enabled: ruled out (chalktalk@chalktalk: true was present in ~/.claude/settings.json; other plugin features worked).
  • Hook output suppressed: ruled out (no stderr, no .hook-errors.log, no diagnostic of any kind).

Hypothesis NOT ruled out

The Claude Code CLI's plugin-loader appears to register the plugin in some sessions and not invoke the hook on subsequent tool calls. This may be lazy-loading, session-state-dependent, or a regression in 2.1.x.

#55644 (closed as duplicate) reported a related-shaped failure for ~/.claude/settings.json hooks not firing in 2.1.119-2.1.126. Our case is the opposite (settings.json works, plugin hooks.json does not), but the symptom shape (silent drop, no diagnostic, ~42-batch persistence) is similar.

Workaround we shipped

We shipped a layered workaround in our codebase (PR #59). The relevant parts:

  1. Always-install to user settings: on first run, our skill's init-trace.py installs the same hook command into ~/.claude/settings.json in addition to the plugin's hooks.json. User-settings hooks empirically fire reliably across every CC version we have tested. If/when the plugin-loader is fixed upstream, the duplicate registration is harmless (idempotent).
  2. Synchronous detection at phase 1: a verify-phase.py --check-hook-firing flag halts the run at the first non-startup phase boundary if zero events with _tool_use_id (the hook signature) have landed in the journal. Loud signal to the operator before any expensive operations, not at end-of-run.
  3. End-of-run content predicate: a hook_events_present self-test predicate checks that at least 50% of sql_query events are hook-captured. If under threshold, blocks downstream artifact publish.

This works around the bug operationally, but does not fix the root cause and is not a substitute for the plugin-loader behaving as documented.

Why it matters

We use plugin-declared hooks for run-trace observability on customer-facing AI skill runs. The trace is the primary verification surface engineers use to audit calculations: every SQL query, every parameter binding, every model reference. When the hook silently does not fire, the run produces a bundle that looks complete (every expected file is present) but is internally broken: cross-references between summary, explainer, and journal don't line up, because the journal is missing entries that are referenced everywhere else.

Without diagnostic surfaces (no error log, no stderr, no exit-code change), the failure is invisible until an engineer manually audits a bundle and notices the cross-reference gap. We caught this on a customer run. Other plugin authors are likely silently affected and not aware.

What would help

  1. A diagnostic surface when the plugin-loader registers a hook but does not invoke it. Even a single line to stderr ("plugin hook trace_emit.py registered but not invoked for tool_use_id=...") would have collapsed our debugging time from a multi-hour audit to a one-grep diagnosis.
  2. Documentation clarifying whether plugin-declared hooks are expected to fire on every matching tool call, or only after some warm-up condition. Our reading of the docs says "every matching call." If there is a session-state condition we do not know about, that should be called out.
  3. Root-cause fix if this is a regression. Happy to provide additional repro context, plugin manifest snippets, or run a debug build if useful.

References

  • Related VS Code extension issue (different surface, same shape): #18547
  • Closest closed regression report (settings.json side): #55644
  • Our workaround: https://github.com/ChalkTalk/claude/pull/59 (squash commit c3b6f3a on main)
  • Specific files in our patch:
    • skills/references/run-trace/init-trace.py — auto-install to user settings
    • skills/references/run-trace/verify-phase.py--check-hook-firing halt
    • skills/references/run-trace/README.md — Platform note section documenting the workaround for future engineers
    • skills/references/adoption-template-for-customer-facing-skills.md — Block 8 (output self-test) spec

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

When a plugin is enabled and declares a PostToolUse hook in hooks.json, the hook script should be invoked once per matching tool call (Bash, Edit, Write, MultiEdit, Read, mcp__* for our matcher), receive the standard hook envelope on stdin, and have its stdout/stderr handled per the documented contract.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING