agent-hooks について
Agent Hooksは、エージェントのライフサイクルイベントでユーザー作成スクリプトを実行し、ルール適用や内容の変更、コンテキスト注入、監視を行います。スクリプトは標準入力・標準出力のJSONで通信し、設定やコマンドから管理できます。
主な機能
- 01ライフサイクルイベントでのスクリプト実行
- 02ツール呼び出しのブロックや書き換え
- 03メッセージやツール結果の書き換え
- 04モデル呼び出し前のコンテキスト注入
- 05イベントの監視と記録
- 06標準入出力によるJSON通信
活用シーン
- 01危険なコマンドや機密情報の送信を防止
- 02監査用のツール呼び出し記録
- 03各モデル呼び出しへのコンテキスト追加
- 04回答を検査して再実行を要求
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ starchild-ai-agent/ official-skills --skill agent-hooksインストール後、Claude Code で/agent-hooksを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
templates/runtime_footer_selftest.pyruntime_footer.pysecurity_guard_selftest.pysecurity_guard.pyverify_code_changes_selftest.pyverify_code_changes.pyverify_commitments_selftest.pyverify_commitments.pyverify_publish_claims_selftest.pyverify_publish_claims.py詳細
Agent Hooks
シェルフックを使うと、ユーザーはエージェントのライフサイクル上の決まったタイミングで自分自身のスクリプトを実行できます。危険な操作をブロックしたり、入力や送信メッセージを書き換えたり、モデルにコンテキストを注入したり、ユーザーに警告したりできます。スクリプトは任意の言語で記述でき、シンプルな JSON-on-stdin、JSON-on-stdout プロトコルでエージェントと通信します。
ツール: read_file、write_file、bash
いつ使うか
ユーザーが、毎回指示しなくてもエージェントに自動的にルールを適用させたい、またはイベントに反応させたい場合にフックを使います。例:
- 「
rm -rf/ 破壊的な bash の実行を止めて」→pre_tool_callブロック - 「秘密鍵が Telegram に送信されないようにして」→
on_outbound_messageブロック - 「監査のためにすべてのツール呼び出しをログに記録して」→
post_tool_callオブザーブ - 「モデル呼び出しのたびに X をエージェントに思い出させて」→
pre_llm_callコンテキスト - 「公開していないのに公開したとエージェントに主張させないで」→
on_completion_claim(/goal内)またはon_stop(通常のチャット内) - 「回答が品質チェックに通らなければ、エージェントにやり直させて」→
on_stopブロック
ユーザーが単発のチェックをしたいだけなら、それはフックではありません。フックは繰り返し発生する自動的なライフサイクルの適用のためのものです。
設定の仕組み(エージェントがエンドツーエンドで行う)
エージェントはユーザーのコピー&ペーストなしでフックをインストールして有効化します。 スクリプトを書き、設定エントリを書き、そしてループバックの自己承認 API を呼び出します。これによりマスタースイッチがオンになり、スクリプトが接続されているすべてのイベントに対して承認され、ライブでホットマウントされます(再起動は不要)。ユーザーはその後にテストするだけです。
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/security_guard.py"}'
# -> {"ok": true, "events": [...], "mounted": N, "master_enabled": true}
⚠️ yaml の
command:とこの呼び出しの両方で、必ず/data/workspace配下の絶対パスを使用してください。skills/agent-hooks/templates/security_guard.pyのような相対パスは決して使わないでください。ブリッジはスクリプトをサーバーの cwd(/app)で起動するため、相対パスは/app/skills/…(空のディレクトリ)に解決され、すべての起動が失敗します。ブリッジはフェイルオープン(実行できないスクリプト = 「続行」)であるため、ガードは何も保護しないままになりますが、/hooks listには依然として「mounted」と表示されます。これを避けるため、標準のインストールではテンプレートを/data/workspace/hooks/にコピーし、yaml をそこに向けます(以下のワークフローを参照)。/app/skillsはスキルディレクトリではありません。実際のディレクトリは/data/workspace/skills/(別名/app/workspace/skills/、シンボリックリンク経由)です。
commandは、shell_hooks.yamlエントリの正確なcommand:文字列(スクリプトの絶対パス)でなければなりません。フックはすでに yaml で宣言されている必要があります。承認は宣言済みのフックをライブにするものであり、何もないところからフックを作り出すことはできません。- エンドポイントはループバック専用です(コンテナ内の同一 uid 信頼境界で、
.envの読み取りと同じです)。マスタースイッチを自動的に有効化するため(enable_masterはデフォルトで true)、フックは即座に発火します。 - 承認はスクリプトの mtime を記録するため、後から編集すると
/hooks list//hooks doctorにドリフトとして現れます。スクリプトを差し替える変更は可視化されたままになります。
これがユーザーにとっての有効化のすべてです。2 つ目の手順はありません。 呼び出しが {"ok": true} を返したら、ライブになったこととテストするよう伝えてください。/hooks approve、/hooks on、または「2 つのゲート」には言及しないでください。それらは内部的なものです。
フォールバック(古いビルドのみ): curl が 404 を返した場合、このランタイムは自己承認 API より前のものです。その場合にのみ、ユーザーに /hooks approve <command> に続けて /hooks on を貼り付けるよう依頼する方法にフォールバックしてください。
2 つのゲート(どちらも自己承認 API が処理します — あなたが表に出すことはありません)
内部的には、フックは両方が成立したときにのみ発火します。上記の自己承認呼び出しは 両方を一度に切り替えるため、ユーザーがどちらを目にすることも、入力することもありません。
- マスタースイッチ ON —
workspace/config/agent.yamlのshell_hooks.enabled: true。API が自動で有効化します(enable_masterの デフォルトは true)。 - フックごとの承認 —
(event, command)のペアが、スクリプトの mtime とともに 許可リストに記録されます(そのため、後から編集すると「承認後に変更された」ドリフトとして 表示されます — すり替えられたスクリプトは見えるままになります)。API は、そのコマンドが 配線されているすべてのイベントを承認します。
これらはユーザーの手順ではなく、セキュリティ境界として存在します。ユーザーにフックを説明する
ときに「承認」や「2 つのゲート」に言及しないでください — セットアップすると伝えて、テストして
もらえばよいだけです。(手動の /hooks on と /hooks approve は、古いランタイム向けの 404
フォールバックとしてのみ存在します。)
/hooks コマンド
web / Telegram / WeChat ではプレーンテキストです(LLM なし、コストなし):
| コマンド | 動作 |
|---|---|
/hooks または /hooks list | マスタースイッチの状態、設定パス、すべてのフック + 承認/ヘルス |
/hooks on | /hooks off | マスタースイッチを切り替え(ホットマウント/アンマウント、再起動不要) |
/hooks doctor | 承認済みの各フックを合成ペイロードに対して実行し、JSON をチェック |
/hooks approve <event> <command> | 承認 + ライブで有効化(再起動不要) |
/hooks revoke <command> | 取り消し + ライブで解除(再起動不要) |
/hooks help | 使用方法 |
イベント(12 個)とそれぞれにできること
| イベント | 発火タイミング | 機能 | stdin がスクリプトに渡すもの |
|---|---|---|---|
on_user_message | ユーザーメッセージが到着し、モデルが見る前 | ブロック / テキストを書き換え | message、channel |
pre_tool_call | ツールが実行される前 | ブロック / 入力を書き換え | tool_name、tool_input |
post_tool_call | ツールが実行された後 | 監視(ログ/メトリクス) | tool_name、tool_result |
transform_tool_result | エージェントが結果を見る前 | メモを追記 | tool_name、tool_result |
pre_llm_call | モデル呼び出しの前 | コンテキストを注入 | system、last_user_message、model |
post_llm_call | モデルの返信の後 | 監視 / 差し替え | model |
on_response_end | 最終返信が組み立てられ、1 ターンにつき一度 | 返信を書き換え | response、model、tokens、tool_names |
on_stop | on_response_end の後のターン境界 | ブロック → やり直しを強制 | response、tool_names、stop_hook_active |
on_outbound_message | TG/WeChat へのプッシュ前 | ブロック / 送信内容を書き換え | notification、type |
on_completion_claim | エージェントが /goal の完了を主張 | ブロック → やり直しを強制 | goal、summary、response、tool_names |
on_session_start | セッションの開始 | 監視 | status |
on_session_end | セッションの終了 | 監視 / クリーンアップ | status |
すべてのペイロードには event、session_id、agent_id、cwd も含まれます。
event フィールドはディスパッチキーです。 これは、どのライフサイクルの瞬間が
発火しているか(pre_tool_call、on_user_message、…)を示します。複数イベントの
スクリプト(security_guard.py のように、1 つのファイルが 5 つのイベントに配線される
もの)は、event を読んでどの分岐を実行するかを決めます — ペイロードに event がなければ
どの分岐にも一致しないため、スクリプトは「続行」にフォールスルーします(空の出力 = 許可)。
ランタイムは常にこれを設定します。テストペイロードを手作りするときにだけ、これを
覚えておく必要があります(以下のドライラン手順を参照)。これは shell_hooks.yaml に
書くものではありません — そこでは event: キーが、バスがいつあなたを呼ぶかを
伝えます。ペイロード内の event フィールドは、バスがスクリプトにどの瞬間かを
伝えているのです。
「エージェントに修正させる」3 つのレバー (混同しないでください)
これら 3 つはターンの終わり近くに発火しますが、威力は大きく異なります — 何か問題が起きたときに何を実現したいかで選んでください:
| イベント | 威力 | 使用する場面 |
|---|---|---|
on_response_end | 書き換えのみ — 保存/転送される返信を編集します (フッター、秘匿化、マスク)。エージェントにやり直させることはできません。ループのリスクはゼロです。 | テキストを変更するだけでよい場合 (漏えいしたキーのマスク、コストフッターの追加)。 |
on_stop | ブロック → やり直し、通常のチャット内で — reason を次の指示として差し戻し、エージェントは作業を続けます。カーネルによる上限 (1 ターンあたり最大 3 回のやり直し) + stop_hook_active フラグがあるため、ターンを罠にかけることはできません。 | 通常の会話でエージェントに自分の出力を実際に修正/検証させる必要がある場合 (品質ゲート、引用/公開チェック)。Claude Code の「Stop」フックと同等です。 |
on_completion_claim | ブロック → やり直し、/goal 内のみで — でっち上げられた「完了」を拒否し、ゴールループを動かし続けます。 | 同じやり直しの威力ですが、実行中の /goal スーパーバイザーループ内でのみ発火します。 |
経験則: マスク → on_response_end、チャット内でやり直し → on_stop、ゴール内でやり直し → on_completion_claim。 on_response_end は保存されたコピーしか書き換えられないことに注意してください — ライブの Web クライアントにすでにストリーミングされたトークンは送信取り消しできないため、ユーザーに修正された回答を実際に見せる必要がある場合は on_stop を優先してください。
出力プロトコル (スクリプトが stdout に出力するもの)
JSON オブジェクト、または「継続」を意味する空。フィールド:
{"decision": "block", "reason": "..."} // deny the action / refuse completion
{"tool_input": {...}} // pre_tool_call: rewrite EXISTING input keys
{"notification": "..."} // on_outbound_message: rewrite the message
{"context": "..."} // pre_llm_call: inject into prompt (AGENT-facing)
{"systemMessage": "..."} // allow, but show the USER a note
{"add_warning": "..."} // same user-facing note channel
<empty> // continue, no change
context はエージェント向けです (プロンプトに挿入されます。pre_llm_call のみ)。
systemMessage / add_warning はユーザー向けです (ツール結果 / 完了 / アウトバウンドの各サーフェスで人間に表示されます) — プロンプトに注入されることは決してありません。
安全性: スクリプトは shell=False + argv 分割 (シェルインジェクションなし) とフックごとのタイムアウトで実行されます。エラー、タイムアウト、または非 JSON を出力したスクリプトは継続にフォールスルーします — 壊れたフックがエージェントを壊すことは決してありません。
読みやすい reason の書き方
reason は ユーザー(ブロックされたアクションのカード上)と モデル の
両方に表示されます。短く、ざっと読み取れる ものにしてください — なぜ を
1 文節で示し、その後に根拠を続けます。段落で書かないでください。reason は、
ユーザーがすでに目にするだけで苛立つカード上に表示されるものであり、長文の壁は
実際の原因を埋もれさせてしまいます。目指す形は
[tag] Blocked (<why>): <evidence> — コロンの前は約 8〜12 語、嘆きの 2 文は
決して書かないでください。
| 避ける (冗長) | 推奨 (簡潔) |
|---|---|
This command is irreversible and would cause permanent data loss, so I've blocked it: rm -rf / | Blocked (recursive force-delete): rm -rf / |
That message contains what looks like an API key, private key, or seed phrase. I won't process it — treat it as exposed and rotate it. | Blocked: message contains a credential. Rotate it. |
You shared a preview link whose id isn't in the registry — it looks made up. Serve the preview first and use its real id | Preview id not in the registry. Serve it first: /preview/x/ |
モデルは行動するのに十分な情報(なぜ + 問題のペイロード)を引き続き受け取り、 ユーザーは一目で読めるカードを受け取ります。UI は 1 つの reason 文字列を 2 つの 部分に分割してくれるので、クライアント側で解析する必要はありません:
- 説明 + コマンドボックス — 最初に人間向けの文を置き、次に
:、そして 問題のコマンド/ペイロードを置きます。最初の": "より後はすべて別の 等幅フォントのボックスにレンダリングされます。この分割は、その末尾が ペイロードらしく見える場合(スペースを含む、または約 12 文字より長い)にのみ 発動するので、たまたまコロンを含む普通の文はそのまま残ります。 - 文のみ —
": "を含まない reason は 1 つの文として表示され、コマンド ボックスはありません。これは引用するものがない場合(例: 貼り付けられた シードフレーズ)に適した形です。 [tag]は除去されます —[security]のような先頭のタグは表示前に 削除され、文は自動的に大文字化されるので、自分のgrep用にタグを残しても UI に漏れ出すことはありません。
// Good — short why + a clean command box:
{"decision": "block",
"reason": "[security] Blocked (formats the disk): mkfs.ext4 /dev/sda1"}
// -> "Blocked (formats the disk)" + [ mkfs.ext4 /dev/sda1 ]
// Avoid — a bare command (no WHY) or a verbose two-sentence lecture:
{"decision": "block", "reason": "mkfs /dev/sda1"}
{"decision": "block", "reason": "This command is irreversible and would cause permanent data loss across the entire filesystem, so I have decided to block it for your safety: mkfs /dev/sda1"}
これに従わないフックも依然として動作します — ただの文字列は 1 つの文として レンダリングされるだけです。この規約は、より見栄えのする「説明 + コマンド」の レイアウトを有効にするだけのものです。
設定ファイルの形式
workspace/config/shell_hooks.yaml:
hooks:
- event: pre_tool_call
matcher: "rm -rf|dd if=|mkfs" # optional regex; script only spawns on a match (perf gate)
command: ./extensions/shell_hooks/examples/block_secrets.py
timeout: 10 # seconds, default 20, max 120
2 つのフックトランスポート
フックはローカルの command(デフォルト)か HTTP endpoint のいずれかです — 入力ペイロードも出力する判定 JSON も同じで、異なるのはトランスポートだけです。
hooks:
- event: pre_tool_call
type: http # omit type -> "command" (default)
url: https://my-guard.example.com/hook
timeout: 10
HTTP の詳細:
- SSRF ガード — URL は http(s) でなければならず、ループバック /
プライベート / リンクローカル(クラウドメタデータ
169.254.169.254を含む)/ 予約済みアドレスに解決されてはなりません(解析時と呼び出し時の両方で ブロックされます)。ローカルサービスに意図的にアクセスする場合にのみSTARCHILD_SHELL_HOOKS_HTTP_ALLOW_LOCAL=1を設定してください。 - URL 上の承認キー:
/hooks approve <event> <url>、/hooks listはそれをPOST <url>として表示し、実行ファイル/mtime のチェックをスキップします。
LLM 判定を追加する (proxy を呼び出し、/chat は呼び出さない)
フックが実際の推論を必要とする場合(「これは秘密情報を漏洩させるか?」、
「この完了は実際に完了しているか?」)、スクリプトから proxy 経由で直接
LLM を呼び出してください — エージェント自身の /chat は決して呼び出さないで
ください。
from core.http_client import proxied_post
import json, sys
event = json.load(sys.stdin)
r = proxied_post(
"https://openrouter.ai/api/v1/chat/completions",
json={
"model": "minimax/minimax-m3", # cheap default (~$0.0002/call)
"messages": [
{"role": "system", "content":
'You are a guard. Output ONLY JSON {"decision":"block|allow","reason":"..."}.'},
{"role": "user", "content": json.dumps(event)},
],
"temperature": 0, "max_tokens": 200,
},
headers={"SC-CALLER-ID": "chat:hook"}, # required for billing
timeout=40,
)
try:
print(json.dumps(json.loads(r.json()["choices"][0]["message"]["content"])))
except Exception:
print("{}") # fail-open on any parse error
proxy を直接呼び出す理由: OpenRouter は外部のステートレス API であるため、
エージェントループに再び入ったり pre_llm_call を発火させたりすることは
ありません -> 再帰なし、完全なエージェントターンの代わりに 1 回の
安価な補完、独自のプロンプト + 純粋な JSON レスポンスが得られます。フックから
/chat を呼び出すと、同じイベントが再発行されます(ブリッジがループを防ぎ
ますが、不要なオーバーヘッドです)— そして /chat を呼び出す LLM フックは
pre_llm_call に 決して 配置してはなりません。ホストドキュメント
sc-proxy.md の「proxy 経由で LLM を呼び出す」セクションを参照してください。
標準ワークフロー (エージェントのチェックリスト)
- ルールを明確化し、上の表からイベントを選びます。
- スクリプトを書く — stdin で JSON を読み、stdout に判定を出力します。
非ゼロ終了 / 非 JSON = 続行です。実行可能にしてください (
chmod +x)。 - スクリプトを
/data/workspace配下の安定した絶対パスに配置します。同梱の テンプレートの場合、スキルの更新で移動されないようスキルディレクトリから コピーしてください:
スクリプトを相対パスで参照しないでください (上の ⚠️ ボックスを参照 — それはmkdir -p /data/workspace/hooks cp /data/workspace/skills/agent-hooks/templates/security_guard.py /data/workspace/hooks/ chmod +x /data/workspace/hooks/security_guard.py ls -l /data/workspace/hooks/security_guard.py # VERIFY it exists before going on/appを基準に解決され、サイレントにフェイルオープンします)。 workspace/config/shell_hooks.yamlに 設定エントリを追加します。絶対command:(/data/workspace/hooks/security_guard.py) を指定し、可能ならmatcher正規表現も追加して、関連する場合にのみスクリプトが起動するようにします。- 自分で
bashを使ってドライランします — サンプルの JSON ペイロードを スクリプトにパイプし、有効な JSON を出力することを確認します。ペイロードには 必ずeventフィールドを含めてください — 複数イベント対応のスクリプトはこれで ディスパッチするため、省略するとすべてのケースが「続行」にフォールスルーし、 ガードが発火しないと誤って結論してしまいます。スクリプトが処理する各イベントを テストしてください:# should BLOCK (note the "event" key): echo '{"event":"pre_tool_call","tool_name":"bash","tool_input":{"command":"rm -rf /"}}' \ | python3 /data/workspace/hooks/security_guard.py # should ALLOW (empty output): echo '{"event":"pre_tool_call","tool_name":"bash","tool_input":{"command":"ls -la"}}' \ | python3 /data/workspace/hooks/security_guard.py - 自分で有効化します — ループバックの自己承認 API 経由で (ユーザーによる
貼り付けは不要)。
command= yaml エントリの正確な絶対パス:curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \ -H 'Content-Type: application/json' \ -d '{"command": "/data/workspace/hooks/security_guard.py"}'{"ok": true}ならフックは有効です。404の場合は、ユーザーに/hooks approve <command>+/hooks onを渡す方法にフォールバックします (「設定の仕組み」を参照)。 /hooks doctorを実行して実際に動作することを確認します — これが 誤ったパス / 実行不可 / 非 JSON スクリプトを検出するステップです。/hooks listで「マウント済み」と表示されてもdoctorでエラーになるガードは サイレントな no-op (フェイルオープン) です。doctorがクリーンになって初めて、 ユーザーに有効でテスト可能な状態であることを伝えてください。
すぐに使えるスクリプト (それぞれに 1 つの明確な役割)
5 つの プロダクション品質のガード がこのスキルの templates/ に同梱されています
(そのままコピー + 承認)。4 つの 単一目的の例 がホストの
extensions/shell_hooks/examples/ に同梱されています (コピー + 改変)。重複は
ありません — 試行錯誤ではなく、役割で選んでください。
/hooks の番号または名前からインストールする
/hooks (空の状態) と /hooks help は 番号付き のすぐに使えるリストを
表示します。/hooks コマンド自体は静的なテキストです — インストールは決して
行いません。ユーザーが番号 ("1"、"1,3")、名前 ("security_guard")、または
"install all" で返信した場合、その返信があなたに届きます: そのリストで番号の
隣に表示されている名前 でテンプレートに解決し (番号→templates/<name>.py)、
それぞれに対して以下の標準ワークフローを実行してください — コピー → yaml エントリ
→ ドライラン → 自己承認 → doctor。表示されている名前でマッピングし、暗記した
番号→パスの表は決して使わないでください (リストは並べ替えられる可能性があります)。
セルフテストが存在する場合 (templates/<name>_selftest.py)、最初に実行してください。
本番用テンプレート (このスキル内、templates/)
⚠️ ここにあるすべてのテンプレートは、編集する前にコピーしてください。 常にテンプレートを
/data/workspace/hooks/にcpし、そのパスでフックを設定してから、 コピー側で変更 (ルールの調整、runtime_footerの CONFIG ブロックなど) を行ってください。skills/agent-hooks/templates/配下のファイルを直接編集しても無意味です。 次のスキル更新で上書きされ、変更は消えてしまいます。hooks/内のコピーはあなたのものであり、 更新によって触れられることはありません。
| テンプレート | イベント | その唯一の役割 |
|---|---|---|
security_guard.py | on_user_message、pre_tool_call、transform_tool_result、on_response_end、on_outbound_message | シークレット + 破壊的な bash。 貼り付けられた/外部に送信されたシークレット (Bearer を含む API キー、PEM/EVM 秘密鍵、BIP-39 シード、Solana バイト配列 & base58 WIF) をブロックし、返信やプッシュ内の漏えいしたキーをマスクし、取り返しのつかないデータ損失を招く bash をブロックします。以下を参照してください。 |
verify_publish_claims.py | on_stop (チャットのやり直し) / on_completion_claim (/goal のやり直し) / on_response_end (書き換えフォールバック) | ハルシネーション対策。 捏造された「公開した / AgentX に投稿した / スケジュールした」という主張を、返信をグラウンドトゥルース (プレビューレジストリ、AgentX 台帳、スケジューラレジストリ) と照合することで検出します。 |
verify_code_changes.py | pre_tool_call (記録) + on_stop (判定) | コードにおける「偽の完了」対策。 ソースファイルを変更したのに、それを確認するテスト/ビルド/リントを実行していない場合、停止を一度ブロックし、完了する前に検証する (または実行するものがないと率直に言う) よう誘導します。edit_file/write_file によるものと、bash 経由で書き込まれたコード (ヒアドキュメント、>/>> リダイレクト、tee、インプレースの sed -i/perl -i) の両方をカウントします。ドキュメント/データの編集 (.md/.json/.yaml/…) は対象外です。編集セットごとに 1 回のみ注意喚起し、自己解除し、フェイルオープンします。両方のイベントを同じスクリプトパスに設定してください。 |
verify_commitments.py | on_stop (チャットのやり直し) | 壊れた約束対策。 返信が将来の通知の約束 (「ビルドが終わったら知らせます」、「明早提醒你」) をしているのに、それを実現するための登録を何もしていない場合、停止を一度ブロックし、実際に登録するよう誘導します。時間に縛られるものは scheduled_task(once)、完了に縛られるものは sessions_spawn (bash ポーリング + announce=followup) です。通知動詞と時間/条件の手がかりの両方が現れた場合にのみ発動します。即時配信の言い回し (「こちらです」、「下面就是」) やラウンドをまたぐ登録 (最近のアクティブなジョブ / 最近の spawn) は抑制します。上限付きで、自己解除し、フェイルオープンします。 |
runtime_footer.py | on_response_end (+ 任意で pre_llm_call) | モデル/コストのフッター。 on_response_end (1 ターンに 1 回) で、返信末尾にモデルが入力したフッターを除去し、ランタイムの実際の model + コストから得た唯一の真のフッターを追加します。任意で pre_llm_call も設定すると、「フッターを入力しないで」という注意喚起 (モデルリクエストごとに発動) が得られます。以下を参照してください。 |
単一目的の例 (ホストリポジトリ、extensions/shell_hooks/examples/)
| スクリプト | イベント | その唯一の役割 |
|---|---|---|
pii_redactor.py | transform_tool_result、on_response_end | メール / 電話番号 (PII — シークレットとは別物) をマスクします。 |
tool_audit_log.py | post_tool_call | 監視のみ: すべてのツール呼び出しを JSONL 監査証跡に追記します。 |
budget_alert.py | on_response_end | ターンのコストがしきい値を超えたときにソフト警告を追記します。 |
inject_website_reminder.sh | pre_llm_call | 予防的な注意喚起: 完了を主張する前に実際に公開するようモデルに促します (verify_publish_claims.py と組み合わせます)。 |
テンプレートに置き換えられました(競合するガードを二重に配布しないでください)
ホストリポジトリは、上記の 2 つのテンプレートと重複する最小限の単一イベントのサンプルも
extensions/shell_hooks/examples/ 配下に同梱しています。これらは学習用の参考資料としては
問題ありませんが、実際の使用ではテンプレートを優先してください。両方を実行すると、
異なるポリシーを持つガードが 2 つできてしまうだけです。
| 最小限のサンプル | 代わりにこれを使用 | 理由 |
|---|---|---|
block_secrets.py | security_guard.py | ガードのシークレット検出は厳密なスーパーセットです(Bearer、Solana バイト配列、base58 WIF、破壊的 bash、マスキングを追加) |
check_publish.sh | verify_publish_claims.py | テンプレートは AgentX 投稿 + スケジュールタスクもカバーし、同じレジストリをチェックします |
完全に削除(孤立した重複で、security_guard.py に完全に統合済み):
secret_guard.py(ベンダーキーのブロック/マスク、Bearer を含む)と
dangerous_bash_guard.py(破壊的 bash のブロック)。インストーラー / force-push も
さらにブロックしたいですか?競合するポリシーを持つ 2 つ目の bash ガードを実行するのではなく、
ガードの DESTRUCTIVE テーブルを調整してください。
上記のいずれもカバーしないルールについては、新しいスクリプトを作成してください。以下の 最小限のブロックのサンプルがテンプレートであり、上記の出力プロトコルがすべての機能を カバーしています。
最小限のブロックのサンプル(pre_tool_call、任意の言語)
#!/usr/bin/env bash
payload="$(cat)"
python3 - "$payload" <<'PY'
import json, sys, re
ev = json.loads(sys.argv[1])
cmd = (ev.get("tool_input") or {}).get("command", "")
if re.search(r"rm\s+-rf\s+/|dd\s+if=|mkfs", cmd):
print(json.dumps({"decision": "block", "reason": f"This command is irreversible and would erase data, so I've blocked it: {cmd}"}))
else:
print("{}") # continue
PY
オールインワンのセキュリティガード(templates/security_guard.py)
1 つのファイルを 5 つのイベントに接続し、よくある「シークレットを漏らさない / マシンを
破壊しない」というベースラインをカバーする、すぐに使える自己完結型のスクリプトです。まず
安定した絶対パスにコピーし、次に 5 つのイベントすべてを config/shell_hooks.yaml 内の
同じ絶対 command: に接続し(イベントごとに 1 ブロック)、その後で有効化します:
cp /data/workspace/skills/agent-hooks/templates/security_guard.py /data/workspace/hooks/
chmod +x /data/workspace/hooks/security_guard.py
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/security_guard.py"}'
これにより、そのコマンドが接続されているすべてのイベントが承認され、ライブでマウントされます
— ユーザーによる貼り付けは不要です。(404 の場合は、/hooks approve <command> +
/hooks on にフォールバックしてください。)
| イベント | 動作 |
|---|---|
on_user_message | モデルが参照する前に、貼り付けられた API キー(Bearer トークンを含む)、秘密鍵(PEM / EVM hex)、シードフレーズ、Solana バイト配列のシークレット、または base58 WIF をブロックします |
pre_tool_call(bash) | 不可逆的なデータ損失(rm -rf /、ブロックデバイスへの dd、mkfs、フォーク爆弾、chmod -R 777、git reset --hard origin/*``)と認証情報の持ち出し(cat .env |
pre_tool_call(メッセージツール) | send_to_telegram / send_to_wechat の引数をガードします — 漏洩したキーをマスクし、シードフレーズをブロックします(これらのツールはプッシュパイプラインをバイパスするため、これがそれらにとっての実質的な送信ゲートです) |
transform_tool_result | ツールの出力にシークレットが含まれている場合に警告します(バックエンドは結果テキストにフラグを立てることしかできず、書き換えはできません) |
on_response_end | 最終的な返信に漏洩したシークレットをマスクします |
on_outbound_message | TG / WeChat にプッシュされる前にシークレットをマスク / ブロックします |
設計ポリシー: 非常に危険であり、かつ通常の作業の一部ではないものだけをブロックします。
curl | bash(インストーラー)や git push --force(自分のフィーチャーブランチのリベース)の
ような一般的な開発操作は、意図的に許可されています — 過剰なブロックは、ユーザーがガードを
無効化するように仕向けてしまいます。
ファイルの先頭にある SECRET_PATTERNS、DESTRUCTIVE、MSG_TOOLS の各テーブルを、独自の
ルールに合わせて調整してください。templates/security_guard_selftest.py はセルフテストです
(編集後は必ず実行してください。危険な文字列はデータとしてのみそこに存在するため、ホストの
bash ガードがそれらで引っかかることはありません)。
ハルシネーション防止ガード (templates/verify_publish_claims.py)
捏造された成功を捕捉します。エージェントがツールを一度も実行していないのに「Published! community.iamstarchild.com/…」、「Posted to AgentX /post/…」、「Reminder scheduled」と書き込んだ場合です。
このスクリプトは返信をグラウンドトゥルース — プレビューレジストリ
(/data/previews.json)、AgentX 投稿台帳、スケジューラレジストリ —
と照合し、返信を書き換えるか、やり直しを強制します。これは意図的に
誤検知が少なく設計されています。実際の公開 URL や「公開の提案」(未来時制) はそのまま通過し、根拠のない過去時制の成功主張だけが引っかかります。
cp /data/workspace/skills/agent-hooks/templates/verify_publish_claims.py /data/workspace/hooks/
chmod +x /data/workspace/hooks/verify_publish_claims.py
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/verify_publish_claims.py"}'
| イベント | 動作 |
|---|---|
on_stop | (推奨) 通常のチャットで、捏造された成功をブロックし、エージェントに実際に公開/やり直しを強制します (ループ上限あり) |
on_completion_claim | /goal ループで、捏造された「完了」をブロックし、実際の公開を強制します (ループ上限あり) |
on_response_end | on_stop が設定されていない場合の書き換えのみのフォールバック: 正直な「未検証」の注記を追加します (エージェントにやり直しをさせることはできません) |
通常のチャットでは
on_stopに設定してください — テキストを編集するだけでなく、エージェントに実際にやり直しをさせる唯一のイベントです。ホストはon_stop/on_completion_claimではdecision: blockのみを尊重します (これらのイベントでは書き換えは無視されます)。そのため、フックは両方でブロックし、on_response_endでのみ書き換えます。templates/verify_publish_claims_selftest.pyはセルフテストです (on_stopブロックパス + ループ上限をカバーします)。
コスト/モデルフッター (templates/runtime_footer.py)
モデルは自身の返信ごとのコストを知ることができません — 多くの場合、自身のモデル ID すら知りません。そのデータはランタイムにしか存在しません。したがって、モデルが自身のフッター (例: Model: GLM-5.2 | Cost: $0.038) を入力すると、その数値は捏造されます。そして、一度実際のフッターがチャット履歴に入ると、モデルのオートコンプリートがそれを模倣し始め、2つ目の捏造されたフッターを生成します。フッターはランタイムの役割であり、モデルの役割ではありません。
runtime_footer.py はこれを1つのイベント — on_response_end — で完全に解決します。これは最終的に組み立てられた返信に対してターンごとに1回発火し、① 返信末尾にモデルが入力したフッターを除去し、次に ② ランタイムの実際の model + コストから唯一の真のフッターを追加します。除去が保証であり、呼び出しごとの何かは必要ありません。
なぜ pre_llm_call ではなく on_response_end だけなのか。 「最終応答の直前に」発火するイベントは存在しません。
pre_llm_callはすべてのモデルリクエストの前に発火し (ツールが使用される場合は1ターンに N 回)、どの呼び出しが最後かを知ることができません — モデルは動的にツールを使うかどうかを決定します。そこで抑制を設定すると、ディレクティブが1ターンに N 回注入されます (呼び出しトレースで繰り返し注入として見えます)。また冗長でもあります。on_response_endがすでに事後にフッターを除去するからです。したがって、デフォルトでは on_response_end のみにしてください。 このスクリプトはpre_llm_callハンドラー (「フッターを入力しない」というディレクティブを注入する) も搭載していますが、追加の後押しが欲しい場合は2つ目のイベントとして設定してください — ただし呼び出しごとに実行されることを受け入れてください。
除去はセーフティネット (FOOTER_STRIP、デフォルトで有効) であり、意図的に範囲を狭くしています。除去するのは罫線の ─ … · $N 行または Model: … Cost: $N 行のみで、しかも末尾の行のみです。したがって、本文中の「Model:」/「Cost:」の文や、シェルの $VAR は決して触れられません (以前のバージョンでは範囲が広すぎる Model: 正規表現を使い、正当な文章を削除する恐れがありました。これが厳密にやり直したものです)。純粋な追加のみにするには FOOTER_STRIP=0 を設定してください。
cp /data/workspace/skills/agent-hooks/templates/runtime_footer.py /data/workspace/hooks/
chmod +x /data/workspace/hooks/runtime_footer.py
curl -s -X POST http://localhost:8000/internal/runtime/hooks/approve \
-H 'Content-Type: application/json' \
-d '{"command": "/data/workspace/hooks/runtime_footer.py"}'
config/shell_hooks.yaml で設定します — matcher はなく、毎ターン実行されます:
hooks:
- event: on_response_end
command: /data/workspace/hooks/runtime_footer.py
timeout: 10
# Optional extra nudge (fires per model-request, N times/turn):
# - event: pre_llm_call
# command: /data/workspace/hooks/runtime_footer.py
# timeout: 10
設定はコピーを編集して行います — 環境変数ではありません。 オプションを有効にする推奨の方法は、スクリプト冒頭の CONFIG ブロックです。/data/workspace/hooks/ 以下に作成したばかりのコピーで編集してください(skills/… 内ではありません — そちらは次回のスキル更新で上書きされます)。フックはサーバープロセスの環境で実行され、あなたのシェルではないため、環境変数の設定は面倒で /hooks list にも表示されません。ファイル内の定数なら信頼でき、可視化され、スクリプトと一緒に移動します。
# ─── CONFIG — edit your copy ───
SHOW_TOKENS = False # True → append "· N in / N out"
SHOW_CREDIT = False # True → append "· 💰 $bal" (1 HTTP call/turn, fail-open)
TEMPLATE = None # custom format str: {model} {cost} {input} {output} {credit}
CREDIT_URL = None # override credit endpoint for self-hosted setups
STRIP = True # strip a model-typed footer at the tail
各定数には対応する環境変数によるオーバーライド(FOOTER_SHOW_TOKENS、FOOTER_SHOW_CREDIT、FOOTER_TEMPLATE、FOOTER_CREDIT_URL、FOOTER_STRIP、FOOTER_SUPPRESS_TEXT)もあり、設定されている場合はそちらが優先されます — ファイルに触れずに一度だけ使いたいときに便利ですが、ファイル内の定数が恒久的なデフォルトです。
デフォルトのフッターはモデル + コストのみです(─ z-ai/glm-5.2 · $0.0211)。
SHOW_TOKENS = True → ─ z-ai/glm-5.2 · $0.0211 · 900 in / 120 out。
残りのクレジットを表示: SHOW_CREDIT = True を設定すると残高が追記されます(─ z-ai/glm-5.2 · $0.0211 · 💰 $271.64)。デフォルトでオフになっているのは、クレジット API に対してターンごとに 1 回の内部 HTTP 呼び出しが追加されるためです — これは credit ツールが読み取るのと同じエンドポイントで、送信元 IPv6 によって自動的に認証されます(キーは不要)。これはフェイルオープンです。2 秒のタイムアウトが待機時間の上限となり、検索がエラーまたはタイムアウトした場合、残高は黙って省略されます(フッターは引き続き出力され、区切り記号が宙に浮くことはありません)。モデルはフックを連携させない限りこの数値を参照できないことに注意してください — コストと同様、ランタイム内にのみ存在します。
重複させないでください: runtime_footer はホストの turn_footer 拡張に相当するシェルフックです — どちらか一方だけを有効にしてください。Telegram の tg_show_usage も同様です。
安全性: 決してブロックしません。on_response_end は、イベントにコストデータが含まれない場合や応答が空の場合には何も追記せず($0.0000 という嘘をつきません)、応答の末尾で厳密に一致したフッターのみを削除します(無効にするには STRIP = False)。任意の pre_llm_call は、ペイロードが欠落または不正な場合には何も注入せず、未知のイベントは何もしません。あらゆるエラーでフェイルオープンです。セルフテスト: templates/runtime_footer_selftest.py(35 ケース — 両方のハンドラー、削除 + 本文途中の散文やシェルの $VAR に対する誤検出ガード、クレジット残高 + フェイルオープン、ファイル内 CONFIG 定数 + 環境変数オーバーライドの優先順位、ディスパッチの安全性)。
Claude Code との互換性
Claude Code 向けに書かれたフックスクリプトはそのまま動作します — その出力は上記のフィールドに自動変換されます:
| Claude Code の出力 | 変換先 |
|---|---|
hookSpecificOutput.permissionDecision: "deny"(+ permissionDecisionReason) | decision: block(+ reason) |
hookSpecificOutput.additionalContext | context |
hookSpecificOutput.updatedInput | tool_input(書き換え) |
continue: false(+ stopReason) | decision: block(+ reason) |
systemMessage | add_warning(ユーザー向けの通知) |
suppressOutput | no-op(私たちの stdout は決してトランスクリプトに入りません) |
| stdout なしで stderr 付きの終了コード 2 | decision: block、stderr が理由 |
変換されるのは出力ペイロードのみです — イベント名は私たちのもののままです(PreToolUse ではなく pre_tool_call)。Claude Code の Stop フックは私たちの on_stop にマッピングされ(block → やり直しを強制)、その UserPromptSubmit は on_user_message にマッピングされます。{"decision":"block","reason":…} を返すか終了コード 2 で終了する Stop スクリプトは、on_stop に接続すればそのまま動作します。
「フックがまったく発火しない」のトラブルシューティング
- イベントは上記 12 個のいずれかですか?(タイプミスはサイレントな no-op になります)
- マスタースイッチはオンですか?
/hooks listに表示されます。セルフ承認 API が自動的に有効にします。それ以外の場合は/hooks onを実行してください。 - フックは承認済みですか?
/hooks listに✗ NOT approvedと表示される場合は、 そのコマンドに対してセルフ承認 API を再実行してください(フォールバックとして/hooks approveでも可)。 matcherの正規表現は実際にマッチしていますか?狭すぎると決して起動しません。/hooks doctorを実行してください — 実行不可 / 改ざん / タイムアウト / 非 JSON を 報告します。- 手動テストで「すべて許可」されますか? テストペイロードに
eventフィールドが 欠けている可能性があります — 複数イベントのスクリプトはそれでディスパッチし、 それがなければ「continue」にフォールスルーします。これはテストハーネスのミスで あり、フックのバグではありません。実際のランタイムは常にeventを設定します。 can't open file '/app/skills/…'/ 「マウントされている」のに何もブロックされ ない?command:が相対パスまたは誤ったパスです。ブリッジはサーバーの cwd (/app) で起動するため、skills/…は空の/app/skillsに解決され、すべての 起動が失敗します — そしてブリッジはフェイルオープンするため、ガードは何も保護 しません。修正方法: yaml と承認呼び出しの両方で ABSOLUTE パス/data/workspace/hooks/<script>.pyを使用し、その後で再承認して/hooks doctorで確認してください。(/app/skillsは skills ディレクトリではありません。 実際のものは/data/workspace/skills/です。)
詳細リファレンス
完全なプロトコル、セキュリティモデル、およびイベントごとのペイロードの詳細は、
エージェント自身のドキュメントにあります: config/context/references/agent-hooks.md
(このスキルが要約しているエッジケースについては、これを読んでください)。