はじめに
前回の記事「【完全ガイド】Claude Code Hooks で開発ワークフローを自動化する ── 全14イベント徹底解説」では、Hooks の基本概念と全14イベントを解説した。
イベントの仕様は理解できたが、実際にまとまったものを作ろうとすると「どう設計すればいいか」で手が止まる。筆者自身がそうだった。
そこで、Claude Code のセッションをリアルタイム監視するダッシュボードを実際に開発した。その過程で見えてきた Hooks の設計パターンと実装上の注意点を本記事にまとめる。
各パターンは独立しているので、自分のプロジェクトに必要なものだけ拾い読みしてほしい。
作ったもの: Claude Code Dashboard
Claude Code のセッション中に発生するイベントを収集し、ブラウザでリアルタイム表示するダッシュボードを開発した。ツール使用統計、タスク進捗、変更ファイル一覧などを可視化できる。
GitHub: nogataka/claude-dashboard
使用した Hook イベントは次の5つ。
-
PreToolUse/PostToolUse-- ツール呼び出しの記録 -
Stop-- 応答完了の検知 -
SessionStart-- セッション開始の記録 -
UserPromptSubmit-- ユーザー入力の記録
以降、このダッシュボード開発で得たパターンを順に紹介する。
パターン 1: 全イベント横断キャプチャ
課題
複数のイベントに対してそれぞれ別のスクリプトを書くと、処理の重複が増え保守が大変になる。
解決策
1つのスクリプトを全イベントに共通で設定する。
イベントの種別は stdin で受け取る JSON の hook_event_name フィールドで判別できる。スクリプト側で分岐すればよい。
{
"hooks": {
"PreToolUse": [
{ "command": "cat | bash /path/to/hook/capture-event.sh" }
],
"PostToolUse": [
{ "command": "cat | bash /path/to/hook/capture-event.sh" }
],
"Stop": [
{ "command": "cat | bash /path/to/hook/capture-event.sh" }
],
"SessionStart": [
{ "command": "cat | bash /path/to/hook/capture-event.sh" }
],
"UserPromptSubmit": [
{ "command": "cat | bash /path/to/hook/capture-event.sh" }
]
}
}
イベントの流れ
メリット
- スクリプトの修正が1箇所で済む
- ログフォーマットが統一される
- 新しいイベントの追加が settings.json の1行で完了する
応用
この「1スクリプト多イベント」パターンは、以下の用途にそのまま使える。
| 用途 | 概要 |
|---|---|
| ログ収集 | 全イベントを時系列で記録する |
| 監査証跡 | 誰がいつ何を実行したかを残す |
| デバッグ | イベントの発火順序を確認する |
| メトリクス | イベント数を集計して傾向を見る |
パターン 2: stdin パイプでの JSON 受け取り
cat | bash script.sh の意味
settings.json の command に書く cat | bash script.sh は、2つの処理を繋いでいる。
-
cat-- stdin から Hook の入力 JSON を読み取る -
| bash script.sh-- その内容をパイプでスクリプトに渡す
Claude Code は Hook の入力データを stdin に JSON として流す。この cat | がないとスクリプト側で stdin を受け取れない。
stdin からの JSON 読み取り
スクリプトの冒頭で、まず stdin を変数に格納する。
#!/bin/bash
# stdin から JSON を読み取る(最初に必ず行う)
INPUT="$(cat)"
# jq が使える場合
if command -v jq >/dev/null 2>&1; then
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"
HOOK_EVENT="$(echo "$INPUT" | jq -r '.hook_event_name // empty')"
CWD="$(echo "$INPUT" | jq -r '.cwd // empty')"
else
# jq がない環境でのフォールバック
SESSION_ID="$(echo "$INPUT" | grep -o '"session_id"\s*:\s*"[^"]*"' \
| head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
HOOK_EVENT="$(echo "$INPUT" | grep -o '"hook_event_name"\s*:\s*"[^"]*"' \
| head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
CWD="$(echo "$INPUT" | grep -o '"cwd"\s*:\s*"[^"]*"' \
| head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
fi
注意点
INPUT="$(cat)" は必ずスクリプトの冒頭で実行する。stdin は一度しか読めないため、後から読み直すことはできない。
jq なしのフォールバックは応急処置として使う。ネストされた JSON やエスケープ文字を含む値では正しく動作しない可能性がある。本格運用では jq のインストールを推奨する。
パターン 3: セッション単位のデータ分離
なぜセッションで分離するのか
Claude Code は複数のターミナルで同時に実行できる。全イベントを1つのファイルに書き込むと、異なるセッションのデータが混在し分析が困難になる。
実装パターン
session_id をファイル名に使い、セッションごとにファイルを分離する。
STATE_DIR="${CLAUDE_DASHBOARD_DIR:-$HOME/.claude/dashboard-events}"
mkdir -p "$STATE_DIR"
# セッションIDでファイルを分離
LOG_FILE="$STATE_DIR/${SESSION_ID}.jsonl"
ファイル構造のイメージ
~/.claude/dashboard-events/
abc123-def456.jsonl # セッション A のイベント
ghi789-jkl012.jsonl # セッション B のイベント
mno345-pqr678.jsonl # セッション C のイベント
複数セッション同時実行での安全性
セッションごとにファイルが分かれるため、複数の Claude Code が同時に動いても書き込みが衝突しない。JSONL 形式(後述)との組み合わせで並行書き込みの安全性がさらに高まる。
パターン 4: JSONL による追記型ログ
なぜ JSON 配列ではなく JSONL か
イベントログの保存形式として JSON 配列と JSONL(JSON Lines)の2つが考えられる。
| 形式 | 書き込み方法 | 並行安全性 | 読み取り |
|---|---|---|---|
JSON 配列 [{...}, {...}]
|
ファイル全体を読み書き | 低い | パース必須 |
| JSONL(1行1オブジェクト) |
>> で追記 |
高い | 1行ずつ処理可 |
JSONL は各行が独立した JSON オブジェクトなので、>> での追記だけで済む。ファイル全体をロックする必要がない。
実装
# タイムスタンプを付与して JSONL として追記
TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
if command -v jq >/dev/null 2>&1; then
echo "$INPUT" | jq -c --arg ts "$TS" '. + {_ts: $ts}' \
>> "$STATE_DIR/${SESSION_ID}.jsonl"
else
# jq なしの場合: 末尾の } を置換してタイムスタンプを付与
echo "$INPUT" | sed "s/}$/,\"_ts\":\"$TS\"}/" \
>> "$STATE_DIR/${SESSION_ID}.jsonl"
fi
ポイント
-
jq -cで1行にコンパクト化する(JSONL の要件) -
_tsフィールドでイベント受信時刻を付与する -
>>による追記はアトミック性が高い(1行が短い場合)
出力される JSONL の例
{"session_id":"abc123","hook_event_name":"SessionStart","cwd":"/home/user/project","_ts":"2026-02-27T10:00:00Z"}
{"session_id":"abc123","hook_event_name":"PreToolUse","tool_name":"Read","_ts":"2026-02-27T10:00:05Z"}
{"session_id":"abc123","hook_event_name":"PostToolUse","tool_name":"Read","_ts":"2026-02-27T10:00:06Z"}
{"session_id":"abc123","hook_event_name":"PostToolUse","tool_name":"Edit","_ts":"2026-02-27T10:00:15Z"}
{"session_id":"abc123","hook_event_name":"Stop","_ts":"2026-02-27T10:00:30Z"}
パターン 5: PostToolUse からツール使用統計を取る
PostToolUse イベントの構造
PostToolUse の入力 JSON には以下のフィールドが含まれる。
| フィールド | 内容 |
|---|---|
tool_name |
ツール名(Read, Edit, Bash など) |
tool_input |
ツールへの入力パラメータ |
tool_response |
ツールの実行結果 |
ツール別の集計
JSONL に記録した PostToolUse イベントを集計すると、Claude がどのツールをどれだけ使ったかが分かる。
interface ToolStats {
[toolName: string]: number;
}
function aggregateToolUsage(events: HookEvent[]): ToolStats {
const tools: ToolStats = {};
let errors = 0;
for (const ev of events) {
if (ev.hook_event_name === 'PostToolUse' && ev.tool_name) {
tools[ev.tool_name] = (tools[ev.tool_name] || 0) + 1;
// エラーの検出
if (ev.tool_response?.error || ev.tool_response?.type === 'error') {
errors++;
}
}
}
return tools;
}
変更ファイルの追跡
tool_input の中身はツールによって異なる。ファイル操作系のツールでは file_path や path フィールドが含まれる。
function trackModifiedFiles(events: HookEvent[]): string[] {
const files = new Set<string>();
for (const ev of events) {
if (ev.hook_event_name !== 'PostToolUse') continue;
const input = ev.tool_input;
if (!input) continue;
// Edit, Write, Read などのファイルパスを収集
const filePath = input.file_path || input.path;
if (filePath && typeof filePath === 'string') {
files.add(filePath);
}
}
return Array.from(files);
}
統計データの活用例
こうした統計は Claude の作業パターンの理解に役立つ。Read が多ければ調査中心のセッション、Edit が多ければ実装中心のセッションだと判断できる。
パターン 6: タスクとユーザー質問のトラッキング
Claude Code のタスク管理ツール
Claude Code には TaskCreate と TaskUpdate という内部ツールがある。Claude がタスクを作成・更新するたびに PostToolUse イベントが発火する。
タスクの状態遷移を追跡する
PostToolUse で tool_name が TaskCreate や TaskUpdate のイベントを監視すると、タスクの進捗を追跡できる。
interface TaskInfo {
id: string;
subject: string;
status: 'pending' | 'in_progress' | 'completed';
updatedAt: string;
}
function trackTasks(events: HookEvent[]): TaskInfo[] {
const tasks = new Map<string, TaskInfo>();
for (const ev of events) {
if (ev.hook_event_name !== 'PostToolUse') continue;
if (ev.tool_name === 'TaskCreate') {
const input = ev.tool_input;
if (input?.subject) {
tasks.set(input.subject, {
id: input.subject,
subject: input.subject,
status: 'pending',
updatedAt: ev._ts || '',
});
}
}
if (ev.tool_name === 'TaskUpdate') {
const input = ev.tool_input;
if (input?.taskId && input?.status) {
const existing = tasks.get(input.taskId);
if (existing) {
existing.status = input.status;
existing.updatedAt = ev._ts || '';
}
}
}
}
return Array.from(tasks.values());
}
タスクの状態遷移
ユーザー質問の記録
UserPromptSubmit イベントを記録しておくと、セッション中にユーザーが何を指示したかの履歴が残る。
同様に、PostToolUse で tool_name が AskUser のイベントを捕捉すると、Claude からユーザーへの質問も記録できる。タスクの文脈とユーザーの指示を合わせて見ることで、セッション全体の流れを把握しやすくなる。
パターン 7: cwd ベースのプロジェクト分類
cwd フィールドの活用
すべての Hook イベントの入力 JSON には cwd(Current Working Directory)フィールドが含まれる。これをプロジェクトの識別子として使える。
CWD="$(echo "$INPUT" | jq -r '.cwd // empty')"
PROJECT_NAME="$(basename "$CWD")"
マルチプロジェクト対応
複数のリポジトリで同時に Claude Code を使う場合、cwd でプロジェクトを自動分類できる。
ダッシュボードでの実装
ダッシュボードでは全セッションの cwd を収集してユニークなプロジェクト一覧を生成し、タブで切り替えられるようにした。
function getProjects(sessions: SessionData[]): string[] {
const projects = new Set<string>();
for (const session of sessions) {
for (const event of session.events) {
if (event.cwd) {
projects.add(event.cwd);
}
}
}
return Array.from(projects);
}
cwd はフルパスで長くなるため、表示時には basename でディレクトリ名だけにすると見やすい。
パターン 8: async: true でパフォーマンスに影響しない Hook
async: true とは
Hook の設定に "async": true を指定すると、Hook がバックグラウンドで実行される。Claude Code は Hook の完了を待たずに次の処理に進む。
{
"hooks": {
"PostToolUse": [
{
"command": "cat | bash /path/to/capture-event.sh",
"async": true
}
]
}
}
使うべきケース
| ケース | async の推奨 |
|---|---|
| ログ記録、メトリクス収集 | true |
| 外部 API への通知送信 | true |
| ツール実行の許可/拒否判定 | false |
| フォーマッタの自動実行 | false |
判断基準はシンプル。「結果で Claude の動作を制御する必要があるか」 で決める。
ダッシュボードのようなイベント記録用途では Hook の結果が Claude の動作に影響しない。このケースでは async: true で Claude の処理速度に影響を与えないようにする。
注意点
async: true の Hook では、exit code による制御(exit 2 でのブロック)が機能しない。JSON 出力による permissionDecision なども無視される。制御が必要な Hook には async: true を使わないこと。
全イベント非同期キャプチャの設定例
ダッシュボード用途に適した設定を示す。全イベントを非同期でキャプチャする。
{
"hooks": {
"PreToolUse": [
{
"command": "cat | bash /path/to/hook/capture-event.sh",
"async": true
}
],
"PostToolUse": [
{
"command": "cat | bash /path/to/hook/capture-event.sh",
"async": true
}
],
"Stop": [
{
"command": "cat | bash /path/to/hook/capture-event.sh",
"async": true
}
],
"SessionStart": [
{
"command": "cat | bash /path/to/hook/capture-event.sh",
"async": true
}
],
"UserPromptSubmit": [
{
"command": "cat | bash /path/to/hook/capture-event.sh",
"async": true
}
]
}
}
Tips: よくあるトラブルと対策
.bashrc / .zshrc の echo が JSON を壊す
Hook スクリプトは bash で実行されるため、.bashrc が読み込まれる場合がある。.bashrc 内の echo やプロンプト設定が stdout に出力されると、Hook の JSON 出力に混入してパースエラーになる。
対策:
#!/bin/bash --norc
# --norc で .bashrc の読み込みをスキップする
INPUT="$(cat)"
# ... 以降の処理
もしくは settings.json 側で bash --norc を明示する。
{
"command": "cat | bash --norc /path/to/capture-event.sh"
}
jq がインストールされていない環境
CI 環境やコンテナ内では jq がない場合がある。パターン 2 で紹介した grep/sed によるフォールバックを入れておくと安全。
ただしフォールバックには限界がある。プロジェクトの要件に応じて以下の方法も検討する。
| 方法 | 長所 | 短所 |
|---|---|---|
| grep/sed フォールバック | 依存なし | 複雑な JSON に非対応 |
| Python ワンライナー | 多くの環境に入っている | 起動が少し遅い |
| jq の事前インストール | 確実 | セットアップが必要 |
Python を使ったフォールバックの例を示す。
if command -v jq >/dev/null 2>&1; then
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // empty')"
elif command -v python3 >/dev/null 2>&1; then
SESSION_ID="$(echo "$INPUT" | python3 -c \
"import sys,json; print(json.load(sys.stdin).get('session_id',''))")"
else
SESSION_ID="$(echo "$INPUT" | grep -o '"session_id"\s*:\s*"[^"]*"' \
| head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
fi
JSONL ファイルが肥大化した場合
長時間のセッションや大量の Hook イベントで JSONL ファイルが大きくなることがある。
対策 1: セッション開始時に古いファイルを削除する
#!/bin/bash --norc
# 7日以上前のイベントファイルを削除
find "${CLAUDE_DASHBOARD_DIR:-$HOME/.claude/dashboard-events}" \
-name "*.jsonl" -mtime +7 -delete
これを SessionStart Hook に設定すると、セッション開始時に古いログが自動で掃除される。
対策 2: ログローテーション
LOG_FILE="$STATE_DIR/${SESSION_ID}.jsonl"
MAX_SIZE=10485760 # 10MB
# ファイルサイズが上限を超えたらローテーション
if [ -f "$LOG_FILE" ]; then
FILE_SIZE="$(stat -f%z "$LOG_FILE" 2>/dev/null \
|| stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)"
if [ "$FILE_SIZE" -gt "$MAX_SIZE" ]; then
mv "$LOG_FILE" "${LOG_FILE}.$(date +%s).bak"
fi
fi
Hook スクリプトのデバッグ方法
Hook が期待どおりに動かないとき、以下の手順で調査する。
手順 1: stdin のダンプ
まず Hook に渡される JSON の中身を確認する。
#!/bin/bash --norc
INPUT="$(cat)"
echo "$INPUT" >> /tmp/hook-debug.log
手順 2: verbose モードでの実行
Claude Code を --verbose フラグ付きで起動すると Hook の実行結果が表示される。
claude --verbose
手順 3: 単体テスト
Hook スクリプトを直接実行してテストする。
echo '{"session_id":"test-123","hook_event_name":"PostToolUse","tool_name":"Edit","cwd":"/home/user/project"}' \
| bash /path/to/capture-event.sh
デバッグ用の Hook は本番運用前に必ず削除するか、async: true にして影響を最小限にする。
まとめ
本記事で紹介した8つのパターンを一覧にまとめる。
| パターン | 概要 | 主な用途 |
|---|---|---|
| 1. 全イベント横断キャプチャ | 1スクリプトで全イベントを処理 | ログ収集、監視 |
| 2. stdin パイプでの JSON 受け取り |
cat | bash で入力を安全に処理 |
全 Hook 共通 |
| 3. セッション単位のデータ分離 | session_id でファイルを分離 | 並行実行対応 |
| 4. JSONL による追記型ログ | 1行1オブジェクトで追記 | 構造化ログ |
| 5. PostToolUse からツール統計 | tool_name で集計 | 使用分析 |
| 6. タスクとユーザー質問の追跡 | TaskCreate/Update を監視 | 進捗管理 |
| 7. cwd ベースのプロジェクト分類 | cwd でプロジェクト判別 | マルチプロジェクト |
| 8. async: true の活用 | 非同期でパフォーマンス維持 | 記録系 Hook |
これらのパターンはダッシュボードに限らず、さまざまな Hooks プロジェクトに応用できる。自分のワークフローに合ったパターンを組み合わせて活用してほしい。
関連リンク
- GitHub: nogataka/claude-dashboard
- シリーズ記事(Part 1): 【完全ガイド】Claude Code Hooks で開発ワークフローを自動化する ── 全14イベント徹底解説