hookを書きたい。でも何から始めればいいか分からない。
公式ドキュメントは読んだ。仕組みは理解した。でも「じゃあ実際にファイルを作って動かすまで」が遠い。
この記事では、コピペしてすぐ動く最小テンプレートを3種類用意した。PreToolUse(ツール実行前)、PostToolUse(ツール実行後)、Stop(セッション終了時)。全部bashスクリプト。全部10行前後。
hookの基本構造
どのhookも同じパターンで動く。
- Claude CodeがstdinにJSONを流し込む
- hookがJSONを読んで判断する
- 終了コードで結果を返す
終了コードの意味:
| exit code | 意味 |
|---|---|
0 |
問題なし。そのまま続行 |
1 |
警告。stderrの内容をClaudeにフィードバック |
2 |
ブロック。ツール実行を止める(PreToolUseのみ) |
stdoutにJSON({"decision":"approve"}等)を出すと、許可判定を上書きできる。stderrに書いた文字列はClaudeへのフィードバックになる。
1. PreToolUse テンプレート(ブロック用)
ツール実行の前に走る。危険な操作を止めたいときに使う。
~/.claude/hooks/block-template.sh:
#!/bin/bash
# PreToolUse: 特定コマンドをブロックするテンプレート
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
# ブロック条件(ここを書き換える)
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+(/|~|\$HOME)'; then
echo "BLOCKED: rm -rf on sensitive path" >&2
exit 2
fi
exit 0
exit 2がブロック。exit 0が許可。stderrに書いた理由はClaudeに伝わるので、Claudeが代替手段を考えてくれる。
応用例: git push --forceをブロック
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force'; then
echo "BLOCKED: force-push detected. Use --force-with-lease instead." >&2
exit 2
fi
2. PreToolUse テンプレート(自動承認用)
安全だと分かっているコマンドの許可プロンプトを消す。Auto Modeじゃなくても、特定のコマンドだけ自動で通したいときに便利。
~/.claude/hooks/approve-template.sh:
#!/bin/bash
# PreToolUse: 安全コマンドを自動承認するテンプレート
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
# 承認条件(ここを書き換える)
BASE=$(echo "$COMMAND" | awk '{print $1}')
case "$BASE" in
cat|ls|head|tail|grep|find|wc|tree|pwd)
echo '{"decision":"approve","reason":"Read-only command"}'
exit 0
;;
esac
# マッチしなければ何もしない(通常の許可フローへ)
exit 0
stdoutに{"decision":"approve"}を出すのがポイント。これでClaude Codeの許可プロンプトがスキップされる。
3. PostToolUse テンプレート
ツール実行の後に走る。結果を検証して問題があればClaudeに伝える。
~/.claude/hooks/post-check-template.sh:
#!/bin/bash
# PostToolUse: 実行結果を検証するテンプレート
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
# ファイル操作以外はスキップ
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
# 検証(ここを書き換える)
EXT="${FILE_PATH##*.}"
case "$EXT" in
py)
ERR=$(python3 -m py_compile "$FILE_PATH" 2>&1)
if [ $? -ne 0 ]; then
echo "SYNTAX ERROR: $ERR" >&2
exit 1
fi
;;
sh)
ERR=$(bash -n "$FILE_PATH" 2>&1)
if [ $? -ne 0 ]; then
echo "SYNTAX ERROR: $ERR" >&2
exit 1
fi
;;
json)
ERR=$(jq empty "$FILE_PATH" 2>&1)
if [ $? -ne 0 ]; then
echo "SYNTAX ERROR: $ERR" >&2
exit 1
fi
;;
esac
exit 0
exit 1で警告を返す。ブロックはしない(PostToolUseではexit 2は使えない。実行はもう終わっている)。stderrの内容がClaudeにフィードバックされ、自動的に修正を試みてくれる。
4. Stop テンプレート
セッション終了時に走る。ログ記録やクリーンアップに使う。
~/.claude/hooks/stop-template.sh:
#!/bin/bash
# Stop: セッション終了時の処理テンプレート
INPUT=$(cat)
REASON=$(echo "$INPUT" | jq -r '.stop_reason // "unknown"' 2>/dev/null)
TIMESTAMP=$(date -Iseconds)
# ログ記録(ここを書き換える)
LOG="$HOME/.claude/session-log.txt"
echo "[$TIMESTAMP] Session ended: reason=$REASON" >> "$LOG"
# 異常終了の通知(オプション)
if [ "$REASON" != "user" ] && [ "$REASON" != "normal" ]; then
echo "WARNING: Unexpected stop reason: $REASON" >&2
fi
exit 0
Stopフックでは終了コードの意味が変わる。何を返してもセッション終了は止まらない。stderrに書いた内容はログとして残る。
settings.jsonへの登録
hookファイルを書いたら~/.claude/settings.jsonに登録する。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/block-template.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/post-check-template.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/stop-template.sh"
}
]
}
]
}
}
matcherはツール名でフィルタする。"Bash"ならBashツールだけ、""(空文字)なら全ツールに反応する。Edit、Write、Readなど任意のツール名を指定できる。
テスト方法
hookを登録する前に、ターミナルで単体テストできる。
# PreToolUseのテスト(rm -rfをブロックするか?)
echo '{"tool_input":{"command":"rm -rf /"}}' | bash ~/.claude/hooks/block-template.sh
echo $?
# → 2(ブロック)
# 安全なコマンドは通るか?
echo '{"tool_input":{"command":"ls -la"}}' | bash ~/.claude/hooks/block-template.sh
echo $?
# → 0(許可)
# PostToolUseのテスト(壊れたJSONを検知するか?)
echo '{"broken":}' > /tmp/test.json
echo '{"tool_input":{"file_path":"/tmp/test.json"}}' | bash ~/.claude/hooks/post-check-template.sh
echo $?
# → 1(警告)
echo JSON | bash hook.sh; echo $?のパターンを覚えておけば、どんなhookでもテストできる。
hookを自動生成する
テンプレートのコピペで足りないとき。「やりたいこと」を日本語で伝えると、hookを自動生成してくれるツールがある。
npx cc-safe-setup --create "git commitする前にlintを実行"
442個のサンプルhookから似たものを探してインストールもできる:
# サンプル一覧
npx cc-safe-setup --examples
# 特定のサンプルをインストール
npx cc-safe-setup --install-example credential-exfil-guard
hookの設計思想から実戦パターンまで体系的にまとめた本: Claude Code 実戦ガイド
テンプレートは4つ。PreToolUseのブロック、PreToolUseの自動承認、PostToolUseの検証、Stopのログ記録。この4パターンを押さえておけば、大抵の要件はカバーできる。まずは1つコピペして動かしてみてほしい。
上の4つのテンプレートは、渡ってきた tool_input を見てその場で判断する「状態を持たない」作りなので安全だ。だが、SessionStart でセッションごとの状態(一時ディレクトリや認可の方針)を用意し、PreToolUse でその状態を見て可否を判断する hook に拡張すると、思わぬ落とし穴がある。
/clear を使うと内部でセッションの ID(session_id)が新しく振り直されるのに、SessionStart は再実行されない、という挙動が報告されている(#70606)。すると PreToolUse は新しい ID から組み立てた場所を見にいくが、そこに状態は無い。ここで hook が「見つからない=該当なし=通す」と書いてあると、安全 hook が /clear の後に何も止めなくなる。しかもエラーは出ない。
直し方の原則は一つ、状態が見つからないときは通す側でなく止める側に倒す(fail closed):
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty')
if [ ! -d "/tmp/claude-session-${SID}" ]; then
echo "セッションの安全の状態が見つかりません(/clear 後の可能性)。" >&2
exit 2 # 通さず止める。守りが黙って外れるより、止まって気づける方が安全
fi
そもそも、上のテンプレートのように状態を持たず tool_input だけで判断する hook を選ぶのが、いちばん壊れにくい。詳しい機構と3つの直し方は事故防止本の第51章で扱っている。
Claude Code は毎月のように仕様が変わり、費用の膨らみ方も事故の新種も毎月出ます。「先月から何が変わったか・今すぐ直すべき設定や貼るべき hook はどれか」を毎月15日ごろ短く受け取りたい人には、Claude Code 事故まとめ(無料・月次)もあります(本は腰を据えて読む手引き、便りは毎月の鮮度で走らせる運用の線、という住み分けです)。
毎月、その月に報告された Claude Code の事故(データ消失・暴走・無断課金など)と設定での防ぎ方を一か所にまとめています → 2026年6月 Claude Code 事故まとめ(無料・毎月更新)。新しい号の通知は note のゆるくさ のフォローで届きます(無料)。
ほかにも、800時間の運用データから、トークン消費の削減・複数ベンダー(Claude / Codex / Gemini / Copilot)の並行運用・サブエージェントの沈黙の失敗対策など、テーマ別の手引きを公開しています。気になる人は著者の本の一覧から、価格と評価を見て選べます。