anthropics / anthropics/claude-code
`once: true` on skill-frontmatter hooks is ignored — the hook keeps firing after a successful run (2.1.273)
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## 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: true` on 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
1. Create a project skill at `/.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.
````
2. From that directory:
```
claude -p "/hook-once" --setting-sources project --strict-mcp-config --allowedTools Bash --max-turns 8 --debug-file ./debug.log
```
3. 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
... 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 stdout `echo`)
- a project skill (`.claude/skills/`, hook output as JSON `systemMessage`)
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
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by running the supplied project-skill reproduction using .claude/skills/hook-once/SKILL.md and inspect debug.log, then compare the behavior with the once semantics described in hooks.md. Done means a successful PreToolUse hook declared in skill frontmatter is removed after its first run, while failed, blocked, or timed-out runs remain eligible.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- powershell, python, shell
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100