Claude Code のフック(hooks)を909本ほど作って配っている。危ないコマンドを止めたり、書き込みの前に退避を取ったり、そういう小さなシェルスクリプトの集まりだ。
その配布物を点検していて、いちばん気まずいものが出てきた。順に書くとこうなる。
- 自分が書いた設定例をそのまま写すと、
settings.jsonが JSON として壊れる - 壊れた設定ファイルに書いたフックは、1本残らず効かなくなる。画面には何も出ない
- 自分の診断ツールは「フックが1本もありません」と言う。壊れているとは言わない
- その案内に従ってフックを追加すると、元の設定が全部消える。しかも成功と表示される
3つとも別の欠陥だが、同じ1人の利用者が順番に踏むようにつながっていた。
以下は2026年8月3日時点の Claude Code 2.1.220 で、隔離した環境に実際に置いて確かめた内容だ。
見た目は親切、中身は構文エラー
技術記事の設定例には、こういう書き方がよくある。
// ~/.claude/settings.json
{
"hooks": { ... }
}
先頭の // ファイル名 は「これは何のファイルか」を示す親切な注記だ。書いた側の意図としては、コードの外にある説明のつもりでいる。
ところが読者は、ブロックごと選択して写す。そして settings.json は厳密な JSON なので、// が1行あるだけでファイル全体が構文として壊れる。
壊れた設定ファイルが読み込まれると、そこに書いたフックが1本残らず効かなくなる。1本の打ち間違いなら1本が死ぬだけだが、こちらはまとめて全部だ。
自分の配布ページを機械で走査したら、この形が4か所あった。全部、自分が書いたものだ。
しかも1か所は、JSON の途中にもコメントを持っていた。
"matcher": "Bash", // Only Bash commands
これは先頭の1行に気づいて消しても、まだ残る。
自分の道具が、その失敗を握り潰していた
配布ページを直したあと、ついでに自分のインストーラを数えた。settings.json を素の JSON.parse で読んでいる箇所が34か所あり、そのうち9か所が同じ行で失敗を完全に握り潰していた。
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch(e) {}
壊れた設定ファイルは、エラーにすらならずに「空の設定」として扱われる。
厄介なのはその直後だ。この関数は環境の健康診断で、読めた設定を見てこう判定する。
if (preHooks.length === 0) {
risks.push({ severity: 'CRITICAL',
issue: 'No PreToolUse hooks — destructive commands can run unchecked' });
}
利用者はフックを設定してある。読めていないだけだ。それを「1本も設定されていません」と報告して、再インストールを勧める。原因(JSONが壊れている)には一言も触れない。
そして、いちばん悪いものが出てきた
9か所を1つずつ見ていったら、そのうち6か所は、読んだあとに設定ファイルを書き戻していた。
let settings = {};
if (existsSync(SETTINGS_PATH)) {
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
}
if (!settings.hooks) settings.hooks = {};
// ...新しいフックを足す...
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
読めなかった時、settings は {} のままだ。そこへ新しいフックを1本足して、元のファイルへ上書きする。
隔離した環境で実際に走らせた。壊れた settings.json に、既存のフック1本と permissions.allow と env を置いておく。そこへ自分のツールでフックを1本追加する。
| 終了コード | 元の設定 | 既存のフック | 環境変数 | |
|---|---|---|---|---|
| 修正前 | 0(成功と表示) | 上書きされた | 消えた | 消えた |
| 修正後 | 1 | 無傷 | 残った | 残った |
終了コードは0で、画面には「Registered in settings.json」と出る。 利用者は成功したと思う。
順番に並べるとこうなる。
- 私が書いた設定例を、読者がブロックごと写す
-
settings.jsonが JSON として壊れる - フックが全部止まる。画面には何も出ない
- 読者が私の診断を走らせる → 「フックが1本もありません」と出る。壊れているとは言わない
- 読者が言われたとおりフックを追加する → 元の設定が全部消える
自分の本には「壊れた設定ファイルを既定に戻したら、フックも権限もバックアップなしで消えた」という事故を1章ぶん書いてある。その事故を、自分の道具が起こす側に回っていた。
直した。壊れていたら書かずに止めて、原因と確認コマンドを出す。ついでに、同じ試験の途中で --protect が ReferenceError で一度も成功していなかったことも分かったので、それも直した。
測り方は3回間違えた。3回とも「問題が少ない」側へ倒れていた。
1回目は、修正前の版を別の場所へ置いて走らせたので依存が解決できずに落ちていた。出力が空なのを「欠陥が無い」と読みかけた。
2回目は、比較の基準を「いまの最新」にしていたので、自分がコミットした瞬間に基準が動いて「修正前も直っている」と出た。
3回目がいちばん危なかった。「握り潰しの後で書き戻すか」を数えるとき、各箇所の後ろ60行しか見ていなかった。ある関数は握り潰しと書き戻しが117行離れていて、「読むだけ」に分類してしまった。関数の範囲で数え直したら、5か所ではなく6か所だった。距離で切った窓は、遠くにある結果を見落とす。
症状が「何も起きない」なのが厄介
フックが正しく動いて危ない操作が一度も来なかった時と、フックが1本も呼ばれていない時とで、画面に出るものは同じだ。何も出ない。
だから、動いていることを別の方法で確かめるしかない。1行で済む。
python3 -m json.tool ~/.claude/settings.json > /dev/null && echo "読める"
何か言ってきたら、その設定ファイルに書いたフックは全部動いていない。
ついでに、登録先の名前が実在するかも見ておくといい。存在しない名前は警告なしで捨てられる(この話は7月末に別の記事で書いたので、ここでは確認の手順だけにする)。
claude doctor
出力に Unknown hook event が出たら、その1本は一度も呼ばれていない。
自分の909本を数えたら、2本が該当した
人に言う前に自分を数えた。配っているフックのうち、登録先の宣言をヘッダに持つのは829本。そのうち2本が、存在しない名前を宣言していた。どちらも Any と書いてある。
他のフックを包んで使う道具なので「どのイベントでも」という散文のつもりだったのだが、写した人が得るのは無視されるキーだ。直して、二度と入らないように名前の検査を CI に入れた。一覧に無い名前があればビルドが落ちる。
まとめ
- 設定例をブロックごと写す時、先頭の
// ファイル名を持っていかない。JSON にコメントは書けない - 記事や README に設定例を書く側は、コメントをブロックの外に置く。読者は必ずブロックごと写す
- 設定ファイルが壊れると、1本ではなく全部が黙って止まる。確認は
python3 -m json.toolで1行 - 設定を読んで書き戻す道具は、読めなかった時に絶対に書かない。 読めないものを「空」と見なして上書きすると、利用者の資産が消える。私は34か所のうち9か所で失敗を握り潰し、そのうち6か所が書き戻していた
- 数える時、窓を距離で切らない。関数や責務の範囲で切る。距離の窓は「問題が少ない」側へ倒れる
- 握り潰した先で出るのは「設定されていません」という原因を隠した診断で、その案内に従うと消える
この記事の調査・検査・是正は Claude Code に実行させた。数値はすべて手元の環境(WSL2 / Claude Code 2.1.220 / 909本のフック)での実測で、是正の内容は MIT ライセンスで公開している。