Claude Codeを育てる - Part 2:強制編
Part 1 で「説得」と「強制」の区別を書いた。
今回は強制側 — permissions.deny と Hooks の話。
ここでの「強制」とは「Claudeの判断に依らず、ハーネス側で実行される」という意味。 役割は2つに分かれる:
- 禁止 —
permissions.denyで危険操作を物理ブロック - 自動化 — Hooks で編集後の lint/format などを自動実行(任意で禁止用にも使える)
Hooks は主に自動化のための仕組みで、permissions.deny のような事前ブロック専用ではない、というのを最初に押さえておく。
■ なぜ強制が必要か
CLAUDE.mdに「rm -rf 禁止」と書いてもClaudeは破ることがある。
理由:
- セッションが長くなると、CLAUDE.mdの内容が注意から漏れる
- 状況によって「これは緊急だから」という判断で禁止を覆す
- そもそも一回も読み返さないことがある
つまり説得には限界がある。
事故が起きたら一発アウトの操作(rm -rf /、git push --force、.env 漏洩等)は説得ではなく強制で止める。
■ permissions.deny を最初に書く
.claude/settings.json:
{
"permissions": {
"deny": [
"Bash(rm -rf*)",
"Bash(git push --force*)",
"Read(.env)",
"Read(.env.*)",
"Read(**/credentials*)",
"Read(**/secrets*)"
]
}
}
特徴:
- ハーネス側で実行前にブロックされる
- Claudeの判断に依存しない
Bash()はコマンド文字列に対するマッチ、Read()はパスに対するマッチ(gitignore準拠の**も使える)
ポイント:
permissions.allowで確認をスキップできる —Bash(npm test)のように具体的に書いたコマンドは、実行のたびの確認プロンプトが出ず素通りする。頻繁に使うコマンドに有効- ただし
Bash(*)は絶対に書かない —denyに書いたコマンドだけはブロックされるが、それ以外は全て確認プロンプトなしで実行されてしまう。allow するなら必ず specific なパターン(Bash(npm run *)、Bash(git status)等)だけにする - DB の deny はあえて書かない。
Bash(*DROP TABLE*)のような SQL 文字列マッチはクォート・コメント・大文字小文字で抜けるし、Bash(psql*)のように DB クライアント自体を deny するとSELECTすら通らず開発が止まる。本番 DB 保護は「接続情報を Claude に渡さない」という運用層で行うのが現実解 .envや credentials はRead()側でブロックする。これがファイルシステム経由で接続情報が漏れる経路を塞ぐ第一防衛線
■ Hooks の使い方
Hooks はスラッシュコマンドのように手動で呼ぶものではない。 Claude Code のライフサイクルで自動的に発火するイベントフックで、設定したコマンドが裏で勝手に走る。
書く場所は .claude/settings.json の hooks キー。専用ディレクトリは存在しない。
主なイベント種別:
PreToolUse/PostToolUse— ツール実行の前後(Edit, Write, Bash 等の前後)UserPromptSubmit— ユーザー入力時Stop/SubagentStop— 応答終了時SessionStart/SessionEnd— セッション開始/終了PreCompact— コンテキスト圧縮直前Notification— 通知時
例:「Edit / Write でファイルが書き換わったら、その都度 lint を走らせる」
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); npm run lint -- \"$FILE\""
}]
}]
}
}
設定の意味:
PostToolUse— ツール実行後に発火するイベントmatcher: "Edit|Write"— Edit ツールまたは Write ツールが使われたときだけ反応command— その時に実行されるシェルコマンド
■ stdin 経由で渡る JSON
ここが見落としやすいポイント。
Claude Code は hook を発火させるとき、何が起きたかを表す JSON をコマンドの標準入力(stdin)に流し込む。
$CLAUDE_FILE_PATHS のような環境変数で渡ってくるわけではない。
PostToolUse(Edit)の場合、stdin に流れてくる JSON はこんな形:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/edited/file.ts",
"old_string": "...",
"new_string": "..."
}
}
このうち編集されたファイルのパスが欲しい。シェルから JSON を取り出すには jq というコマンドを使う:
jq -r '.tool_input.file_path'
意味はシンプルで、
jq— コマンドラインの JSON パーサ-r— 取り出した文字列のクォートを外す(raw output).tool_input.file_path— JSON のtool_input.file_pathを取り出す指定
これを $( ... ) でシェル変数に入れて、後続のコマンド(npm run lint -- "$FILE")に渡している、というのが上の例の流れ。
■ Hooks に重い処理を入れない
ここを間違うと開発体験が崩壊する。
❌ ダメ:
- テスト全件実行を
PostToolUseに入れる → 編集のたびに数分待機 - ビルド全体を毎回走らせる → 同上
- 巨大リポジトリで
git status全体スキャン → 遅延
✅ OK:
- 編集したファイルだけ lint / format
- type check(高速な範囲のみ)
- secret scanner(差分のみ)
最初は lint と format だけでいい。テストは別途、別のタイミング(Stop hook など)で考える。
■ チーム共通と個人用を分ける
.claude/settings.json → Git管理(チーム共通)
.claude/settings.local.json → Git無視(個人用)
settings.local.json を .gitignore に入れる。
チーム共有すべきもの:
- 危険コマンドの deny
- 共通の lint/format hooks
個人用に置くもの:
- 絶対パスを含む設定
- 個人の認証情報
- 実験中の hook
■ 今回のまとめ
事故防止と自動化はCLAUDE.mdではなく settings.json で実現する。
- 危険操作 →
permissions.denyで物理ブロック - 編集後の品質チェック → Hooks で自動実行
- ただし Hook に重い処理を入れない
- チーム共通と個人用を
settings.json/settings.local.jsonで分ける
次回 Part 3 は説得編。CLAUDE.md は何を書くべきか、何を書かないべきか、判断できるルールの粒度をどう決めるか。