Shell Hooks详解
Shell Hooks 允许用纯 Shell 脚本挂载 Hermes Agent 的生命周期钩子,无需写 Python 插件。脚本通过 subprocess 调用,JSON 作为 stdin/stdout 的通信协议。
支持的事件
| 事件 | 时机 | 能否阻止/注入 |
|---|---|---|
pre_tool_call |
工具执行前 | ✅ 可 block |
post_tool_call |
工具执行后 | ❌ 仅观察 |
pre_llm_call |
LLM 调用前 | ✅ 可注入 context |
post_llm_call |
LLM 调用后 | ❌ 仅观察 |
subagent_stop |
子 Agent 结束时 | ❌ 仅观察 |
on_session_start |
会话开始时 | ❌ 仅观察 |
on_session_end |
会话结束时 | ❌ 仅观察 |
通信协议
Hermes Shell 脚本
│ │
│ ─── stdin: JSON payload ───────► │
│ ◄──── stdout: JSON response ──── │
stdin 格式
{
"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": "..."}
}
stdout 响应
// 阻止工具
{"action": "block", "message": "禁止执行 rm -rf"}
// 注入 context
{"context": "当前 git 状态: Untracked files..."}
// 不拦截
{}
配置方式
在 ~/.hermes/config.yaml 中注册:
hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-rm-rf.sh"
timeout: 5
post_tool_call:
- matcher: "write_file|patch"
command: "~/.hermes/agent-hooks/auto-format.sh"
pre_llm_call:
- command: "~/.hermes/agent-hooks/inject-context.sh"
subagent_stop:
- command: "~/.hermes/agent-hooks/log-subagent.sh"
真实案例
案例1:阻止危险的 rm -rf 命令
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:]]+(/|/home|/root|/www)'; then
printf '{"action": "block", "message": "危险操作:禁止对系统目录执行 rm -rf"}\n'
else
printf '{}\n'
fi
案例2:Agent 写完 Python 文件后自动格式化
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')
if [[ "$path" == *.py ]]; then
if command -v black &>/dev/null; then
black "$path" 2>/dev/null
elif command -v yapf &>/dev/null; then
yapf -i "$path" 2>/dev/null
fi
fi
printf '{}\n'
案例3:每次 LLM 调用前自动注入 git status
hooks:
pre_llm_call:
- command: "~/.hermes/agent-hooks/git-status.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/git-status.sh
cat - >/dev/null
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
jq --null-input --arg s "$status" \
'{context: ("📋 当前目录有未提交变更:\n" + $s)}'
else
printf '{}\n'
fi
案例4:记录每个子 Agent 完成日志
hooks:
subagent_stop:
- command: "~/.hermes/agent-hooks/log-subagent.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-subagent.sh
log=~/.hermes/logs/subagent.log
jq -c '{
ts: now | strftime("%Y-%m-%d %H:%M:%S"),
session_id: .session_id,
event: .hook_event_name,
duration_ms: .extra.duration_ms,
exit_code: .extra.exit_code,
goal: .extra.goal[:50]
}' < /dev/stdin >> "$log"
printf '{}\n'
案例5:禁止对项目目录外的文件进行 write_file
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/restrict-write.sh
ALLOWED_DIRS=("/www/wwwroot" "/home/project" "/root/reports")
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
tool=$(echo "$payload" | jq -r '.tool_name')
if [[ "$tool" == "write_file" ]]; then
allowed=false
for dir in "${ALLOWED_DIRS[@]}"; do
if [[ "$path" == "$dir"* ]]; then
allowed=true
break
fi
done
if [[ "$allowed" == false ]]; then
printf '{"action": "block", "message": "禁止写入非授权目录: %s"}\n' "$path"
else
printf '{}\n'
fi
else
printf '{}\n'
fi
CLI 运维命令
hermes hooks list # 列出所有已注册的 hook
hermes hooks test pre_tool_call --for-tool terminal # 测试 hook
hermes hooks doctor # 检查 hook 健康状态
hermes hooks revoke "~/.hermes/agent-hooks/block-rm-rf.sh" # 撤销授权
与 Python 插件 Hook 的区别
| Shell Hooks | Python 插件 Hook | |
|---|---|---|
| 语言 | 任意(Bash/Python/Go...) | Python |
| 配置位置 | config.yaml 的 hooks: 块 |
~/.hermes/plugins/<name>/ |
| 进程隔离 | ✅ 独立 subprocess | ❌ 同进程 |
| 能 block 工具 | ✅ | ✅ |
| 能注入 LLM context | ✅ | ✅ |
| 适合场景 | 轻量拦截/日志/格式化 | 复杂状态/指标/中间件 |
核心优势:Shell Hooks 让运维人员无需写 Python 代码,直接写 Bash 脚本即可实现安全拦截、自动化格式化、日志记录等需求。