はじめに
Claude Codeには3つの自動化の基本機能があります。Hooks(イベント駆動)、Scheduler(時間駆動)、Skills(ワークフロー定義)です。
それぞれ単体でも便利ですが、本当の力を発揮するのは組み合わせたときです。
- Hooksだけだと「検知はできるが、定期実行ができない」
- Schedulerだけだと「定期実行はできるが、イベントに反応できない」
- Skillsだけだと「手順は定義できるが、トリガーがない」
3つを組み合わせると、検知→判断→実行→報告が自律的に連鎖する仕組みが作れます。この記事では、この組み合わせを「イベント駆動型ワークフロー自動化」と呼び、設計パターンと実装例を解説します。
すべてのサンプルはコピペで動く設定ファイルとして掲載しています。
3つの基本機能を30秒で理解する
| 機能 | 役割 | トリガー | 設定場所 |
|---|---|---|---|
| Hooks | イベント検知と制御 | Claude Codeの25種のイベント |
settings.json または SKILL.md内 |
| Scheduler | 定期実行 | cron式 / インターバル |
/loop、Desktop、Cloud |
| Skills | ワークフロー定義 | 手動 or 他の機能から呼び出し | .claude/skills/*/SKILL.md |
単体で使うと「便利なツール」です。組み合わせると「自律的なシステム」になります。
イベント駆動型ワークフロー自動化の4つのパターン
パターン1: Scheduler → Skill → Hook(定期収集+異常検知)
最も実用的なパターンです。Schedulerが定期的にデータを収集し、結果ファイルをHookが監視して異常を検知します。
まず、PR状況を収集するSkillを定義します。
---
name: pr-collector
description: レビュー待ちPRを収集してJSONファイルに書き出す
user-invocable: true
---
# PR収集
## 手順
1. `gh pr list --search "review-requested:@me"` でレビュー待ちPRを取得する
2. 各PRについて以下の情報を収集する
- リポジトリ名、PR番号、タイトル、作成者
- 作成日時から経過日数を計算する
3. 結果を以下のJSON形式で `~/.claude/data/pr-status.json` に書き出す
## 出力フォーマット
{
"updated_at": "2026-03-31T09:00:00+09:00",
"total": 5,
"stale_count": 2,
"prs": [
{
"repo": "myapp",
"number": 234,
"title": "DB接続プール最適化",
"author": "tanaka",
"days_old": 5,
"is_stale": true
}
]
}
## 注意
- PRが0件の場合も空配列で正常に書き出す
- gh CLIの認証エラーが出た場合はエラー内容をファイルに記録する
次に、このファイルの変更を監視するHookを設定します。
{
"hooks": {
"FileChanged": [
{
"matcher": "pr-status\\.json$",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/check-stale-prs.py",
"timeout": 30
}
]
}
]
}
}
Hookから呼び出されるスクリプトです。
#!/usr/bin/env python3
"""放置PRを検知してSlack通知する。"""
import json
import sys
import subprocess
from pathlib import Path
data_path = Path.home() / ".claude" / "data" / "pr-status.json"
try:
data = json.loads(data_path.read_text())
except (FileNotFoundError, json.JSONDecodeError):
sys.exit(0)
stale_prs = [pr for pr in data.get("prs", []) if pr.get("is_stale")]
if not stale_prs:
sys.exit(0)
lines = [f"- {pr['repo']}#{pr['number']} {pr['title']} ({pr['days_old']}日経過)"
for pr in stale_prs]
message = f"放置PRが{len(stale_prs)}件あります\n" + "\n".join(lines)
# Slack Webhook経由で通知(SLACK_WEBHOOK_URLは環境変数で設定)
import os
webhook = os.environ.get("SLACK_WEBHOOK_URL")
if webhook:
payload = json.dumps({"text": message})
subprocess.run(
["curl", "-s", "-X", "POST", "-H", "Content-Type: application/json",
"-d", payload, webhook],
capture_output=True,
)
print(json.dumps({"message": f"Notified {len(stale_prs)} stale PRs"}))
最後に、Schedulerで毎朝実行を登録します。
/loop 24h /pr-collector
またはDesktopスケジューラーで 0 9 * * 1-5(平日9時)に設定します。
この構成のポイントは、Schedulerは収集だけ、判断はHookに委ねている点です。収集ロジックと通知ロジックが分離されているため、それぞれ独立して改善できます。
パターン2: Hook → Skill(イベント駆動ワークフロー)
特定のイベントを検知して、自動的にSkillを実行するパターンです。Skill内にHookを定義することで、そのSkillがアクティブな間だけHookが有効になります。
実用例として、git commitのたびにCHANGELOGを自動更新する仕組みを作ります。
---
name: auto-changelog
description: コミット時にCHANGELOGを自動更新する
user-invocable: false
hooks:
PostToolUse:
- matcher: "Bash"
hooks:
- type: prompt
prompt: |
ユーザーがgit commitを実行しましたか?
実行していればyesと回答してください。
---
# CHANGELOG自動更新
直前のgit commitの内容をもとに、CHANGELOG.mdを更新してください。
## 手順
1. `git log -1 --format="%H %s"` で直前のコミット情報を取得する
2. コミットメッセージからカテゴリを判定する
- `feat:` → Added
- `fix:` → Fixed
- `refactor:` → Changed
- `docs:` → Documentation
3. CHANGELOG.md の最新セクションに追記する
4. 追記だけ行い、既存のエントリは変更しない
type: prompt はLLMに判定を委ねるハンドラーです。Bashツール実行後に「git commitだったか?」を判定し、yesの場合のみSkillの本体が実行されます。commitではないBash実行(例: lsやnpm install)では何も起きません。
Skill内のHook定義(フロントマターの hooks:)は、そのSkillがアクティブな間だけ有効です。プロジェクト全体のsettings.jsonに書くHookと異なり、スコープが限定されるため副作用を抑えられます。
パターン3: Scheduler → Skill + Skill内Hook(自己監視する自動化)
Schedulerで定期実行するSkillの内部にHookを持たせ、実行品質を自己監視するパターンです。
日次レポートの生成を例にします。
---
name: daily-report
description: 日次レポートを生成し、品質を自己検証する
user-invocable: true
hooks:
Stop:
- hooks:
- type: command
command: "python3 ~/.claude/scripts/validate-report.py"
---
# 日次レポート生成
## 手順
1. Google Calendarから今日の予定を取得する
2. `~/daily/` から前日のレポートを読み、未完了タスクを引き継ぐ
3. 以下のフォーマットで `~/daily/{YYYY-MM-DD}.md` に出力する
## 出力フォーマット
# {日付} デイリーレポート
## 今日の予定
| 時間 | 予定 | メモ |
|------|------|------|
## 引き継ぎタスク
- [ ] {前日から引き継いだタスク}
## 本日のTODO
- [ ] {新規タスク}
## エッジケース
- 予定が0件: 「予定なし - 集中作業日」と記載する
- 前日レポートが存在しない: 引き継ぎセクションを「(前日レポートなし)」とする
- 土日祝: 「休日」と記載して終了する
品質検証スクリプトです。
#!/usr/bin/env python3
"""日次レポートの品質を検証する。"""
import json
import sys
from datetime import date
from pathlib import Path
today = date.today().isoformat()
report_path = Path.home() / "daily" / f"{today}.md"
errors = []
if not report_path.exists():
errors.append("レポートファイルが生成されていません")
else:
content = report_path.read_text()
if len(content) < 50:
errors.append(f"内容が短すぎます({len(content)}文字)")
if "## 今日の予定" not in content:
errors.append("「今日の予定」セクションがありません")
if "## 本日のTODO" not in content:
errors.append("「本日のTODO」セクションがありません")
if errors:
result = {"status": "error", "errors": errors}
print(json.dumps(result, ensure_ascii=False))
sys.exit(2) # exit 2 = ブロッキングエラー
print(json.dumps({"status": "ok"}, ensure_ascii=False))
StopイベントのHookでレポート生成直後に検証が走ります。検証に失敗した場合(exit code 2)、Claudeは停止せずに会話を継続し、エラー内容をもとに出力の修正を試みます。
Schedulerでの登録は以下の通りです。
Desktop: 0 8 * * 1-5(平日8時)
プロンプト: /daily-report を実行してください
Skillが自分の出力を検証して、問題があれば自己修正する。 これが単体のSchedulerでは実現できない、イベント駆動型ワークフロー自動化ならではの振る舞いです。
パターン4: SessionStart Hook → Scheduler登録(セッション連動型)
セッション開始時にHookで当日の予定を確認し、必要なSchedulerを動的に登録するパターンです。
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/session-setup.py"
}
]
}
]
}
}
#!/usr/bin/env python3
"""セッション開始時に当日の作業コンテキストを注入する。"""
import json
import os
from datetime import date
from pathlib import Path
today = date.today()
weekday = today.weekday() # 0=月曜
context = {"suggestions": []}
# 平日のみ提案
if weekday < 5:
daily_path = Path.home() / "daily" / f"{today.isoformat()}.md"
if not daily_path.exists():
context["suggestions"].append(
"/daily-report がまだ実行されていません。実行を推奨します。"
)
pr_path = Path.home() / ".claude" / "data" / "pr-status.json"
if pr_path.exists():
try:
pr_data = json.loads(pr_path.read_text())
stale = pr_data.get("stale_count", 0)
if stale > 0:
context["suggestions"].append(
f"放置PRが{stale}件あります。/pr-collector で最新状況を確認できます。"
)
except json.JSONDecodeError:
pass
# 金曜日は週次レポートを提案
if weekday == 4:
context["suggestions"].append(
"金曜日です。週次振り返りレポートの作成を推奨します。"
)
if context["suggestions"]:
# 環境ファイル経由でClaudeにコンテキスト注入
env_file = os.environ.get("CLAUDE_ENV_FILE")
if env_file:
Path(env_file).write_text(
"CLAUDE_SESSION_CONTEXT="
+ json.dumps(context, ensure_ascii=False)
)
print(json.dumps(context, ensure_ascii=False))
セッションを開くだけで、その日に必要な作業の提案を受け取れます。前のパターンで生成されたデータを参照しているのがポイントです。パターン1のPR収集結果を、パターン4が活用しています。
パターンを組み合わせた全体アーキテクチャ
4つのパターンを組み合わせると、以下のような自律的なワークフローが構成されます。
データの流れに注目してください。各コンポーネントが独立して動きながら、ファイルを介して連携しています。Scheduler → Skill → ファイル出力 → Hook検知 → 次のアクションという連鎖が、設計のどこにも「呼び出し」のハードコーディングなしに実現されています。
設計原則
イベント駆動型ワークフロー自動化を設計するときに守るべき原則をまとめます。
1. データの受け渡しはファイル経由にする
コンポーネント間の連携には、JSONファイルを共有ストレージとして使います。
Skill → JSON書き出し → Hook(FileChanged)で検知 → 次の処理
直接的なAPI呼び出しで連携しようとすると、障害時の切り分けが困難になります。ファイル経由にすると、途中のデータをいつでも人間が確認・修正できます。
2. 検知と判断と実行を分離する
1つのSkillに「収集・判断・通知」を全部詰め込むと、どこで問題が起きたか分からなくなります。
# 分離した設計
Skill: データ収集 → ファイル書き出し
Hook: ファイル監視 → 条件判定
Script: 通知送信
# 避けたい設計
Skill: データ収集 → 条件判定 → 通知送信(全部入り)
3. exit codeで制御フローを表現する
Hookのcommandハンドラーは、exit codeで結果を伝えます。
| exit code | 意味 | Claudeの挙動 |
|---|---|---|
| 0 | 成功 | stdout JSONを処理する |
| 2 | ブロッキングエラー | イベントに応じて操作をブロックまたは会話を継続して修正を試みる |
| その他 | 非ブロッキングエラー | 警告として記録し、処理を続行する |
exit code 2の挙動はイベントによって異なります。PreToolUseではツール実行をブロックし、Stopでは停止を防いで会話を継続します。SessionStartやNotificationではエラー表示のみです。
品質検証にはexit code 2が有効です。Stopイベントで使うと、Claudeが停止せずにエラー内容を確認し、自動で修正を試みます。
4. Skill内Hookでスコープを限定する
settings.jsonに書いたHookはすべてのセッションで有効です。特定のワークフローでのみ必要なHookは、Skill内のフロントマターで定義します。
# Skill内Hook: auto-changelogがアクティブな間だけ有効
hooks:
PostToolUse:
- matcher: "Bash"
hooks:
- type: prompt
prompt: "git commitが実行されましたか?"
グローバルHookの数を最小限に保つことで、想定外の副作用を防げます。
5. 段階的に育てる
いきなり4パターン全部を構築しようとしないでください。以下の順番で1つずつ追加します。
- まずSkillを1つ作り、手動で実行して出力を確認する
- 安定したらSchedulerに登録する
- データファイルが安定したらHookを追加する
- SessionStart Hookで全体を統合する
フォルダ構成
イベント駆動型ワークフロー自動化に必要なファイルの全体像です。
~/.claude/
├── settings.json # グローバルHook定義
├── scripts/
│ ├── check-stale-prs.py # パターン1: PR異常検知
│ ├── validate-report.py # パターン3: レポート検証
│ └── session-setup.py # パターン4: セッション初期化
├── skills/
│ ├── pr-collector/
│ │ └── SKILL.md # パターン1: PR収集
│ ├── daily-report/
│ │ └── SKILL.md # パターン3: 日次レポート
│ └── auto-changelog/
│ └── SKILL.md # パターン2: CHANGELOG更新
└── data/
└── pr-status.json # パターン1: PR状況データ
~/daily/
├── 2026-03-31.md # パターン3: 日次レポート
└── ...
よくある落とし穴
Hook内で重い処理を実行しない
Hookのデフォルトタイムアウトはcommandタイプで600秒、promptタイプで30秒です。API呼び出しを含む処理はタイムアウトの設定を意識してください。
Hookは軽量な判定に徹して、重い処理はSkill側に寄せるのが安定します。
FileChanged Hookの連鎖に注意する
Hook AがファイルXを更新 → FileChangedでHook Bが発火 → Hook BがファイルXを更新 → 無限ループ。この連鎖を防ぐには、監視対象のファイルと更新対象のファイルを分離します。
# 安全な設計
Skill → data/pr-status.json に書き出し
Hook → data/pr-status.json を読み取り → notifications/alert.log に書き出し
# 危険な設計
Skill → data/report.md に書き出し
Hook → data/report.md を読み取り → data/report.md を更新(ループ)
Cloudスケジューラーではローカルファイルが使えない
Cloud環境ではローカルファイルシステムにアクセスできません。パターン1〜4はDesktop or CLIが前提です。Cloudに昇格させる場合は、ファイル出力をNotionやGoogle Driveへの保存に置き換える必要があります。
Cloudスケジューラーの最小実行間隔は1時間です。リアルタイム性が必要な処理にはDesktopスケジューラーか/loopを使ってください。
まとめ
| 機能 | 単体での限界 | 組み合わせで得られる力 |
|---|---|---|
| Hooks | 検知できるが、定期実行できない | Schedulerと組み合わせて時間+イベントの両トリガーに対応 |
| Scheduler | 時間で実行できるが、結果に反応できない | Hookと組み合わせて異常検知・自己修正を追加 |
| Skills | 手順を定義できるが、トリガーがない | Hook/Schedulerをトリガーに、かつSkill内Hookで自己検証 |
イベント駆動型ワークフロー自動化の本質は、コンポーネントが独立して動きながら、ファイルを介して自然に連携することです。1つのSkillが倒れても他は動き続けます。
まずはパターン3(Scheduler → Skill + 自己検証Hook)から始めてみてください。Skillの出力を自動で検証する仕組みを1つ入れるだけで、自動化の信頼性が大きく変わります。
参考リンク