Claude Code Hooks: Windows の罠と対策集
July 23, 2026 · View on GitHub
claude-code-hooks-windows-traps — Windows(特に日本語ロケール cp932 環境)で Claude Code の hooks を運用して実際に踏んだ罠と、実機検証済みの対策をまとめたリポジトリです。
English summary: Field-tested pitfalls and fixes for Claude Code hooks on Windows (Japanese locale / cp932). Covers: (1) only exit code 2 blocks — everything else fails open, (2) stdin corruption caused by cp932 decoding of UTF-8 JSON payloads, (3) relative-path hook commands breaking all matched tools after a
cd, (4) hot-reload behavior useful for debugging, and (5) bonus traps for Codex CLI hooks (trust gating and PowerShell exit-code mangling). All findings were reproduced on a real Windows 11 machine.
前提環境
- Windows 11 + 日本語ロケール(システムコードページ cp932)
- Python 製の hook スクリプト(PreToolUse など)。サンプルは Python 3.10+ 前提
- Git Bash(MINGW64)併用環境
すべて実機で再現・検証した内容です。バージョンアップで挙動が変わる可能性があるため、公式ドキュメント(Claude Code hooks)もあわせて確認してください。
罠 1: ブロックできるのは exit 2 だけ(それ以外は fail-open)
PreToolUse hook でツール実行をブロックできるのは exit code 2 のみです(公式仕様)。exit 1 や未捕捉例外は「非ブロッキングエラー」として扱われ、ツールは実行され続けます。
つまり、防御目的の hook が例外で死ぬと、その瞬間に防御は消えます(fail-open)。
対策: main 全体を包んで exit 2 に倒す
防御 hook は main() 全体を except BaseException で包み、何が起きても exit 2 に倒す構造にします。
def main() -> int:
try:
payload = json.loads(sys.stdin.buffer.read().decode("utf-8"))
# ... 検査ロジック ...
return 0 # 問題なし → 許可
except BaseException:
# 入力不正・内部バグを含むすべての異常 = 検証不能 → ブロック
return 2
if __name__ == "__main__":
sys.exit(main())
完全なサンプル: examples/hooks/fail_closed_guard.py
なお、これでも防げないのは「hook 自体が起動できない」ケース(python が PATH にない等)です。これは Claude Code 側で fail-open になるため、hook は settings.json の permissions.deny や OS レベルの分離と併用する多層防御の一層として設計してください。
逆に、誤爆してもよい「お知らせ系」hook(ブロックが目的でないもの)は、解釈できない入力を exit 0 で素通しする fail-open 設計が正解です。fail-closed か fail-open かは hook の目的で選びます。
罠 2: stdin が cp932 で読まれて JSON が構造ごと壊れる
hook の stdin に渡される JSON ペイロードは UTF-8 バイト列ですが、日本語 Windows では Python がこれを cp932 (+ errors=surrogateescape) で読みます。本体からのスポーン・Bash 子プロセス経由の両方で実測しました。
surrogateescape のおかげで例外はめったに出ませんが、もっと悪いことが起きます:
- 全角括弧
)(U+FF09)等の UTF-8 最終バイトが、cp932 のリードバイトとして解釈される - その直後に
\"などのエスケープシーケンスが来ると、バックスラッシュ(0x5C = cp932 の有効なトレイルバイト)が前の文字に飲み込まれる - 結果、JSON のエスケープ構造が崩れて
json.JSONDecodeError
発生はペイロードのバイト並び依存で確率的です。「たまに hook が落ちる」という最悪の形で現れます。決定的な再現子は )"x を含む tool_input です。
罠 1 と組み合わさると更に凶悪です: fail-closed hook がこの罠を踏むと「日本語を含む特定の編集だけ確率的にブロックされる」という怪奇現象になります。
対策: stdin をバイトで読んで UTF-8 明示デコード
# NG: ロケール依存デコード(日本語 Windows では cp932)
payload = json.load(sys.stdin)
# OK: バイトで読んで UTF-8 を明示
payload = json.loads(sys.stdin.buffer.read().decode("utf-8"))
stderr への出力(ブロック理由のフィードバック)も対で対策します:
sys.stderr.buffer.write(message.encode("utf-8"))
sys.stderr.buffer.flush()
PYTHONUTF8=1 を環境に立てる方法もありますが、hook がどの環境変数で起動されるかに依存しないぶん、コード側で明示する方が堅牢です。
罠 3: hook コマンドの相対パス起動は「全ツール封鎖」の地雷
settings.json に hook を相対パスで登録すると:
// NG
{ "type": "command", "command": "python .claude/hooks/guard.py" }
このパスは cwd 依存です。セッション中に cd で作業ディレクトリが変わると python がスクリプトを見つけられず、can't open file で exit 2 を返します。
罠 1 の裏返しがここで発動します: 意図しない exit 2 は「ブロック成功」として扱われるため、matcher に一致する全ツールが封鎖されます。cd 一発で Read も Edit も Bash も全部動かなくなる、という壊れ方をします(実被弾済み)。
対策: $CLAUDE_PROJECT_DIR で絶対パス化
// OK: プロジェクト配下の hook
{ "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.py\"" }
$CLAUDE_PROJECT_DIR は Windows 環境でも展開されることを実機検証済みです。ユーザーレベル(~/.claude/hooks/)に置く場合は、フル絶対パスで登録します:
{ "type": "command", "command": "python \"C:\\Users\\<you>\\.claude\\hooks\\guard.py\"" }
登録例の全体: examples/settings.example.json
罠 4(知見): hooks 設定はホットリロードされる
罠というより、デバッグに使える実測知見です。
- settings.json / settings.local.json への hook の登録・変更はセッション再起動なしで即時発効します
- hook スクリプト本体もツール呼び出しごとにディスクから実行されます(プロセス常駐しない)
これを利用すると、「一時診断 hook を settings.local.json に挿す → 観測 → 撤去」 というループがセッションを止めずに回せます。罠 2 のようなペイロード依存の問題は、stdin・exit code・stderr をすべてファイルにログするラッパースクリプトへ一時的に差し替えるのが決定打でした。
# 診断ラッパーの骨格: 実ペイロードを丸ごと捕獲してから本物へ委譲する
raw = sys.stdin.buffer.read()
with open(LOG_PATH, "ab") as f:
f.write(raw + b"\n---\n")
おまけ: Codex CLI の hooks にも Windows の罠がある
Claude Code ではなく OpenAI Codex CLI の話ですが、同じ Windows 環境で hook を運用するなら知っておくべき 2 つの罠です(こちらも実機で根本原因まで特定済み)。
Trust の二層ゲートで hook が無言でスキップされる
Codex の hook は (a) プロジェクトの trust と (b) hook 個別の trusted hash(~/.codex/config.toml の [hooks.state])の両方が揃わないと、エラーも出さずスキップされます。
- hooks.json の matcher やコマンド文字列を変えるとハッシュ不一致 → 対話 CLI の
/hooksで再 trust が必要 - ハッシュ対象はコマンド定義であり、スクリプトファイルの中身は含まれない(.py の修正は trust 維持)
- hooks.state のキーは cwd の大文字小文字をそのまま記録する(
g:\とG:\が別キーになる)
PowerShell 経由で exit 2 が 1 に化けて fail-open
Codex は hook を「セッションのシェル」経由で起動します。PowerShell 経由だとネイティブコマンド(python 等)の exit 2 が 1 に化けて届き、Codex は「exit 2 + stderr 非空」だけをブロックと認識するため、素通りします。
対策: exit code に頼らず、exit 0 + stdout に JSON を返す方式でブロックします。
{"decision": "block", "reason": "..."}
json.dumps の ensure_ascii 既定(True)なら出力が純 ASCII になるため、cp932 の再エンコードを経ても壊れません。
サンプル一覧
| ファイル | 内容 |
|---|---|
| examples/hooks/fail_closed_guard.py | 罠 1・2 対策込みの fail-closed 防御 hook の最小骨格 |
| examples/hooks/diagnostic_wrapper.py | 罠 4 を利用したペイロード捕獲用の診断ラッパー |
| examples/settings.example.json | 罠 3 対策込みの hooks 登録例 |