Hermes Event Hooks — ライフサイクルでフックする自動化設計
Hermes Agentには 3種類のフックシステム があり、エージェントのライフサイクル(セッション開始→ツール呼び出し→LLM推論→応答)の各段階に任意のコードを差し込める。
Gateway hooks → ~/.hermes/hooks/ の HOOK.yaml + handler.py
Plugin hooks → ctx.register_hook() で14種のイベントをフック
Shell hooks → config.yaml の hooks: ブロックでシェルスクリプト
全システム共通で ノンブロッキング — フックがエラーを出してもエージェント本体には影響しない。
3種のフックの違い
| 種類 | 登録場所 | 動作環境 | 主な用途 |
|---|---|---|---|
| Gateway hooks | ~/.hermes/hooks/ |
Gateway専用 | ログ、アラート、Webhook通知 |
| Plugin hooks | ctx.register_hook() |
CLI + Gateway | ツールインターセプト、メトリクス、ガードレール |
| Shell hooks | config.yaml |
CLI + Gateway | ブロッキング、自動フォーマット、コンテキスト注入 |
Gateway hooks は Telegram/Discord 等のゲートウェイでのみ動作し、CLIセッションではロードされない。CLI でもフックしたい場合は Plugin hooks か Shell hooks を使う。
Gateway Event Hooks
ゲートウェイのライフサイクルイベントに反応して Python ハンドラを実行する仕組み。
ディレクトリ構成
~/.hermes/hooks/
└── my-hook/
├── HOOK.yaml # 購読するイベントを宣言
└── handler.py # handle() 関数を実装
購読可能なイベント
| イベント | 発火タイミング | 主なコンテキスト |
|---|---|---|
gateway:startup |
ゲートウェイ起動時 | platforms |
session:start |
新セッション作成時 | platform, user_id, session_id |
session:end |
セッション終了時 | platform, user_id |
session:reset |
/new /reset 実行時 |
platform, user_id |
agent:start |
メッセージ処理開始時 | platform, user_id, message |
agent:step |
ツール呼び出しループの各イテレーション | iteration, tool_names |
agent:end |
メッセージ処理完了時 | message, response |
command:* |
スラッシュコマンド実行時 | command, args |
command:* はワイルドカードで、すべてのスラッシュコマンドを一括監視できる。
実用例:BOOT.md パターン
~/.hermes/BOOT.md にチェックリストを書いておき、ゲートウェイ起動時に自動実行させるパターン。
# ~/.hermes/hooks/boot-md/HOOK.yaml
name: boot-md
description: Run ~/.hermes/BOOT.md on gateway startup
events:
- gateway:startup
# ~/.hermes/hooks/boot-md/handler.py
import threading
from pathlib import Path
BOOT_FILE = Path.home() / ".hermes" / "BOOT.md"
def _run_boot_agent(content: str) -> None:
from gateway.run import _resolve_gateway_model, _resolve_runtime_agent_kwargs
from run_agent import AIAgent
agent = AIAgent(
model=_resolve_gateway_model(),
**_resolve_runtime_agent_kwargs(),
platform="gateway",
quiet_mode=True,
skip_memory=True,
max_iterations=20,
)
result = agent.run_conversation(
f"Execute this startup checklist:\n\n{content}\n\n"
"If nothing needs attention, reply with ONLY: [SILENT]"
)
async def handle(event_type: str, context: dict) -> None:
if not BOOT_FILE.exists():
return
content = BOOT_FILE.read_text(encoding="utf-8").strip()
if not content:
return
thread = threading.Thread(target=_run_boot_agent, args=(content,), daemon=True)
thread.start()
_resolve_gateway_model() と _resolve_runtime_agent_kwargs() を使うことで、カスタムエンドポイントやOAuthプロバイダーの認証情報が正しく引き継がれる。これらを使わないと、bare AIAgent() はデフォルト設定で401エラーを出す。
Plugin Hooks
プラグインの register() 関数で ctx.register_hook() を呼ぶことで、14種のイベントにフックを登録できる。CLI と Gateway の両方で動作する。
フック一覧
| フック | 発火タイミング | 戻り値の効果 |
|---|---|---|
pre_tool_call |
ツール実行直前 | {"action": "block"} でツールを中止 |
post_tool_call |
ツール実行直後 | 監視専用 |
pre_llm_call |
LLM呼び出し直前(1回/ターン) | {"context": str} でユーザーにメッセージにコンテキスト注入 |
post_llm_call |
LLM呼び出し直後(1回/ターン) | 監視専用 |
on_session_start |
新セッション開始時(初回ターンのみ) | 監視専用 |
on_session_end |
セッション終了時 | 監視専用 |
on_session_finalize |
セッション破棄時 | 監視専用 |
on_session_reset |
セッションリセット時 | 監視専用 |
subagent_stop |
delegate_task 子エージェント終了時 | 監視専用 |
pre_gateway_dispatch |
ゲートウェイでメッセージ受信時 | {"action": "skip"} でメッセージ破棄 |
pre_approval_request |
承認プロンプト表示直前 | 監視専用 |
post_approval_response |
承認プロンプト回答後 | 監視専用 |
transform_tool_result |
ツール結果をモデルに渡す直前 | str で結果を置換 |
transform_terminal_output |
terminal ツール出力時 | str で出力を置換 |
transform_llm_output |
最終応答を配信する直前 | str で応答を置換 |
pre_tool_call — ツールをブロック
DANGEROUS = {"terminal", "write_file", "patch"}
def block_dangerous(tool_name, args, task_id, **kwargs):
if tool_name in DANGEROUS:
return {"action": "block", "message": f"Tool {tool_name} is blocked by policy"}
return None
def register(ctx):
ctx.register_hook("pre_tool_call", block_dangerous)
pre_llm_call — コンテキスト注入
def inject_memory(session_id, user_message, is_first_turn, **kwargs):
memories = recall_memories(session_id, user_message)
if memories:
return {"context": "Relevant memories:\n" + "\n".join(f"- {m}" for m in memories)}
return None
def register(ctx):
ctx.register_hook("pre_llm_call", inject_memory)
注入されたコンテキストは ユーザーメッセージに追加 される。システムプロンプトは変更しないため、プロンプトキャッシュが壊れない。
transform_terminal_output — 出力加工
transform_terminal_output フックは、terminal ツールの出力をモデルに渡す前に加工できる。例えば、機密情報のマスクや、不要な行のフィルタリングに使える。
Shell hooks
~/.hermes/config.yaml の hooks: ブロックでシェルスクリプトを登録する。最もシンプルなフック方式。
hooks:
pre_tool_call:
command: ~/.hermes/hooks-scripts/pre-tool-call.sh
シェルスクリプトの標準出力がフックの戻り値として使われる。
設計パターン
1. 安全なフックの原則
- フックは ノンブロッキング — エラーを出してもエージェントは続行
- 重い処理は バックグラウンドスレッド に投げる
- フック同士の実行順序は プラグイン発見順(アルファベット)
2. フックの使い分け
| やりたいこと | 使うフック |
|---|---|
| ツールをブロックしたい | pre_tool_call |
| ツール実行を記録したい | post_tool_call |
| 毎ターンコンテキストを注入したい | pre_llm_call |
| セッション開始時に初期化したい | on_session_start |
| ゲートウェイ起動時にチェックしたい | Gateway hooks (gateway:startup) |
| メッセージを傍受したい | pre_gateway_dispatch |
| ツール出力を加工したい | transform_terminal_output |
| 最終応答を加工したい | transform_llm_output |
3. BOOT.md パターンの利点
- 組み込み機能じゃない → ユーザーが明示的にファイルを作って有効化する
- 何をするかが Markdown で見える → 透明性がある
[SILENT]で不要な通知を抑制できる
あーしメモ
今日のセッションで「write_file前にls確認をhookで強制できないか?」って話が出たけど、Plugin hooks の pre_tool_call で実現できるかもしれない。
write_file が呼ばれる前にフックが走るので、そこで ls してファイルの存在確認して、問題なければ通す、問題があれば {"action": "block"} で止める。
ただし、フックを書くには Python でプラグインを作る必要がある。Shell hooks でもできるけど、戻り値の制約があるかも。
あと transform_terminal_output も面白い。terminal ツールの出力を加工できるから、「ls の結果を整形してからモデルに渡す」みたいなこともできる。
Hermes のフックシステムはかなり柔軟で、「エージェントの挙動を外から制御」する本格的な拡張ポイント。特に pre_tool_call でツールをブロックできるのは、セキュリティポリシーの実装に使えそう。
See Also
- hermes-cron-troubleshooting.md — Cron トラブルシューティング
- hermes-agent-usage.md — Hermes Agent 運用の教訓
- Claude Code Hooksで会話ログを蒸留する — SessionEnd起点のナレッジ捕捉設計 — 同じ「フックでライフサイクルに介入する」設計。Hermes側の
subagent_stopは監視専用だが、こちらは同種のフックを蒸留パイプラインの起点として使う提案