ストップフック
ストップフックはテストが失敗、ビルドが壊れている、またはlintがエラー状態のうちはClaude Codeがターンを終了するのをブロックする。4つの強制パターンとループ保護のセーフガード。
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
問題: Claudeがレスポンスをまとめるが、作業は実際には終わっていない。テストはまだ失敗している。ファイルは半分しか書けていない。「終わった?」と聞くと「はい」と答えるが、ビルドはレッドのままだ。
即効策: このストップフックを追加すれば、テストがグリーンになるまでClaudeはターンを終了できなくなる:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/test-gate.py"
}
]
}
]
}
}#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
# CRITICAL: Prevent infinite loops
if input_data.get('stop_hook_active', False):
sys.exit(0)
# Run tests
result = subprocess.run(['npm', 'test'], capture_output=True, timeout=60)
if result.returncode != 0:
output = {
"decision": "block",
"reason": "Tests are failing. Fix them before completing."
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)これ以降、Claudeはテストスイートが失敗した状態でターンを閉じることが文字通りできなくなる。
ストップフックの仕組み
Claudeがレスポンスを終了しようとするたびにこのフックが発火する。起こりうることは3つ:
- 停止を許可 - Exit 0で終了し、ターンが正常に終わる。
- 停止をブロック -
{"decision": "block", "reason": "..."}を返すと、Claudeが継続する。 - バリデーションを実行 - テスト、チェック、任意のスクリプトを起動する。
ペイロード
{
"session_id": "uuid-string",
"stop_hook_active": false,
"transcript_path": "/path/to/transcript.jsonl"
}stop_hook_active に注意する。trueの場合、Claudeはすでに以前のブロックによる強制継続状態にある。このフラグを見落とすと無限ループになる。
パターン1: テストゲート
すべてのテストが通過するまでターンを開いたままにする:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
result = subprocess.run(
['npm', 'test', '--passWithNoTests'],
capture_output=True,
timeout=120
)
if result.returncode != 0:
# Extract last 10 lines of test output for context
stderr = result.stderr.decode()[-500:] if result.stderr else ""
print(json.dumps({
"decision": "block",
"reason": f"Tests failing. Output: {stderr}"
}))
sys.exit(0)
sys.exit(0)パターン2: ビルドバリデーション
プロジェクトがコンパイルされるまでブロックする:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
result = subprocess.run(
['npm', 'run', 'build'],
capture_output=True,
timeout=180
)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "Build failed. Fix compilation errors before completing."
}))
sys.exit(0)
sys.exit(0)パターン3: リントチェック
lintエラーが残ったままターンを終了させない:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
result = subprocess.run(
['npx', 'eslint', 'src/', '--max-warnings=0'],
capture_output=True,
timeout=60
)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "Lint errors detected. Run eslint --fix or resolve manually."
}))
sys.exit(0)
sys.exit(0)パターン4: タスク完了マーカー
特定のタスクフラグでターンをゲートする:
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
# Check for incomplete task marker
marker = Path('.claude/incomplete-task')
if marker.exists():
task_info = marker.read_text().strip()
print(json.dumps({
"decision": "block",
"reason": f"Task incomplete: {task_info}. Finish it before stopping."
}))
sys.exit(0)
sys.exit(0)作業開始時にマーカーを配置する:
echo "Implement user authentication" > .claude/incomplete-task
作業が完了したらクリアする:
rm .claude/incomplete-task
無限ループの防止
stop_hook_active フラグが重要な理由はこれだ。なければこうなる:
Claude responds → Stop hook fires → "block" → Claude continues
↓
Claude responds → Stop hook fires → INFINITE LOOP (without flag check)常にフラグを最初に確認する:
if input_data.get('stop_hook_active', False):
sys.exit(0) # Allow stopping, break the loop複数チェックの組み合わせ
1つのフックで複数のゲートを連鎖できる:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
checks = [
(['npm', 'run', 'lint'], "Lint errors"),
(['npm', 'run', 'typecheck'], "Type errors"),
(['npm', 'test'], "Test failures"),
]
for cmd, error_msg in checks:
result = subprocess.run(cmd, capture_output=True, timeout=120)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": f"{error_msg} detected. Fix before completing."
}))
sys.exit(0)
sys.exit(0)ストップフックを使うべき場面
適切なユースケース:
- 「タスク完了」前にテストスイートをグリーンにする
- ビルドが正常にコンパイルされていることを確認する
- lintやタイプエラーを検出する
- 自分なりの「完了」の定義に合わせたカスタムルール
適切でないユースケース:
- 60秒のタイムアウトより長く実行されるもの
- ネットワークにアクセスしてフレークするチェック
- 人間の回答が必要なプロンプト(ここではインタラクションできない)
設定
.claude/settings.json に設定する:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/stop-validation.py"
}
]
}
]
}
}複数のフックを同時に並列で実行できる。いずれか1つから block が返るとClaudeは継続する。
デバッグ
ループにはまった場合
- スクリプトの先頭で
stop_hook_activeを読み込んでいるか再確認する - ログに記録する:
echo "stop_hook_active: $stop_hook_active" >> ~/.claude/stop-debug.log
ブロックが機能しない場合
- JSONは
{"decision": "block", "reason": "..."}の形式でなければならない - Exit code 0を使う。2ではない。Exit 2は別のブロックパスに使われる。
テストが長時間実行される場合
- フックのタイムアウトは60秒
- より小さなサブセットを実行するか、テストスイートを高速化する
「Ralph Wilgum」パターン
このパターンはコミュニティのテクニックで、ストップフックを使って永続的なタスクループを強制する:
- セッション開始時にタスクマーカーを配置する
- マーカーが存在する間はストップフックがブロックする
- Claudeに完了の証明としてマーカーを削除させる
- 作業が残っているのに誤って「完了」と言われることがなくなる
結果: Claudeはベストエフォートから保証された完了にシフトする。
次のステップ
- メインのフックガイドですべてのフックタイプを確認する
- コンテキストリカバリーを設定してセッションをコンパクション後も存続させる
- スキルアクティベーションで自動的なスキル読み込みを設定する
- 自動承認フローのパーミッションフックを確認する
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。