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 hooks

HOOK.YAML

handler.py trong

~/.Hermes/hooks/ ` | Chỉ cổng | Ghi nhật ký, cảnh báo, webhook | | Plugin hooks |

ctx.register_hook() trong [plugin](/docs/user-guide/features/plugins) | CLI + Cổng | Công cụ chặn, số liệu, lan can | | **[Shell hooks](#shell-hooks)** | Khối hooks: ` trong

~/.Hermes/config.yaml ` trỏ vào tập lệnh shell | CLI + Cổng | Tập lệnh thả vào để chặn, định dạng tự động, chèn 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 phát hiện 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, Teams) 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 # Declares which events to listen for
└── handler.py # Python handler function

`

#### HOOK.YAML

`YAML
name: my-hook
description: Log all agent activity to a file
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

``` python
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):
"""Called for each subscribed event. Must be named '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ện | Khi nó cháy | Phím 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
` | Đại lý 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
` | Đại lý 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ụ

#### 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:

``` yaml

# ~/.Hermes/hooks/long-task-alert/HOOK.YAML
name: long-task-alert
description: Alert when agent is taking many steps
events:
- agent:step

`

`
``` python

# ~/.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 has been running for \{iteration} steps. Last tools: \{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:

``` yaml

# ~/.Hermes/hooks/command-logger/HOOK.YAML
name: command-logger
description: Log slash command usage
events:
- command:*

`

`
``` python

# ~/.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:

``` yaml

# ~/.Hermes/hooks/session-webhook/HOOK.YAML
name: session-webhook
description: Notify external service on new sessions
events:
- session:start
- session:reset

`

`
``` python

# ~/.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)

`

### Hướng dẫn: BOOT.md — Chạy Danh sách kiểm tra khởi động trên mỗi lần khởi động cổng

Một mẫu phổ biến từ cộng đồng: thả danh sách kiểm tra Markdown tại

~/.Hermes/BOOT.md
` và yêu cầu đại lý chạy danh sách đó một lần mỗi khi cổng khởi động. Hữu ích cho "mỗi lần khởi động, hãy kiểm tra lỗi cron qua đêm và ping tôi trên Discord nếu có lỗi xảy ra" hoặc "tóm tắt 24 giờ qua của triển khai.log và đăng lên Slack #ops."

Hướng dẫn này chỉ ra cách tự xây dựng nó như một hook do người dùng xác định. Hermes không cung cấp hook BOOT.md tích hợp sẵn — bạn thực hiện chính xác hành vi mà bạn muốn.

#### Những gì chúng tôi đang xây dựng
1. Một tệp tại

~/.Hermes/BOOT.md
` có hướng dẫn khởi động bằng ngôn ngữ tự nhiên.
2. Móc cổng kích hoạt trên
`gateway:startup

, tạo ra tác nhân một lần với mô hình/thông tin xác thực đã được giải quyết của cổng của bạn và chạy hướng dẫn BOOT.md.
3. Quy ước

[SILENT]
` để đại lý có thể từ chối gửi tin nhắn khi không có gì để báo cáo.

#### Bước 1: Viết danh sách kiểm tra của bạnTạo

~/.Hermes/BOOT.md

. Viết nó như thể bạn đang đưa ra hướng dẫn cho một trợ lý con người:

`markdown
# Startup Checklist
1. Run
`Hermes cron list
` and check if any scheduled jobs failed overnight.
2. If any failed, send a summary to Discord #ops using the
`send_message
` tool.
3. Check if

/opt/app/deploy.log
` has any ERROR lines from the last 24 hours. If yes, summarize them and include in the same Discord message.
4. If nothing went wrong, reply with only

[SILENT]
` so no message is sent.

`
``Tác nhân coi đây là một phần lời nhắc của nó, vì vậy bất cứ điều gì bạn có thể mô tả bằng ngôn ngữ đơn giản đều hoạt động - lệnh gọi công cụ, lệnh shell, gửi tin nhắn, tóm tắt tệp.

#### Bước 2: Tạo hook

``` text
~/.Hermes/hooks/boot-md/
├── HOOK.YAML
└── handler.py

`
``**

~/.Hermes/hooks/boot-md/HOOK.YAML

`
**

``` yaml
name: boot-md
description: Run ~/.Hermes/BOOT.md on gateway startup
events:

- gateway:startup

`
``**

~/.Hermes/hooks/boot-md/handler.py

**

``` python
"""Run ~/.Hermes/BOOT.md on every gateway startup."""`import logging
import threading
from pathlib import Path`logger = logging.getLogger("hooks.boot-md")

BOOT_FILE = Path.home() / ".Hermes" / "BOOT.md"

def _build_prompt(content: str) -> str:
return (
"You are running a startup boot checklist. Follow the instructions "
"below exactly.\n\n"
"---\n"
f"\{content}\n"
"---\n\n"
"Execute each instruction. Use the send_message tool to deliver any "
"messages to platforms like Discord or Slack.\n"
"If nothing needs attention and there is nothing to report, reply "
"with ONLY: [SILENT]"
)

def _run_boot_agent(content: str) -> None:
"""Spawn a one-shot agent and execute the checklist.

Uses the gateway's resolved model and runtime credentials so this works
against custom endpoints, aggregators, and OAuth-based providers alike.
"""
try:
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_context_files=True,
skip_memory=True,
max_iterations=20,
)
result = agent.run_conversation(_build_prompt(content))
response = result.get("final_response", "")
if response and "[SILENT]" not in response:
logger.info("boot-md completed: %s", response[:200])
else:
logger.info("boot-md completed (nothing to report)")
except Exception as e:
logger.error("boot-md agent failed: %s", e)

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`logger.info("Running BOOT.md (%d chars)", len(content))

# Background thread so gateway startup isn't blocked on a full agent turn.
thread = threading.Thread(
target=_run_boot_agent,
args=(content,),
name="boot-md",
daemon=True,
)
thread.start()

`
``Hai dòng chính:
-
`_resolve_gateway_model()
` đọc mô hình hiện được cấu hình của cổng.
-
`_resolve_runtime_agent_kwargs()
` phân giải thông tin xác thực của nhà cung cấp giống như cách chuyển cổng thông thường — bao gồm khóa API, URL cơ sở, mã thông báo OAuth và nhóm thông tin xác thực.

Nếu không có những thứ này,
`AIAgent()
` trần sẽ quay trở lại các giá trị mặc định tích hợp sẵn và sẽ 401 đối với bất kỳ điểm cuối không mặc định nào.

#### Bước 3: Kiểm tra

Khởi động lại cổng:

``` bash
Hermes gateway restart

`
``Xem nhật ký:

`bash
Hermes logs --follow --level INFO | grep boot-md

`
``Bạn sẽ thấy
`Running BOOT.md (N chars)
` theo sau là
`boot-md completed: ...
` (tóm tắt những gì nhân viên hỗ trợ đã làm) hoặc
`boot-md completed (nothing to report)
` khi nhân viên trả lời

[SILENT]

.

Xóa

~/.Hermes/BOOT.md
` để tắt danh sách kiểm tra — hook vẫn được tải nhưng âm thầm bỏ qua khi không có tệp.

#### Mở rộng mẫu
- **Danh sách kiểm tra nhận biết lịch trình:** tắt
`datetime.now().weekday()
` bên trong hướng dẫn của BOOT.md ("nếu là Thứ Hai, hãy kiểm tra cả nhật ký triển khai hàng tuần"). Các hướng dẫn là văn bản dạng tự do, vì vậy bất cứ điều gì mà nhân viên có thể đưa ra lý do đều là trò chơi công bằng.

- **Nhiều danh sách kiểm tra:** trỏ hook vào một tệp khác (
`STARTUP.md

,
`MORNING.md

, v.v.) và đăng ký các thư mục hook riêng biệt cho mỗi danh sách.
- **Biến thể không phải tác nhân:** nếu bạn không cần vòng lặp tác nhân đầy đủ, hãy bỏ qua hoàn toàn
`AIAgent
` và yêu cầu trình xử lý đăng thông báo cố định trực tiếp qua
`httpx

. Rẻ hơn, nhanh hơn và không phụ thuộc vào nhà cung cấp.

#### Tại sao tính năng này không được tích hợp sẵn

Phiên bản trước đó của Hermes đã vận chuyển nó dưới dạng một cái móc tích hợp và âm thầm tạo ra một tác nhân với các giá trị mặc định trần trên mỗi lần khởi động cổng. Điều đó khiến người dùng ngạc nhiên với các điểm cuối tùy chỉnh và khiến tính năng này trở nên vô hình đối với những người dùng không biết nó đang chạy. Giữ nó như một mẫu tài liệu — do bạn xây dựng, trong thư mục hooks — có nghĩa là bạn thấy chính xác những gì nó làm và chọn tham gia bằng cách ghi các tệp.

### 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, Teams). 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](#plugin-hooks).
:::

## Móc plugin`[Plugins](/docs/user-guide/features/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.

``` python
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 tham 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à đại lý 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.
- Giá trị trả về của hai hook ảnh hưởng đến hành vi: [XPROTECTX147XPROTECTX](#pre_tool_call) có thể **chặn** công cụ và [XPROTECTX148XPROTECTX](#pre_LLM_call) có thể **đưa ngữ cảnh** vào lệnh gọi LLM. Tất cả các móc khác đều là những người quan sát lửa và quên.

### Tham khảo nhanh| Móc | Kích hoạt khi | Trả về |
|------|-------------|----------|
| [XPROTECTX149XPROTECTX](#pre_tool_call) | Trước khi bất kỳ công cụ nào thực thi |

\{"action": "block", "message": str}
` phủ quyết cuộc gọi |
| [XPROTECTX151XPROTECTX](#post_tool_call) | Sau khi bất kỳ công cụ nào trả về | bỏ qua |
| [XPROTECTX152XPROTECTX](#pre_LLM_call) | Mỗi lượt một lần, trước vòng lặp gọi công cụ |

\{"context": str}
` để thêm ngữ cảnh vào thông báo của người dùng |
| [XPROTECTX154XPROTECTX](#post_LLM_call) | Mỗi lượt một lần, sau vòng gọi công cụ | bỏ qua |
| [XPROTECTX155XPROTECTX](#on_session_start) | Đã tạo phiên mới (chỉ lượt đầu tiên) | bỏ qua |
| [XPROTECTX156XPROTECTX](#on_session_end) | Phiên kết thúc | bỏ qua |
| [XPROTECTX157XPROTECTX](#on_session_finalize) | CLI/gateway phá bỏ một phiên hoạt động (xóa, lưu, thống kê) | bỏ qua |
| [XPROTECTX158XPROTECTX](#on_session_reset) | Hoán đổi cổng trong khóa phiên mới (ví dụ:

/new

,

/reset

) | bỏ qua |
| [XPROTECTX161XPROTECTX](#subagent_stop) | Một đứa trẻ
`delegate_task
` đã thoát | bỏ qua |
| [XPROTECTX163XPROTECTX](#pre_gateway_dispatch) | Gateway đã nhận được tin nhắn của người dùng, trước auth + Clark |

\{"action": "skip" \| "rewrite" \| "allow", ...}
` ảnh hưởng đến dòng chảy |
| [XPROTECTX165XPROTECTX](#pre_approval_request) | Lệnh nguy hiểm cần có sự chấp thuận của người dùng trước khi gửi lời nhắc/thông báo | bỏ qua |
| [XPROTECTX166XPROTECTX](#post_approval_response) | Người dùng đã phản hồi lời nhắc phê duyệt (hoặc đã hết thời gian chờ) | bỏ qua |
| [XPROTECTX167XPROTECTX](#transform_tool_result) | Sau khi bất kỳ công cụ nào trả về, trước khi kết quả được trả lại cho mô hình |

str
` để thay thế kết quả,
`None
` để không thay đổi |
| [XPROTECTX170XPROTECTX](#transform_terminal_output) | Bên trong công cụ
`terminal

, trước khi cắt bớt/dải ANSI/làm lại |
`str
` để thay thế đầu ra thô,
`None
` không thay đổi |
| [XPROTECTX174XPROTECTX](#transform_LLM_output) | Sau khi vòng gọi công cụ hoàn tất, trước khi phản hồi cuối cùng được gửi |

str
` để thay thế văn bản phản hồi,
`None

/trống để không thay đổi |

---

###
`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:**

``` python
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ề — phủ quyết lệnh gọi:**

`Python
return \{"action": "block", "message": "Reason the tool call was blocked"}

`
``Tác nhân làm chập mạch công cụ bằng
`message
` khi lỗi quay trở lại mô hình. Lệnh khối khớp đầu tiên sẽ thắng (plugin Python được đăng ký trước, sau đó là shell hook). Mọi giá trị trả về khác đều bị bỏ qua, do đó, các lệnh gọi lại chỉ dành cho người quan sát hiện tại vẫn tiếp tục hoạt động không thay đổi.

**Các trường hợp sử dụng:** Ghi nhật ký, kiểm tra đường đi, bộ đếm cuộc gọi công cụ, chặn các hoạt động nguy hiểm, giới hạn tốc độ, thực thi chính sách cho mỗi người dùng.

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

`Python
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:**

`Python
DANGEROUS = \{"terminal", "write_file", "patch"}

def warn_dangerous(tool_name, **kwargs):
if tool_name in DANGEROUS:
print(f"⚠ Executing potentially dangerous tool: \{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:**

`Python
def my_callback(tool_name: str, args: dict, result: str, task_id: str,
duration_ms: int, **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. |
|
`duration_ms
` |
`int
` | Thời gian gửi công cụ mất bao lâu, tính bằng mili giây (được đo bằng
`time.monotonic()
` xung quanh
`registry.dispatch()

). |`**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, tỷ lệ thành công/thất bại của công cụ theo dõi, bảng điều khiển độ trễ, cảnh báo ngân sách cho mỗi 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ụ:**

`Python
from collections import Counter, defaultdict
import JSON`_tool_counts = Counter()
_error_counts = Counter()
_latency_ms = defaultdict(list)

def track_metrics(tool_name, result, duration_ms=0, **kwargs):
_tool_counts[tool_name] += 1
_latency_ms[tool_name].append(duration_ms)
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:**

`Python
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ẫu máy (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.

`Python

# Inject context
return \{"context": "Recalled memories:\n- User likes Python\n- Working on Hermes-agent"}

# Plain string (equivalent)
return "Recalled memories:\n- User likes Python"

# No injection
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ớ:**

``` python
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:**

`Python
POLICY = "Never execute commands that delete files without explicit user confirmation."`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:**

`Python
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 đại lý 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:**

`Python
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:**

`Python
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:**

`Python
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:**

`Python
_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:**

`Python
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 đại lý đư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:

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=False
` và
`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, duy trì trạng thái phiên, 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:**

``` python
_session_caches = \{}

def cleanup_session(session_id, completed, interrupted, **kwargs):
cache = _session_caches.pop(session_id, None)
if cache:

# Flush accumulated data to disk or external service
status = "completed" if completed else ("interrupted" if interrupted else "failed")
print(f"Session \{session_id} ended: \{status}, \{cache['tool_calls']} tool calls")

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

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

``` python
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)

`

---

###
`on_session_finalize
``Kích hoạt khi CLI hoặc cổng **xóa** một phiên hoạt động — ví dụ: khi người dùng chạy

/new

, cổng GC có một phiên không hoạt động hoặc CLI thoát với một tác nhân đang hoạt động. Đây là cơ hội cuối cùng để xóa trạng thái gắn liền với phiên gửi đi trước khi danh tính của nó không còn nữa.

**Chữ ký gọi lại:**

`Python
def my_callback(session_id: str | None, platform: str, **kwargs):

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`session_id
` |
`str
` hoặc
`None
` | ID phiên đi. Có thể là
`None
` nếu không tồn tại phiên hoạt động nào. |
|
`platform
` |
`str
` |

"CLI"
` hoặc tên nền tảng nhắn tin (

"Telegram"

,

"Discord"

, v.v.). |`**Kích hoạt:** Trong
`CLI.py
` (trên

/new
` / lối ra CLI) và
`gateway/run.py
` (khi phiên được đặt lại hoặc GC'd). Luôn ghép nối với
`on_session_reset
` ở phía cổng.

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

**Các trường hợp sử dụng:** Duy trì số liệu phiên cuối cùng trước khi ID phiên bị loại bỏ, đóng tài nguyên mỗi phiên, phát ra sự kiện đo từ xa cuối cùng, thoát khỏi hàng đợi ghi.

---

###
`on_session_reset
``Kích hoạt khi cổng **hoán đổi khóa phiên mới** cho một cuộc trò chuyện đang hoạt động — người dùng đã gọi

/new

,

/reset

,

/clear
` hoặc bộ điều hợp đã chọn một phiên mới sau một cửa sổ nhàn rỗi. Điều này cho phép các plugin phản ứng với thực tế là trạng thái hội thoại đã bị xóa mà không cần đợi
`on_session_start
` tiếp theo.

**Chữ ký gọi lại:**

`Python
def my_callback(session_id: str, platform: str, **kwargs):

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`session_id
` |
`str
` | ID của phiên mới (đã được xoay sang giá trị mới). |
|
`platform
` |
`str
` | Tên nền tảng nhắn tin. |**Kích hoạt:** Trong
`gateway/run.py

, ngay sau khi khóa phiên mới được phân bổ nhưng trước khi xử lý tin nhắn gửi đến tiếp theo. Trên cổng, thứ tự là:
`on_session_finalize(old_id)
` → trao đổi →
`on_session_reset(new_id)
` →
`on_session_start(new_id)
` ở lượt vào đầu tiên.

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

**Các trường hợp sử dụng:** Đặt lại bộ nhớ đệm mỗi phiên được khóa bởi
`session_id

, phát ra phân tích "xoay vòng phiên", tạo ra một nhóm trạng thái mới.

---

Xem **[Build a Plugin guide](/docs/guides/build-a-Hermes-plugin)** để 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.

---

###
`subagent_stop
``Kích hoạt **một lần cho mỗi tác nhân con** sau khi
`delegate_task
` kết thúc. Cho dù bạn ủy quyền một nhiệm vụ hay một loạt ba nhiệm vụ, hook này sẽ kích hoạt một lần cho mỗi nhiệm vụ con, được tuần tự hóa trên luồng gốc.

**Chữ ký gọi lại:**

`Python
def my_callback(parent_session_id: str, child_role: str | None,
child_summary: str | None, child_status: str,
duration_ms: int, **kwargs):

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`parent_session_id
` |
`str
` | ID phiên của đại lý mẹ ủy quyền |
|
`child_role
` |
`str \| None
` | Thẻ vai trò người điều phối được đặt cho trẻ em (
`None
` nếu tính năng này không được bật) |
|
`child_summary
` |
`str \| None
` | Câu trả lời cuối cùng đứa trẻ gửi lại cho cha mẹ |
|
`child_status
` |
`str
` |

"completed"

,

"failed"

,

"interrupted"
` hoặc

"error"
` |
|
`duration_ms
` |
`int
` | Thời gian trên đồng hồ treo tường dành cho việc chạy của trẻ, tính bằng mili giây |

**Cháy:** Trong
`tools/delegate_tool.py

, sau khi
`ThreadPoolExecutor.as_completed()
` tiêu tốn tất cả tương lai của trẻ em. Quá trình kích hoạt được sắp xếp theo chuỗi gốc nên tác giả hook không cần phải lo lắng về việc thực thi lệnh gọi lại đồng thời.

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

**Các trường hợp sử dụng:** Ghi nhật ký hoạt động điều phối, tích lũy thời lượng con để thanh toán, viết hồ sơ kiểm tra sau ủy quyền.

**Ví dụ — nhật ký hoạt động của người điều phối:**

`Python
import logging
logger = logging.getLogger(__name__)

def log_subagent(parent_session_id, child_role, child_status, duration_ms, **kwargs):
logger.info(
"SUBAGENT parent=%s role=%s status=%s duration_ms=%d",
parent_session_id, child_role, child_status, duration_ms,
)

def register(ctx):
ctx.register_hook("subagent_stop", log_subagent)

`

:::info
Với sự ủy quyền nặng nề (ví dụ: vai trò người điều phối × 5 lá × độ sâu lồng nhau),
`subagent_stop
` sẽ kích hoạt nhiều lần mỗi lượt. Giữ cuộc gọi lại của bạn nhanh chóng; đẩy công việc đắt tiền vào hàng đợi ở chế độ nền.

:::

---

###
`pre_gateway_dispatch
``Kích hoạt **một lần cho mỗi
`MessageEvent

** đến trong cổng, sau trình bảo vệ sự kiện nội bộ nhưng **trước** xác thực/ghép nối và gửi tác nhân. Đây là điểm chặn đối với các chính sách luồng thông báo cấp cổng (cửa sổ chỉ nghe, chuyển giao của con người, định tuyến trên mỗi cuộc trò chuyện, v.v.) không hoàn toàn phù hợp với bất kỳ bộ chuyển đổi nền tảng đơn lẻ nào.

**Chữ ký gọi lại:**

``` python
def my_callback(event, gateway, session_store, **kwargs):

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`event
` |
`MessageEvent
` | Tin nhắn gửi đến được chuẩn hóa (có

.text

,

.source

,

.message_id

,

.internal

, v.v.). |
|
`gateway
` |
`GatewayRunner
` | Trình chạy cổng đang hoạt động, vì vậy các plugin có thể gọi
`gateway.adapters[platform].send(...)
` để trả lời kênh bên (thông báo của chủ sở hữu, v.v.). |
|
`session_store
` |
`SessionStore
` | Để nhập bản ghi im lặng qua
`session_store.append_to_transcript(...)

. |`**Kích hoạt:** Trong
`gateway/run.py

, bên trong
`GatewayRunner._handle_message()

, ngay sau khi
`is_internal
` được tính toán. **Các sự kiện nội bộ hoàn toàn bỏ qua hook** (chúng được tạo bởi hệ thống — các quá trình hoàn thành trong nền, v.v. — và không được giữ ở cổng theo chính sách đối với người dùng).

**Giá trị trả về:**
`None
` hoặc một lệnh. Lệnh hành động được công nhận đầu tiên sẽ thắng; kết quả plugin còn lại bị bỏ qua. Các ngoại lệ trong lệnh gọi lại plugin được phát hiện và ghi lại; cổng luôn chuyển sang chế độ gửi bình thường do lỗi.

| Trở về | Hiệu ứng |
|--------|--------|
|

\{"action": "skip", "reason": "..."}
` | Bỏ tin nhắn - không có nhân viên trả lời, không có luồng ghép nối, không có xác thực. Plugin được cho là đã xử lý nó (ví dụ: được nhập thầm vào bản ghi). |
|

\{"action": "rewrite", "text": "new text"}
` | Thay thế
`event.text

, sau đó tiếp tục gửi bình thường với sự kiện đã sửa đổi. Hữu ích cho việc thu gọn các thông báo xung quanh được lưu vào bộ đệm vào một lời nhắc duy nhất. |
|

\{"action": "allow"}
` /
`None
` | Công văn thông thường - chạy toàn bộ chuỗi vòng lặp xác thực / ghép nối / tác nhân. |

**Các trường hợp sử dụng:** Trò chuyện nhóm chỉ nghe (chỉ phản hồi khi được gắn thẻ; lưu các tin nhắn xung quanh vào ngữ cảnh); chuyển giao của con người (nhập tin nhắn của khách hàng trong im lặng trong khi chủ sở hữu xử lý cuộc trò chuyện theo cách thủ công); giới hạn tỷ lệ trên mỗi hồ sơ; định tuyến theo hướng chính sách.**Ví dụ — gửi tin nhắn trực tiếp trái phép một cách im lặng mà không kích hoạt mã ghép nối:**

`Python
def deny_unauthoriZed_dms(event, **kwargs):
src = event.source
if src.chat_type == "dm" and not _is_approved_user(src.user_id):
return \{"action": "skip", "reason": "unauthoriZed-dm"}
return None`def register(ctx):
ctx.register_hook("pre_gateway_dispatch", deny_unauthoriZed_dms)

`
``**Ví dụ — viết lại vùng đệm thông báo xung quanh thành một lời nhắc duy nhất khi được đề cập:**

`Python
_buffers = \{}

def buffer_or_rewrite(event, **kwargs):
key = (event.source.platform, event.source.chat_id)
buf = _buffers.setdefault(key, [])
if _bot_mentioned(event.text):
combined = "\n".join(buf + [event.text])
buf.clear()
return \{"action": "rewrite", "text": combined}
buf.append(event.text)
return \{"action": "skip", "reason": "ambient-buffered"}

def register(ctx):
ctx.register_hook("pre_gateway_dispatch", buffer_or_rewrite)

`

---

###
`pre_approval_request
``Kích hoạt **ngay lập tức trước khi** yêu cầu phê duyệt được hiển thị cho người dùng - bao gồm mọi bề mặt: CLI tương tác, Ink TUI, nền tảng cổng (Telegram, Discord, Slack, WhatsApp, Matrix, v.v.) và ứng dụng khách ACP (VS Code, Zed, JetBrains).

Đây là nơi thích hợp để kết nối trình thông báo tùy chỉnh — ví dụ: ứng dụng thanh menu macOS hiển thị thông báo cho phép/từ chối hoặc nhật ký kiểm tra ghi lại mọi yêu cầu phê duyệt kèm theo ngữ cảnh.

**Chữ ký gọi lại:**

`Python
def my_callback(
command: str,
description: str,
pattern_key: str,
pattern_keys: list[str],
session_key: str,
surface: str,

**kwargs,
):

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`command
` |
`str
` | Lệnh shell đang chờ phê duyệt |
|
`description
` |
`str
` | (Các) lý do con người có thể đọc được lệnh được gắn cờ (kết hợp khi có nhiều mẫu khớp với nhau) |
|
`pattern_key
` |
`str
` | Khóa mẫu chính đã kích hoạt phê duyệt (ví dụ:

"rm_rf"

,

"sudo"

) |
|
`pattern_keys
` |
`list[str]
` | Tất cả các khóa mẫu khớp với |
|
`session_key
` |
`str
` | Mã nhận dạng phiên, hữu ích để xác định phạm vi thông báo trên mỗi cuộc trò chuyện |
|
`surface
` |
`str
` |

"CLI"
` dành cho lời nhắc CLI/TUI tương tác,

"gateway"
` dành cho phê duyệt nền tảng không đồng bộ |`**Giá trị trả về:** bị bỏ qua. Móc ở đây chỉ dành cho người quan sát; họ không thể phủ quyết hoặc trả lời trước sự chấp thuận. Sử dụng [XPROTECTX372XPROTECTX](#pre_tool_call) để chặn một công cụ trước khi nó đến hệ thống phê duyệt.

**Các trường hợp sử dụng:** Thông báo trên màn hình, cảnh báo đẩy, ghi nhật ký kiểm tra, webhooks Slack, định tuyến leo thang, số liệu.

**Ví dụ — thông báo trên màn hình trên macOS:**

``` python
import subprocess`def notify_approval(command, description, session_key, **kwargs):
title = "Hermes needs approval"
body = f"\{description}: \{command[:80]}"
subprocess.Popen([
"osascript", "-e",
f'display notification "\{body}" with title "\{title}"',
])

def register(ctx):
ctx.register_hook("pre_approval_request", notify_approval)

`

---

###
`post_approval_response
``Kích hoạt **sau khi** người dùng phản hồi lời nhắc phê duyệt (hoặc lời nhắc hết thời gian chờ).

**Chữ ký gọi lại:**

`Python
def my_callback(
command: str,
description: str,
pattern_key: str,
pattern_keys: list[str],
session_key: str,
surface: str,
choice: str,

**kwargs,
):

`
``Các kwargs tương tự như
`pre_approval_request

, cộng thêm:

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`choice
` |
`str
` | Một trong các

"once"

,

"session"

,

"always"

,

"deny"
` hoặc

"timeout"
` |`**Giá trị trả về:** bị bỏ qua.

**Các trường hợp sử dụng:** Đóng thông báo phù hợp trên màn hình, ghi lại quyết định cuối cùng vào nhật ký kiểm tra, cập nhật số liệu, triển khai bộ giới hạn tốc độ.

``` python
def log_decision(command, choice, session_key, **kwargs):
logger.info("approval %s: %s for session %s", choice, command[:60], session_key)

def register(ctx):
ctx.register_hook("post_approval_response", log_decision)

`

---

###
`transform_tool_result
``Kích hoạt **sau** một công cụ trả về và **trước** kết quả được thêm vào cuộc trò chuyện. Cho phép plugin viết lại BẤT KỲ chuỗi kết quả nào của công cụ — không chỉ đầu ra đầu cuối — trước khi mô hình nhìn thấy nó.

**Chữ ký gọi lại:**

`Python
def my_callback(
tool_name: str,
arguments: dict,
result: str,
task_id: str | None,

**kwargs,
) -> str | None:

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`tool_name
` |
`str
` | Công cụ tạo ra kết quả (
`read_file

,
`web_extract

,
`delegate_task

, …). |
|
`arguments
` |
`dict
` | Đối số mà mô hình gọi là công cụ. |
|
`result
` |
`str
` | Chuỗi kết quả thô, dải sau cắt ngắn và dải sau ANSI của công cụ. |
|
`task_id
` |
`str \| None
` | ID tác vụ/phiên khi chạy trong môi trường RL/điểm chuẩn. |

**Giá trị trả về:**
`str
` để thay thế kết quả (chuỗi trả về là những gì mô hình nhìn thấy),
`None
` để giữ nguyên kết quả.

**Các trường hợp sử dụng:** Biên tập lại PII dành riêng cho tổ chức từ đầu ra
`web_extract

, gói các phản hồi công cụ JSON dài trong tiêu đề tóm tắt, đưa các gợi ý tăng cường truy xuất vào kết quả
`read_file

, viết lại báo cáo tác nhân phụ
`delegate_task
` vào lược đồ dành riêng cho dự án.

``` python
import re
SECRET = re.compile(r"sk-[A-Za-z0-9]\\{32,}")

def redact_secrets(tool_name, result, **kwargs):
if SECRET.search(result):
return SECRET.sub("[REDACTED]", result)
return None`def register(ctx):
ctx.register_hook("transform_tool_result", redact_secrets)

`
``Áp dụng cho mọi công cụ. Để biết cách viết lại chỉ dành cho terminal, hãy xem
`transform_terminal_output
` bên dưới — nó hẹp hơn và chạy sớm hơn trong quy trình (cắt trước, chỉnh sửa trước).

---

###
`transform_terminal_output
``Kích hoạt bên trong đường dẫn đầu ra tiền cảnh của công cụ
`terminal

, **trước** việc cắt bớt 50 KB mặc định, dải ANSI và biên tập bí mật. Cho phép các plugin viết lại thiết bị xuất chuẩn/thiết bị xuất chuẩn thô của lệnh shell trước khi bất kỳ quá trình xử lý xuôi dòng nào chạm vào nó.

**Chữ ký gọi lại:**

`Python
def my_callback(
command: str,
output: str,
exit_code: int,
cwd: str,
task_id: str | None,

**kwargs,
) -> str | None:

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`command
` |
`str
` | Lệnh shell tạo ra đầu ra. |
|
`output
` |
`str
` | Thiết bị xuất chuẩn/thiết bị xuất chuẩn kết hợp thô (có thể rất lớn - việc cắt bớt xảy ra sau móc). |
|
`exit_code
` |
`int
` | Mã thoát quy trình. |
|
`cwd
` |
`str
` | Thư mục làm việc lệnh chạy trong. |

**Giá trị trả về:**
`str
` để thay thế đầu ra,
`None
` để giữ nguyên.

**Các trường hợp sử dụng:** Chèn các bản tóm tắt cho các lệnh tạo ra đầu ra lớn (
`du -ah

,
`find

,
`tree

), đầu ra thẻ bằng điểm đánh dấu dành riêng cho dự án để các hook xuôi dòng biết cách xử lý nó, loại bỏ nhiễu thời gian xuất hiện giữa các lần chạy và đánh bại bộ nhớ đệm nhanh chóng.

``` python
def summarize_find(command, output, **kwargs):
if command.startswith("find ") and len(output) > 50_000:
lines = output.count("\n")
head = "\n".join(output.splitlines()[:40])
return f"\{head}\n\n[summary: \{lines} paths total, showing first 40]"
return None`def register(ctx):
ctx.register_hook("transform_terminal_output", summarize_find)

`
``Kết hợp tốt với
`transform_tool_result
` (bao gồm mọi công cụ khác).

---

###
`transform_LLM_output
``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 tất và mô hình đã tạo ra phản hồi cuối cùng, **trước** phản hồi đó được gửi đến người dùng (CLI, cổng hoặc trình gọi có lập trình). Cho phép plugin viết lại văn bản cuối cùng của trợ lý bằng các phương pháp lập trình cổ điển — không có mã thông báo suy luận bổ sung nào được ghi trên văn bản hương vị SOUL hoặc biến đổi dựa trên kỹ năng.

**Chữ ký gọi lại:**

`Python
def my_callback(
response_text: str,
session_id: str,
model: str,
platform: str,

**kwargs,
) -> str | None:

`

| Tham số | Loại | Mô tả |
|----------|------|-------------|
|
`response_text
` |
`str
` | Văn bản phản hồi cuối cùng của trợ lý cho lượt này. |
|
`session_id
` |
`str
` | ID phiên cho cuộc trò chuyện này (có thể trống đối với các lần chạy một lần). |
|
`model
` |
`str
` | Tên mẫu đã tạo ra phản hồi (ví dụ:
`Anthropic/Claude-sonnet-4.6

). |
|
`platform
` |
`str
` | Nền tảng phân phối (
`CLI

,
`Telegram

,
`Discord

, …; trống khi không được đặt). |`**Giá trị trả về:**
`str
` không trống để thay thế văn bản phản hồi,
`None
` hoặc chuỗi trống để không thay đổi. **Chuỗi không trống đầu tiên sẽ thắng** khi nhiều plugin đăng ký - phản ánh
`transform_tool_result

.

**Các trường hợp sử dụng:** Áp dụng biến đổi tính cách/từ vựng (pirate-speak, Spongebob), sắp xếp lại các mã định danh dành riêng cho người dùng khỏi văn bản cuối cùng, nối thêm chân trang chữ ký dành riêng cho dự án, thực thi hướng dẫn về kiểu nhà mà không ghi mã thông báo theo hướng dẫn SOUL.

``` python
import os, re`def spongebob(response_text, **kwargs):
if os.environ.get("SPONGEBOB_MODE") != "on":
return None # pass through unchanged
return re.sub(r"!", "!! Tartar sauce!", response_text)

def register(ctx):
ctx.register_hook("transform_LLM_output", spongebob)

`
``Móc được bảo vệ trên một phản hồi không trống, không bị gián đoạn - nó sẽ không kích hoạt khi bị ngắt bằng nút dừng hoặc các lượt trống. Các ngoại lệ được ghi lại dưới dạng cảnh báo và không phá vỡ quá trình thực thi tác nhân.

---

## Móc vỏ

Khai báo các hook-script shell trong
`CLI-config.yaml
` của bạn và Hermes sẽ chạy chúng dưới dạng các quy trình con bất cứ khi nào sự kiện hook-plugin tương ứng kích hoạt - trong cả phiên CLI và cổng. Không cần soạn thảo plugin Python.

Sử dụng móc shell khi bạn muốn một tập lệnh thả vào, một tệp (Bash, Python, bất kỳ thứ gì có Shebang) để:
- **Chặn lệnh gọi công cụ** — từ chối các lệnh
`terminal
` nguy hiểm, thực thi các chính sách trên mỗi thư mục, yêu cầu phê duyệt đối với các hoạt động phá hoại
`write_file
` /
`patch

.

- **Chạy sau lệnh gọi công cụ** — tự động định dạng tệp Python hoặc TypeScript mà tác nhân vừa viết, ghi nhật ký lệnh gọi API, kích hoạt quy trình làm việc CI.
- **Đưa ngữ cảnh vào lượt LLM tiếp theo** — thêm đầu ra
`git status

, ngày trong tuần hiện tại hoặc truy xuất tài liệu vào thông báo người dùng (xem [XPROTECTX437XPROTECTX](#pre_LLM_call)).
- **Quan sát các sự kiện trong vòng đời** — viết dòng nhật ký khi tác nhân phụ hoàn thành (
`subagent_stop

) hoặc phiên bắt đầu (
`on_session_start

).

Móc vỏ được đăng ký bằng cách gọi
`agent.shell_hooks.register_from_config(cfg)
` khi khởi động CLI (
`Hermes_CLI/main.py

) và khởi động cổng (
`gateway/run.py

). Chúng soạn thảo một cách tự nhiên bằng các móc nối plugin Python — cả hai đều thông qua cùng một bộ điều phối.

### So sánh sơ qua| Kích thước | Móc vỏ | [Plugin hooks](#plugin-hooks) | [Gateway hooks](#gateway-event-hooks) |
|----------|-------------|-------------------------------|---------------------------------------|
| Khai báo trong | Khối
`hooks:
` trong

~/.Hermes/config.yaml
` |
`register()
` trong plugin
`plugin.YAML
` | Thư mục
`HOOK.YAML

+
`handler.py
` |
| Sống dưới |

~/.Hermes/agent-hooks/
` (theo quy ước) |

~/.Hermes/plugins/<name>/
` |

~/.Hermes/hooks/<name>/
` |
| Ngôn ngữ | Bất kỳ (Bash, Python, Go nhị phân,…) | Chỉ Python | Chỉ Python |
| Chạy vào | CLI + Cổng | CLI + Cổng | Chỉ cổng |
| Sự kiện |

VALID_HOOKS
` (bao gồm
`subagent_stop

) |
`VALID_HOOKS
` | Vòng đời của cổng (
`gateway:startup

,
`agent:*

,
`command:*

) |
| Có thể chặn cuộc gọi công cụ | Có (
`pre_tool_call

) | Có (
`pre_tool_call

) | Không |
| Có thể thêm ngữ cảnh LLM | Có (
`pre_LLM_call

) | Có (
`pre_LLM_call

) | Không |
| Đồng ý | Lời nhắc sử dụng lần đầu cho mỗi cặp

(event, command)
` | Tiềm ẩn (Tin cậy plugin Python) | Tiềm ẩn (tin cậy trực tiếp) |
| Cách ly giữa các quá trình | Có (quy trình con) | Không (đang xử lý) | Không (đang xử lý) |

### Lược đồ cấu hình

``` yaml
hooks:
<event_name>: # Must be in VALID_HOOKS

- matcher: "<regex>" # Optional; used for pre/post_tool_call only
command: "<shell command>" # Required; runs via shlex.split, shell=False
timeout: <seconds> # Optional; default 60, capped at 300`hooks_auto_accept: false # See "Consent model" below

`
``Tên sự kiện phải là một trong [plugin hook events](#plugin-hooks); lỗi chính tả tạo ra "Ý bạn là X?" cảnh báo và bị bỏ qua. Các khóa không xác định bên trong một mục nhập sẽ bị bỏ qua; thiếu
`command
` là cảnh báo bỏ qua.
`timeout > 300
` được kẹp kèm theo cảnh báo.

### Giao thức dây JSON

Mỗi khi sự kiện diễn ra, Hermes sẽ tạo ra một quy trình con cho mỗi hook phù hợp (cho phép trình so khớp), chuyển tải trọng JSON tới **stdin** và đọc **stdout** trở lại dưới dạng JSON.

**stdin — tải trọng mà tập lệnh nhận được:**

``` json
{
"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
` dành cho các sự kiện không phải công cụ (
`pre_LLM_call

,
`subagent_stop

, vòng đời phiên). Lệnh
`extra
` mang tất cả các kwargs dành riêng cho sự kiện (
`user_message

,
`conversation_history

,
`child_role

,
`duration_ms

, ...). Các giá trị không thể tuần tự hóa được xâu chuỗi thay vì bị bỏ qua.

**thiết bị xuất chuẩn — phản hồi tùy chọn:**

`JSONc
// Block a pre_tool_call (both shapes accepted; normalised internally):
\{"decision": "block", "reason": "Forbidden: rm -rf"} // Claude-Code style
\{"action": "block", "message": "Forbidden: rm -rf"} // Hermes-canonical`// Inject context for pre_LLM_call:
\{"context": "Today is Friday, 2026-04-17"}`// Silent no-op — any empty / non-matching output is fine:

`
``JSON không đúng định dạng, mã thoát khác 0 và thời gian chờ sẽ ghi lại cảnh báo nhưng không bao giờ hủy vòng lặp tác nhân.

### Ví dụ đã hoạt động

#### 1. Tự động định dạng file Python sau mỗi lần ghi

`YAML

# ~/.Hermes/config.yaml
hooks:
post_tool_call:
- matcher: "write_file|patch"
command: "~/.Hermes/agent-hooks/auto-format.sh"

`

`
``` bash
#!/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'

`
``Chế độ xem tệp trong ngữ cảnh của tác nhân **không** được đọc lại tự động — việc định dạng lại chỉ ảnh hưởng đến tệp trên đĩa. Các cuộc gọi
`read_file
` tiếp theo sẽ nhận phiên bản được định dạng.

#### 2. Chặn các lệnh phá hoại
`terminal

`
``` yaml
hooks:
pre_tool_call:

- matcher: "terminal"
command: "~/.Hermes/agent-hooks/block-rm-rf.sh"
timeout: 5

`

`
``` bash
#!/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. Tiêm
`git status
` vào mỗi lượt (tương đương Claude-Code
`UserPromptSubmit

)

``` yaml
hooks:
pre_LLM_call:

- command: "~/.Hermes/agent-hooks/inject-cwd-context.sh"

`

`
``` bash
#!/usr/bin/env bash

# ~/.Hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null # discard 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 cố ý không phải là một sự kiện Hermes riêng biệt -
`pre_LLM_call
` diễn ra tại cùng một địa điểm và đã hỗ trợ chèn ngữ cảnh. Sử dụng nó ở đây.

#### 4. Ghi nhật ký mỗi lần hoàn thành của tác nhân phụ

``` yaml
hooks:
subagent_stop:

- command: "~/.Hermes/agent-hooks/log-orchestration.sh"

`

`
``` bash
#!/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ẽ nhắc người dùng phê duyệt lần đầu tiên Hermes nhìn thấy nó, sau đó vẫn giữ nguyên quyết định

~/.Hermes/shell-hooks-allowlist.JSON

. Các lần chạy tiếp theo (CLI hoặc cổng) bỏ qua lời nhắc.

Ba lối thoát bỏ qua lời nhắc tương tác — bất kỳ lối nào cũng đủ:
1. Cờ

--accept-hooks
` trên CLI (ví dụ:
`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 phải TTY (gateway, cron, CI) cần một trong ba điều này - nếu không thì mọi hook mới được thêm vào sẽ âm thầm không được đăng ký và ghi lại cảnh báo.

**Các chỉnh sửa tập lệnh được tin cậy một cách âm thầm.** Các khóa trong danh sách cho phép trên chuỗi lệnh chính xác, không phải hàm băm của tập lệnh, nên việc chỉnh sửa tập lệnh trên đĩa không làm mất hiệu lực sự đồng ý.
`Hermes hooks doctor
` gắn cờ thời gian lệch để bạn có thể phát hiện các chỉnh sửa và quyết định xem có phê duyệt lại hay không.

### CLI
`Hermes hooks

| Lệnh | Nó làm gì |
|----------|--------------|
|
`Hermes hooks list
` | Kết xuất các móc được định cấu hình với trình so khớp, thời gian chờ và trạng thái đồng ý |
|
`Hermes hooks test <event [--for-tool X] [--payload-file F]
` | Bắn mọi hook phù hợp vào tải trọng tổng hợp và in phản hồi được phân tích cú pháp |
|
`Hermes hooks revoke <command
` | Xóa mọi mục trong danh sách cho phép khớp với

<command
` (có hiệu lực vào lần khởi động lại tiếp theo) |
|
`Hermes hooks doctor
` | Đối với mỗi hook được định cấu hình: kiểm tra bit exec, trạng thái danh sách cho phép, độ trôi thời gian, tính hợp lệ của đầu ra JSON và thời gian thực hiện thô |

### Bảo mật

Shell hook chạy với **thông tin xác thực người dùng đầy đủ của bạn** — ranh giới tin cậy giống như mục nhập cron hoặc bí danh shell. Coi khối
`hooks:
` trong
`config.yaml
` là cấu hình đặc quyền:
- Chỉ tham khảo các kịch bản bạn đã viết hoặc xem xét đầy đủ.
- Giữ các tập lệnh bên trong

~/.Hermes/agent-hooks/
` để đường dẫn dễ kiểm tra.
- Chạy lại
`Hermes hooks doctor
` sau khi bạn kéo cấu hình được chia sẻ để phát hiện các hook mới được thêm vào trước khi chúng đăng ký.
- Nếu config.yaml của bạn được kiểm soát phiên bản trong một nhóm, hãy xem xét các PR thay đổi phần
`hooks:
` giống như cách bạn xem lại cấu hình CI.

### Thứ tự và quyền ưu tiên

Cả móc plugin và móc shell Python đều chảy qua cùng một bộ điều phối
`invoke_hook()

. Các plugin Python được đăng ký đầu tiên (
`discover_and_load()

), shell hook thứ hai (
`register_from_config()

), do đó, các quyết định khối Python
`pre_tool_call
` được ưu tiên trong các trường hợp ràng buộc. Khối hợp lệ đầu tiên sẽ thắng - trình tổng hợp sẽ trả về ngay khi bất kỳ lệnh gọi lại nào tạo ra

\{"action": "block", "message": str}
` với một thông báo không trống.