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ý qua | Chạy vào | Trường hợp sử dụng |
|---|---|---|---|
| Gateway hooks | HOOK.yaml + handler.py trong ~/.hermes/hooks/ | Chỉ Gateway | Ghi nhật ký, cảnh báo, webhook |
| Plugin hooks | ctx.register_hook() trong plugin | CLI + Gateway | Chặn công cụ, số liệu, guardrails |
| Shell hooks | Khối hooks: trong ~/.hermes/config.yaml trỏ đến shell scripts | CLI + Gateway | Script 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 defhoặcdefthô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ện | Khi nó cháy | Khóa ngữ cảnh |
|---|---|---|
gateway:startup | Quá trình cổng bắt đầu | platforms (danh sách tên nền tảng đang hoạt động) |
session:start | Đã tạo phiên nhắn tin mới | platform, user_id, session_id, session_key |
session:end | Phiên kết thúc (trước khi đặt lại) | platform, user_id, session_key |
session:reset | Người dùng chạy /new hoặc /reset | platform, user_id, session_key |
agent:start | Agent bắt đầu xử lý tin nhắn | platform, user_id, session_id, message |
agent:step | Mỗ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:end | Agent 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 thi | platform, 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.
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
- Khi khởi động cổng,
HookRegistry.discover_and_load()quét~/.Hermes/hooks/ - Mỗi thư mục con có
HOOK.YAML+handler.pyđược tải động - Trình xử lý được đăng ký cho các sự kiện đã khai báo của họ
- 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 - 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óc | Kích hoạt khi | Trả về |
|---|---|---|
| pre_tool_call | Trước khi bất kỳ công cụ nào thực thi | bỏ qua |
| post_tool_call | Sau khi bất kỳ công cụ nào trả về | bỏ qua |
| pre_LLM_call | Mỗ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_call | Mỗ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_end | Phiên kết thúc | bỏ 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ại | Mô tả |
|---|---|---|
tool_name | str | Tên công cụ sắp thực thi (ví dụ "terminal", "web_search", "read_file") |
args | dict | Các đối số mà mô hình truyền cho công cụ |
task_id | str | Mã đị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ại | Mô tả |
|---|---|---|
tool_name | str | Tên công cụ vừa thực thi |
args | dict | Các đối số mà mô hình truyền cho công cụ |
result | str | Giá trị trả về của công cụ (luôn là chuỗi JSON) |
task_id | str | Mã đị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ại | Mô tả |
|---|---|---|
session_id | str | Mã định danh duy nhất cho phiên hiện tại |
user_message | str | Tin 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_history | list | Bản sao danh sách tin nhắn đầy đủ (định dạng OpenAI: [{"role": "user", "content": "..."}]) |
is_first_turn | bool | True nếu đây là lượt đầu tiên của phiên mới, False ở các lượt tiếp theo |
model | str | Mã định danh mô hình (ví dụ: "anthropic/claude-sonnet-4-6") |
platform | str | Nơ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ại | Mô tả |
|---|---|---|
session_id | str | Mã định danh duy nhất cho phiên hiện tại |
user_message | str | Tin nhắn ban đầu của người dùng cho lượt này |
assistant_response | str | Phản hồi văn bản cuối cùng của Agent cho lượt này |
conversation_history | list | Bản sao danh sách tin nhắn đầy đủ sau khi hoàn thành lượt |
model | str | Mã định danh mô hình |
platform | str | Phiê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ại | Mô tả |
|---|---|---|
session_id | str | Mã định danh duy nhất cho phiên mới |
model | str | Mã định danh mô hình |
platform | str | Phiê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ại | Mô tả |
|---|---|---|
session_id | str | Mã định danh duy nhất cho phiên |
completed | bool | True nếu Agent đưa ra phản hồi cuối cùng, nếu không thì False |
interrupted | bool | True 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) |
model | str | Mã định danh mô hình |
platform | str | Phiên đang chạy ở đâu |
Cháy: Ở hai nơi:
run_agent.py— ở cuối mỗi cuộc gọirun_conversation(), sau khi đã dọn dẹp xong. Luôn bắn, ngay cả khi lượt bị lỗi.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à/exittrong quá trình xử lý. Trong trường hợp này làcompleted=Falsevàinterrupted=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ều | Shell hooks | Plugin hooks | Gateway hooks |
|---|---|---|---|
| Khai báo trong | Khối hooks: trong ~/.hermes/config.yaml | register() trong plugin plugin.yaml | Thư 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ỉ Python | Chỉ Python |
| Chạy trong | CLI + Gateway | CLI + Gateway | Chỉ Gateway |
| Sự kiện | VALID_HOOKS (bao gồm subagent_stop) | VALID_HOOKS | Vòng đời gateway (gateway:startup, agent:*, command:*) |
| Có thể chặn tool call | Có (pre_tool_call) | Có (pre_tool_call) | Không |
| Có thể inject ngữ cảnh LLM | Có (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ình | Có (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_name và tool_input là null 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:
- Flag
--accept-hookstrên CLI (vd:hermes --accept-hooks chat) - Biến môi trường
HERMES_ACCEPT_HOOKS=1 hooks_auto_accept: truetrongcli-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ệnh | Chức năng |
|---|---|
hermes hooks list | Liệ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 doctor | Cho 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 doctorsau 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.