#143

fp-check Stop hook with matcher "*" fires on every unrelated session, causing forced LLM turns

OpenGeeksikhSecurityopened 6mo ago

Summary

The fp-check plugin's Stop hook in plugins/fp-check/hooks/hooks.json is registered with "matcher": "*", so it runs on every Claude Code session stop — not just sessions that actually used fp-check. This produces a noisy and expensive feedback loop on unrelated sessions.

Reproduction

  1. Install the fp-check@trailofbits plugin via the marketplace.
  2. Start any Claude Code session that has nothing to do with false-positive verification (e.g. a docs edit, an install troubleshooting session, a refactor).
  3. When the assistant tries to stop, the Stop hook fires and runs the 30-second LLM completeness checker prompt.
  4. The checker (correctly) recognizes the conversation is not about fp-check and tries to return approve per the prompt's own fallback rule, but the hook still consumes a full agent turn and injects a large block of stop-feedback into the conversation context.
  5. Repeat on every stop attempt — the hook fires again and again, burning context and tokens with no benefit.

Why this is wrong

  • Stop hooks with matcher: "*" run unconditionally. The completeness check is only meaningful when fp-check was actually used in the session.
  • The hook prompt itself acknowledges this with the fallback "If the conversation is not about fp-check verification at all, return 'approve'" — meaning the author knew unrelated sessions would hit it but relied on a self-describing prompt to no-op out. That doesn't prevent the LLM call, the context bloat, or the forced turn.
  • The companion SubagentStop hook is correctly scoped (it only matters for fp-check subagents), but the top-level Stop hook is over-broad.

Suggested fixes (any of)

  1. Scope the hook to fp-check sessions. Set an environment variable or session marker when the fp-check skill activates and key the matcher off that, instead of "*".
  2. Move the completeness check out of Stop and into the skill's own on-completion logic (e.g. as a final required step in the SKILL.md instructions). The skill knows when it's running; the harness doesn't.
  3. Delete the top-level Stop hook entirely. The SubagentStop hook already enforces structured-output completeness on the actual fp-check subagents (data-flow-analyzer, exploitability-verifier, poc-builder), which is where the verification rigor lives. The Stop hook is redundant on top of that and only adds noise on unrelated sessions.

Option 3 is the simplest and loses no real coverage.

Workaround for users hitting this

```bash claude plugin disable fp-check@trailofbits ```

Re-enable only when actively running false-positive verification.

Affected file

`plugins/fp-check/hooks/hooks.json`, lines 4–15 (the `Stop` block).

Environment

  • Claude Code (current as of 2026-04-08)
  • macOS, fp-check plugin v1.0.0 from the trailofbits marketplace

Activity

5
remiforall5mo ago

Confirming this is still active 3 weeks later (tested 2026-05-14, fp-check v1.0.0 from current marketplace). Same matcher: "*" behavior — every Stop in any unrelated session burns a full 30s LLM turn that ends up returning approve after the model realizes fp-check wasn't in use. Token cost and stop-feedback bloat in parent context are both real.

The in-prompt fallback rule ("If the conversation is not about fp-check verification at all, return 'approve'") doesn't prevent the issue because the hook still fires and consumes a full turn before the model gets to that branch.

Local workaround for users who need fp-check active

If disabling the plugin isn't an option, restructuring the prompt to put the fallback rule first as a hard gate (rather than buried as the last sentence) gets the model to short-circuit faster and stops the noisy "you're missing phases X/Y/Z" stop-feedback that the current ordering produces.

Concretely: prepend a STEP 0 — MANDATORY GATE: this hook MUST default to 'approve' block listing the specific evidence of fp-check usage (slash command, subagent names, phase markers, verdict assembly). Only proceed to the completeness check if at least one piece of evidence is present.

This is a patch, not a fix. Agreeing with @GeeksikhSecurity that option 3 (delete the top-level Stop hook) is the right answer — SubagentStop already enforces structured-output completeness on the actual fp-check subagents (data-flow-analyzer, exploitability-verifier, poc-builder), which is where verification rigor lives. The top-level Stop hook is redundant.

Happy to open a PR for option 3 if that would help land the fix.

remiforall5mo ago

Confirming this is still active 3 weeks later (tested 2026-05-14, fp-check v1.0.0 from current marketplace). Same matcher: "*" behavior — every Stop in any unrelated session burns a full 30s LLM turn that ends up returning approve after the model realizes fp-check wasn't in use. Token cost and stop-feedback bloat in parent context are both real.

The in-prompt fallback rule ("If the conversation is not about fp-check verification at all, return 'approve'") doesn't prevent the issue because the hook still fires and consumes a full turn before the model gets to that branch.

Local workaround for users who need fp-check active

If disabling the plugin isn't an option, restructuring the prompt to put the fallback rule first as a hard gate (rather than buried as the last sentence) gets the model to short-circuit faster and stops the noisy "you're missing phases X/Y/Z" stop-feedback that the current ordering produces.

Concretely: prepend a STEP 0 — MANDATORY GATE: this hook MUST default to 'approve' block listing the specific evidence of fp-check usage (slash command, subagent names, phase markers, verdict assembly). Only proceed to the completeness check if at least one piece of evidence is present.

This is a patch, not a fix. Agreeing with @GeeksikhSecurity that option 3 (delete the top-level Stop hook) is the right answer — SubagentStop already enforces structured-output completeness on the actual fp-check subagents (data-flow-analyzer, exploitability-verifier, poc-builder), which is where verification rigor lives. The top-level Stop hook is redundant.

Happy to open a PR for option 3 if that would help land the fix.

remiforall5mo ago

Quick follow-up after attempting the local patch workaround — turns out there's a subtlety that matters for anyone trying option 1 (local patch):

The plugin file is mirrored in two places, and only the one in the runtime cache is actually executed:

``` ~/.claude/plugins/marketplaces//plugins/fp-check/hooks/hooks.json # source ~/.claude/plugins/cache//fp-check//hooks/hooks.json # what actually runs ```

Patching only the marketplaces/ source has no effect — Claude Code reads from the cache. The patch must be applied to both files, and even then it'll be overwritten the next time the plugin cache is rebuilt (any update, reinstall, or marketplace refresh).

This makes the local-patch workaround significantly more fragile than it looks. For anyone hitting this bug, I'd now recommend claude plugin disable fp-check@trailofbits (option 2 in my previous comment) over the local patch. The disable state survives plugin updates cleanly and has zero maintenance cost.

Confirms the original point: option 3 (remove the top-level Stop hook in upstream) really is the only durable fix. Will look at sending a PR.

remiforall5mo ago

Quick follow-up after attempting the local patch workaround — turns out there's a subtlety that matters for anyone trying option 1 (local patch):

The plugin file is mirrored in two places, and only the one in the runtime cache is actually executed:

``` ~/.claude/plugins/marketplaces//plugins/fp-check/hooks/hooks.json # source ~/.claude/plugins/cache//fp-check//hooks/hooks.json # what actually runs ```

Patching only the marketplaces/ source has no effect — Claude Code reads from the cache. The patch must be applied to both files, and even then it'll be overwritten the next time the plugin cache is rebuilt (any update, reinstall, or marketplace refresh).

This makes the local-patch workaround significantly more fragile than it looks. For anyone hitting this bug, I'd now recommend claude plugin disable fp-check@trailofbits (option 2 in my previous comment) over the local patch. The disable state survives plugin updates cleanly and has zero maintenance cost.

Confirms the original point: option 3 (remove the top-level Stop hook in upstream) really is the only durable fix. Will look at sending a PR.

sandr85mo ago

@remiforall can you please sign the CLA so your PR can move along?