Claude Codeを育てる - Part 2:強制編

Part 1 で「説得」と「強制」の区別を書いた。 今回は強制側 — permissions.deny と Hooks の話。

ここでの「強制」とは「Claudeの判断に依らず、ハーネス側で実行される」という意味。 役割は2つに分かれる:

Hooks は主に自動化のための仕組みで、permissions.deny のような事前ブロック専用ではない、というのを最初に押さえておく。


■ なぜ強制が必要か

CLAUDE.mdに「rm -rf 禁止」と書いてもClaudeは破ることがある。

理由:

つまり説得には限界がある。 事故が起きたら一発アウトの操作(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*)"
    ]
  }
}

特徴:

ポイント:


■ Hooks の使い方

Hooks はスラッシュコマンドのように手動で呼ぶものではない。 Claude Code のライフサイクルで自動的に発火するイベントフックで、設定したコマンドが裏で勝手に走る。

書く場所は .claude/settings.jsonhooks キー。専用ディレクトリは存在しない。

主なイベント種別:

例:「Edit / Write でファイルが書き換わったら、その都度 lint を走らせる」

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path'); npm run lint -- \"$FILE\""
      }]
    }]
  }
}

設定の意味:

■ 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'

意味はシンプルで、

これを $( ... ) でシェル変数に入れて、後続のコマンド(npm run lint -- "$FILE")に渡している、というのが上の例の流れ。


■ Hooks に重い処理を入れない

ここを間違うと開発体験が崩壊する。

❌ ダメ:

✅ OK:

最初は lint と format だけでいい。テストは別途、別のタイミング(Stop hook など)で考える。


■ チーム共通と個人用を分ける

.claude/settings.json        → Git管理(チーム共通)
.claude/settings.local.json  → Git無視(個人用)

settings.local.json.gitignore に入れる。

チーム共有すべきもの:

個人用に置くもの:


■ 今回のまとめ

事故防止と自動化はCLAUDE.mdではなく settings.json で実現する。

次回 Part 3 は説得編。CLAUDE.md は何を書くべきか、何を書かないべきか、判断できるルールの粒度をどう決めるか。