> 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-zhong/hooks.md).

# 钩子

钩子是一段你自己拥有的脚本，OpenHuman 会在特定时刻运行它——在工具执行之前、文件被编辑之后、某个回合结束时——而代理会服从它的回答。你可以用它让代理遵循一条存在于你的代码库中而不是我们代码里的规则：阻止 `rm -rf`，每次编辑后运行格式化器，为每次工具调用写入一行审计记录，拒绝读取 `.env`.

这个契约故意与 [Cursor](https://cursor.com/docs/hooks)相同：相同的文件名、相同的事件名、相同的 stdin 封装、相同的 stdout 决策、相同的退出码。为任一宿主编写的钩子脚本，在另一边无需修改即可运行。

> 在这个代码库里，“hook” 还有第二种、无关的含义：位于 `crates/openhuman-core/src/agent/hooks.rs` 中的进程内 Rust trait， *嵌入式宿主* 在链接核心库编译时安装这些 trait。它们用于在 OpenHuman 之上构建产品。本页讲的是基于文件的那种，用于使用 OpenHuman。

## 文件

`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$" }
    ]
  }
}
```

读取四个位置，并且 **它们会拼接——更具体的文件不能移除更宽泛文件的规则**:

| 层   | 路径                                                                                                                       |
| --- | ------------------------------------------------------------------------------------------------------------------------ |
| 系统  | `/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`                                                                                     |

拼接是安全的，因为 **最严格的裁决获胜**：在所有已运行的钩子中，deny 优先于 ask，ask 优先于 allow。添加一个钩子绝不会放宽另一个钩子设定的策略，这正是让仓库能够把自己的 `hooks.json` 带到已经被运维人员锁定的机器上所依赖的。

### 字段

| 字段           | 含义                                                                              |
| ------------ | ------------------------------------------------------------------------------- |
| `command`    | 要运行的程序（或者在 `"type": "prompt"`时，为提示文本）。以自身目录作为 cwd 运行。 `hooks.json` 目录作为 cwd 运行。 |
| `type`       | `command` （默认）或 `prompt`.                                                       |
| `matcher`    | 哪些出现会到达这个钩子——见下文。不写则表示全部。                                                       |
| `timeout`    | 秒。回退到 `[hooks] default_timeout_secs` (30).                                      |
| `failClosed` | 将崩溃、缺失或超时的钩子视为拒绝。默认 `false`.                                                    |
| `loop_limit` | 这个钩子每个会话最多可注入的后续次数。默认 5； `0` 表示无限制。                                             |
| `model`      | 为某个 `prompt` 钩子覆盖模型。                                                            |
| `enabled`    | 设为 `false` 可在不删除的情况下将钩子停放起来。                                                    |

## 协议

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

| 退出     | 含义                                     |
| ------ | -------------------------------------- |
| `0`    | stdout 就是决策。空 stdout 视为无操作。            |
| `2`    | 拒绝，不管 stdout 说了什么。stderr 会成为告诉代理的理由。   |
| 其他任何情况 | 失败。 **失败时放行** ——操作继续——除非 `failClosed`. |

超时、缺少解释器以及无法解析的 stdout 都走同一条失败路径。这种对称性正是重点：只有在能运行时才拒绝的钩子不是安全控制，因此 `failClosed` 覆盖了脚本可能无法答复的每一种方式。

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

### 决策对象

每个字段都是可选的，而且每个事件都会遵循它所定义的子集：

```json
{
  "permission": "allow" | "deny" | "ask",
  "user_message": "显示给人类的内容",
  "agent_message": "显示给模型的内容",
  "updated_input": { "…": "替换后的工具参数" },
  "additional_context": "附加到工具结果中的内容",
  "continue": false,
  "followup_message": "作为另一个用户回合发送",
  "env": { "KEY": "value" }
}
```

`ask` 会升级到可用的审批门；在没有审批通道的工具中间件里，它会拒绝，而不是悄悄放行。

## 事件

`hook_event_name` 在封装中告诉脚本它处于哪个时刻。名称匹配是宽松的—— `preToolUse`, `PreToolUse` 和 `pre_tool_use` 是同一个事件，而 Claude Code 的 `UserPromptSubmit` 别名为 `beforeSubmitPrompt`.

| 事件                                         | 触发                                        | 遵循                                             |
| ------------------------------------------ | ----------------------------------------- | ---------------------------------------------- |
| `preToolUse`                               | 在任何工具之前                                   | `permission`, `updated_input`, `agent_message` |
| `postToolUse`                              | 在工具成功之后                                   | `additional_context`                           |
| `postToolUseFailure`                       | 在工具失败之后                                   | —                                              |
| `beforeShellExecution`                     | 在……之前 `shell` / `node_exec` / …           | `permission`, `agent_message`                  |
| `afterShellExecution`                      | 在一个完成之后                                   | —                                              |
| `beforeReadFile`                           | 在……之前 `file_read` / `read_diff`           | `permission`                                   |
| `afterFileEdit`                            | 在……之后 `file_write` / `编辑` / `apply_patch` | —                                              |
| `beforeMCPExecution` / `afterMCPExecution` | 围绕一个 MCP 工具                               | `permission`                                   |
| `beforeSubmitPrompt`                       | 在聊天消息上，在模型                                | `继续`, `permission`, `additional_context`       |
| `subagentStart`                            | 在委派之前                                     | `permission`                                   |
| `subagentStop`                             | 之后——目前还未触发，见下文                            | `followup_message`                             |
| `停止`                                       | 在一个回合之后                                   | `followup_message`                             |
| `afterAgentResponse`                       | 在助手消息上                                    | —                                              |

`sessionStart`, `sessionEnd`, `preCompact`, `afterAgentThought` 和 `subagentStop` 都已定义——它们会解析、匹配、执行，并且可以通过 `hooks test` 进行测试——但核心尚未触发它们。（`subagentStop` 是一个险些命中的情况：处理程序 `hooks::ops::subagent_stopped` 已经完成，但进行中的 `agent/subagent_host` 切换目前只触发 `subagentStart` 并返回，而从未调用停止侧。）配置其中一个会产生一条加载警告说明这一点，并且 `hooks list` 报告 `"wired": false` 。这是故意的：一个悄无声息地从不运行的钩子，是这个系统能带给你的最糟糕的东西。

### 派生事件

OpenHuman 没有单独的“shell 执行”或“文件读取”调用点——这些都是 `shell`, `file_read` 和 `file_write` 通过普通工具接口的工具。因此 shell、文件和 MCP 事件都是 *派生的* 自工具调用，而它们的载荷会按 Cursor 钩子所期望的方式重塑：一个 `command` 字符串、一个 `file_path`，一个 `edits` 数组。通用事件和专用事件都会触发，且通用事件先触发。 `preToolUse` 以及专用事件都会触发，且通用事件先触发。

## 匹配器

一个字符串，匹配事件选择的对象：工具事件对应工具名称，shell 事件对应命令行，文件事件对应路径，subagent 事件对应代理 ID。

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

无效的正则会匹配 **什么都不** 并记录日志。

## 延迟

门控事件会按顺序运行其钩子，且回合会等待；拒绝会短路其余部分。观察型事件（`afterShellExecution`, `postToolUseFailure`, `afterAgentResponse`，……）会派发到后台任务中，且回合永远不会等待——一个卡住的审计钩子不能把代理也卡住。

当没有任何配置时，harness bridge 根本不会安装，因此未配置的宿主每次工具调用都不会付出任何代价。

## 环境

钩子进程会继承核心的环境以及：

`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" }
```

每个事件都要消耗一次模型调用，所以把它放在少见但高风险的时刻——不要放在每次工具调用上。

## 检查与调试

三个 RPC 方法，位于 `hooks` 命名空间：

```bash
openhuman hooks list      # 配置了什么、来自哪个文件，以及是否已接线
openhuman hooks reload    # 重新读取每一层
openhuman 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，也不安装 bridge
default_timeout_secs = 30 # 适用于未自行指定 timeout 的钩子
```

## 示例

`.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`.

## 实现

`crates/openhuman-core/src/hooks/` — `types` （线协议契约）， `config` （文件及其分层）， `matcher`, `exec` （一个钩子：stdin、超时、退出码）， `engine` （选择、排序、汇总）， `context` （封装）， `bridge` （挂载到 harness 现有的工具与回合接口上）， `ops` （没有现有接口的那些时刻）， `后续`.
