◉CLAUDE LABJP
●2.1.300 — Adds virtualText for Mod prompt decorations; background commands still running 1.5 seconds after exit are now force-killed●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 46 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●CACHE — Prompt cache reads on Sonnet 5.5 dropped from $0.20 to $0.10 per million tokens (Oct 7)●NEW — I Put a Ten-Line Promise File at the To…●2.1.300 — Adds virtualText for Mod prompt decorations; background commands still running 1.5 seconds after exit are now force-killed●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 46 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●CACHE — Prompt cache reads on Sonnet 5.5 dropped from $0.20 to $0.10 per million tokens (Oct 7)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-10-15Intermediate

The Hook That Was Meant to Guard My Japanese Folder Never Blocked Anything — Python Was Reading stdin as cp932

A PreToolUse hook guarding folders like 経理 or 議事録 silently lets edits through when Python's stdin defaults to cp932. How I reproduced it, the three-line fix, and one command to check your own hooks.

Claude Code264hooks22PreToolUse5Windows4encoding3Python22

As an indie developer, I once wrote a small PreToolUse hook because I wanted one folder, named 経理 (accounting), to stay off-limits to Claude Code. About ten lines of Python: if the path contains that word, exit with code 2. On my Mac it blocked exactly what it should.

Then I found a Zenn report saying that on Windows, hooks quietly break only when Japanese is involved, with cp932 as the suspected cause. I had written the same kind of hook, so it didn't feel like someone else's problem.

I don't have a Windows machine. So I reproduced the symptom by making Python read stdin as cp932 and counted what happened. That is a simulation, not a Windows run, and I'd rather say so up front.

The symptom is only "it doesn't stop", and nothing tells you

The thing I most want to flag is that there are two failure modes, and neither shows up loudly.

Claude Code passes the tool call to a hook as JSON on stdin, with the path in tool_input.file_path. Python picks the stdin encoding from the OS default, so on Japanese Windows it tries to read UTF-8 bytes as cp932.

Here is the hook I tested.

# guard_bad.py — block edits under paths containing 経理 (the broken version)
import json, sys
 
data = json.load(sys.stdin)  # uses whatever stdin encoding Python defaulted to
path = data.get("tool_input", {}).get("file_path", "")
if "経理" in path:
    print("The 経理 folder is off-limits", file=sys.stderr)
    sys.exit(2)  # only 2 means "block"
sys.exit(0)

I fed it UTF-8 JSON with PYTHONIOENCODING=cp932, the quickest way I know to imitate the Windows default from a Mac.

echo '{"tool_name":"Edit","tool_input":{"file_path":"/Users/demo/経理/売上.csv"}}' \
  | PYTHONIOENCODING=cp932 python3 guard_bad.py
echo "exit=$?"

The result was a UnicodeDecodeError traceback and exit=1. With UTF-8 the hook prints its message and returns exit=2.

In the official hooks documentation, only exit code 2 blocks the call; any other non-zero code is treated as a failure that does not stop it. So a hook that dies with 1 lets the edit to the protected folder go through and leaves, at most, a modest error behind.

The second failure mode doesn't even raise

What made me wince was that some paths raise nothing at all. When UTF-8 bytes happen to form a valid cp932 sequence, Python returns a different string without a word.

I took 12 words that commonly show up as Japanese folder names, encoded them as UTF-8 and decoded them as cp932.

Result of decoding as cp932WordsExamples
Became a different string, no exception9議事録 → 隴ー莠矩鹸 / 日報 → 譌・蝣ア
Raised UnicodeDecodeError3契約書, 資料, 売上

For those 9 words, "議事録" in path is false and the hook ends with exit 0. No error, no warning, and the edit you meant to forbid goes through. The crash, oddly, was the kinder outcome, because it at least leaves a trace.

I've written before about Japanese text going wrong inside files, in Fixing Japanese Mojibake in Claude Code. Garbled hook input, rather than garbled file contents, was a blind spot for me.

Why it looks like it's working

The reason I never imagined this bug is that I had only ever tested hooks with ASCII paths. A path like src/main.py produces identical bytes in cp932 and UTF-8, so both readings agree and the only thing left behind is the fact that the check passed.

The other reason is how little a miss leaves behind. When the block works, a reason appears on screen; when it doesn't, nothing does. "Nothing happened" and "it was protected" look exactly alike, which makes a guard hook the hardest kind to doubt.

The fix is to declare UTF-8 before reading

Three lines. Rebuild stdin and stderr as UTF-8 before parsing the JSON.

# guard_good.py — fixed version
import json, sys
 
sys.stdin.reconfigure(encoding="utf-8")   # read input as UTF-8
sys.stderr.reconfigure(encoding="utf-8")  # keep the refusal message readable
 
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
if "経理" in path:
    print("The 経理 folder is off-limits", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

Under the same cp932 condition it returned exit=2 with the message intact. reconfigure works on Python 3.7 and later.

The other route is launching the hook as python -X utf8 guard.py, which turns on UTF-8 mode. One caveat: if PYTHONIOENCODING is set, it wins. -X utf8 didn't help on my machine only because I had left PYTHONIOENCODING=cp932 in place for testing. Calling reconfigure inside the script avoided that, which is why I'd rather rely on it.

Check it with Japanese input, not English

Encoding bugs never appear with ASCII paths. To confirm a fix, feed in JSON that really contains Japanese.

# Should block: expect exit=2
echo '{"tool_name":"Edit","tool_input":{"file_path":"/Users/demo/経理/売上.csv"}}' \
  | PYTHONIOENCODING=cp932 python3 guard_good.py; echo "exit=$?"
 
# Should allow: expect exit=0
echo '{"tool_name":"Edit","tool_input":{"file_path":"/Users/demo/docs/invoice.csv"}}' \
  | PYTHONIOENCODING=cp932 python3 guard_good.py; echo "exit=$?"

In PowerShell, setting $env:PYTHONIOENCODING = "cp932" first gives the same check. If you have a real Windows machine, please also run it without the variable and compare.

Where I draw the line

Which way a guard hook falls when it fails is something I covered separately in When onFailure Is Set to block but jq Is Missing. The cp932 case is the doorway to that one: the hook can't read its input at all.

The folders I most want to protect tend to have Japanese names, which is exactly why a failure here would take me a long time to notice.

A guard hook counts as finished only after it has passed an ASCII path and then stopped a Japanese one. I intend to keep that check every time I add a new hook.

One thing to try today

Pick one hook that reads a path or message to make its decision, pipe in JSON containing Japanese, and look at the value of exit. If you don't get 2, that hook isn't protecting your Japanese-named folders.

I'm starting with the two reconfigure lines at the top of my own hooks.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-10-14
I Set onFailure to block, and the Docs' rm -rf Guard Still Let Everything Through Without jq
When a PreToolUse guard hook fails, does the action go ahead or stop? The docs' own rm -rf example exits 0 and waves everything through when jq is missing. Ten failure modes run for real, the fix, and a script that finds fail-open guards in settings.json.
⟐ Claude Code2026-10-05
I Wrote "I'll Do the Commits" in CLAUDE.md. The Next Morning git log Had One More Line.
When Claude Code commits despite being told not to: undo with git reset --soft, then stop it physically with permissions.deny plus a PreToolUse hook. Covers the git -c form that deny rules let through, and how to test the hook without starting Claude Code.
⟐ Claude Code2026-09-29
Two Default-Model Changes Haven't Reached Claude Code's Stable Channel Yet
This morning, Claude Code's stable channel sat 6 releases and 10 days behind latest. A small standard-library Python script that counts the gap in real, published releases instead of version arithmetic, and lists what hasn't arrived yet with a headline for each.