Docs: hooks reference doesn't document the format of tool_input.file_path (absolute; backslash-separated on Windows)
Summary
The hooks reference documents the input schema, exit codes, and JSON output fields in detail, but it does not specify the format of tool_input.file_path — specifically:
- whether it is an absolute or relative path, and
- that on Windows it is delivered with backslash separators (
C:\project\src\file.ts).
Both matter for any hook that matches on paths, and getting them wrong fails silently and open.
Why this is worth documenting
A path-guarding hook typically looks like this:
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
if [[ "$FILE_PATH" =~ ^src/ ]]; then # or /src/ , or ${FILE_PATH#"$PWD"/}
# deny
fi
On macOS/Linux this behaves as intended. On Windows the same condition never matches, so the guard allows everything. There is no error and no warning — the hook exits 0 and the tool proceeds. From the outside, "not blocked" is indistinguishable from "there was nothing to block."
In our case four separate path-guard hooks had been registered for months and none of them had ever blocked anything. We found out by accident while testing an unrelated change.
A related trap for bash hooks on Windows: $PWD under Git Bash is /c/project while file_path is C:\project\..., so the common REL="${FILE_PATH#"$PWD"/}" idiom for making the path relative silently produces a non-relative string, and every subsequent comparison fails.
Already known — but only in the issue tracker
The backslash behavior has been reported several times, each time as a bug in some consumer of the path rather than as a documentation gap:
- #64432 — "The Write tool always delivers
file_pathwith Windows backslashes." It also gives the workaround: normalize withreplace("\\", "/")inside a hook. - #36884 — "May be related to Windows path normalization (forward slash patterns vs backslash actual paths)"
- #40076, #52962 — the same glob-matching failure under WSL2
All are closed as not planned / stale. Whatever the decision is on the matching behavior itself, the input format is stable, observable, and currently undocumented, so hook authors keep rediscovering it the hard way.
Note the asymmetry this creates: someone who searches the issue tracker finds this in a minute. Someone who reads the documentation carefully does not find it at all.
Suggested addition
In the hook input section, alongside the existing PreToolUse example, add a file-tool example plus a short note:
For file tools (Write,Edit,Read,NotebookEdit),tool_input.file_pathis an absolute path. On Windows it uses backslash separators: ``json { "hook_event_name": "PreToolUse", "tool_name": "Write", "tool_input": { "file_path": "C:\Users\me\project\src\index.ts", "content": "..." } }`Hooks that match on paths should normalize separators before comparing — for exampleFILE_PATH="${FILE_PATH//\//}"in bash, orfile_path.replace("\\", "/")` in Python.
The PreToolUse example currently on the page uses Bash, whose tool_input has no path field, so nothing on the page shows what a file path actually looks like.
A smaller, related gap: stderr on exit 0
The exit-code table is clear that exit 2 feeds stderr back to Claude, and that matches the observed behavior. However, the exit 0 description only mentions stdout ("written to the debug log but not shown in the transcript") and says nothing about stderr.
We had a PostToolUse hook that wrote warnings to stderr and returned exit 0, reasoning that PostToolUse cannot block anyway. Those warnings reached neither the user nor Claude — they were silently dropped. Switching to exit 2 surfaced them correctly.
One sentence stating that stderr is only surfaced on non-zero exit would prevent that misreading. "Non-blocking" and "exit 0" are easy to conflate, especially since the table's Can block? = No for PostToolUse suggests exit 2 has no purpose there.
Environment
- Claude Code 2.1.221
- Windows 11 (native), Git Bash (MINGW64) as the hook shell
- Observed with
PreToolUseandPostToolUsehooks onWrite/Edit