`once: true` on skill-frontmatter hooks is ignored — the hook keeps firing after a successful run (2.1.273)
Summary
A PreToolUse hook declared in a skill's frontmatter with once: true is never removed. It keeps firing on every subsequent matching tool call for the rest of the session, even though every run exits 0 and is logged as success.
Expected
Per the docs, once is honored for hooks declared in skill frontmatter:
- hooks.md, Common fields: "If
true, Claude Code removes the hook after its first successful run. A run that fails, blocks with exit code 2, or times out leaves the hook in place, so it runs again on the next matching event. Only honored for hooks declared in skill frontmatter; ignored in settings files and agent frontmatter" - hooks.md, Hooks in skills and agents: "To have Claude Code remove a hook after its first successful run instead, set
once: trueon it."
Actual
The hook runs on the first matching tool call and on every one after it. The debug log contains no trace of once being handled: no "remove", "unregister" or similar entry appears at any point.
Reproduction
- Create a project skill at
<project>/.claude/skills/hook-once/SKILL.md:
````
---
name: hook-once
description: repro for once:true on a skill frontmatter hook
disable-model-invocation: true
hooks:
PreToolUse:
- matcher: "Bash|PowerShell"
hooks:
- type: command
command: "echo '{\"systemMessage\": \"hook fired\"}'"
once: true
---
Run echo one in the shell. After it finishes, run echo two as a second, separate call. Do nothing else.
````
- From that directory:
claude -p "/hook-once" --setting-sources project --strict-mcp-config --allowedTools Bash --max-turns 8 --debug-file ./debug.log
- Inspect
debug.log.
Observed log (2.1.273, Windows 11)
[DEBUG] Registered 1 hooks from skill 'hook-once'
[DEBUG] Added session hook for event PreToolUse in session <id>
... first tool call ...
[DEBUG] "Hook PreToolUse:PowerShell (PreToolUse) success:\n{\"systemMessage\": \"hook fired\"}"
... second tool call, different toolUseId ...
[DEBUG] "Hook PreToolUse:PowerShell (PreToolUse) success:\n{\"systemMessage\": \"hook fired\"}"
Both tool calls resolved with outcome=ok, and both hook runs are logged as success, so the "a run that fails, blocks, or times out leaves the hook in place" exception does not apply.
Scope of the repro
Reproduced in both places a skill can live, so it is not specific to plugin skills:
- a plugin skill (
--plugin-dir, hook output as plain stdoutecho) - a project skill (
.claude/skills/, hook output as JSONsystemMessage)
Same result in all combinations: the hook is never removed.
Environment
- Claude Code 2.1.273
- Windows 11
- Shell used by the hook: Git Bash; the tool call itself dispatched as PowerShell