Claude Codeに守らせたい決まりは、CLAUDE.mdよりhookに書く
もくじ
僕は、日々のメモを Obsidian というメモアプリで書いている。Obsidian のメモは、Markdown という書式の普通のテキストファイル(拡張子は .md)として、パソコンの1つのフォルダーにまとめて保存される。このフォルダーの整理を、Claude Code に手伝わせている。Claude Code は Anthropic が作った、ファイルの編集やコマンドの実行を AI に任せられる開発支援ツールだ。
このフォルダーは git で管理していない。git は、ファイルの変更の履歴を記録しておき、あとで以前の状態に戻せるようにする道具だ。それを使っていないので、Claude Code がファイルの中身を間違って書き換えると、簡単には元に戻せない。
だから Claude Code への指示書に「メモのファイルを、ターミナルのコマンドで直接書き換えないで」と書いておいた。ターミナルは、文字でコマンドを打ってパソコンを操作する画面のことだ(どんな書き換えを指すのかは、後で実物を見せる)。ところが、AI がこの指示を守るかどうかは、そのときの AI の判断しだいだった。元に戻せない操作についての決まりは、確実に守ってほしかった。
戻せない操作は、指示書だけに任せず hook で止める
元に戻せない操作のうち、機械で判定できるものは、指示書に頼らず Claude Code の hook で止めることにした。hook は、Claude Code が決まったタイミングで、あらかじめ登録しておいたスクリプト(処理の手順を書いた小さなプログラムのファイル)を必ず実行する仕組みだ。僕は「コマンドを実行する直前」にスクリプトを実行させ、禁止した書き方が見つかったら止めている。AI が決まりを忘れても、スクリプトは忘れない。
指示書は「たぶん守る」、hook は「書いた条件に当たれば必ず止まる」。この差がすべてである。
指示書は、読まれても守られるとは限らない
Claude Code には CLAUDE.md という指示書がある。作業を始めるときに AI が読み込むファイルで、「このフォルダーで作業するときは、こう動いて」という決まりを書いておける。便利だし、たいていは守ってくれる。
とはいえ、「たいてい」は「必ず」ではない。AI は指示書を読んだうえで、その場その場で次に何をするかを選ぶ。選ぶ以上、指示から外れることがある。人間の新人に「そこの棚は触らないでね」と伝えるのと同じで、伝えたことと守られることの間には、どうしても隙間が残る。
公式の hooks ガイドにも、hook の役割がそのまま書いてある。
Hooks are user-defined shell commands. Claude Code runs them at specific points in its lifecycle, which gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them.
訳すと「hook は利用者が決めたコマンドで、Claude Code の決まった時点で実行される。だから、AI(大規模言語モデル)が実行を選ぶかどうかに頼らず、決まった動作を必ず起こせる」となる。(Automate actions with hooks)
事故は sed の一括置換で起きた
実際に、指示書だけでは防げなかった事故が起きた。
僕のメモには、ファイルの先頭に作成日などの情報を書いた数行がある。AI が、たくさんのメモのこの数行を sed で一括で書き換えようとした。sed は、ファイルの中の文字をターミナルのコマンド1行で置き換える、昔からある道具だ。Word の「すべて置換」を、画面を開かずにやるようなものである。
たとえば note.txt というファイルに、次の2行が書いてあるとする。
status: draft
本文はここ
sed に -i を付けると、置き換えた結果でファイルそのものを書き換える。次は macOS 標準の sed の書き方で、-i '' は「控えのファイルを残さずに書き換える」という指定だ。
sed -i '' 's/draft/done/' note.txt
実行した後の note.txt の中身は、こうなる。
status: done
本文はここ
冒頭の「ターミナルのコマンドで直接書き換える」とは、この sed のようなやり方のことだ。便利である。便利なのだが、置き換えの指定を間違えると、何十ファイルでも一度に壊れる。しかも、Word の「元に戻す」にあたる操作が無い。事故のときは、書き換えの指定に間違いがあり、先頭の数行だけ直すはずが本文ごと上書きされた。git で管理していないので、1ファイルずつ以前の状態に戻すこともできなかった。
hook で、実行の直前にコマンドを調べて止める
そこで hook を入れた。.md のファイルを書き換えるコマンドを、実行の直前に見つけて止める。流れは次のようになる。
ただし、見つからなかったことは、安全だと確かめたことを意味しない。スクリプトが知らない書き方は、そのまま通る。
スクリプトはシェルスクリプトで書く。ターミナルに打ったコマンドを受け取って実行するプログラムを「シェル」と呼び、シェルに実行させるコマンドを上から順にファイルへ並べたものがシェルスクリプトだ。Python ではなく、bash というシェルの書き方で書く。
スクリプトの中でやっていることは3つだけだ。
- Claude Code から「これから実行するコマンド」を受け取り、
jqで文字列を取り出す。Claude Code は、この情報を JSON({"名前": "値"}のように、名前と値の組で書くデータの形式)で渡してくる。jqは JSON から値を取り出す道具で、Mac なら Homebrew(Mac にソフトを入れる道具)などで先に入れておく grep(文字列の中から決まった並びを探す道具)で、禁止した書き方を探す- 見つかったら止める
次のスクリプトを ~/.claude/hooks/block-md-sed.sh として保存する。~ は、自分のホームフォルダー(Mac なら /Users/自分の名前)を指す書き方だ。このスクリプトは、コマンドの中に sed -i と、その後ろに .md があるかだけを調べる簡易版である。書き方が少し違えば見逃すし、コマンドではないただの文章に反応することもある。
#!/bin/bash
# AI が実行しようとしているコマンドを受け取る
COMMAND=$(jq -r '.tool_input.command')
# sed -i で .md を書き換えようとしていたら止める
if echo "$COMMAND" | grep -q 'sed -i.*\.md'; then
echo "止めました: .md はシェルで書き換えず、Edit で直してください" >&2
exit 2
fi
exit 0
スクリプトは終わるときに、結果を表す数字(終了コード)を返す。exit 2 や exit 0 がそれだ。コマンドを実行する直前の hook では、exit 2 で終わると Claude Code はそのコマンドを実行せず、>&2 で出した理由の文章を Claude に返す。exit 0 なら、このスクリプトは止めない。その後は、いつもの権限の確認(実行してよいかを利用者に尋ねる画面など)に進む。
保存したら、このファイルを実行してよいという許可を付ける。
chmod +x ~/.claude/hooks/block-md-sed.sh
最後に、Claude Code の設定ファイル(~/.claude/settings.json)に登録する。PreToolUse は「Claude Code が道具を使う直前」、matcher の Bash は「ターミナルのコマンドを実行するとき」を対象にする、という意味だ。$HOME も ~ と同じく、自分のホームフォルダーを指す。設定ファイルがすでにあるなら、丸ごと置き換えず、hooks の部分を書き足す。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$HOME/.claude/hooks/block-md-sed.sh\""
}
]
}
]
}
}
この場所に登録すると、どのフォルダーで作業していても効く。僕はこの形で使っている。特定のフォルダーだけに絞りたいなら、そのフォルダーの中の .claude/settings.json に書く。
試すときは、Claude Code を通さずに、スクリプトへ直接入力を渡せばいい。Claude Code が渡してくるのと同じ形で、調べたいコマンドを書いたファイル input-bad.json を、作業用のフォルダーに用意する。
{"tool_input":{"command":"sed -i 's/draft/done/' note.md"}}
作業用のフォルダーで次のコマンドを打つと、input-bad.json の中身をスクリプトに渡し、スクリプトが返した終了コードを表示する。sed 自体は実行されない。
~/.claude/hooks/block-md-sed.sh < input-bad.json; echo "exit code: $?"
止めました: .md はシェルで書き換えず、Edit で直してください
exit code: 2
中身を cat note.md(ファイルの中身を表示するだけのコマンド)に変えた input-ok.json を渡すと、何も言わずに通す。
~/.claude/hooks/block-md-sed.sh < input-ok.json; echo "exit code: $?"
exit code: 0
僕が実際に使っているスクリプトは、これをもう少し厚くしたものだ。sed のほかに、> という記号でコマンドの結果をファイルに書き込む操作なども止めている。AI には代わりに、Claude Code に備わっている編集の機能(Edit)を使わせる。Edit は「この文字列を、こう差し替える」と1か所ずつ指定する方式なので、一括置換のように指定の間違い1つで何十ファイルも巻き込むことは起きにくい。もっとも、差し替える範囲を間違えれば、Edit でも文章は消える。
ちなみに、この記事のサンプルを作るときにも、Claude に sed -i を試しに打たせたら、本物の hook に止められた。止める相手を選ばない。頼もしい。
git なら、ここまで厳しくしなくていい
ここまで厳しく止めるのは、メモのフォルダーが git で管理されていないからだ。git で変更を記録していれば、ファイルが壊れても、記録した時点の状態までは戻せる。戻せるかどうかで、どこまで厳しく止めるかは変わる。
それに、hook にも見落としはある。だから僕は、壊れたときに戻すためのバックアップも別に取っている。hook は壊させないための備え、バックアップは壊れた後の備えだ。
hook に移せない決まりは、指示書に残す
とはいえ、すべての決まりを hook に移せるわけではない。
僕の hook が調べているのは、Claude が実行しようとしているコマンドの文字列だけだ。たとえば Python のプログラムの中でファイルに書き込む処理は、書き方によっては、コマンドの文字列を見ても書き込むとは分からない。別のファイルに書いたプログラムを呼び出す場合は、そのプログラムの中身までは調べていない。だから僕の hook は、こうした書き込みを見逃す。この決まりだけは、今も指示書に「メモは Edit で直して」と書いて守らせている。
つまり、決まりは2つに分かれる。機械が判定できる決まりは、hook に書く。判定できない決まりは、指示書に書いて、AI が守ってくれるのに任せる。確実に守らせたいものから順に、機械で判定できる形に言い換えられないかを考える。それでも無理なものだけが、指示書に残る。
hook のほかの使い道
止めるだけが hook ではない。公式の hooks ガイドには、ほかにもこんな使い道が載っている。
| したいこと | 動かすタイミング |
|---|---|
| Claude が入力を待っているとき、画面に通知を出す | Claude が入力や許可を待つとき |
| 編集したファイルに整形ツール(コードの字下げや改行の位置をそろえるソフト)をかける | ファイルを編集した後 |
守りたいファイル(パスワードなどを書いた .env など)への編集を止める | 編集する前 |
| 大事な前提を、もう一度 Claude に読ませる | 会話が長くなり、古いやり取りが要約された後 |
| 設定ファイルの変更を記録に残す | 設定が変わったとき |
| 決まった種類の確認を、自動で許可する | 操作の許可を求められたとき |
どれも「AI の判断に任せず、毎回必ずやってほしいこと」だ。「毎回必ず」と言いたくなったら、それは指示書でなく hook に書くべき決まりである。
なお、この記事の簡易版スクリプトの動きは、Claude Code 2.1.283 の環境で確かめた。
まとめ
AI に守らせたい決まりのうち、破られたら元に戻せないもので、機械が判定できるものは hook に書く。指示書は AI の判断を通るが、hook は判断を通らない。判断を通らないから、書いた条件に当たれば確実に止まる。
そして、判定できない決まりだけを、指示書に書いて祈る。
おわり😊

