[BUG] PreToolUse hook "if" field (permission-rule syntax) never matches Skill tool invocations (v2.1.278)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Expected: A PreToolUse hook with an "if" field using permission-rule syntax, e.g. "if": "Skill(subagent-driven-development)", should only fire the hook command when the Skill tool is invoked with a matching skill name — mirroring how the identical rule string works as a top-level permissions.deny/permissions.allow rule, and how "if": "Bash(git *)" correctly gates Bash invocations.
Actual: The if condition has no gating effect at all. With "matcher": "Skill" and no if, the hook fires on every Skill invocation, as expected. Adding "if": "Skill(subagent-driven-development)" (or the Skill(name ) / Skill(namespaced:name ) variants) changes nothing observed: the hook still fires on every Skill invocation, matching or not, exactly as if the if field were absent. The same rule string used as a top-level permissions.deny entry correctly matches/blocks the named skill, confirming the rule syntax itself is valid and accepted — the gap is specifically in PreToolUse's if evaluation against Skill tool calls. The equivalent pattern for Bash ("if": "Bash(<pattern>)") works correctly, so this appears isolated to the Skill tool's if evaluation path.
What Should Happen?
The if condition should evaluate against the invoked skill and only fire the hook when the skill name matches — the same way top-level permissions.deny/permissions.allow rules and Bash's "if": "Bash(<pattern>)" already do.
Current workaround: drop the if field entirely, keep "matcher": "Skill" (fires unconditionally on every Skill invocation), and do the filtering manually inside the hook script by inspecting tool_input.skill from the hook's JSON stdin payload:
const input = JSON.parse(await readStdin());
if (input.tool_input?.skill !== "subagent-driven-development") process.exit(0);
// ... proceed with hook logic
This works reliably but defeats the purpose of declarative if filtering and pays the cost of spawning the hook process on every Skill call.
See also #62108 — a related feature request for a dedicated active_skill field in PreToolUse, which would also resolve this gap.
Error Messages/Logs
No error is thrown — this is a silent behavior gap, not a crash. The hook process runs successfully but the if condition has no observable effect on whether it fires.
Steps to Reproduce
- Create a project with a skill named subagent-driven-development (or any custom skill).
- In settings.local.json, register a PreToolUse hook with "matcher": "Skill" and a command that writes a marker (e.g. appends a line to a log file) when it runs. Confirm baseline: invoke any skill via the Skill tool — the marker is written every time, the hook fires unconditionally, as expected with no if.
- Add "if": "Skill(subagent-driven-development)" to the same hook entry, without changing anything else.
- Invoke a skill that is not subagent-driven-development via the Skill tool. Observe: the marker is still written — the hook fires despite the if condition not matching.
- Invoke subagent-driven-development itself via the Skill tool. Observe: the marker is written too — no difference in behavior from step 4, confirming the if clause has zero observable effect in either direction.
- Compare: replace the hook's if with the same string under top-level permissions.deny — it correctly blocks/matches. Replace the tool in the if string with Bash(<pattern>) on a Bash-triggering hook — it correctly gates.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.278
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
Confirmed via two independent tests in a fresh subprocess (no session state):
invoking Skill(subagent-driven-development) with the if condition active still
fired the hook unconditionally, identical to invoking an unrelated skill
(Skill(dispatching-parallel-agents)) — both produced the same hook behavior,
confirming if had zero discriminating effect between matching and non-matching
skill invocations.