← Claude Code Hub
✦ Tip #193 Sep 10, 2026

Conditional hooks in Claude Code: run only when it matters

Your PreToolUse hook spawns a process on every single call, match or not. The `if` field filters before that process exists, using the same permission-rule syntax your allowlist already uses.

Without if, five Bash calls spawn the same script five times; with if: "Bash(git commit *)" only the commit crosses the filter and the other four never spawn a process

TL;DR Put "if": "Bash(git commit *)" on the hook entry and Claude Code never spawns the process when the command doesn't match. It's the same Tool(pattern) syntax as your permission rules, one rule per entry, and with the tool name inside you can drop matcher entirely. Watch where you put it: if is only evaluated on the five tool events. On a Stop or a SessionStart that hook isn't skipped once, it's gone for good, and without --debug hooks nothing tells you.

You have a PreToolUse hook on Bash. It fires on every call: every ls, every cat, every two-word echo. The script reads the JSON, sees this one isn't its business and exits 0. You started a whole process to do nothing, and you do it dozens of times a session.

There's a field that cuts it off before any of that happens.

The result, from a real session with two commands back to back:

> run git status --short, then echo hola

# hook.log
{"hook_event_name":"PreToolUse","tool_name":"Bash",
 "tool_input":{"command":"git status --short", ...}}

# One entry. The echo never launched anything.

How it works

if lives on the hook entry, next to type and command. It takes a permission rule in the Tool(pattern) syntax your allowlist already uses, the same one behind deny, allow and ask rules.

Claude Code evaluates it while working out which hooks match the call, before spawning the process. When the rule doesn't match there is no fork, no interpreter starting up, no stdin to write. The difference against a script that reads the JSON and bails early isn't milliseconds: one of the two never exists.

One rule per entry. No &&, no ||. Two conditions means two entries.

Setting it up

1. Filter by command

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit.sh",
            "if": "Bash(git commit *)"
          }
        ]
      }
    ]
  }
}

Note what's missing: matcher. Once the tool name sits inside if, the matcher has nothing left to do.

2. Filter by path

{
  "type": "command",
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/lint.sh",
  "if": "Write(src/**)"
}

For Write and Edit the pattern matches the file path, relative to your working directory. A Write to src/a.ts fires the hook; one to b.ts at the root doesn't. Since version 2.1.214 src/** means the src at your root and everything under it; for a folder named src at any depth, the pattern is **/src/**.

3. No parentheses, the whole tool

{ "type": "command", "command": "...", "if": "Bash" }

With no rule content, if matches every call to that tool. Handy when you want the whole filter written on the entry instead of split between matcher and if.

The five events where if is evaluated

if is only evaluated on five events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest and PermissionDenied. Those are the ones carrying a tool call to match against.

On anything else (Stop, SessionStart, Notification, PreCompact) the entry isn't skipped this once. It's dropped outright, every time, silently. Your hook is written, the JSON is valid, and it never runs.

Seeing it takes asking for it. Start the session with --debug hooks, then look for the line in that session's log:

grep "if condition" ~/.claude/debug/<session-id>.txt
2026-09-10T19:04:47.314Z [DEBUG] Hook if condition "Bash(git *)" cannot be evaluated for non-tool event Stop

If your Stop hook isn't firing, check for an if before you go debugging the script.

if filters, it doesn't protect

With Bash the filter is best-effort. Claude Code takes the command apart to see what actually runs, and when it can't tell, it fires the hook anyway: $TOOL git push triggers a Bash(git *) condition because nothing can know what that variable expands to. A pattern carrying more than the command name also fires on $(), backticks or variables.

The docs spell out the consequence: because the filter is best-effort, use the permission system rather than a hook to enforce a hard allow or deny. if is a performance tool.

Reference

Piece Value
Events where it's evaluated PreToolUse · PostToolUse · PostToolUseFailure · PermissionRequest · PermissionDenied
Syntax One permission rule: Bash(git *), Edit(*.ts), Write(src/**), or the bare name Bash
Combinations None. One rule per entry; two conditions, two entries
Where it works Every hook source: user, project and local settings.json, a plugin's hooks/hooks.json, and skill and subagent frontmatter
Hook types command, http, mcp_tool, prompt, agent

Official docs: Intercept and control agent behavior with hooks

Don't mix it up with conditional rules in .claude/rules/: those decide what enters your context, this decides whether a process gets spawned. It's the piece plain old hooks were missing, where matcher can only filter by tool name. It's also the same idea as the narrow matcher that auto-approves just the plan, one notch finer: there you scope by tool, here by what that tool is about to do.

Requirements

  • Verified on Claude Code 2.1.267. The src/** change landed in 2.1.214.
Workshop for teams

Multiply your team's output without sacrificing quality: a 6 to 8 hour AI First workshop, online, on the Claude platform.

See the workshop
Free guide

The 51 essentials, as a PDF.

One page per tip. Five chapters. What I actually use daily in production — no theory, no fluff.

  • I. Getting started 10 tips
  • II. Awareness 3 tips
  • III. Mastery 22 tips
  • IV. Autonomy 10 tips
  • V. Comparison 6 tips
Are you a professional Web developer?

You'll receive the guide by email · You join the Gravitas newsletter · Unsubscribe anytime

of 51
#

Wmedia · 51 Tips
Free guide · 51 tips · 5 chapters

The 51 essentials, as a PDF.

Are you a professional Web developer? · Unsubscribe anytime