anthropics / anthropics/claude-code

`once: true` on skill-frontmatter hooks is ignored — the hook keeps firing after a successful run (2.1.273)

Open
#95,280 0 comments 0 reactions 0 assignees View on GitHub
area:hooks area:skills bug has repro platform:windows
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.