Windows日本語環境におけるClaude Codeのフック入出力の文字コードと、自作プラグイン3本で起きた無言の失敗
フックはUTF-8で受け取りUTF-8で返す前提なのに、WindowsのPythonは何も言わずにShift_JISで読み書きする。自分の道具で起きた失敗と、それを見逃したテストの書き方の記録
Claude Codeのフックは、道具を使う前後などにClaude Codeが呼び出す小さなプログラムで、情報を標準入力で受け取り、標準出力で返す。やりとりはUTF-8である。ところがWindowsのPythonは、指定しない限り標準入力も標準出力もShift_JIS(コードページ932)として扱う。自作の3本のプラグインはどれもこの落とし穴にはまっており、日本語が入ると記録し損ねる、危険なgit操作の確認を出さなくなる、表示が化ける、という失敗を起こしていた。どれもエラーとしては表に出なかった。さらに、これを確かめるはずのテストは、日本語を記号に置き換えて送っていたため、壊れていても通っていた。
| 方法 | Claude Code 2.1.280を、Windows 11(コードページ932)、Python 3.11で動かした。入力側は、Writeの道具のあとに呼ばれるフックが受け取った標準入力を、加工せずバイト列のまま保存して調べた。出力側は、起動時に呼ばれるフックに日本語の一文を返させ、Claudeにその文をそのまま引用させた。返し方は、日本語をそのまま出力する、記号に置き換えて出力する、出力をUTF-8に切り替えてから出力する、の3通りとした。自作のプラグイン3本(knowledge-ledger、commit-coach、token-lens)には、本物と同じ形で日本語を含む入力を与える試験を足し、直す前に落ちること、直した後に通ること、直した部分を元に戻すと再び落ちることを確かめた。あわせて、Claudeに PowerShell だけを使わせて日本語のファイルを編集させる試験を、Sonnet 5 と Opus 5.5 で2回ずつ行った。 |
|---|---|
| 結果 | 入力側では、Claude Codeは日本語を記号に置き換えず、UTF-8のバイト列のまま送っていた。これをWindowsのPythonが既定のまま読むと、日本語を含む内容が化ける。出力側では、日本語をそのまま出力したフックの文は、置換文字(U+FFFD)16個を含む化けた文としてClaudeに届き、記号に置き換えた場合とUTF-8に切り替えた場合は正しく届いた。自作プラグインでは、日本語の入ったファイルの記録失敗、日本語の名前のフォルダにあるリポジトリでの確認の不発、日本語のモデル名の文字化けが起きていた。PowerShellでの編集では、APPLEをORANGEに置き換える依頼で Sonnet 5 が2回とも日本語を壊し、Opus 5.5 は2回とも正しく編集した。1行足す依頼と新しく作る依頼は、どちらのモデルでも正常だった。 |
| 知見 | いちばん危なかったのは、git の破壊的な操作の前に確認を出す道具が、日本語の名前のフォルダでは黙ってしまったことである。受け取ったフォルダの場所が化けてgitを呼べず、道具は「何も失われない」と判断して素通しした。安全のための道具は、自分が失敗したときに「止めない」側に倒れるように書くことが多いので、文字コードの失敗はそのまま安全装置の停止になる。もう一つの教訓はテストの書き方である。Pythonの json.dumps は既定で日本語を記号に置き換えるので、テストの入力をこれで作ると、文字コードの問題が起きる余地がなくなり、壊れた実装でもテストが通る。本物がどんな形で送ってくるかを、先に捕まえて確かめる必要があった。 |
| 限界 | Node.jsとWindows PowerShell 5.1は同じ日に追加で試した(本文の表)が、Go、Rust、PowerShell 7は試していない。PowerShellでの編集の試験は各2回で、頻度を言える数ではない。Windows 10や、システムのコードページを UTF-8 にする設定(ベータ版の「ワールドワイド言語サポートで Unicode UTF-8 を使用」)を有効にした環境では、結果が変わる可能性が高い。 |
何が起きるか
フックは、Claude Code が道具を使う前後などに呼び出す小さなプログラムである。何が起きたかを標準入力の JSON で受け取り、判断や追加の情報を標準出力の JSON で返す。やりとりは UTF-8 だが、そのことは説明書に書かれていない。Mac や Linux では Python の標準入出力が最初から UTF-8 なので問題にならず、Windows だけで壊れる。
受け取るとき
Write の道具で「合言葉はさくら」と書かせたとき、フックに届いたバイト列を保存すると、日本語は e5 90 88 ... という UTF-8 のままだった。これを json.load(sys.stdin) で読むと、Windows の Python は Shift_JIS として解釈するので、日本語が化ける。
返すとき
| フックの返し方 | Claudeに届いた文 |
|---|---|
print(json.dumps(obj, ensure_ascii=False)) | 化ける(置換文字16個) |
print(json.dumps(obj))(日本語を \u の記号にする) | 合言葉は「さくら並木」です |
出力を UTF-8 に切り替えてから ensure_ascii=False | 合言葉は「さくら並木」です |
言語による違い
同じ2方向の試験を、よく見かける書き方のまま3つの言語で行った。
| フックの言語と書き方 | 受け取る | 返す |
|---|---|---|
Python:json.load(sys.stdin) と print(json.dumps(..., ensure_ascii=False)) | 失敗(中身を保存できず空になった) | 化ける |
Node.js:process.stdin を文字列につなげて JSON.parse、process.stdout.write(JSON.stringify(...)) | 正しい | 正しい |
Windows PowerShell 5.1:[Console]::In.ReadToEnd() と ConvertTo-Json を Write-Output | 失敗(中身を保存できず空になった) | 化ける |
Node.js は標準入出力を最初から UTF-8 で扱うので、何もしなくても正しかった。Python と PowerShell 5.1 は、どちらも Windows のコードページ932で読み書きする。Claude Code の説明書は、Windows 向けのフックの例に powershell.exe を使っているので、その例をまねて日本語を扱うと壊れることになる。PowerShell で書くなら、最初に [Console]::InputEncoding と [Console]::OutputEncoding を UTF-8 にする必要がある。この1行をスクリプトの最初に足すと、受け取る・返すの両方が正しくなった。
自作のプラグインで起きていたこと
- knowledge-ledger(エージェントが書いた行の台帳):日本語の入ったファイルを書いたとき、記録に失敗していた。失敗は内部で握りつぶされ、利用者には見えなかった。
- commit-coach(git の破壊的な操作の前に、失われるものを示して確認する):リポジトリが日本語の名前のフォルダにあると、受け取った場所が化けて git を呼べず、確認を出さずに
git reset --hardを通していた。 - token-lens(状態行の表示):日本語のモデル名やパスが化けていた。
直し方はどれも同じで、標準入力をバイト列のまま受け取り、UTF-8 として読む。commit-coach は、git が日本語のファイル名を \346\227... の形で出さないように core.quotepath=false も付けた。
テストが見逃した理由
3本とも、フックに渡す入力をテストの中で json.dumps で作っていた。json.dumps は既定で日本語を \u8a2d のような記号に置き換えるので、テストの入力は ASCII だけになり、文字コードの問題が起きない。壊れた実装のままでもテストは通っていた。本物の入力を捕まえて UTF-8 のままであることを確かめ、テストも ensure_ascii=False で同じ形に送るように直すと、直した部分を元に戻したときに落ちるテストになった。
日本語を数えるときの落とし穴
knowledge-ledger には「説明は40文字以上」という決まりがあった。これが「二つの数を受け取り、その和を total に入れて、そのまま返す関数です。」という37文字の日本語を短すぎるとして弾いていた。英語の40文字と日本語の40文字では、入る中身が違う。全角の文字を2文字分と数えるように直した。
ClaudeにPowerShellで編集させたとき
| 依頼 | Sonnet 5 | Opus 5.5 |
|---|---|---|
| UTF-8 の日本語ファイルに1行足す | 正常 2/2 | 正常 2/2 |
| UTF-8 の日本語ファイルの APPLE を ORANGE に置き換える | 2回とも日本語が壊れた | 正常 2/2 |
| 日本語入りのファイルを新しく作る | 正常 2/2 | 正常 2/2 |
壊れた回は「# 設定ファイル」が「# 險ュ螳壹ヵ繧。繧、繝ォ」、「テスト」が「繝・せ繝・」になった。Windows 標準の PowerShell 5.1 は、BOM の無い UTF-8 のファイルを Get-Content で Shift_JIS として読む。化けた状態で書き戻すので、「・」になった部分は元の文字の情報が残らない。Opus 5.5 は読み書きのたびに文字コードを指定していた。
Windowsでフックを書くときの確認表
- 標準入力は
sys.stdin.buffer.read().decode("utf-8")で読む。 - PowerShell で書くなら、最初に
[Console]::InputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)を置く。 - 迷ったら Node.js で書く。何もしなくても UTF-8 で読み書きする。
- 標準出力は
sys.stdout.reconfigure(encoding="utf-8")にしてから書くか、JSON の日本語を記号のまま出す。 - テストの入力は
json.dumps(..., ensure_ascii=False)で作る。本物は日本語を記号に置き換えない。 - git の出力を読むなら
git -c core.quotepath=falseを付ける。 - 文字数で判断するなら、全角を2文字分と数える。
- フックのコマンドは
python3 ... || python ...と書く。Windows のpython3は Microsoft Store を開くだけの別物であることが多い。 - Claude Code の中から呼ぶコマンドは、今いるフォルダのプログラムを探さない設定(
NoDefaultCurrentDirectoryInExePath=1)で動くので、.bat などはフルパスで呼ぶ。 - 日本語や記号(& % ! ( ' $ `)を含むフォルダにプラグインを置いても、
${CLAUDE_PLUGIN_ROOT}は正しく渡る(実測で問題なし)。
報告
フックの入出力が UTF-8 であることを説明書に書くよう、Claude Code の開発元に報告した(anthropics/claude-code #96285、出力側の測定は追記コメント)。自作プラグインの修正は tsurutanmen/claude-plugins の #5。