Skill-scoped hooks (PreToolUse/Stop) still persist after skill completes — re-report of #30874, reproducible on 2026-07-31

Status Fixed / completed
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jul 31, 2026 · closed Aug 25, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

This is a re-report of #30874, which was auto-closed by the stale bot without a fix. I independently reproduced the exact same bug on 2026-07-31 and want to flag that it's still affecting people, including at least one other user (yuke-x68) who reported in that issue's comments that the problem also affects PostToolUse and Stop, not just PreToolUse.

Description

Hooks defined in a skill's YAML frontmatter (hooks: field) are documented as being "scoped to the component's lifecycle and only run when that component is active... cleaned up when it finishes" (hooks doc). In practice, once a skill with a hooks block is invoked, its hooks remain registered and continue firing for the rest of the session, even in turns where the skill is never invoked again.

Reproduction (2026-07-31)

  1. Create a throwaway skill with a PreToolUse hook that logs a timestamp on every Bash call:
---
name: hook-scope-test
description: test skill
user-invocable: false
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "date '+%Y-%m-%d %H:%M:%S PreToolUse fired' >> /path/to/log.txt"
---
  1. Invoke the skill once via the Skill tool. Confirm the hook fires on subsequent Bash calls within the same turn (it does — this part matches the docs).
  2. Send a completely unrelated follow-up user message, starting a new turn, without re-invoking the skill.
  3. Run any Bash command in that new turn.

Expected behavior

Per the docs, the hook should have been cleaned up when the skill's turn finished, so step 4's Bash call should not trigger it.

Actual behavior

The hook still fires in step 4. The log file keeps accumulating new timestamped lines for every subsequent Bash call for the rest of the session, regardless of whether the skill is invoked again.

Why this matters

This makes skill-scoped hooks unusable for their apparently intended purpose: gating tool access only while a specific skill is doing its work (e.g., a read-only audit skill that wants to disallowed-tools/hook-block writes only during its own execution). Right now, invoking such a skill once has session-wide side effects indistinguishable from putting the same hook in settings.json directly — except it's implicit and easy to forget about, since nothing in the UI indicates "this hook is still active from a skill you ran 20 turns ago."

Related

  • #30874 (original report, same bug, auto-closed as stale without a fix — filing this because it's still reproducible today and per the bot's own suggestion: "Please open a new issue if this is still relevant")

View original on GitHub ↗

4 Comments

kaheidt · 2 months ago

Independently reproduced on Claude Code 2.1.229, Windows 11 (win32) — still present, and I can confirm the part that was previously only reported in a comment: all three of PreToolUse, PostToolUse, and Stop leak, not just PreToolUse.

Setup

A slash-invoked skill (/hook-test-skill) with *-matched hooks on all three events in its SKILL.md frontmatter, each appending to a file:

hooks:
  PreToolUse:
    - matcher: "*"
      hooks:
        - type: command
          command: "cmd /c echo PRE TOOL FIRED >> ${CLAUDE_PROJECT_DIR}/hooks/hook-fire-pre.txt"
  PostToolUse:  # same shape -> hook-fire-post.txt
  Stop:         # same shape -> hook-fire-stop.txt

Result

The skill ran to completion (its instructions reached their final "Stop" step at turn 4). I then continued the session with unrelated work — reading files, running bash, and WebFetch-ing the Claude Code docs. Every one of those post-skill tool calls fired the skill's hooks.

Ordered PreToolUse firing timeline (tool name per firing):

1  Write        \
2  Write         |  in-skill (the only 4 calls the skill made)
3  PowerShell    |
4  Write        /
5  Write         \
6  Read           |
7  Bash           |
8  Read           |
9  ToolSearch     |  ALL post-skill, in later turns, skill never re-invoked
10 WebFetch       |  (#10 and #15 are WebFetch calls reading the hooks doc
11 Grep           |   that documents this exact "cleaned up when it finishes"
12 Bash           |   contract — the hook fired while reading its own spec)
13 Bash           |
14 Read           |
15 WebFetch       |
16 Bash           |
17 Bash          /

Only firings 1–4 belong to the skill's lifecycle. Firings 5–17 are all later, unrelated turns. Event totals at time of capture: PreToolUse 17, PostToolUse 16, Stop 10 — and the logs kept growing with each new tool call.

Secondary anomaly (Windows)

On Windows the type: command hook doesn't appear to execute as written. The command is cmd /c echo PRE TOOL FIRED >> <file>, which should append the literal line once per firing. Instead the log contains 1 literal PRE TOOL FIRED line and 18 raw hook-input JSON payloads, each preceded by a Microsoft Windows [Version …] banner and a C:\...> prompt — as though an interactive cmd is being spawned and the hook's stdin JSON is captured into the redirect target. Possibly separate from the lifecycle bug, but it reproduced in the same run and makes cmd-based hook commands behave unexpectedly on Windows.

Happy to share sanitized log excerpts if useful.

bcherny collaborator · 1 month ago

Thanks for the detailed re-report and repro. I verified this on the current release (2.1.233): a PreToolUse hook defined in a skill's frontmatter does keep firing on later turns of the same session after the skill's own turn ends — your observation is accurate.

However, the current documentation now explicitly describes this as the intended behavior for skill hooks, which differ from subagent hooks:

  • Subagent hooks run only while that subagent is running and are removed when it finishes.
  • Skill hooks are registered when the skill is invoked and "keep running … for the rest of the session, on turns after the skill's own turn as well." To run a hook a single time instead, you can set once: true on the hook entry.

See Hooks in skills and agents. The "cleaned up when it finishes" wording you quoted matches the subagent-hooks behavior; the docs have since been clarified to spell out the skill-hooks difference.

So this is working as currently documented, and I'm going to close this as such. The underlying request — hooks scoped strictly to a skill's active lifecycle (e.g. for gating tool access only while an audit skill runs) — is a reasonable feature request; feel free to file it as an enhancement if once: true doesn't cover your use case.

🤖 Generated with Claude Code

kaheidt · 1 month ago

@bcherny we observed the behavior to be different in Claude Cowork. In there, it seemed that it would fire only for the life of the skill, and only on turns prior to the last turn. Are you able to confirm that Cowork and Claude Code observe the same behavior as far as skill-based hooks? Thanks!

khayasaka · 1 month ago

Filed the enhancement request suggested above: #89669 (hook scope limited to a skill's active execution period, as distinct from once: true).