Claude CodeのHooksをHTTPエンドポイントで受ける
type: httpのHooksは、イベントのJSONをPOSTで送りレスポンスボディで結果を返します。設定項目とレスポンスの成否判定、コマンド型との違いを扱います。
Hooksの基本設定はシェルコマンドを走らせるcommandタイプが中心ですが、HooksにはほかにHTTPエンドポイントを直接呼び出すtype: "http"もあります。イベントのJSONをPOSTのリクエストボディとして送り、レスポンスボディから許可・拒否・コンテキスト追加といった結果を受け取ります。この記事では設定項目、認証ヘッダーの書き方、レスポンスの成否判定、コマンド型Hooksとの違いを扱います。
HTTP hookとは何か
HTTP hookとは、Hooksのイベントが発火したときにシェルコマンドではなくHTTP POSTリクエストを送る設定タイプです。常駐する検証サービスや、社内の承認システムと連携したいときに使います。コマンド型のようにローカルのプロセスを毎回起動する必要がなく、既に立てているサーバーへイベントを送るだけで済みます。
Claude Codeはイベントの共通入力フィールドをリクエストボディとしてContent-Type: application/jsonで送信します。レスポンスボディは、コマンド型Hooksの標準出力と同じJSON形式で読み取られます。
HTTP hookを設定する
type: "http"のHookは、共通フィールドに加えてurl・headers・allowedEnvVarsの3つを使います。
| フィールド | 必須 | 説明 |
|---|---|---|
url | 必須必須 | 説明POST先のURL |
headers | 必須任意 | 説明追加のHTTPヘッダー(key-value) |
allowedEnvVars | 必須任意 | 説明ヘッダー値への環境変数補間を許可する変数名の一覧 |
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}timeoutの既定値はコマンド型と同じ600秒です。ただしUserPromptSubmit・PreModelSwitch・PostModelSwitchでは30秒、MessageDisplayでは10秒に下がります。
環境変数をヘッダーに埋め込む
headersの値は$VAR_NAMEまたは${VAR_NAME}の書式で環境変数を参照できます。ただし補間されるのはallowedEnvVarsに列挙した変数だけです。列挙していない変数への参照は空文字列に置き換わります。認証トークンをハードコードせず、環境変数経由で渡すための仕組みです。
さらに、settingsのhttpHookAllowedEnvVarsをどこかのレベルで定義すると、その一覧がすべてのHTTP hookに対する外側の上限として働きます。Hookが変数を使えるのは、個別のallowedEnvVarsとこのキーの両方に名前があるときだけです。
{
"httpHookAllowedEnvVars": ["MY_TOKEN", "HOOK_SECRET"]
}送信先URLを制限するキーはallowedHttpHookUrlsで、*をワイルドカードにしたURLパターンの配列を書きます。キーを定義した時点で、パターンに一致しないHTTP hookは実行されずに止められます。空配列ならHTTP hookはすべて止まり、未設定なら任意のURLが許可されます。ホスト名の比較は大文字小文字を区別せず、末尾にドットの付いたhooks.example.com.もhooks.example.comと同じ扱いです。
{
"allowedHttpHookUrls": ["https://hooks.example.com/*", "http://localhost:*"]
}2つの許可リストは、管理ポリシー設定を含むどのソース由来のHTTP hookにも適用されます。配列は設定ファイル間でマージされます。送信先の絞り込み方はClaude CodeのallowedHttpHookUrls解説にまとめています。
レスポンスを返す — 成功・エラーの判定基準
HTTP hookはコマンド型と違い、終了コードではなくHTTPステータスコードとレスポンスボディで結果を判定します。
| レスポンス | 扱い |
|---|---|
| 2xx + 空ボディ | 扱い成功(終了コード0・出力なしと同じ) |
| 2xx + JSONオブジェクト | 扱いコマンド型と同じJSON出力スキーマで解釈。スキーマに合わない本文は非ブロッキングエラー |
| 2xx + それ以外(プレーンテキスト等) | 扱い非ブロッキングエラー扱い。テキストはClaudeのコンテキストに追加されない |
| 非2xx | 扱い非ブロッキングエラー扱い。実行は続行 |
| 接続失敗 | 扱い非ブロッキングエラー扱い。実行は続行 |
| タイムアウト | 扱いHookはキャンセルされ、出力は破棄される |
コマンド型Hooksは終了コード2を返すだけでツール呼び出しをブロックできますが、HTTP hookはステータスコードだけではブロックできません。ツール呼び出しを止めたい、あるいは権限を拒否したい場合は、2xxのレスポンスにJSONボディを載せ、その中でpermissionDecision: "deny"のような決定フィールドを指定します。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "外部審査サービスが承認していません"
}
}WorktreeCreateのように、イベント側に独自の失敗規則がある場合は、失敗したHTTP hookにもその規則が当てはまります。
最小のエンドポイントを動かして試す
PreToolUseイベントを受けて.envファイルへの書き込みだけを拒否する、依存パッケージなしのNode.js実装です。server.mjsとして保存します。
import { createServer } from "node:http";
createServer((req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const { tool_name, tool_input } = JSON.parse(body || "{}");
const filePath = tool_input?.file_path ?? "";
res.setHeader("Content-Type", "application/json");
if (req.url === "/hooks/pre-tool-use" && tool_name === "Write" && filePath.endsWith(".env")) {
res.end(JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Cannot modify .env files",
},
}));
return;
}
res.end("{}");
});
}).listen(8080);Claude Codeに繋ぐ前に、curlで同じ形のJSONを投げれば、エンドポイント単体の応答を確かめられます。
繋ぐ前の動作確認
- 1
サーバーを起動する
node server.mjs - 2
.envへの書き込みを模したJSONを送る
curl -s -X POST http://localhost:8080/hooks/pre-tool-use \ -H 'Content-Type: application/json' \ -d '{"tool_name":"Write","tool_input":{"file_path":"app/.env"}}'返ってくるのは、200のステータスと
permissionDecisionがdenyのJSONです。{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cannot modify .env files"}} - 3
別のファイルでは空のオブジェクトが返ることを見る
file_pathをapp/main.tsに変えて同じcurlを送ると、本文は{}だけです。2xxで空のオブジェクトなので、Claude Code側では何も起きません。 - 4
設定ファイルに登録する
ここまでの応答が確かめられたら、前の節の
urlをこのエンドポイントに向けてPreToolUseに登録します。
判定を伴わないイベントは、空オブジェクトの2xxレスポンスを返せば十分です。エンドポイント側で例外が起きてステータスが5xxになった場合は非2xxの扱いで、非ブロッキングエラーとして処理が続きます。
matcherとifで呼び出しを絞り込む
HTTP hookでも、対象を絞る仕組みはコマンド型と共通です。matcherは英数字・_・-・スペース・,・|だけで書いた値なら完全一致(|や,で区切って複数指定も可。FileChangedとStopFailureだけは|区切りのみ)、それ以外の文字を含めば正規表現として評価されます。正規表現は前後を固定しないので、Edit.*はEditにもNotebookEditにも一致します。完全一致させたいなら^Edit$のように書きます。
ifフィールドは権限ルール構文で絞り込みを重ねます。評価されるのはPreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDeniedだけで、それ以外のイベントでifを書いたHookは一度も実行されません。
{
"type": "http",
"if": "Bash(git push *)",
"url": "http://localhost:8080/hooks/git-push-review",
"allowedEnvVars": ["REVIEW_TOKEN"]
}この例ではBashツールのうち、git pushで始まるサブコマンドのときだけHTTP hookが呼ばれます。ifは1つのHookハンドラーにつき1つの権限ルールしか持てないため、複数条件を組み合わせたい場合は条件ごとにハンドラーを分けてhooks配列に並べます。なおifのBash判定はあくまで補助的な絞り込みなので、許可や拒否を確実に強制したいときはHookではなく権限システムで書きます。
HTTP hookが使えないイベントとタイムアウトの落とし穴
SessionStartとSetupはcommandとmcp_toolだけをサポートし、HTTP hookは使えません。それ以外の主要イベントはPreToolUse・PostToolUse・Stop・Notification・PermissionRequestなども含めてHTTP hookを受け付けます。promptとagentまで使えるイベントは限られ、PermissionRequestはagentを使えません。
WorktreeCreateではコマンド型とHTTP型で結果の返し方が変わります。コマンド型はワークツリーのパスをそのまま標準出力に印字しますが、HTTP hookはhookSpecificOutput.worktreePathというJSONフィールドでパスを返します。
タイムアウトした場合の挙動もイベントで違います。PreToolUseのHTTP hookがタイムアウトしても、ツール呼び出しは通常の権限フローに進みます。止まったエンドポイントをゲート代わりには使えません。逆にPreModelSwitchでは、タイムアウトでキャンセルされたHookがモデルの切り替えを止めます。
どのイベントでHTTP hookが向くか
HTTP hookは、判定に外部の状態を必要とするイベントで使います。PreToolUseで社内の承認システムに問い合わせてから危険なコマンドを止める、PostToolUseで編集内容を静的解析サービスへ送って結果をログに残す、Notificationで権限プロンプトの通知を社内チャットへ転送する、といった用途です。
同じイベントに複数のHookを登録した場合、コマンド型・HTTP型を問わず、一致したHookはすべて並列に実行されます。同じハンドラーを複数のsettingsファイルに書いたときは、1回だけ実行されます。エンドポイントは同時に届く複数のPOSTを捌ける実装にしておく必要があります。
コマンド型との違い
どちらでHookを書くか
コマンド型(command)
毎回プロセスを起動し、終了コードとstdoutで結果を返します。終了コード2だけでブロックでき、認証情報は環境変数をプロセスが直接読みます。ローカルのファイル整形や単純なlintのように、その場で終わる処理に向きます。
HTTP型(http)
常駐サービスへPOSTし、HTTPステータスとレスポンスボディで結果を返します。社内の承認システム、静的解析サービス、チャット通知のように、すでにHTTPで公開されている相手との連携に向きます。サーバーを持つ分、運用の手間は増えます。
ローカルで完結する軽い処理にHTTP型を使うと、サーバーを立てて保守する分だけ構成が複雑になります。
よくあるつまずき
ブロックしたいのに素の403や500を返しているだけ、という構成では何も止まりません。非2xxは非ブロッキングエラーで、実行は続行されます。2xxを返した上でJSONボディにpermissionDecision等の決定フィールドを含める必要があります。
headersにトークンを書いたのに空文字列で届く場合は、allowedEnvVarsへの列挙漏れが疑わしい原因です。settingsにhttpHookAllowedEnvVarsを定義している場合は、そちらにも名前が要ります。
エンドポイントの応答が遅く、指定したtimeoutを超えるとHookはキャンセルされ、出力は破棄されます。PreToolUseではツール呼び出しがそのまま進むため、判定を返す前に確実に終わる設計にするか、timeoutを長めに設定してください。
URLをallowedHttpHookUrlsのパターンに入れ忘れたHTTP hookは、実行されずに止められます。Hookが動かないときは、許可リストのパターンも確かめてください。
よくある質問
HTTP hookとMCP tool hookはどう使い分けますか
MCP tool hookは、設定済みのMCPサーバー上のツールを呼び出し、そのテキスト出力をコマンド型のstdoutと同じように扱います。すでにMCPサーバーとして公開している機能を判定に使うならMCP tool hookが自然です。MCPサーバーを立てていない任意のWebサービスと連携したい場合は、HTTP hookが直接的です。
認証が無いエンドポイントを指定しても動きますか
動きます。headersもallowedEnvVarsも任意項目なので、認証なしのエンドポイントでも設定は成立します。ただしイベントのJSONにはツール名や引数、セッションIDなどの情報が含まれるため、社外からアクセスできるURLを認証なしで指定するのは避けたほうが安全です。
まとめ
HTTP hookで押さえるのは、止めたいときは必ず2xxのJSONで返すことと、認証ヘッダーの変数はallowedEnvVarsに列挙し、httpHookAllowedEnvVarsを定義しているならそちらにも入れることの2点です。判定が外部サービスの状態に左右されるなら、毎回プロセスを起動するコマンド型よりも、既存のHTTPサーバーへ投げるHTTP hookが合います。
Hooksの全体像と5種類の実行タイプの比較はClaude Code Hooks完全ガイド、コピペで使えるレシピはClaude Code Hooks実例カタログにまとめています。