◉CLAUDE LABEN
●2.1.300 — Mod のプロンプト装飾 virtualText が追加され、終了後1.5秒たっても動くバックグラウンドコマンドは強制終了されます●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り46日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●CACHE — Sonnet 5.5 のキャッシュ読み取りが 0.20 から 0.10 ドル/100万トークンに下がりました(10/07)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.300 — Mod のプロンプト装飾 virtualText が追加され、終了後1.5秒たっても動くバックグラウンドコマンドは強制終了されます●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り46日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●CACHE — Sonnet 5.5 のキャッシュ読み取りが 0.20 から 0.10 ドル/100万トークンに下がりました(10/07)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-10-15中級

日本語フォルダ名を守るはずのフックが、一度も止めてくれませんでした — Python の stdin が cp932 で読んでいた話

「経理」「議事録」などの日本語フォルダを守る PreToolUse フックが、Python の stdin 既定が cp932 の環境では素通りします。再現の手順と 3 行の直し方、確認用の 1 コマンドを書き残します。

Claude Code265hooks22PreToolUse5Windows4文字コードPython22

個人開発の手元で、「経理」という名前のフォルダだけは Claude Code に触らせたくない、と考えて PreToolUse フックを書いた夜がありました。パスに「経理」が含まれていたら終了コード 2 で止める、という十行ほどの Python です。Mac の私の手元では、期待どおりに止まりました。

ところが、Zenn で Windows のフックが日本語のときだけ黙って壊れるという報告を見つけ、胃のあたりが重くなりました。原因は cp932 とされています。同じ形のフックを書いた身として、他人事ではありませんでした。

私の iMac に Windows の実機はありません。そこで、Python の標準入力を cp932 で読ませて症状を手元で作り、どこで止まり、どこで素通りするのかを数えました。Windows 実機での確認ではない点は、先にお伝えしておきます。

症状は「止まらない」だけで、エラーは見えません

最初にお伝えしたいのは、壊れ方が 2 通りあり、どちらも画面に目立つ形では現れない、ということです。

フックには、ツール呼び出しの内容が JSON で標準入力に渡されます。パスは tool_input.file_path に入っています。Python は標準入力の文字コードを OS の既定で決めるため、日本語版 Windows のように既定が cp932 の環境では、UTF-8 のバイト列を cp932 として読もうとします。

試したフックは次のとおりです。

# guard_bad.py — 「経理」を含むパスの編集を止める(問題のある版)
import json, sys
 
data = json.load(sys.stdin)  # ここで stdin の既定の文字コードが使われる
path = data.get("tool_input", {}).get("file_path", "")
if "経理" in path:
    print("経理フォルダは編集できません", file=sys.stderr)
    sys.exit(2)  # 2 だけが「止める」
sys.exit(0)

この版に、PYTHONIOENCODING=cp932 を付けて、UTF-8 の JSON を流しました。Mac 上で Windows の既定を模す、いちばん手軽な方法です。

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

結果は UnicodeDecodeError の Traceback と exit=1 でした。UTF-8 のときは「経理フォルダは編集できません」と出て exit=2 になります。

公式の hooks ドキュメントでは、終了コード 2 だけが呼び出しを止め、それ以外の非ゼロは「止めない失敗」として扱われます。つまり 1 で落ちたフックは、守るべきフォルダの編集をそのまま通しながら、エラーを控えめに残すだけです。

もう一つの壊れ方は、例外すら出ません

厄介だったのは、パスの文字によっては例外が出ないことでした。UTF-8 のバイト列が、たまたま cp932 として読める並びになると、Python は何も言わずに別の文字列を返します。

日本語のフォルダ名になりやすい 12 語を、UTF-8 のバイト列にして cp932 で読み直しました。

読み直した結果語数例
例外なしで別の文字列になった9 語議事録 → 隴ー莠矩鹸/日報 → 譌・蝣ア
UnicodeDecodeError になった3 語契約書・資料・売上

9 語のほうでは、"議事録" in path が偽になり、フックは exit 0 で終わります。エラーも警告もなく、禁止したはずの編集が通ります。「落ちてくれた」ほうが、まだ気づく手がかりが残る分だけ親切でした。

文字化けと日本語ファイルの扱いは、これまでも Claude Code で日本語ファイルが文字化けする時の対処で書いてきました。ただ、ファイルの中身ではなく、フックに渡る入力が化ける場面は、私の中では盲点でした。

なぜ、動いているように見えてしまうのか

私がこの不具合を想像できなかった理由は、フックの動作確認を英数字のパスだけで済ませていたからでした。src/main.py のようなパスなら、cp932 でも UTF-8 でも同じバイト列になります。どちらで読んでも結果が変わらないので、確認が通ったという事実だけが残ります。

もう一つは、止まらなかったときの手がかりの少なさです。ブロックが働いた日は画面に理由が出ますが、働かなかった日には何も出ません。「何も起きなかった」と「守れていた」は、見た目が同じなのです。この非対称が、守るためのフックをいちばん疑いにくいものにしているのだと思います。

直し方は、読む前に UTF-8 と宣言することです

直しは 3 行です。stdin と stderr を UTF-8 に作り直してから JSON を読みます。

# guard_good.py — 修正版
import json, sys
 
sys.stdin.reconfigure(encoding="utf-8")   # 入力を UTF-8 として読む
sys.stderr.reconfigure(encoding="utf-8")  # 拒否理由も化けないように
 
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
if "経理" in path:
    print("経理フォルダは編集できません", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

同じ cp932 の条件で流すと、exit=2 と日本語の拒否理由が返りました。reconfigure は Python 3.7 以降で使えます。

もう一つの手は、フックのコマンドを python -X utf8 guard.py にして、UTF-8 モードで起動することです。ここには一点だけ注意があります。PYTHONIOENCODING が設定されていると、そちらが優先されます。私が手元で -X utf8 を付けても直らなかったのは、検証用に PYTHONIOENCODING=cp932 を残していたためでした。スクリプトの中で reconfigure するほうが、環境変数の影響を受けないぶん確実だと感じています。

直ったかどうかは、日本語の入力を流して確かめます

文字コードの不具合は、英数字のパスでは再現しません。確認には、日本語を含む JSON を実際に流します。

# 止まるべき入力で exit=2 になるか
echo '{"tool_name":"Edit","tool_input":{"file_path":"/Users/demo/経理/売上.csv"}}' \
  | PYTHONIOENCODING=cp932 python3 guard_good.py; echo "exit=$?"
 
# 通すべき入力で exit=0 になるか
echo '{"tool_name":"Edit","tool_input":{"file_path":"/Users/demo/docs/invoice.csv"}}' \
  | PYTHONIOENCODING=cp932 python3 guard_good.py; echo "exit=$?"

Windows の PowerShell では、$env:PYTHONIOENCODING = "cp932" を先に設定すれば同じ検証になります。実機をお持ちの方は、環境変数を付けずに流した結果もあわせてご覧ください。

線引き

守る側のフックが転んだとき、どちらへ倒れるかは別の記事でも書きました。onFailure を block にしても jq が無い環境では素通りした話です。今回の cp932 は、その入口にあたる「入力を読めない」ケースでした。

守りたいと思うフォルダの名前は、たいてい日本語でつけています。だからこそ、ここで転ぶと気づくのが遅れるのだと思います。

守るフックは、英数字のパスで動いたあとに、日本語のパスで止まるところまで見て、はじめて完成と考えます。 この確認だけは、新しいフックを足すたびに続けるつもりです。

結び

今日できる一番小さな確認は、いまお使いのフックのうち、パスや本文を読んで判断しているものを一つ選び、日本語を含む JSON を流して exit の値を見ることです。2 が返らなければ、そのフックは日本語のフォルダを守れていません。

私も、自分の手元のフックの先頭に reconfigure の 2 行を足すところから始めようと思っております。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⟐ Claude Code2026-10-14
onFailure を block にしても、jq が無い環境では公式例の rm -rf ブロックは素通りしました
PreToolUse の門番フックは、落ちたときに通すのか止めるのか。公式例を jq の無い環境で走らせると exit 0 で素通りしました。終了コード10通りの実測、直し方、settings.json を走査して素通りを洗い出すスクリプトをまとめます。
⟐ Claude Code2026-10-05
「コミットは私がします」と書いたのに git log に一行増えていた朝、指示を強めるのをやめました
Claude Code に「コミットしないで」と頼んだのにコミットされたとき、まず git reset --soft で戻し、permissions.deny と PreToolUse hook の二層で物理的に止める手順です。deny ルールが素通しする git -c の形と、hook の試し方を書き残します。
⟐ Claude Code2026-09-29
安定版の Claude Code には、二度の既定モデル交代がまだ届いていません
安定版を追う Claude Code は、今朝の時点で最新版から 6 リリース・10 日ぶん後ろにいました。版番号の引き算に頼らず、実在するリリースだけを数えて「届いていない変更」を見出し付きで一覧にする小さなスクリプトを、標準ライブラリだけで組みます。