Chuyển tới nội dung chính

Móc sự kiện

Hermes có ba hệ thống hook chạy mã tùy chỉnh tại các điểm quan trọng trong vòng đời:

Hệ thốngĐã đăng ký quaChạy vàoTrường hợp sử dụng
Gateway hooksHOOK.yaml + handler.py trong ~/.hermes/hooks/Chỉ GatewayGhi nhật ký, cảnh báo, webhook
Plugin hooksctx.register_hook() trong pluginCLI + GatewayChặn công cụ, số liệu, guardrails
Shell hooksKhối hooks: trong ~/.hermes/config.yaml trỏ đến shell scriptsCLI + GatewayScript dạng drop-in để chặn, tự động format, inject ngữ cảnh

Cả ba hệ thống đều không bị chặn — lỗi trong bất kỳ hook nào đều được bắt và ghi lại, không bao giờ làm hỏng tác nhân.

Móc sự kiện cổng

Cổng kết nối tự động kích hoạt trong quá trình vận hành cổng (Telegram, Discord, Slack, WhatsApp) mà không chặn đường ống tác nhân chính.

Tạo một Hook

Mỗi hook là một thư mục trong ~/.Hermes/hooks/ chứa hai tệp:

~/.Hermes/hooks/
└── my-hook/
├── HOOK.YAML # Khai báo các sự kiện cần lắng nghe
└── handler.py # Hàm xử lý Python

HOOK.YAML

name: my-hook
description: Ghi nhật ký tất cả hoạt động của tác nhân vào tệp
events:
- agent:start
- agent:end
- agent:step

Danh sách events xác định sự kiện nào kích hoạt trình xử lý của bạn. Bạn có thể đăng ký bất kỳ sự kết hợp sự kiện nào, bao gồm các ký tự đại diện như command:*.

handler.py

import JSON
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / ".Hermes" / "hooks" / "my-hook" / "activity.log"

async def handle(event_type: str, context: dict):
"""Được gọi cho mỗi sự kiện đã đăng ký. Phải được đặt tên là 'handle'."""
entry = {
"timestamp": datetime.now().isoformat(),
"event": event_type,
**context,
}
with open(LOG_FILE, "a") as f:
f.write(JSON.dumps(entry) + "\n")

Quy tắc xử lý:

  • Phải đặt tên là handle
  • Nhận event_type (chuỗi) và context (dict)
  • Có thể là async def hoặc def thông thường - cả hai đều hoạt động
  • Lỗi được bắt và ghi lại, không bao giờ làm hỏng tác nhân

Sự kiện có sẵn

Sự kiệnKhi nó cháyKhóa ngữ cảnh
gateway:startupQuá trình cổng bắt đầuplatforms (danh sách tên nền tảng đang hoạt động)
session:startĐã tạo phiên nhắn tin mớiplatform, user_id, session_id, session_key
session:endPhiên kết thúc (trước khi đặt lại)platform, user_id, session_key
session:resetNgười dùng chạy /new hoặc /resetplatform, user_id, session_key
agent:startAgent bắt đầu xử lý tin nhắnplatform, user_id, session_id, message
agent:stepMỗi lần lặp của vòng lặp gọi công cụplatform, user_id, session_id, iteration, tool_names
agent:endAgent hoàn tất xử lýplatform, user_id, session_id, message, response
command:*Bất kỳ lệnh gạch chéo nào được thực thiplatform, user_id, command, args

So khớp ký tự đại diện

Trình xử lý đã đăng ký kích hoạt command:* cho bất kỳ sự kiện command: nào (command:model, command:reset, v.v.). Giám sát tất cả các lệnh gạch chéo bằng một lần đăng ký.

Ví dụ

Danh sách kiểm tra khởi động (BOOT.md) - Tích hợp sẵn

Cổng này được trang bị móc boot-md tích hợp để tìm kiếm ~/.Hermes/BOOT.md mỗi lần khởi động. Nếu tệp tồn tại, tác nhân sẽ chạy hướng dẫn của nó trong phiên nền. Không cần cài đặt - chỉ cần tạo tệp.

Tạo ~/.Hermes/BOOT.md:

# Danh sách kiểm tra khởi động

1. Kiểm tra xem có tác vụ cron nào thất bại qua đêm không — chạy `Hermes cron list`
2. Gửi tin nhắn đến kênh Discord #general nói rằng "Cổng đã khởi động lại, tất cả hệ thống hoạt động"
3. Kiểm tra xem /opt/app/deploy.log có bất kỳ lỗi nào trong 24 giờ qua không

Tác nhân chạy các hướng dẫn này trong một luồng nền để nó không chặn quá trình khởi động cổng. Nếu không có gì cần chú ý, nhân viên sẽ trả lời bằng [SILENT] và không có tin nhắn nào được gửi.

mẹo

Không có BOOT.md? Cái móc âm thầm bỏ qua - không có chi phí. Tạo tệp bất cứ khi nào bạn cần tự động hóa khởi động, xóa nó khi không cần.

Cảnh báo Telegram về các nhiệm vụ dài

Gửi tin nhắn cho chính bạn khi nhân viên thực hiện hơn 10 bước:

# ~/.Hermes/hooks/long-task-alert/HOOK.YAML
name: long-task-alert
description: Cảnh báo khi tác nhân chạy nhiều bước
events:
- agent:step
# ~/.Hermes/hooks/long-task-alert/handler.py
import os
import httpx

THRESHOLD = 10
BOT_TOKEN = os.getenv("Telegram_BOT_TOKEN")
CHAT_ID = os.getenv("Telegram_HOME_CHANNEL")

async def handle(event_type: str, context: dict):
iteration = context.get("iteration", 0)
if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
tools = ", ".join(context.get("tool_names", []))
text = f"⚠️ Agent đã chạy {iteration} bước. Công cụ cuối: {tools}"
async with httpx.AsyncClient() as client:
await client.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={"chat_id": CHAT_ID, "text": text},
)

Trình ghi nhật ký sử dụng lệnh

Theo dõi lệnh gạch chéo nào được sử dụng:

# ~/.Hermes/hooks/command-logger/HOOK.YAML
name: command-logger
description: Ghi nhật ký sử dụng lệnh gạch chéo
events:
- command:*
# ~/.Hermes/hooks/command-logger/handler.py
import json
from datetime import datetime
from pathlib import Path

LOG = Path.home() / ".Hermes" / "logs" / "command_usage.jsonl"

def handle(event_type: str, context: dict):
LOG.parent.mkdir(parents=True, exist_ok=True)
entry = {
"ts": datetime.now().isoformat(),
"command": context.get("command"),
"args": context.get("args"),
"platform": context.get("platform"),
"user": context.get("user_id"),
}
with open(LOG, "a") as f:
f.write(json.dumps(entry) + "\n")

Webhook bắt đầu phiên

POST lên dịch vụ bên ngoài trong các phiên mới:

# ~/.Hermes/hooks/session-webhook/HOOK.YAML
name: session-webhook
description: Thông báo dịch vụ bên ngoài khi có phiên mới
events:
- session:start
- session:reset
# ~/.Hermes/hooks/session-webhook/handler.py
import httpx

WEBHOOK_URL = "https://your-service.example.com/hermes-events"

async def handle(event_type: str, context: dict):
async with httpx.AsyncClient() as client:
await client.post(WEBHOOK_URL, json={
"event": event_type,
**context,
}, timeout=5)

Cách thức hoạt động

  1. Khi khởi động cổng, HookRegistry.discover_and_load() quét ~/.Hermes/hooks/
  2. Mỗi thư mục con có HOOK.YAML + handler.py được tải động
  3. Trình xử lý được đăng ký cho các sự kiện đã khai báo của họ
  4. Tại mỗi điểm trong vòng đời, hooks.emit() sẽ kích hoạt tất cả các trình xử lý phù hợp
  5. Lỗi trong bất kỳ trình xử lý nào đều được phát hiện và ghi lại - một hook bị hỏng không bao giờ làm hỏng tác nhân:::info Móc cổng chỉ kích hoạt trong gateway (Telegram, Discord, Slack, WhatsApp). CLI không tải các móc nối cổng. Đối với các móc hoạt động ở mọi nơi, hãy sử dụng plugin hooks. :::

Móc plugin

Plugins có thể đăng ký các hook kích hoạt trong phiên cả CLI và cổng. Chúng được đăng ký theo chương trình thông qua ctx.register_hook() trong chức năng register() của plugin của bạn.

def register(ctx):
ctx.register_hook("pre_tool_call", my_tool_observer)
ctx.register_hook("post_tool_call", my_tool_logger)
ctx.register_hook("pre_LLM_call", my_memory_callback)
ctx.register_hook("post_LLM_call", my_sync_callback)
ctx.register_hook("on_session_start", my_init_callback)
ctx.register_hook("on_session_end", my_cleanup_callback)

Quy tắc chung cho tất cả các hook:

  • Lệnh gọi lại nhận đối số từ khóa. Luôn chấp nhận **kwargs để tương thích về sau — các thông số mới có thể được thêm vào trong các phiên bản sau này mà không làm hỏng plugin của bạn.
  • Nếu lệnh gọi lại bị lỗi, lệnh gọi lại đó sẽ được ghi lại và bị bỏ qua. Các hook khác và Agent vẫn tiếp tục bình thường. Một plugin hoạt động sai không bao giờ có thể phá vỡ tác nhân.
  • Tất cả các hook đều là trình quan sát lửa và quên có giá trị trả về bị bỏ qua — ngoại trừ pre_LLM_call, có thể inject context.

Tham khảo nhanh

MócKích hoạt khiTrả về
pre_tool_callTrước khi bất kỳ công cụ nào thực thibỏ qua
post_tool_callSau khi bất kỳ công cụ nào trả vềbỏ qua
pre_LLM_callMỗi lượt một lần, trước vòng lặp gọi công cụtiêm ngữ cảnh
post_LLM_callMỗi lượt một lần, sau vòng gọi công cụbỏ qua
on_session_startĐã tạo phiên mới (chỉ lượt đầu tiên)bỏ qua
on_session_endPhiên kết thúcbỏ qua

pre_tool_call

Kích hoạt ngay lập tức trước mỗi lần thực thi công cụ — các công cụ tích hợp sẵn cũng như công cụ plugin tương tự.

Chữ ký gọi lại:

def my_callback(tool_name: str, args: dict, task_id: str, **kwargs):
Tham sốLoạiMô tả
tool_namestrTên công cụ sắp thực thi (ví dụ "terminal", "web_search", "read_file")
argsdictCác đối số mà mô hình truyền cho công cụ
task_idstrMã định danh phiên/nhiệm vụ. Chuỗi trống nếu không được đặt.

Kích hoạt: Trong model_tools.py, bên trong handle_function_call(), trước khi trình xử lý của công cụ chạy. Kích hoạt một lần cho mỗi lệnh gọi công cụ — nếu mô hình gọi song song 3 công cụ thì thao tác này sẽ kích hoạt 3 lần.

Giá trị trả về: Bỏ qua.

Các trường hợp sử dụng: Ghi nhật ký, kiểm tra đường đi, bộ đếm lệnh gọi công cụ, chặn các hoạt động nguy hiểm (in cảnh báo), giới hạn tốc độ.

Ví dụ — nhật ký kiểm tra lệnh gọi công cụ:

import json, logging
from datetime import datetime

logger = logging.getLogger(__name__)

def audit_tool_call(tool_name, args, task_id, **kwargs):
logger.info("TOOL_CALL session=%s tool=%s args=%s",
task_id, tool_name, json.dumps(args)[:200])

def register(ctx):
ctx.register_hook("pre_tool_call", audit_tool_call)

Ví dụ — cảnh báo về các công cụ nguy hiểm:

DANGEROUS = {"terminal", "write_file", "patch"}

def warn_dangerous(tool_name, **kwargs):
if tool_name in DANGEROUS:
print(f"⚠ Đang thực thi công cụ có khả năng nguy hiểm: {tool_name}")

def register(ctx):
ctx.register_hook("pre_tool_call", warn_dangerous)

post_tool_call

Kích hoạt ngay sau mỗi lần thực thi công cụ trở lại.

Chữ ký gọi lại:

def my_callback(tool_name: str, args: dict, result: str, task_id: str, **kwargs):
Tham sốLoạiMô tả
tool_namestrTên công cụ vừa thực thi
argsdictCác đối số mà mô hình truyền cho công cụ
resultstrGiá trị trả về của công cụ (luôn là chuỗi JSON)
task_idstrMã định danh phiên/nhiệm vụ. Chuỗi trống nếu không được đặt.

Kích hoạt: Trong model_tools.py, bên trong handle_function_call(), sau khi trình xử lý của công cụ quay trở lại. Kích hoạt một lần cho mỗi lệnh gọi công cụ. không kích hoạt nếu công cụ đưa ra một ngoại lệ chưa được xử lý (thay vào đó, lỗi được phát hiện và trả về dưới dạng một chuỗi JSON lỗi và post_tool_call kích hoạt với chuỗi lỗi đó dưới dạng result).

Giá trị trả về: Bỏ qua.

Các trường hợp sử dụng: Ghi lại kết quả của công cụ, thu thập số liệu, theo dõi tỷ lệ thành công/thất bại của công cụ, gửi thông báo khi các công cụ cụ thể hoàn tất.

Ví dụ — theo dõi số liệu sử dụng công cụ:

from collections import Counter
import json

_tool_counts = Counter()
_error_counts = Counter()

def track_metrics(tool_name, result, **kwargs):
_tool_counts[tool_name] += 1
try:
parsed = json.loads(result)
if "error" in parsed:
_error_counts[tool_name] += 1
except (json.JSONDecodeError, TypeError):
pass

def register(ctx):
ctx.register_hook("post_tool_call", track_metrics)

pre_LLM_call

Bắn một lần mỗi lượt, trước khi vòng lặp gọi công cụ bắt đầu. Đây là hook duy nhất có giá trị trả về được sử dụng — nó có thể đưa ngữ cảnh vào thông báo người dùng của lượt hiện tại.

Chữ ký gọi lại:

def my_callback(session_id: str, user_message: str, conversation_history: list,
is_first_turn: bool, model: str, platform: str, **kwargs):
Tham sốLoạiMô tả
session_idstrMã định danh duy nhất cho phiên hiện tại
user_messagestrTin nhắn ban đầu của người dùng cho lượt này (trước khi tiêm bất kỳ kỹ năng nào)
conversation_historylistBản sao danh sách tin nhắn đầy đủ (định dạng OpenAI: [{"role": "user", "content": "..."}])
is_first_turnboolTrue nếu đây là lượt đầu tiên của phiên mới, False ở các lượt tiếp theo
modelstrMã định danh mô hình (ví dụ: "anthropic/claude-sonnet-4-6")
platformstrNơi phiên đang chạy: "CLI", "Telegram", "Discord", v.v.

Kích hoạt: Trong run_agent.py, bên trong run_conversation(), sau khi nén ngữ cảnh nhưng trước vòng lặp while chính. Kích hoạt một lần cho mỗi lệnh gọi run_conversation() (tức là một lần cho mỗi lượt người dùng), không phải một lần cho mỗi lệnh gọi API trong vòng lặp công cụ.

Giá trị trả về: Nếu lệnh gọi lại trả về một lệnh có khóa "context" hoặc một chuỗi đơn giản không trống thì văn bản sẽ được thêm vào thông báo người dùng của lượt hiện tại. Trả lại None nếu không tiêm.

# Tiêm ngữ cảnh
return {"context": "Recalled memories:\n- User likes Python\n- Working on Hermes-agent"}

# Chuỗi đơn giản (tương đương)
return "Recalled memories:\n- User likes Python"

# Không tiêm
return None

Nơi ngữ cảnh được chèn: Luôn là thông báo của người dùng, không bao giờ là lời nhắc của hệ thống. Điều này sẽ duy trì bộ nhớ đệm lời nhắc — lời nhắc hệ thống vẫn giống hệt nhau qua các lượt, do đó các mã thông báo đã lưu trong bộ nhớ đệm sẽ được sử dụng lại. Lời nhắc của hệ thống là lãnh thổ của Hermes (hướng dẫn mô hình, thực thi công cụ, tính cách, kỹ năng). Các plugin đóng góp ngữ cảnh cùng với thông tin đầu vào của người dùng.

Tất cả ngữ cảnh được chèn đều phù du — chỉ được thêm vào thời điểm gọi API. Tin nhắn ban đầu của người dùng trong lịch sử hội thoại không bao giờ bị thay đổi và không có gì được lưu lại trong cơ sở dữ liệu phiên.

Khi nhiều plugin trả về ngữ cảnh, kết quả đầu ra của chúng được nối với hai dòng mới theo thứ tự khám phá plugin (theo bảng chữ cái theo tên thư mục).

Các trường hợp sử dụng: Thu hồi bộ nhớ, chèn ngữ cảnh RAG, lan can, phân tích mỗi lượt.

Ví dụ — thu hồi bộ nhớ:

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall(session_id, user_message, is_first_turn, **kwargs):
try:
resp = httpx.post(f"{MEMORY_API}/recall", json={
"session_id": session_id,
"query": user_message,
}, timeout=3)
memories = resp.json().get("results", [])
if not memories:
return None
text = "Recalled context:\n" + "\n".join(f"- {m['text']}" for m in memories)
return {"context": text}
except Exception:
return None

def register(ctx):
ctx.register_hook("pre_LLM_call", recall)

Ví dụ — lan can:

POLICY = "Không bao giờ thực thi các lệnh xóa tệp mà không có sự xác nhận rõ ràng từ người dùng."

def guardrails(**kwargs):
return {"context": POLICY}

def register(ctx):
ctx.register_hook("pre_LLM_call", guardrails)

post_LLM_call

Kích hoạt một lần mỗi lượt, sau khi vòng lặp gọi công cụ hoàn thành và tác nhân đã đưa ra phản hồi cuối cùng. Chỉ kích hoạt ở lượt thành công - không kích hoạt nếu lượt bị gián đoạn.

Chữ ký gọi lại:

def my_callback(session_id: str, user_message: str, assistant_response: str,
conversation_history: list, model: str, platform: str, **kwargs):
Tham sốLoạiMô tả
session_idstrMã định danh duy nhất cho phiên hiện tại
user_messagestrTin nhắn ban đầu của người dùng cho lượt này
assistant_responsestrPhản hồi văn bản cuối cùng của Agent cho lượt này
conversation_historylistBản sao danh sách tin nhắn đầy đủ sau khi hoàn thành lượt
modelstrMã định danh mô hình
platformstrPhiên đang chạy ở đâu

Kích hoạt: Trong run_agent.py, bên trong run_conversation(), sau khi vòng lặp công cụ thoát ra với phản hồi cuối cùng. Được bảo vệ bởi if final_response and not interrupted - vì vậy nó không kích hoạt khi người dùng ngắt giữa lượt hoặc tác nhân đạt đến giới hạn lặp lại mà không tạo ra phản hồi.

Giá trị trả về: Bỏ qua.

Trường hợp sử dụng: Đồng bộ hóa dữ liệu cuộc hội thoại với hệ thống bộ nhớ ngoài, tính toán số liệu chất lượng phản hồi, ghi nhật ký tóm tắt lượt, kích hoạt các hành động tiếp theo.

Ví dụ — đồng bộ với bộ nhớ ngoài:

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def sync_memory(session_id, user_message, assistant_response, **kwargs):
try:
httpx.post(f"{MEMORY_API}/store", json={
"session_id": session_id,
"user": user_message,
"assistant": assistant_response,
}, timeout=5)
except Exception:
pass # best-effort

def register(ctx):
ctx.register_hook("post_LLM_call", sync_memory)

Ví dụ — theo dõi độ dài phản hồi:

import logging
logger = logging.getLogger(__name__)

def log_response_length(session_id, assistant_response, model, **kwargs):
logger.info("RESPONSE session=%s model=%s chars=%d",
session_id, model, len(assistant_response or ""))

def register(ctx):
ctx.register_hook("post_LLM_call", log_response_length)

on_session_start

Kích hoạt một lần khi một phiên hoàn toàn mới được tạo. không kích hoạt khi tiếp tục phiên (khi người dùng gửi tin nhắn thứ hai trong phiên hiện có).

Chữ ký gọi lại:

def my_callback(session_id: str, model: str, platform: str, **kwargs):
Tham sốLoạiMô tả
session_idstrMã định danh duy nhất cho phiên mới
modelstrMã định danh mô hình
platformstrPhiên đang chạy ở đâu

Kích hoạt: Trong run_agent.py, bên trong run_conversation(), trong lượt đầu tiên của phiên mới — cụ thể là sau khi lời nhắc hệ thống được tạo nhưng trước khi vòng lặp công cụ bắt đầu. Kiểm tra là if not conversation_history (không có tin nhắn trước = phiên mới).

Giá trị trả về: Bỏ qua.

Các trường hợp sử dụng: Đang khởi tạo trạng thái trong phạm vi phiên, làm nóng bộ nhớ đệm, đăng ký phiên với dịch vụ bên ngoài, phiên ghi nhật ký bắt đầu.

Ví dụ — khởi tạo bộ đệm phiên:

_session_caches = {}

def init_session(session_id, model, platform, **kwargs):
_session_caches[session_id] = {
"model": model,
"platform": platform,
"tool_calls": 0,
"started": __import__("datetime").datetime.now().isoformat(),
}

def register(ctx):
ctx.register_hook("on_session_start", init_session)

on_session_end

Kích hoạt vào cuối cùng của mỗi lệnh gọi run_conversation(), bất kể kết quả như thế nào. Đồng thời kích hoạt từ trình xử lý thoát của CLI nếu tác nhân đang ở giữa lượt khi người dùng thoát.

Chữ ký gọi lại:

def my_callback(session_id: str, completed: bool, interrupted: bool,
model: str, platform: str, **kwargs):
Tham sốLoạiMô tả
session_idstrMã định danh duy nhất cho phiên
completedboolTrue nếu Agent đưa ra phản hồi cuối cùng, nếu không thì False
interruptedboolTrue nếu lượt bị gián đoạn (người dùng gửi tin nhắn mới, /stop hoặc thoát)
modelstrMã định danh mô hình
platformstrPhiên đang chạy ở đâu

Cháy: Ở hai nơi:

  1. run_agent.py — ở cuối mỗi cuộc gọi run_conversation(), sau khi đã dọn dẹp xong. Luôn bắn, ngay cả khi lượt bị lỗi.
  2. CLI.py — trong trình xử lý atexit của CLI, nhưng chỉ nếu tác nhân đang ở giữa lượt (_agent_running=True) khi xảy ra lần thoát. Điều này bắt Ctrl+C và /exit trong quá trình xử lý. Trong trường hợp này là completed=Falseinterrupted=True.

Giá trị trả về: Bỏ qua.

Các trường hợp sử dụng: Xóa bộ đệm, đóng kết nối, trạng thái phiên duy trì, thời lượng phiên ghi nhật ký, dọn sạch các tài nguyên được khởi tạo trong on_session_start.

Ví dụ — xả và dọn dẹp:

_session_caches = {}

def cleanup_session(session_id, completed, interrupted, **kwargs):
cache = _session_caches.pop(session_id, None)
if cache:
# Đẩy dữ liệu tích lũy ra đĩa hoặc dịch vụ bên ngoài
status = "completed" if completed else ("interrupted" if interrupted else "failed")
print(f"Phiên {session_id} kết thúc: {status}, {cache['tool_calls']} lần gọi công cụ")

def register(ctx):
ctx.register_hook("on_session_end", cleanup_session)

Ví dụ — theo dõi thời lượng phiên:

import time, logging
logger = logging.getLogger(__name__)

_start_times = {}

def on_start(session_id, **kwargs):
_start_times[session_id] = time.time()

def on_end(session_id, completed, interrupted, **kwargs):
start = _start_times.pop(session_id, None)
if start:
duration = time.time() - start
logger.info("SESSION_DURATION session=%s seconds=%.1f completed=%s interrupted=%s",
session_id, duration, completed, interrupted)

def register(ctx):
ctx.register_hook("on_session_start", on_start)
ctx.register_hook("on_session_end", on_end)

Xem Build a Plugin guide để biết hướng dẫn đầy đủ bao gồm lược đồ công cụ, trình xử lý và mẫu móc nâng cao.

Shell Hooks

Shell hooks cho phép bạn kết nối bất kỳ script thực thi nào (Bash, Python, Go binary, ...) vào các sự kiện vòng đời của agent thông qua khối hooks: trong ~/.hermes/config.yaml. Mỗi hook chạy trong subprocess riêng, nhận JSON qua stdin và trả JSON qua stdout.

Shell hooks được đăng ký bằng cách gọi agent.shell_hooks.register_from_config(cfg) tại cả khởi động CLI (hermes_cli/main.py) và khởi động gateway (gateway/run.py). Chúng kết hợp tự nhiên với các hook plugin Python — cả hai đều chạy qua cùng một dispatcher.

So sánh tổng quan

ChiềuShell hooksPlugin hooksGateway hooks
Khai báo trongKhối hooks: trong ~/.hermes/config.yamlregister() trong plugin plugin.yamlThư mục HOOK.yaml + handler.py
Nằm tại~/.hermes/agent-hooks/ (theo quy ước)~/.hermes/plugins/<name>/~/.hermes/hooks/<name>/
Ngôn ngữBất kỳ (Bash, Python, Go binary, …)Chỉ PythonChỉ Python
Chạy trongCLI + GatewayCLI + GatewayChỉ Gateway
Sự kiệnVALID_HOOKS (bao gồm subagent_stop)VALID_HOOKSVòng đời gateway (gateway:startup, agent:*, command:*)
Có thể chặn tool callCó (pre_tool_call)Có (pre_tool_call)Không
Có thể inject ngữ cảnh LLMCó (pre_llm_call)Có (pre_llm_call)Không
Đồng ýHỏi lần đầu cho mỗi cặp (event, command)Ngầm định (tin tưởng plugin Python)Ngầm định (tin tưởng thư mục)
Cách ly tiến trìnhCó (subprocess)Không (in-process)Không (in-process)

Schema cấu hình

hooks:
<event_name>: # Phải nằm trong VALID_HOOKS
- matcher: "<regex>" # Tùy chọn; dùng cho pre/post_tool_call
command: "<lệnh shell>" # Bắt buộc; chạy qua shlex.split, shell=False
timeout: <giây> # Tùy chọn; mặc định 60, tối đa 300

hooks_auto_accept: false # Xem "Mô hình đồng ý" bên dưới

Tên sự kiện phải là một trong các sự kiện plugin hook; lỗi đánh máy sẽ tạo cảnh báo "Did you mean X?" và bị bỏ qua. Các key không xác định bên trong một entry sẽ bị bỏ qua; thiếu command sẽ bị bỏ qua kèm cảnh báo. timeout > 300 sẽ bị giới hạn kèm cảnh báo.

Giao thức JSON wire

Mỗi khi sự kiện kích hoạt, Hermes sinh ra một subprocess cho mỗi hook khớp (nếu matcher cho phép), đẩy JSON payload vào stdin, và đọc stdout trở lại dạng JSON.

stdin — payload mà script nhận được:

{
"hook_event_name": "pre_tool_call",
"tool_name": "terminal",
"tool_input": {"command": "rm -rf /"},
"session_id": "sess_abc123",
"cwd": "/home/user/project",
"extra": {"task_id": "...", "tool_call_id": "..."}
}

tool_nametool_inputnull cho các sự kiện không phải tool (pre_llm_call, subagent_stop, vòng đời phiên). Dict extra chứa tất cả kwargs cụ thể theo sự kiện (user_message, conversation_history, child_role, duration_ms, …). Các giá trị không thể serialize được chuyển thành chuỗi thay vì bị bỏ qua.

stdout — phản hồi tùy chọn:

// Chặn một pre_tool_call (chấp nhận cả hai dạng; chuẩn hóa nội bộ):
{"decision": "block", "reason": "Forbidden: rm -rf"} // Kiểu Claude-Code
{"action": "block", "message": "Forbidden: rm -rf"} // Kiểu Hermes-canonical

// Inject ngữ cảnh cho pre_llm_call:
{"context": "Today is Friday, 2026-04-17"}

// Im lặng không làm gì — bất kỳ output rỗng / không khớp nào đều ổn:

JSON không hợp lệ, exit code khác 0, và timeout chỉ ghi cảnh báo nhưng không bao giờ dừng vòng lặp agent.

Ví dụ thực tế

1. Tự động format file Python sau mỗi lần ghi

# ~/.hermes/config.yaml
hooks:
post_tool_call:
- matcher: "write_file|patch"
command: "~/.hermes/agent-hooks/auto-format.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/auto-format.sh
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
[[ "$path" == *.py ]] && command -v black >/dev/null && black "$path" 2>/dev/null
printf '{}\n'

Nội dung file trong ngữ cảnh của agent không được đọc lại tự động — việc format chỉ ảnh hưởng file trên đĩa. Các lệnh read_file tiếp theo sẽ lấy phiên bản đã format.

2. Chặn lệnh terminal phá hủy

hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-rm-rf.sh"
timeout: 5
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh
payload="$(cat -)"
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/'; then
printf '{"decision": "block", "reason": "blocked: rm -rf / is not permitted"}\n'
else
printf '{}\n'
fi

3. Inject git status vào mỗi lượt (tương đương UserPromptSubmit của Claude-Code)

hooks:
pre_llm_call:
- command: "~/.hermes/agent-hooks/inject-cwd-context.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null # bỏ qua stdin payload
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
jq --null-input --arg s "$status" \
'{context: ("Uncommitted changes in cwd:\n" + $s)}'
else
printf '{}\n'
fi

Sự kiện UserPromptSubmit của Claude Code không phải là một sự kiện riêng của Hermes — pre_llm_call kích hoạt tại cùng vị trí và đã hỗ trợ inject ngữ cảnh. Sử dụng nó ở đây.

4. Ghi log mỗi lần subagent hoàn thành

hooks:
subagent_stop:
- command: "~/.hermes/agent-hooks/log-orchestration.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-orchestration.sh
log=~/.hermes/logs/orchestration.log
jq -c '{ts: now, parent: .session_id, extra: .extra}' < /dev/stdin >> "$log"
printf '{}\n'

Mô hình đồng ý

Mỗi cặp (event, command) duy nhất sẽ yêu cầu người dùng phê duyệt lần đầu tiên Hermes gặp nó, sau đó lưu quyết định vào ~/.hermes/shell-hooks-allowlist.json. Các lần chạy tiếp theo (CLI hoặc gateway) bỏ qua lời nhắc.

Ba cách bỏ qua prompt tương tác — chỉ cần một trong ba:

  1. Flag --accept-hooks trên CLI (vd: hermes --accept-hooks chat)
  2. Biến môi trường HERMES_ACCEPT_HOOKS=1
  3. hooks_auto_accept: true trong cli-config.yaml

Các lần chạy không có TTY (gateway, cron, CI) cần một trong ba cách trên — nếu không, hook mới thêm sẽ im lặng không được đăng ký và ghi cảnh báo.

Chỉnh sửa script được tin tưởng ngầm. Allowlist dựa trên chuỗi command chính xác, không phải hash của script, nên việc chỉnh sửa script trên đĩa không làm mất hiệu lực đồng ý. hermes hooks doctor sẽ báo mtime drift để bạn có thể phát hiện các thay đổi và quyết định có phê duyệt lại không.

CLI hermes hooks

LệnhChức năng
hermes hooks listLiệt kê các hook đã cấu hình với matcher, timeout, và trạng thái đồng ý
hermes hooks test <event> [--for-tool X] [--payload-file F]Kích hoạt mọi hook khớp với payload giả lập và in phản hồi đã phân tích
hermes hooks revoke <command>Xóa mọi entry allowlist khớp với <command> (có hiệu lực lần khởi động tiếp theo)
hermes hooks doctorCho mỗi hook đã cấu hình: kiểm tra exec bit, trạng thái allowlist, mtime drift, tính hợp lệ JSON output, và thời gian thực thi ước tính

Bảo mật

Shell hooks chạy với toàn bộ quyền user của bạn — cùng ranh giới tin tưởng như một cron entry hoặc shell alias. Hãy coi khối hooks: trong config.yaml là cấu hình đặc quyền:

  • Chỉ tham chiếu đến scripts bạn đã viết hoặc đã review đầy đủ.
  • Giữ scripts trong ~/.hermes/agent-hooks/ để đường dẫn dễ kiểm tra.
  • Chạy lại hermes hooks doctor sau khi bạn pull config chung để phát hiện hook mới thêm trước khi chúng được đăng ký.
  • Nếu config.yaml được quản lý phiên bản trong team, review các PR thay đổi phần hooks: giống cách bạn review cấu hình CI.

Thứ tự và ưu tiên

Cả plugin hooks Python và shell hooks đều chạy qua cùng dispatcher invoke_hook(). Plugin Python được đăng ký trước (discover_and_load()), shell hooks sau (register_from_config()), nên quyết định chặn của pre_tool_call Python có ưu tiên cao hơn trong trường hợp trùng. Quyết định chặn hợp lệ đầu tiên thắng — aggregator trả về ngay khi bất kỳ callback nào tạo ra {"action": "block", "message": str} với message không trống.