PostToolUse hook receives empty stdin intermittently on rapid consecutive Edit calls (Windows, claude-mem plugin parallel hook)
Summary
A PostToolUse hook command (python -m pba.adapter.claude post_tool_use) configured for theWrite|Edit|Bash|Read matcher intermittently receives empty stdin when the same Edit tool is
invoked repeatedly in quick succession. The hook process runs (exit 0, no stderr), butsys.stdin.read() returns "", so the JSON payload (tool_name, tool_input.file_path, etc.) is
unavailable for that invocation. This causes our hook's logic (record a file_changed audit event)
to silently no-op for that tool call.
Environment
- Claude Code:
2.1.172 - OS: Windows 11 Enterprise 10.0.26200, PowerShell shell
- Plugin:
claude-mem(thedotmack/claude-mem)v13.2.0enabled — this plugin also registers a
PostToolUse hook for the same tool types, so two hook commands run in parallel for each
Write/Edit/Bash/Read tool call.
settings.json (relevant excerpt)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [
{ "type": "command", "command": "python -m pba.adapter.claude pre_tool_use" }
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|Bash|Read",
"hooks": [
{ "type": "command", "command": "python -m pba.adapter.claude post_tool_use" }
]
}
]
}
}
Repro steps
- Enable the
claude-memplugin (so a secondPostToolUsehook is registered for the same
matcher and runs in parallel with the project's own hook).
- Configure a project-level
PostToolUsehook command (as above) that reads the JSON payload
from stdin via sys.stdin.read().
- Have Claude Code perform several
Editoperations on different files in quick succession
(e.g. two Edit tool calls within ~1-2 minutes of each other, while the session is otherwise
idle).
- Inspect the hook's stdin in each invocation (we logged
len(stdin)/parsed JSON keys to a file
for diagnosis).
Observed behavior
- Most invocations receive the expected JSON (
tool_name,tool_input.file_path,session_id,
model, ...).
- Some invocations (in our session, roughly 1 in ~6, but it varies) receive empty stdin
(sys.stdin.read() == ""). The hook process still exits 0 with no stderr — there is no error
signal that anything went wrong.
Concrete evidence from our audit log
Our adapter appends a JSON line to an audit log on every PostToolUse:Edit/Write invocation.
After we added detection for "stdin was empty", we reproduced this twice within ~5 minutes by
making two consecutive Edit calls to files in the same repo:
{"event_id": "evt-000054", "timestamp": "2026-06-11T03:40:40Z", "event_type": "file_changed", "payload": {"tool": "Edit", "file": "...\pba\util\lock.py"}}
{"event_id": "evt-000055", "timestamp": "2026-06-11T03:43:37Z", "event_type": "event_schema_violation", "payload": {"rejected_event_type": "hook_input_empty", ...}}
{"event_id": "evt-000056", "timestamp": "2026-06-11T03:45:02Z", "event_type": "event_schema_violation", "payload": {"rejected_event_type": "hook_input_empty", ...}}
evt-000054 corresponds to an Edit on pba/util/lock.py where stdin was received correctly.evt-000055 and evt-000056 correspond to subsequent Edit calls on pba/adapter/claude.py
(within the same session, ~3 and ~4.5 minutes later) where sys.stdin.read() returned empty, so
our hook recorded a synthetic "hook_input_empty" marker instead of file_changed.
Hypothesis
claude-mem's PostToolUse hook (a Node/Bun script invoked via a generated shell command) and our
project's PostToolUse hook are both registered for the same matcher (Write|Edit|Bash|Read) and,
per the docs, "all hooks matching the same tool pattern run in parallel". We suspect that on
Windows, the stdin payload is not always delivered independently to each parallel hook process —
i.e. one of the two sibling hook processes may consume/exhaust the stdin stream (or never receives
it), leaving the other with an empty read. We were not able to confirm the exact mechanism without
access to Claude Code's hook-dispatch implementation.
Impact
Any PostToolUse/PreToolUse hook that relies on stdin JSON for its core logic can silently lose
data for a fraction of invocations, with no error, no non-zero exit, no stderr — making this
extremely hard to detect without independent instrumentation (in our case, a separate audit log
that we cross-checked against git log to notice a coverage gap).
Suggested fixes / asks
- Ensure each parallel hook subprocess for the same event gets its own independent stdin stream
(e.g. write the JSON payload separately to each child's stdin, or use per-process temp
files/pipes rather than a shared stream).
- Alternatively/additionally, document this limitation and recommend hook authors not rely solely
on stdin (e.g. provide the JSON payload via an env var or a path to a temp file as a fallback).
- If this is a known/expected limitation, surfacing a non-zero exit code or stderr message when
stdin could not be delivered would at least make the failure detectable.
Workaround we applied
We changed our hook to treat "empty/unparseable stdin" as a distinct, recordable condition (instead
of silently treating it as "no JSON, default to {}"), so at least the gap becomes visible in our
own audit trail. This does not recover the lost tool_name/file_path for that invocation.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗