[BUG] PostToolUse hook on Skill matcher does not receive tool_input payload on Windows + Git Bash

Resolved 💬 3 comments Opened May 8, 2026 by toni-pih Closed May 11, 2026

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: PostToolUse matcher Skill, command bash .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).
  • stdin is empty.
  • Positional $@ is empty.
  • Only generic CLAUDE_CODE_PROJECT_DIR and CLAUDE_CODE_HOOK_NAME env vars are present.
  • No tool_name, no tool_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)

  1. 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
``

  1. Wire it as a PostToolUse hook on the Skill matcher in .claude/settings.json:

``json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Skill",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/debug-capture.sh" }
]
}
]
}
}
``

  1. From inside Claude Code on Windows + Git Bash, execute any slash command whose body uses the Skill tool.
  1. Inspect /tmp/claude-hook-capture.log after 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.exe invocation 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_NAME and CLAUDE_TOOL_INPUT_JSON env 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)

  1. 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
``

  1. Wire it as a PostToolUse hook on the Skill matcher in .claude/settings.json:

``json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Skill",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/debug-capture.sh" }
]
}
]
}
}
``

  1. From inside Claude Code on Windows + Git Bash, execute any slash command whose body uses the Skill tool.
  1. Inspect /tmp/claude-hook-capture.log after 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_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗