> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/hooks.md).

# 钩子

基于文件的钩子，用于观察和拦截一个代理回合，采用与 Cursor 兼容的契约。

钩子是你自己拥有的脚本。OpenHuman 会在特定时刻运行它，例如在工具执行前、文件被编辑后或一轮结束时，而代理会遵从它的回答。钩子让你可以强制执行存放在你仓库中的规则，而不是写在我们的代码里。你可以阻止 `rm -rf`，在每次编辑后运行格式化器，为每次工具调用写一条审计日志，或者拒绝读取 `.env`.

该契约与 [Cursor 的](https://cursor.com/docs/hooks)：相同的文件名、事件名称、stdin 信封、stdout 决策和退出码。为任一宿主编写的钩子脚本都能在另一方不经修改地运行。

> “Hook”在这个代码库中还有第二个、无关的含义：来自以下内容的进程内 Rust 契约 `tinyagents-hooks`。嵌入式宿主通过以下方式注册它们： `openhuman-embed`。本页讲的是基于文件的那种。

## 文件

`hooks.json`，模式版本 1：

```json
{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      {
        "command": "./.openhuman/deny-destructive.sh",
        "matcher": "^\s*(rm|dd|mkfs)\b",
        "timeout": 5,
        "failClosed": true
      }
    ],
    "afterFileEdit": [
      { "command": "./.openhuman/format.sh", "matcher": "\\.rs$" }
    ]
  }
}
```

OpenHuman 会读取四个位置并将它们拼接起来。更具体的文件不能移除更宽泛文件中的规则。

| 层级  | 路径                                                                                                                       |
| --- | ------------------------------------------------------------------------------------------------------------------------ |
| 系统  | `/etc/openhuman/hooks.json` · `/Library/Application Support/OpenHuman/hooks.json` · `%ProgramData%\OpenHuman\hooks.json` |
| 用户  | `~/.openhuman/hooks.json`                                                                                                |
| 工作区 | `<workspace_dir>/hooks.json`                                                                                             |
| 项目  | `<action_dir>/.openhuman/hooks.json`                                                                                     |

拼接是安全的，因为最严格的裁决会胜出。在所有已运行的钩子中，拒绝优先于询问，询问优先于允许。添加钩子永远不会放宽其他钩子设定的策略。因此，一个仓库可以随附自己的 `hooks.json` 并将其部署到一台操作员已经加固过的机器上。

### 字段

| 字段      | 含义                                                           |
| ------- | ------------------------------------------------------------ |
| `命令`    | 要运行的程序，或者用于 `"type": "prompt"`。它会以其所在目录 `hooks.json` 作为工作目录。 |
| `类型`    | `命令` （默认）或 `提示`.                                             |
| `匹配器`   | 哪些出现会到达此钩子。见下方的匹配器。缺省表示全部。                                   |
| `超时`    | 秒。回退到 `[hooks] default_timeout_secs` (30).                   |
| `失败即关闭` | 将崩溃、缺失或超时的钩子视为拒绝。默认 `false`.                                 |
| `循环限制`  | 此钩子每个会话可注入的后续动作。默认 5； `0` 表示无限制。                             |
| `模型`    | 用于以下项的模型覆盖： `提示` 钩子。                                         |
| `启用`    | 设置 `false` 以在不删除钩子的情况下将其停用。                                  |

## 协议

事件会以一个 JSON 对象的形式到达 stdin。决策写入 stdout。退出码决定如何读取 stdout：

| 退出码   | 含义                                   |
| ----- | ------------------------------------ |
| `0`   | stdout 即决策。空 stdout 表示不执行任何操作。       |
| `2`   | 无论 stdout 说什么，都拒绝。stderr 会成为告诉代理的理由。 |
| 其他任何值 | 失败。它会失败开放（动作继续执行），除非 `失败即关闭` 已设置。    |

超时、缺少解释器以及无法解析的 stdout 都会走同一条失败路径。一个只有在成功运行时才拒绝的钩子并不是安全控制，因此 `失败即关闭` 涵盖了脚本无法作答的所有情况。

stdout 的解析较为宽松。最后一个独立的 JSON 对象会获胜，因此在作答前记录进度的脚本可以按原样工作。

### 决策对象

所有字段都是可选的。每个事件都会遵循其定义的子集：

```json
{
  "permission": "allow" | "deny" | "ask",
  "user_message": "shown to the human",
  "agent_message": "shown to the model",
  "updated_input": { "...": "replacement tool arguments" },
  "additional_context": "appended to the tool result",
  "continue": false,
  "followup_message": "sent as another user turn",
  "env": { "KEY": "value" }
}
```

`询问` 会进入存在的审批关卡。在没有审批通道的工具中间件内部，它会拒绝，而不是静默放行。

## 事件

`钩子事件名称` 在信封中告诉脚本它正处于哪个时刻。名称是宽松匹配的： `preToolUse`, `PreToolUse` 和 `pre_tool_use` 是同一个事件，而 Claude Code 的 `UserPromptSubmit` 映射为 `beforeSubmitPrompt`.

| 事件                                         | 触发时机                      | 遵循                   |
| ------------------------------------------ | ------------------------- | -------------------- |
| `preToolUse`                               | 在任何工具之前                   | `权限`, `更新输入`, `代理消息` |
| `postToolUse`                              | 在工具成功后                    | `附加上下文`              |
| `postToolUseFailure`                       | 在工具失败后                    | -                    |
| `beforeShellExecution`                     | 之前 `shell`                | `权限`, `代理消息`         |
| `afterShellExecution`                      | 在一个完成后                    | -                    |
| `beforeReadFile`                           | 之前 `文件读取` / `读取差异`        | `权限`                 |
| `afterFileEdit`                            | 之后 `文件写入` / `编辑` / `应用补丁` | -                    |
| `beforeMCPExecution` / `afterMCPExecution` | 围绕一个 MCP 工具               | `权限`                 |
| `beforeSubmitPrompt`                       | 在聊天消息上，在模型之前              | `继续`, `权限`, `附加上下文`  |
| `subagentStart`                            | 在委派之前                     | `权限`                 |
| `subagentStop`                             | 在一个之后（尚未触发，见下文）           | `后续消息`               |
| `停止`                                       | 在一轮之后                     | `后续消息`               |
| `afterAgentResponse`                       | 在助手消息上                    | -                    |

`sessionStart`, `sessionEnd`, `preCompact`, `afterAgentThought` 和 `subagentStop` 已定义。它们会解析、匹配并执行，你可以使用 `hooks test`，但核心尚未触发它们。配置其中一个会产生加载警告，并且 `hooks list` 显示 `"wired": false` 针对它。一个悄无声息地从不运行的钩子会更糟，因此这个警告是刻意为之。

### 派生事件

OpenHuman 没有单独的“shell 执行”或“文件读取”调用点。它们是 `shell`, `文件读取` 和 `文件写入` 这些工具走的是普通工具路径。因此，shell、文件和 MCP 事件是由工具调用派生出来的，它们的载荷会按 Cursor 钩子期望的方式重塑：一个 `命令` 字符串，一个 `file_path`，一个 `edits` 数组。通用的 `preToolUse` 先触发，然后触发专门事件。

## 匹配器

匹配器是一个字符串，会与事件选择的一个主体进行匹配。对于工具事件，这个主体是工具名称；对于 shell 事件，是命令行；对于文件事件，是路径；对于子代理事件，是代理 ID。

* 缺失或 `*`：全部
* `Shell`：一个字面、大小写不敏感的名称
* `Read|Write|Shell`：或选
* `MCP:search_docs`：按名称匹配的 MCP 工具
* 任何包含标点符号的内容：正则表达式（`^rm\b`, `\.rs$`)

无效的正则表达式不会匹配任何内容，并会被记录。

## 延迟

门控事件会依次运行其钩子，回合会等待。一次拒绝会跳过其余部分。观察性事件（`afterShellExecution`, `postToolUseFailure`, `afterAgentResponse` 以及其他事件）会在后台任务中运行，回合不会等待，因此卡死的审计钩子不会卡住代理。

当没有任何配置时，不会安装 harness 桥接，因此未配置的宿主每次工具调用都不会付出任何开销。

## 环境

钩子进程会继承核心环境，并额外包含：

`OPENHUMAN_PROJECT_DIR` （也导出为 `CLAUDE_PROJECT_DIR` 和 `CURSOR_PROJECT_DIR`), `OPENHUMAN_VERSION`, `OPENHUMAN_HOOK_EVENT`, `OPENHUMAN_SESSION_ID`, `OPENHUMAN_AGENT_ID`.

## 提示钩子

使用 `"type": "prompt"` 你用英语而不是 shell 来编写策略。文本会被发送给一个模型，并将事件 JSON 替换为 `$ARGUMENTS`，而模型会回答 `{"ok": true}` 或者 `{"ok": false, "reason": "..."}`.

```json
{ "command": "如果 $ARGUMENTS 删除了 /tmp 之外的任何内容，则拒绝。", "type": "prompt" }
```

每个事件都要消耗一次模型调用，因此请将提示钩子用于罕见且高风险的时刻，而不是每次工具调用。

## 检查与调试

三条命令即可搞定：

```bash
openhuman-core hooks list      # 配置了什么、来自哪个文件，以及是否已接线
openhuman-core hooks reload    # 重新读取每一层
openhuman-core hooks test --event beforeShellExecution \
  --payload '{"command":"rm -rf /","sandbox":false}'
```

通过 JSON-RPC，它们是 `openhuman.hooks_list`, `openhuman.hooks_reload` 和 `openhuman.hooks_test`.

`hooks test` 会在前台触发一个合成事件，并报告每个匹配的钩子作出的决定。这包括观察性事件上的钩子，而真实派发会在后台运行。用它来调试钩子，而不是让代理去做危险的事情来看看规则是否触发。

## 宿主切换

`config.toml`:

```toml
[hooks]
enabled = true            # 关闭时不会读取 hooks.json，也不会安装桥接
default_timeout_secs = 30 # 适用于未指定自身超时的钩子
```

## 示例

`.openhuman/hooks.json`:

```json
{
  "version": 1,
  "hooks": {
    "beforeReadFile": [{ "command": "./.openhuman/no-secrets.sh", "matcher": "\.env" }],
    "afterFileEdit": [{ "command": "./.openhuman/fmt.sh", "matcher": "\.rs$" }]
  }
}
```

`.openhuman/no-secrets.sh`:

```sh
#!/bin/sh
echo '{"permission":"deny","agent_message":"秘密文件禁止访问。请向用户索取你需要的值。"}'
```

`.openhuman/fmt.sh`:

```sh
#!/bin/sh
cat > /dev/null            # 清空 stdin，此钩子会忽略该事件
cargo fmt >/dev/null 2>&1
echo '{}'
```

两者都需要 `chmod +x`.

## 实现

引擎以及整个 `hooks.json` 契约都位于上游的 `tinyagents_hooks::command`。这涵盖了类型、文件及其分层、匹配器、运行单个钩子（stdin、超时、退出码）、选择、排序与聚合、上下文信封以及后续动作。

`crates/openhuman-core/src/hooks/` 包含宿主端部分： `ops` （运行时生命周期）， `宿主`, `schemas` （RPC 接口）以及 `prompt_eval`。 `README.md` 代码旁边的说明文件就是权威划分。
