Hook `if` field is silently ignored for Edit/Write tools (works for Bash) — v2.1.100
Version
Claude Code 2.1.100 (Windows 11, Node.js hook scripts).
Summary
The hook if field works for Bash tool patterns (e.g. Bash(git *)) but is silently ignored for Edit and Write tools. Any if value on a PostToolUse hook targeting Edit/Write causes the entire hook entry to be skipped — the hook never spawns, regardless of the value.
Related: #39612 (docs gap for if), #41376 (closed as dup of #36389), #36389, #36332.
Expected behavior
Per the undocumented if field (introduced v2.1.85 per #39612) using permission rule syntax, the following should filter a hook to only fire on matching file paths:
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "node /path/to/script.js",
"if": "Edit(*.resx)|Write(*.resx)"
}]
}
The hook should fire on Edit/Write of .resx files and be skipped for other extensions.
Actual behavior
The hook never fires as long as the if field is present on an Edit/Write hook entry, regardless of the pattern value. Removing the if field makes the hook fire correctly on every Edit/Write.
Reproduction
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"if": "Bash(git *)",
"command": "bash -c 'echo HOOK_FIRED_BASH >> /tmp/hook.log'"
}]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"if": "Edit(**)|Write(**)",
"command": "bash -c 'echo HOOK_FIRED_EDIT >> /tmp/hook.log'"
}]
}
]
}
}
Steps:
- Ask Claude to run
git statusvia Bash →HOOK_FIRED_BASHis appended ✅ - Ask Claude to run
echo hellovia Bash → log unchanged (correctly filtered out) ✅ - Ask Claude to Write any file → log unchanged ❌ (hook should have fired)
- Ask Claude to Edit any file → log unchanged ❌ (hook should have fired)
- Remove
iffrom the PostToolUse entry → Write/Edit now fires the hook ✅
Patterns tested (all fail for Edit/Write)
| if value | Fires? |
|------------|--------|
| Edit|Write | ❌ |
| Edit(*)|Write(*) | ❌ |
| Edit(**)|Write(**) | ❌ |
| Edit(.*)|Write(.*) | ❌ |
| Edit(*.resx)|Write(*.resx) | ❌ |
| Edit(.*Resources\.resx)|Write(.*Resources\.resx) | ❌ |
| Edit(**/Resources.resx)|Write(**/Resources.resx) | ❌ |
| Edit(/.claude/hooks/test/Resources.resx) | ❌ |
| Edit(.claude/hooks/test/Resources.resx) | ❌ |
| Edit(/**/Resources.resx) | ❌ |
| Write(src/.*) (copying #41376 comment syntax) | ❌ |
| (removed entirely) | ✅ |
Equivalent Bash(git *) vs Bash(echo *) filtering in the same settings.json works correctly as expected — confirming the if field infrastructure is wired up but the Edit/Write code path is missing or broken.
Impact
The official workaround (filter inside the hook script by parsing tool_input.file_path from stdin) still works, but forces a Node process spawn on every Write/Edit even for unrelated files. For a codebase with frequent Edit/Write traffic this is measurable overhead, and defeats the purpose of having a declarative if filter at the configuration level.
Request
- Make
Edit/Writeifpatterns consumetool_input.file_pathconsistently with howBashconsumestool_input.command. - Document the
iffield officially (currently absent fromcode.claude.com/docs/en/hooks; see #39612). - Clarify the exact syntax — gitignore spec, permission rule syntax, or regex — since #39612 mentions permission rule syntax but existing permission rule docs describe gitignore patterns for Read/Edit.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗