Build1 publisher3 min readPublished
A guard that only speaks in exit codes cannot tell you it stopped guarding
A PreToolUse hook on an AI coding agent returned "allow" for every file under a non-ASCII directory name, for weeks, with no log line and no exception.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- The guards are PreToolUse hooks: before the AI coding agent is allowed to perform an action, the proposed tool call is handed to a small Python script as JSON on stdin.
- The hook contract is two exit codes: exit 0 means allow, exit 2 means block and send the reason back to the agent as feedback.
- The author runs several guards: one refuses access to credential paths, one intercepts destructive shell commands, one enforces a directory boundary, and one is malformed-read-guard.py.
- malformed-read-guard.py blocks the agent from reading files that contain corrupted tool-call syntax, because reading that syntax makes the model start emitting it too and the session locks up.
- The guards had been working for weeks, and one of them had also, for some of that time, been doing nothing at all.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A local policy guard wrapped around an AI coding agent spent part of several weeks returning "allow" for every file that sat under a directory whose name was not ASCII, and it never logged, raised, or exited nonzero while doing it [5][6][7]. According to the author's writeup on dev.to, the durable lesson is not the encoding bug but the interface: a hook whose only output channel is an exit code has no way to say that it could not reach a decision [12].
The setup is conventional. Before the agent performs an action, the proposed tool call is handed to a small Python script as JSON on stdin, and the script answers with one of two exit codes: 0 to allow, 2 to block and return the reason to the agent as feedback [1][2]. The author describes four such guards: one refusing credential paths, one intercepting destructive shell commands, one enforcing a directory boundary, and malformed-read-guard.py, which blocks reads of files containing corrupted tool-call syntax because the model starts emitting that syntax itself and the session locks up [3][4]. One of the four was silently inert [20].
The failure composes from three individually reasonable decisions. Hook input is always UTF-8, but on Windows Python opens sys.stdin with the locale encoding, cp932 on that machine, so json.load(sys.stdin) decoded UTF-8 bytes as cp932 [8]. cp932 is permissive enough that those bytes map onto some sequence of characters, so there is no UnicodeDecodeError to catch and log, only a wrong string that travels onward as valid data [9]. The guard then built a Path and returned exit 0 if the target was not a file, which is the correct behaviour when there is nothing to inspect [10]. A mangled path is indistinguishable from a missing one, so that valve fired on every non-ASCII path in the system [11]. The affected class, as the author puts it, is any project whose folders are not named in English [17].
The measurements are blunt. Under Python 3.14.2 with host stdout encoding cp932, the same file at an ASCII path arrived intact and exited 2, blocked; under a Japanese-named directory the path did not survive decoding and the guard exited 0 [13]. After reading stdin as bytes and decoding UTF-8 explicitly, both cases exited 2 [14]. The fix is one line and the matching logic was untouched: read sys.stdin.buffer, decode with errors="replace", exit 0 only on empty input or a JSONDecodeError [16].
What makes this an operator problem rather than a Windows trivia item is the observability. Exit 0 means allowed and also means ran fine, with no third value for "I could not tell", so every signal said the guard was healthy [12]. At the call site, a guard that skipped its check is byte-identical to a guard that approved [19]. An ASCII-only test suite goes green on a dead guard, and the author notes there was no failing test to write because the test they would have written passed [15].
Two things worth checking in your own hooks. First, whether any guard can express a third outcome, and what the agent does when it sees one. Second, whether the fixture set contains a single directory named in a non-Latin script; the author's own fix comment records the measurement date and is written in Japanese, because the codebase is [18].