じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Claude CodeのHooksは設定したほうがいい

Claude Codeを使い始めて、様々な発信をしてきました。俺の(n)vimerとしてのアイデンティティを取り戻してくれたので感謝しています。settings.jsonやCLAUDE.md、.claude/commands/**.mdの設定について書いてきました。今回は「Hooks」について。これも設定しておくと、Claude Codeがグッと使いやすくなる機能です。

syu-m-5151.hatenablog.com

このブログが良ければ読者になったり、nwiizoのXやGithubをフォローしてくれると嬉しいです。では、早速はじめていきます。

はじめに

ここで読むのをやめる人のために言っておくと、Hooksは「Claude Codeがファイル編集した後に必ずフォーマッターを実行する」みたいなことを自動化できる機能です。CLAUDE.mdに書いても忘れちゃうようなことを、システムレベルで強制できます。

Claude Codeって本当に優秀なんですよ。でも、定期的に記憶喪失する新人エンジニアみたいなところがある。

「フォーマッター実行してからコミットしてね」って言っても、次の瞬間には忘れてる。CLAUDE.mdに大きく書いても、「## 重要:必ずフォーマッターを実行すること!!」って赤字で書いても(Markdownに赤字はないけど)、やっぱり忘れる。

人間の新人なら「すみません、忘れてました...」って反省するけど、Claude Codeは「あ、そうでしたっけ?」みたいな顔して(顔はないけど)、また同じミスを繰り返す。

そんな時に救世主となるのがHooksです。

Hooksとは何か

Claude Code Hooksは、Claude Codeのライフサイクルの特定のタイミングで自動実行されるシェルスクリプトです。「Claude Codeがファイルを編集した後に必ずフォーマッターを実行する」「特定のディレクトリへの書き込みを制限する」といったことが可能になります。

docs.anthropic.com

要するに、「お前が忘れても俺が代わりに実行してやるよ」っていう機能です。CI/CDまで進んでから問題に気づくと手戻りが多いので、できればローカルで早めに確認したい。

エンジニアに馴染み深いGit Hooksの話

Git使ってる人なら、pre-commitとかpost-commitとか聞いたことあるでしょ?あれと同じ発想です。

git-scm.com

でも設定の入口は手軽です。JSONに実行コマンドを書けます。ただし、外部のシェルスクリプトを直接呼び出す場合は、その実行権限も確認します。

なぜHooksを設定したほうがいいのか

もちろん、フォーマットやテストの実行はGitHub ActionsなどのCIに設定しておくのが大前提です。でも、CIまで行ってから「あ、フォーマット忘れてた」「テスト壊れてる」って気づくのは遅すぎる。手戻りのコストが大きすぎるんです。

プッシュして、CI待って、失敗して、ローカルに戻って修正して、また プッシュして...この時間、本当にもったいない。特にチーム開発だと、その間に他のメンバーのPRがマージされて、コンフリクト解決まで必要になったり。

だからこそ、ローカルの段階で、しかもClaude Codeが作業した瞬間に問題を発見・修正する仕組みが必要なんです。それがHooksです。

github.com

1. Claude Codeは優秀だけど忘れっぽい

朝:「必ずテスト実行してからコミットしてね」
昼:テストを忘れてコミット
夕方:「あ、すみません。次は気をつけます」
翌日:また忘れる

CLAUDE.mdに書いた指示でも、毎回実行してくれるとは限りません。そこで、必要な処理をHooksから呼び出しておきます。

2. 人間も忘れるけど、AIはもっと忘れる

私も昔は「フォーマッター?後で実行すればいいじゃん」って思ってました。でも実際は忘れる。人間でさえ忘れるのに、AIはもっと忘れる。

しかも厄介なのは、AIは「忘れた」って自覚がないこと。人間なら罪悪感があるけど、AIは「え?そんな話ありました?」みたいな態度。(態度っていうか、本当に覚えてない)

3. コードの品質を自動で保てる(CIより前に!)

人間がコード書いてた頃は、エディターの保存時自動フォーマットに頼ってました。でもClaude Codeはエディタじゃない。ターミナルツールです。

だから明示的に「フォーマッター実行して」って言わないといけない。でも毎回言うのダルい。そして言い忘れる(俺もお前も)。結果、コードがぐちゃぐちゃになる。

Hooksを使えば、以下のように設定できます。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path | select(endswith(\".js\") or endswith(\".ts\"))' | xargs -r prettier --write"
      }]
    }]
  }
}

これだけで、JSやTSファイルを編集するたびに自動でPrettierが走る。最高じゃないですか?(というか今までは適正なコードを出さなかったので⋯)

実際、開発フローで考えてみてください。

  1. Claude Codeで編集 → Hooksでフォーマット(即座に修正)
  2. git commit → pre-commitフック(ローカルで最終チェック)
  3. git push → CI/CD(チーム全体の品質担保)

この3段階のうち、最初の段階で問題を解決できれば、後の段階での手戻りがなくなる。シフトレフトってやつです。問題の発見と修正を可能な限り早い段階に移動させる。

CIで「フォーマットエラー」なんて出たら、正直イライラするでしょ?編集直後に直せれば、その手戻りを減らせます。ただし、この例が対象にするのは指定したツールによる編集です。Bashなど別の経路での変更まで、すべて拾えるわけではありません。

4. やらせたくないことをやらせない

Claude Codeって基本的に何でもやってくれるんですが、それが怖い時もある。

「ちょっとこのバグ直して」って言ったら、なぜか本番環境の設定ファイルまで書き換えようとしたり。「いや、そこじゃない!」って叫んでも後の祭り。

実際にはこのような形で動作する。

Hooksなら事前に止められます。

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "jq -r 'if .tool_input.file_path | test(\"production|.env|secrets\") then {\"decision\": \"block\", \"reason\": \"本番環境のファイルは触るな!開発環境でテストしてから。\"} else empty end'"
      }]
    }]
  }
}

これは、指定したパターンに一致するファイルへのWriteやEditをブロックする例です。terraform applyやcdk deployなどのコマンドを止めるには、別途Bashを対象とするチェックが必要です。

これもCIで検出するより、ローカルで止める方が圧倒的に安全。間違えてコミットしちゃった秘密情報は、git履歴から多くの場合消すのが大変ですからね。

5. 作業履歴も残せる(後で絶対役立つ)

「昨日何やったっけ?」「このファイル誰がいつ変更した?」

Git見ればわかる?いや、Claude Codeが実行したコマンドまでは分からないでしょ。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "echo \"[$(date)] $USER: $(jq -r '.tool_input.command')\" >> ~/.claude/command_history.log"
      }]
    }]
  }
}

これで全コマンドの履歴が残る。デバッグの時めちゃくちゃ助かることがあった。

speakerdeck.com

github.com

6. フィードバックループの短縮(開発速度の本質)

結局のところ、開発速度を上げるって「フィードバックループを短くする」ことなんですよ。

  • Hooksなし - 編集 → コミット → プッシュ → CI失敗 → 修正(5-10分)
  • Hooksあり - 編集 → 即座に修正(数秒)

この差、積み重なると膨大な時間になります。1日10回この差が出たら、50-100分の差。1週間で...計算したくないですね。

もちろん、最終的にはCIでチェックします。でも、CIは「最後の砦」であって、「最初の砦」じゃない。最初の砦はローカル、それもClaude Codeが動いてる瞬間です

Hooksの基本的な使い方

設定方法

Hooksの設定は/hooksコマンドを使うのが簡単ではある

/hooks

でも正直、最初はJSON直接編集した方が分かりやすいかも。

設定できる場所は3つあります。

  • ~/.claude/settings.json:全プロジェクト共通(グローバル)
  • .claude/settings.json:プロジェクト単位
  • .claude/settings.local.json:プロジェクト単位(Git管理外)

私は基本的にプロジェクト単位で設定してます。チームで共有できるから。

Hook Events(いつ実行するか)

4つのイベントがあります。

  1. PreToolUse:ツール実行前(ここで止められる!)
  2. PostToolUse:ツール実行後(後処理に便利)
  3. Notification:通知時(Claude Codeが入力待ちやパーミッション要求時)
  4. Stop:Claude Codeの応答完了時

dev.classmethod.jp

最初はPreToolUseとPostToolUseだけ覚えとけばOK。

実用的なHooks設定例

1. 自動フォーマッター(これは絶対設定すべき)

azukiazusa.dev

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path | select(endswith(\".js\") or endswith(\".ts\") or endswith(\".jsx\") or endswith(\".tsx\"))' | xargs -r prettier --write"
      }]
    }]
  }
}

これマジで便利。設定してから「あ、Prettier忘れた」がゼロになった。

開発生産性の観点からも、フォーマットの統一は重要です。コードレビューで「ここインデント違う」みたいな不毛な議論がなくなって、本質的な設計の話に集中できるようになりました。

2. Rustの人向け(というか、どの言語でも応用可能)

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path | select(endswith(\".rs\"))' | xargs -r cargo fmt --"
      }]
    }]
  }
}

cargo fmt --の代わりに、お好みのフォーマッターを使ってください。例えば以下のようなものがあります。

  • Python: black や ruff format
  • Go: gofmt -w
  • Ruby: rubocop -a
  • Java: google-java-format
  • C/C++: clang-format -i

重要なのは、どの言語でも同じパターンで設定できるということ。ファイル拡張子を判定して、最も適したフォーマッターを実行するだけです。

3. ヤバいコマンドを実行させない

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -r 'if .tool_input.command | test(\"rm -rf|dd if=|:(){ :|:& };:\") then {\"decision\": \"block\", \"reason\": \"危険なコマンドは実行できません。別の方法を検討してください。\"} else empty end'"
      }]
    }]
  }
}

rm -rf /とか無限増殖シェルが実行されたら泣くでしょ?ただし、単純な文字列パターンだけで危険な操作を網羅することはできません。この例も正規表現が意図どおりに動くかを検証し、権限設定と組み合わせて使う必要があります。

4. テスト忘れ防止(私の実体験)

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -r 'if .tool_input.command | test(\"^git (commit|push)\") then if (.tool_input.command | test(\"--no-verify\") | not) then {\"decision\": \"block\", \"reason\": \"コミット前にテストを実行してください。`cargo test`を先に実行するか、本当に必要な場合は--no-verifyを付けてください。\"} else empty end else empty end'"
      }]
    }]
  }
}

これ設定してから、テスト壊したままpushすることがなくなった。ただし、この例自体はテストを実行せず、実行済みかどうかも記録していません。cargo testを先に実行しても同じ条件でブロックされます。また、--no-verifyはGit側のフックを省略する指定なので、通常の回避手段として使うべきではありません。

実は、私のチームではこれを導入してから変更失敗率がしっかり下がりました。コミット前に立ち止まるきっかけとしては役立ちましたが、テストの自動実行や成功確認まで行うなら、その処理を別途組み込む必要があります。

5. コードスタイルのフィードバック

PostToolUseで問題を検出した場合、exit code 2を使ってClaude Codeにフィードバックを返すことができます。

#!/bin/bash
# ~/.claude/hooks/style-check.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# Goファイルの場合
if [[ "$FILE_PATH" == *.go ]]; then
  # gofmtでチェック
  if ! gofmt -l "$FILE_PATH" | grep -q "^$"; then
    echo "Goファイルのフォーマットが正しくありません。gofmtを実行してください。" >&2
    exit 2  # Claude Codeに自動的にフィードバックされる
  fi
fi

exit 0

exit code 2の場合、stderrの内容がClaude Codeに自動的に伝わり、問題を修正しようとします。

6. MCP(Model Context Protocol)ツールとの連携

MCPツールを使用している場合、特別な命名規則でHooksを設定できます。

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "mcp__filesystem__",
      "hooks": [{
        "type": "command",
        "command": "echo '[$(date)] MCPファイルシステムアクセス' >> ~/.claude/mcp_access.log"
      }]
    }]
  }
}

MCPツールはmcp__<server>__<tool>の形式で名前が付けられるので、特定のサーバーやツールに対してHooksを設定できます。

7. 通知のカスタマイズ

Notificationイベントを使って、Claude Codeの通知をカスタマイズできます。

{
  "hooks": {
    "Notification": [{
      "hooks": [{
        "type": "command",
        "command": "echo \"Claude Code: $(jq -r '.message')\" | terminal-notifier -title 'Claude Code'"
      }]
    }]
  }
}

macOSのterminal-notifierを使った例です。Linuxならnotify-sendなど、お好みの通知方法を使えます。

HooksでのJSON制御(ちょっと高度だけど超便利)

Hooksの本当の力は、JSON出力による制御です。

基本的な仕組み

標準出力に特定のJSONを出力すると、Claude Codeの動作を制御できます。

PreToolUseの場合

{
  "decision": "approve" | "block",
  "reason": "理由の説明"
}
  • approve:権限チェックをスキップして強制的に許可
  • block:実行を拒否(reasonがClaude Codeに伝わる)

共通フィールド

{
  "continue": true | false,
  "stopReason": "ユーザーに表示される理由",
  "suppressOutput": true | false
}
  • continue: falseの場合、Claude Codeは処理を停止
  • suppressOutput: trueの場合、標準出力を隠す(トランスクリプトモードでは非表示)

実例:賢い制限

#!/bin/bash
# ~/.claude/scripts/smart-file-guard.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# 本番環境のファイル
if echo "$FILE_PATH" | grep -qE "(production|prod\.env)"; then
  echo '{"decision": "block", "reason": "本番環境のファイルは直接編集できません。開発環境で変更を確認してから、適切なデプロイプロセスを使用してください。"}'
  exit 0
fi

# node_modules(よくある事故)
if echo "$FILE_PATH" | grep -q "node_modules"; then
  echo '{"decision": "block", "reason": "node_modules内のファイルは編集しないでください。package.jsonを変更してnpm installを実行してください。"}'
  exit 0
fi

# それ以外はOK
exit 0

設定:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "~/.claude/scripts/smart-file-guard.sh"
      }]
    }]
  }
}

Stopイベントでの制御

Claude Codeが処理を終えようとした時に、強制的に続行させることもできます。

#!/bin/bash
# ~/.claude/hooks/check-completion.sh

INPUT=$(cat)
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

# すでにstop hookが動作している場合は無限ループを防ぐ
if [ "$STOP_ACTIVE" = "true" ]; then
  exit 0
fi

# 未完了のタスクがある場合
if [ -f "/tmp/claude_tasks_pending" ]; then
  echo '{"decision": "block", "reason": "まだ完了していないタスクがあります。続けてください。"}'
  exit 0
fi

セキュリティ上の注意点

docs.anthropic.com

Hooksはフルユーザー権限で実行されます。つまり、あなたができることは全部できる。

だから次のことに注意してください。

  1. 信頼できないHooksは使わない(当たり前だけど)
  2. JSONの検証は必須(jqでパースしてから使う)
  3. シェル変数は必ずクォート("$VAR"を使う、$VARは危険)
  4. パストラバーサル攻撃に注意(ファイルパスに..が含まれていないかチェック)
  5. 絶対パスを使う(スクリプトの場所を明確に)

実際、私も一度危険な設定を作っちゃったことがあります。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "echo 'ファイル変更を検知' && touch .claude_modified && claude 'このファイルも更新して'"
      }]
    }]
  }
}

例はだいぶ単純化しています。ファイルを編集するたびに新しいClaude Codeのセッションを起動し、それがまたファイルを編集して……という連鎖反応を起こしかけました。すぐに気づいてCtrl+Cで止めたけど、Hookから同じHookを再び呼び出すような構成には注意してください。

設定の安全性

記事執筆時のClaude Codeは、起動時にHooksの設定をスナップショットとして保存し、セッション中はそれを使う仕組みでした。ただし、設定を保持することと、呼び出す外部スクリプトの内容を保護することは別です。Hookとして実行するコードの内容と権限は、自分で確認する必要があります。Hooksの公式リファレンス

私が実際に使ってるHooks

開発環境全体のHooks(~/.claude/settings.json)

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "~/.claude/hooks/auto-format.sh"
      }]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "~/.claude/scripts/command-logger.sh"
      }]
    }]
  }
}

auto-format.shは拡張子見て最も良いフォーマッター実行するスクリプト。長いので省略。

プロジェクト単位のHooks(.claude/settings.json)

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | grep -E '\\.(test|spec)\\.(js|ts|rs)$' | xargs -r npm test -- --findRelatedTests"
      }]
    }]
  }
}

テストファイル編集したら、関連テストを自動実行。便利すぎて泣ける。

認知的負荷の観点から言うと、「テスト実行したっけ?」って考えなくて済むのは本当に楽。フロー状態を維持できるんですよね。集中が途切れない。

デバッグ方法

Hooksがうまく動かない時は、以下を確認してください。

  1. /hooksコマンドで設定を確認
  2. settings.jsonが正しいJSONフォーマットか確認
  3. コマンドを手動で実行してテスト
  4. 終了コードを確認
  5. 標準出力と標準エラー出力のフォーマットを確認
  6. クォートのエスケープが適切か確認

進行状況はトランスクリプトモード(Ctrl+R)で確認できます。

  • 実行中のHook
  • 実行されているコマンド
  • 成功/失敗の状態
  • 出力またはエラーメッセージ

また、claude --debugで起動すると、より詳細なデバッグ情報が得られます。

まとめ

Claude Codeに毎回思い出してもらう必要がある処理は、Hooksで自動化できます。私は特に、編集直後のフォーマットとテストの実行が助かっています。

特に重要なのは以下の点です。

  • 自動フォーマット:もう「フォーマッター忘れた」とは言わせない
  • セキュリティ制御:本番環境を守れ
  • 作業記録:後で絶対助かる
  • フィードバック機能:コード品質の問題を自動で指摘
  • MCP連携:高度なツールとの統合も可能

最初は「めんどくさそう」って思うかもしれない。私もそう思ってた。でも、一度設定したら手放せなくなる。

settings.json、CLAUDE.md、commandsに加えて、まずは一つ、よく忘れる処理をHooksに任せてみてください。CIの結果を待って直す作業が減るだけでも、開発はずいぶん楽になります。