目次を表示する

Claude Code 自走の作法 ─ 副業煽りを解体し、本物の自動化を設計する

L2 ─ Hooks で自分を監視させる

L2 ─ Hooks で自分を監視させる

L1 で型は作った。だが Claude は型を呼ばずに自前でやり始めることがある。CLAUDE.md に「絶対 X するな」と書いても忘れる。

L2 = Hooks はその対策。Claude の行動の前後に shell script を挟み、機械的にルールを強制する

なぜ CLAUDE.md だけでは足りないのか

CLAUDE.md は LLM への指示だ。重要だが、確率的にしか守られない。長くなるほど忘れる、優先度の解釈がブレる、Recency Bias で末尾の指示が強く効く ─ これは LLM の本質的な性質で、完全には制御できない。

zenn で話題になった kazuph の記事「Claude Code のすぐルール忘れる問題を Hooks で解決する」 はこの問題を直球で扱っている:

CLAUDE.md にルールを書いても、長い conversation の途中で忘れる。Hook で会話履歴をパースして、ルール違反していないか別 LLM で検閲する。

これが Hooks の本懐 ── 「ソフトな指示」を「ハードな強制」に変換する仕組み。

Hook の 5 タイプ

公式 Hooks ドキュメント(日本語版あり)にある主要なイベント:

graph LR
  S[Session開始] --> SS[SessionStart]
  SS --> U[ユーザー入力]
  U --> UPS[UserPromptSubmit]
  UPS --> C[Claude 思考]
  C --> PRE[PreToolUse]
  PRE -->|allow| T[Tool 実行]
  PRE -->|deny| Block[blocked]
  T --> POST[PostToolUse]
  POST --> C
  C --> STOP[Stop]
  STOP --> END[Session 終了]

  style PRE fill:#fff4e1
  style POST fill:#fff4e1
  style UPS fill:#fff4e1
  style STOP fill:#fff4e1
  style SS fill:#fff4e1
Hook発火タイミング主な用途
SessionStartsession 開始時コンテキスト inject、状態リセット
UserPromptSubmitユーザー入力直前プロンプト整形、ルール再注入
PreToolUsetool 実行前危険コマンド block、permission 補強
PostToolUsetool 実行後format / lint / test 自動実行
Stopsession 終了時履歴検閲、ログ出力

設定の基本構造

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-bash.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
          }
        ]
      }
    ]
  }
}

3 つの軸の組み合わせで設計する:

  • イベントPreToolUse / PostToolUse / …)
  • matcher(正規表現で tool 名を絞り込む。BashEdit|Writemcp__.* など)
  • command(実行する shell script)

zenn / biki の記事 はこの 3 軸の使い分けを丁寧に整理している。

実装例 1:危険コマンドを止める

PreToolUserm -rf / 系を block する。

.claude/hooks/check-bash.sh

#!/bin/bash
# stdin から JSON を受け取る
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# 危険パターンを検出
if echo "$COMMAND" | grep -qE '(rm\s+-rf?\s+/|sudo\s+rm|>\s*/dev/sd|dd\s+if=.*of=/dev/)'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
  exit 0
fi

# 通常の git push --force もブロック(main/master 限定)
if echo "$COMMAND" | grep -qE 'git\s+push\s+(--force|-f).*\b(main|master)\b'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "ask",
      permissionDecisionReason: "Force push to main detected, please confirm"
    }
  }'
  exit 0
fi

exit 0

ポイント:

  • stdin で JSON が渡るtool_input に tool 引数)
  • stdout で JSON を返すと Claude の挙動を制御
  • permissionDecision: "deny" で完全 block、"ask" で人間確認を要求

これで「Claude が dangerously-skip-permissions で動いている時でも、絶対に rm -rf / は実行されない」状態を作れる。

実装例 2:自動 format / lint

PostToolUse で Edit/Write の後に自動 format。

.claude/hooks/format.sh

#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE" ]]; then
  exit 0
fi

# 拡張子で format 振り分け
case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx)
    npx prettier --write "$FILE" 2>/dev/null
    ;;
  *.py)
    ruff format "$FILE" 2>/dev/null
    ruff check --fix "$FILE" 2>/dev/null
    ;;
  *.go)
    gofmt -w "$FILE"
    ;;
  *.rs)
    rustfmt "$FILE" 2>/dev/null
    ;;
esac

exit 0

これで Claude が編集したファイルが自動的に整形される。人間が pre-commit hook でやっていたことを、Claude の編集後にも適用する発想。

実装例 3:CLAUDE.md の再注入

UserPromptSubmit でユーザー入力の前にプロジェクト固有のルールを inject。

.claude/hooks/inject-rules.sh

#!/bin/bash
INPUT=$(cat)
USER_PROMPT=$(echo "$INPUT" | jq -r '.user_prompt // empty')

# 重要なルールだけを再注入(CLAUDE.md 全体ではない)
RULES=$(cat <<'EOF'
[Reminder] このプロジェクトの絶対ルール:
- DB マイグレーションは必ず確認を取る
- main branch への直接 commit は禁止
- 新規依存追加は事前承認
EOF
)

# 既存プロンプトの前に rule を inject
NEW_PROMPT="${RULES}

${USER_PROMPT}"

jq -n --arg p "$NEW_PROMPT" '{
  hookSpecificOutput: {
    hookEventName: "UserPromptSubmit",
    additionalContext: $p
  }
}'

これで毎回ユーザー入力の前にルールが注入される。長い CLAUDE.md ではなく、絶対に守らせたい数項目だけを毎ターン強制する設計。

実装例 4:Stop hook で会話の検閲

kazuph の記事 の本丸。Session 終了時に会話履歴を取得し、別 LLM(あるいは安価な Claude Haiku)でルール違反をチェックする。

.claude/hooks/audit.sh

#!/bin/bash
INPUT=$(cat)
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')

if [[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]; then
  exit 0
fi

# 直近のアクションだけ抽出
RECENT=$(tail -100 "$TRANSCRIPT_PATH" | jq -r 'select(.type == "tool_use") | .name + ": " + (.input | tostring)' 2>/dev/null | head -20)

# 別の安価モデルでチェック
RESULT=$(echo "$RECENT" | claude -p --model claude-haiku-4 \
  "次の行動履歴を見て、main branch への直接 commit や DB マイグレーションの無確認実行など、ルール違反があれば 'VIOLATION: <内容>' の形式で報告。なければ 'OK'。")

if echo "$RESULT" | grep -q "VIOLATION"; then
  # Slack / log に通知
  echo "[$(date)] $RESULT" >> "$CLAUDE_PROJECT_DIR/.claude/audit.log"
  # 必要なら notification skill / MCP 経由で人間に通知
fi

exit 0

「Claude が Claude を監視する」構図。安価モデル(Haiku)を使えばコストも抑えられる。これは L5 への伏線でもある(multi-agent の入り口)。

Hook 設計の落とし穴

落とし穴 1:実行時間が長すぎる

Hook は session に組み込まれるため、遅いと体感品質が落ちる。目安は 1 秒以内。format みたいに必要なら背景化(& で nohup)する。

# 背景化
( npx prettier --write "$FILE" 2>/dev/null & )
exit 0

落とし穴 2:exit code の意味を間違える

  • exit 0:成功、stdout の JSON で挙動制御
  • exit 2:blocking error(Claude に「失敗」を伝える)
  • それ以外:non-blocking error(stderr に出るが続行)

exit 2 を使うと Claude が再試行や修正に動く。exit 1 だと「何か起きたが続けろ」になる。意図的に止めたい時は exit 2

落とし穴 3:JSON エスケープの罠

stdin で受け取る JSON、stdout で返す JSON、いずれも厳密。jq を使わずに sed / awk で組み立てると壊れやすい。素直に jq -n --arg ... でビルドする。

落とし穴 4:debug が困難

Hook の中で何が起きているか、Claude 側からは見えない。ログを取る習慣が必須

# 各 hook の冒頭
LOG="$CLAUDE_PROJECT_DIR/.claude/hooks.log"
echo "[$(date)] $(basename $0) called with $(echo "$INPUT" | jq -r '.tool_name // .hook_event_name')" >> "$LOG"

これで「あれ、hook 動いてる?」を tail -f .claude/hooks.log で確認できる。

Hook の組み合わせパターン

実用上効くのは複数 hook の組み合わせ:

graph TB
  A[UserPromptSubmit<br/>ルール再注入] --> B[PreToolUse<br/>危険コマンド block]
  B --> C[PostToolUse<br/>auto format + lint]
  C --> D[Stop<br/>違反 audit]

  D -.notify.-> S[Slack / log]

  style A fill:#fff4e1
  style B fill:#ffe1e1
  style C fill:#e1ffe1
  style D fill:#e1f5ff

これで 入口(UserPromptSubmit)から出口(Stop)まで 全段に監視が入る。Claude は「自由に動いているように見えて、レールから外れたら止まる」状態になる。

L2 でよく使う書き方の例

調査で集めた事例から、汎用性が高いパターンを 4 つ:

A. 特定ディレクトリへの書き込み禁止

# PreToolUse / matcher: Edit|Write
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *"/secrets/"* || "$FILE" == *".env"* ]]; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Cannot edit secrets directory"
    }
  }'
fi

B. テストが落ちたら commit を止める

# PreToolUse / matcher: Bash
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if [[ "$COMMAND" == *"git commit"* ]]; then
  if ! npm test --silent 2>/dev/null; then
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Tests are failing, fix them first"
      }
    }'
  fi
fi

C. 編集行数が多すぎる時に警告

# PostToolUse / matcher: Write
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
LINES=$(wc -l < "$FILE")
if [[ "$LINES" -gt 500 ]]; then
  echo "[warn] $FILE is now $LINES lines, consider splitting" >> "$CLAUDE_PROJECT_DIR/.claude/warnings.log"
fi

D. Activity tracker

zenn / exwzd の活動可視化記事 のパターン。Hook で「いつ、何の tool を、何回」使ったかを記録し、後から分析する。

# PreToolUse / matcher: ".*"
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
echo "$(date -u +%Y-%m-%dT%H:%M:%S),${TOOL}" >> "$CLAUDE_PROJECT_DIR/.claude/activity.csv"

これで自分の Claude 利用パターンを可視化できる。「何に時間を使っているか」が見えると、L1 の型化候補も自然に浮かぶ。

L2 の典型的な ROI

L2 は L1 と違い、すぐに「時間が浮いた」体感が出にくい。代わりに:

  • 失敗回数が減る(危険コマンド、ルール忘れ、format 漏れ)
  • 手戻りが減る(CI で落ちなくなる)
  • 安心感が増す(dangerously-skip-permissions を打てるようになる)

特に最後が重要。Hook で危険行動を block できるなら、permission prompt なしで Claude を走らせる勇気が出る。これが L3(GitHub Actions)への伏線

この章の要点

  • L2 = Hooks。CLAUDE.md の「ソフト指示」を「ハード強制」に変換
  • 5 種類のイベント:SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Stop
  • 設計は イベント × matcher × command の 3 軸
  • 実装例:危険コマンド block / 自動 format / ルール再注入 / Stop audit
  • 落とし穴:実行時間 / exit code / JSON エスケープ / debug
  • 入口〜出口の組み合わせで「自由なのに外れない」状態を作る
  • L2 が固まると、permission を緩めて L3 に進める勇気が出る

次章への問いかけ

監視が固まれば、人間が常に画面を見る必要はなくなる。

だが session を起動するのは依然として人間だ。次章では GitHub Actions で起動を event-driven 化する。PR が作られる、issue が立つ、push が起きる ── これらが起動トリガーになる世界に進む。