Windows: hooks leak shell redirection - zero-byte files created in CWD from '>' tokens in tool input (e.g. '-> Signal:' creates file 'Signal')

Open 💬 0 comments Opened Jul 11, 2026 by ScalperTraders

Summary

On Windows, when hooks are configured (PreToolUse/PostToolUse via cmd /c ...), Claude Code sessions leave stray zero-byte files in the project CWD whose names are tokens taken from tool input / file content that follows a > character — i.e. the hook input is apparently going through a shell layer that interprets > as output redirection.

Example: a Write tool call creating a Python file containing the type annotation -> Signal: results in an empty file named Signal appearing in the repo root.

Environment

  • Claude Code CLI on Windows 11 Home (10.0.26200)
  • Primary shell: PowerShell; hooks configured with cmd /c "IF EXIST ... (node ...\hook-handler.cjs pre-edit) ELSE (node ...)" in .claude/settings.json (claude-flow-style setup)
  • Model: claude-sonnet-5 session
  • Hook handler script itself only reads stdin and writes to fixed paths (audited — it never composes shell commands from tool input)

Observed stray files (all 0 bytes, created in project root)

Over two sessions, names clearly derived from edited file contents / prompts:

| Stray file | Matching fragment in tool input |
|---|---|
| Signal | ... ) -> Signal: (return annotation in a written .py file) |
| `Signal ` (with trailing backtick!) | same fragment, different occurrence |
|
MT5 | -> tuple[MT5Client ... / MT5 ... content |
|
tuple[MT5Client | -> tuple[MT5Client, int]: |
|
dict[str | x: dict[str, int]) -> ... |
|
date('now' | date('now', ?) inside a written SQL string |
|
Zgoda, przyjmujemy, allow, TV,+, c, 0` | fragments of Polish-language user prompts |

The trailing-backtick variant (Signal + ` `) is notable: backtick is PowerShell's escape character, suggesting at least one code path routes the data through PowerShell, while the plain variants look like cmd.exe >` redirection targets.

Additional evidence

The captured hook stdout for SessionStart / UserPromptSubmit events shows the hook input JSON echoed as if typed at a cmd.exe prompt:

Microsoft Windows [Version 10.0.26200.8737]
(c) Microsoft Corporation. Wszelkie prawa zastrze�one.

C:\Users\<user>\<project>>{"session_id":"...","transcript_path":"...","hook_event_name":"UserPromptSubmit","prompt":"..."}

C:\Users\<user>\<project>>

i.e. the JSON payload appears to be delivered to (or through) an interactive-looking cmd.exe layer rather than purely via stdin. If any such layer receives text containing >, cmd.exe creates the redirect file — which matches every stray filename observed (token following > up to the next delimiter).

Repro steps (as observed)

  1. Windows 11, hooks configured in .claude/settings.json with cmd /c "IF EXIST ... (node <script>) ELSE (node <script>)" for PreToolUse/PostToolUse matching Write|Edit.
  2. Ask Claude to write a Python file containing a return annotation such as def f() -> Signal: ....
  3. Observe a zero-byte file named Signal created in the project root at the same timestamp as the Write call.

Expected

Hook payloads should never pass through a shell in a way that lets > / < / & / | inside tool input act as shell operators. Payload delivery should be stdin-only (or fully escaped).

Impact

  • Pollutes repositories with junk files (which then show up as untracked in git; some users may accidentally commit them).
  • Same mechanism is in principle capable of clobbering an existing file via redirection (> existingfile truncates), so this is not purely cosmetic.

View original on GitHub ↗