[BUG] PostToolUse hook on Skill matcher does not receive tool_input payload on Windows + Git Bash
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?
Environment
- OS: Windows 11 Pro (10.0.26200)
- Shell for hooks: Git Bash (bash 5.x via Git for Windows)
- Claude Code: invoked from VS Code on Windows; model
claude-opus-4-7[1m] - Hook channel:
PostToolUsematcherSkill, commandbash .claude/hooks/telemetry-hook.sh - Settings file:
.claude/settings.json(project scope)
## Expected behavior (per the official Claude Code hook contract)
The hook's stdin should receive a JSON object describing the tool call that just completed. For the Skill matcher, the document should include at least:
``json``
{
"tool_name": "Skill",
"tool_input": { "skill": "<skill-name>", "args": "..." }
}
The hook's job is to read stdin, parse with jq, and act on .tool_name + .tool_input.skill.
## Observed behavior
When a slash command body inlines a skill invocation, the hook fires but receives no payload. After instrumenting the hook with debug logging that captures stdin, all positional arguments,
and all environment variables to a sidecar file on every fire:
- A sidecar file is created on every invocation (so the hook IS being executed by the runtime).
stdinis empty.- Positional
$@is empty. - Only generic
CLAUDE_CODE_PROJECT_DIRandCLAUDE_CODE_HOOK_NAMEenv vars are present. - No
tool_name, notool_input.skill, no JSON anywhere in the process inputs.
The downstream script (which receives the skill name as a positional argument) works correctly when invoked directly from the shell, so the failure is upstream in the hook delivery, not in the
script logic.
## Reproduction (minimal)
- Create a tiny capture hook:
``bash``
#!/usr/bin/env bash
# .claude/hooks/debug-capture.sh
{
echo "=== $(date) ==="
echo "STDIN:"
cat /dev/stdin || true
echo "ARGS:"
printf ' %s\n' "$@"
echo "ENV (CLAUDE_CODE_*):"
env | grep ^CLAUDE_CODE_ || true
echo
} >> /tmp/claude-hook-capture.log 2>&1
- Wire it as a
PostToolUsehook on theSkillmatcher in.claude/settings.json:
``json``
{
"hooks": {
"PostToolUse": [
{
"matcher": "Skill",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/debug-capture.sh" }
]
}
]
}
}
- From inside Claude Code on Windows + Git Bash, execute any slash command whose body uses the
Skilltool.
- Inspect
/tmp/claude-hook-capture.logafter the model runs.
Observed: each entry shows non-empty === <date> === (the hook fires) but empty STDIN: and empty ARGS:.
Expected: STDIN: should contain a JSON document with at least .tool_name == "Skill" and .tool_input.skill == "<the skill name>".
## Diagnosis
On Windows + Git Bash, Claude Code's subprocess spawn appears to not pipe the JSON payload to bash hooks. Possible causes:
- A pty/pipe handling difference between Windows and POSIX runtimes.
- The Git Bash
bash.exeinvocation path losing the inherited stdin descriptor on subprocess launch. - A platform-conditional code path in the harness that skips JSON delivery for
cmd.exe-launched hooks.
The hook script is correctly designed per the official contract — this is not a hook authoring issue. On a Linux or macOS runtime, equivalent hooks reportedly receive the payload as documented.
## Workaround applied (downstream repo)
We pivoted skill-invocation telemetry capture from the PostToolUse hook to slash-command-internal Bash calls. Each slash command now invokes the increment script inline immediately before invoking
the skill — same writer, same arguments, same target file, just executed from inside the model's tool loop where the Bash tool delivery is reliable on this platform.
The hook script itself is not deleted — it is archived with a README.md documenting the restoration recipe for when this upstream issue is resolved.
## Why this matters (impact)
The PostToolUse hook channel is intended for governance / observability writes that the model-side workflow shouldn't have to think about. Workflows that use this channel for telemetry, audit
logging, or post-hoc validation will silently produce empty data on Windows + Git Bash, making any downstream lifecycle / sufficiency gates that depend on the captured data either inaccurate or
no-ops.
## Suggested fix shape
- If the platform constraint is a known limitation: document it explicitly in the hook docs ("PostToolUse stdin payload delivery is not supported on Windows + Git Bash; use
slash-command-internal instrumentation instead").
- Better: deliver the payload via an alternative channel on this platform — e.g., set
CLAUDE_TOOL_NAMEandCLAUDE_TOOL_INPUT_JSONenv vars on the spawned hook process, so portable hook
authors can read either source.
- Best: fix the stdin delivery so the contract is the same on every platform.
## Diagnosis transcript
Available on request.
What Should Happen?
Expected behavior (per the official Claude Code hook contract)
The hook's stdin should receive a JSON object describing the tool call that just completed. For the Skill matcher, the document should include at least:
``json``
{
"tool_name": "Skill",
"tool_input": { "skill": "<skill-name>", "args": "..." }
}
The hook's job is to read stdin, parse with jq, and act on .tool_name + .tool_input.skill.
Error Messages/Logs
Steps to Reproduce
Reproduction (minimal)
- Create a tiny capture hook:
``bash``
#!/usr/bin/env bash
# .claude/hooks/debug-capture.sh
{
echo "=== $(date) ==="
echo "STDIN:"
cat /dev/stdin || true
echo "ARGS:"
printf ' %s\n' "$@"
echo "ENV (CLAUDE_CODE_*):"
env | grep ^CLAUDE_CODE_ || true
echo
} >> /tmp/claude-hook-capture.log 2>&1
- Wire it as a
PostToolUsehook on theSkillmatcher in.claude/settings.json:
``json``
{
"hooks": {
"PostToolUse": [
{
"matcher": "Skill",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/debug-capture.sh" }
]
}
]
}
}
- From inside Claude Code on Windows + Git Bash, execute any slash command whose body uses the
Skilltool.
- Inspect
/tmp/claude-hook-capture.logafter the model runs.
Observed: each entry shows non-empty === <date> === (the hook fires) but empty STDIN: and empty ARGS:.
Expected: STDIN: should contain a JSON document with at least .tool_name == "Skill" and .tool_input.skill == "<the skill name>".
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.128
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
IntelliJ IDEA terminal
Additional Information
_No response_
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗