
# 插件钩子 {#plugin-hooks}

在 `index.server.ts` 中使用 `server.addHook(name, handler)`。钩子在插件启用时运行，即使没有连接的应用。`addHook` 从 Paseo 0.14 起可用；将 `requirements.paseo` 设置为你开发插件时使用的版本。

```ts
import type { PluginServerContext } from "@getpaseo/plugin/server";

export default function contribute(server: PluginServerContext) {
  const remove = server.addHook("agent.open.before", async ({ request }) => ({
    ...request,
    env: { ...request.env, REGION: "eu" },
  }));
  return remove;
}
```

## 命名与等待 {#naming-and-timing}

名称遵循 `subject.operation.position`：主体、操作、`before` 或 `after`。名称标识生命周期中的位置；Paseo 是否等待由每个钩子决定，见下表。`workspace` 包括所有工作区，`worktree` 仅包括 git worktree。

- 等待型钩子结束后 Paseo 才继续。抛出错误、返回无效数据或超过 30 秒会让操作失败，错误包含 `Plugin <id> <hook> failed: …`。修改请求的钩子收到 `{ request }`，可以返回完整的替换请求或 `undefined`。
- 通知收到事件对象，返回 `Promise<void>`。Paseo 不等待；错误会记录在插件名下，原操作继续。`agent.turn.before` 和 `agent.permission.before` 分别在轮次开始或权限进入待处理状态时发出通知。

所有处理函数都必须为异步函数。轮次完成包括成功、失败和取消，请查看 `outcome`。权限完成包括所有处理结果，请查看 `resolution`。

创建 Agent 会发生一次，`agent.create.before` 修改已保存的配置。打开 Agent 指启动 Provider 进程，包括首次运行、daemon 重启后恢复、读取历史、导入外部会话和重新加载。`agent.open.before` 为每次启动提供临时的环境变量、工具和 MCP 服务器。创建会打开 Agent；打开已有 Agent 不会创建它；关闭会结束运行中的实例。在应用中打开标签页不属于此操作。

`worktree.create.after` 在工作区注册前运行，随后是 `workspace.create.after`，两者都早于第一个 Agent 的 `agent.create.before` / `agent.open.before`。

### 从等待型钩子提前返回 {#return-early-from-a-waiting-hook}

如果操作应立即继续，可以启动独立任务并返回：

```ts
server.addHook("worktree.create.after", async ({ worktree }) => {
  void warmCaches(worktree.path);
});
```

Paseo 只观察返回的 Promise，因此独立任务没有错误报告，也没有取消 `signal`。

## 钩子一览 {#hooks}

| 钩子                      | 等待 | 载荷                                   | 可修改                                    |
| ------------------------- | ---- | -------------------------------------- | ----------------------------------------- |
| `workspace.create.before` | 是   | `{ request }`                          | 整个创建请求                              |
| `workspace.create.after`  | 否   | `{ workspace }`                        |                                           |
| `workspace.archive.after` | 否   | `{ workspace }`                        |                                           |
| `worktree.create.before`  | 是   | `{ request }`                          | 创建选项和目标路径                        |
| `worktree.create.after`   | 是   | `{ worktree }`                         | worktree 中的文件；抛出错误中止创建       |
| `agent.create.before`     | 是   | `{ request: { config, env? } }`        | 除 `cwd` 外的已保存配置；`env`            |
| `agent.create.after`      | 否   | `{ agent }`                            |                                           |
| `agent.open.before`       | 是   | `{ request }`                          | 仅用于启动的 `env`、`tools`、`mcpServers` |
| `agent.close.after`       | 否   | `{ agent }`                            |                                           |
| `agent.archive.after`     | 否   | `{ agent, archivedAt }`                |                                           |
| `agent.turn.before`       | 否   | `{ agent, turnId }`                    |                                           |
| `agent.turn.after`        | 否   | `{ agent, turnId, outcome, timeline }` |                                           |
| `agent.permission.before` | 否   | `{ agent, request }`                   |                                           |
| `agent.permission.after`  | 否   | `{ agent, requestId, resolution }`     |                                           |

## 共享载荷 {#shared-payloads}

```ts
interface PluginHookAgent {
  id: string;
  workspaceId: string | null;
  parentAgentId: string | null;
  provider: string;
  cwd: string;
  title: string | null;
}

interface PluginHookWorkspace {
  id: string;
  projectId: string;
  cwd: string;
  name: string | null;
  archivedAt: string | null;
}

type PluginTurnOutcome =
  | { kind: "completed" }
  | { kind: "failed"; error: { message: string; code?: string } }
  | { kind: "canceled"; reason: string };
```

| 字段         | 结构 / 含义                                                                              |
| ------------ | ---------------------------------------------------------------------------------------- |
| `turnId`     | Provider 报告的 `string` 或 `null`；会话重新打开后可能重复                               |
| `timeline`   | 已弃用，始终为 `[]`；通过 SDK 获取历史。                                                 |
| `request`    | SDK 的 `AgentPermissionRequest`；`kind` 为 `tool`、`plan`、`question`、`mode` 或 `other` |
| `resolution` | SDK 的 `AgentPermissionResponse`                                                         |
| `archivedAt` | 时间戳字符串                                                                             |

## workspace.create.before {#workspacecreatebefore}

收到 `{ request }`，其中包含 `source`、可选的 `title` 和 `firstAgentContext`。可替换整个显式创建请求；抛出错误、无效返回或超时会让创建失败。已有工作区和目录查找、导入不受影响。

### 选择工作区隔离方式 {#choose-workspace-isolation}

```ts
server.addHook("workspace.create.before", async ({ request }) => {
  if (request.source.kind !== "directory") {
    return request;
  }

  return {
    ...request,
    source: {
      kind: "worktree",
      cwd: request.source.path,
      action: "branch-off",
    },
  };
});
```

**结果**：显式的目录创建请求会变成 worktree 请求。已有的工作区以及目录查找/导入操作不受影响。

## workspace.create.after {#workspacecreateafter}

工作区记录创建且目录可用后触发，收到 `{ workspace }`。

```ts
server.addHook("workspace.create.after", async ({ workspace }) => {
  console.log(workspace);
});
```

## workspace.archive.after {#workspacearchiveafter}

归档状态保存后触发，收到 `{ workspace }`；worktree 清理可能尚未完成。

```ts
server.addHook("workspace.archive.after", async ({ workspace }) => {
  console.log(workspace);
});
```

## worktree.create.before {#worktreecreatebefore}

为新 worktree 选择路径和创建选项。通过应用、CLI、MCP 或创建 Agent 新建 worktree 时，Paseo 在 `git worktree add` 之前等待此钩子。钩子收到 `{ request }`，返回完整的替换请求，或返回 `undefined` 保持原值。抛出错误、返回无效数据或超过 30 秒会终止创建，错误包含插件和钩子名称。

```ts
import { basename, dirname, join } from "node:path";

server.addHook("worktree.create.before", async ({ request }) => ({
  ...request,
  path: join(
    dirname(request.sourceCheckout),
    `${basename(request.sourceCheckout)}.${request.branch.replaceAll("/", "-")}`,
  ),
}));
```

源检出目录为 `/home/me/src/paseo`、分支为 `feature/x` 时，目标为 `/home/me/src/paseo.feature-x`。完整插件见 [worktree-placement 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/worktree-placement)。

| 请求字段                                  | 含义                                                                                                                   |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `kind`                                    | `branch-off`、`checkout-branch`、`checkout-change-request` 或 `checkout-github-pr`                                     |
| `branch`                                  | 请求的本地分支名称                                                                                                     |
| `baseRef`                                 | branch-off 的起始引用；变更请求的比较基准；`checkout-branch` 不含此字段。要从引用创建分支，返回 `kind: "branch-off"`。 |
| `slug`                                    | 规范化的 worktree 名称                                                                                                 |
| `path`                                    | 绝对目标路径，初始值为已处理现有路径后缀的默认位置                                                                     |
| `repoRoot`                                | 只读，主仓库根目录                                                                                                     |
| `sourceCheckout`                          | 只读，源检出根目录，也支持从另一个 worktree 发起创建                                                                   |
| `forge`、`changeRequestNumber`、`headRef` | 变更请求检出标识；旧版 GitHub 检出用 `githubPrNumber` 代替 `forge` 和 `changeRequestNumber`                            |
| `headRepositoryOwner`、`headRepository`   | 可选的源仓库信息；`headRepository` 仅用于变更请求检出                                                                  |
| `checkoutRefs`                            | 可选的获取源：`{ remoteName?: string, remoteRef: string }[]`                                                           |
| `pushRemoteUrl`、`trackOriginHead`        | 可选的推送目标和跟踪策略                                                                                               |

返回 `path` 后，`slug` 仅用于 branch-off 分支命名；CLI 使用路径的最后一级目录名查找 worktree。

除 `repoRoot` 和 `sourceCheckout` 外，所有字段都可以修改。返回所选 `kind` 的全部必填字段。Paseo 在钩子之后解析 Git 源计划，因此钩子可以在获取引用之前修改分支和基准，而 Git 之后仍可能选择唯一的最终分支名称。

Paseo 解析父目录中的符号链接并验证返回的路径。路径必须为绝对路径且不存在，不能是文件系统根目录、用户主目录、仓库根目录、`.git` 目录内的路径或仓库的祖先目录。目标已存在时创建失败，Paseo 不会为插件选择的路径添加后缀。插件应让 Paseo 创建目录。

顺序：`worktree.create.before` → `git worktree add` → `worktree.create.after` → 注册工作区。恢复缺失的 worktree 时使用记录的原路径，不运行这两个创建钩子。

## worktree.create.after {#worktreecreateafter}

新的 git worktree 在磁盘上创建后，Paseo 会等待此钩子，然后才注册工作区、运行 setup、打开终端或启动 Agent。收到 `{ worktree: { path, branch, sourceCheckout } }`，分别是 worktree 绝对路径、分支名、来源 checkout 目录。此时还没有工作区 id。无需返回数据，直接写入 worktree 即可。抛出错误或超过 30 秒会中止创建并删除新 worktree。

此钩子覆盖新建分支、检出分支和检出 PR。恢复缺失的已归档 worktree 使用独立路径，不触发此钩子。

```ts
import { copyFile } from "node:fs/promises";

server.addHook("worktree.create.after", async ({ worktree }) => {
  await copyFile(`${worktree.sourceCheckout}/.env`, `${worktree.path}/.env`);
});
```

## agent.create.before {#agentcreatebefore}

收到 `{ request: { config, env? } }`。配置修改会保存，即使插件以后被停用或移除；`env` 只用于首次启动，不会保存。不能修改 `config.cwd`。抛出错误、无效返回或超时会让创建失败。

**`agent.create.before.config`** 使用 `AgentSessionConfig`：

| 字段                                          | 约束                                                     |
| --------------------------------------------- | -------------------------------------------------------- |
| `provider`, `model`                           | 独立的字段；修改 Provider 可能需要同时修改模型/模式/选项 |
| `modeId`, `thinkingOptionId`, `featureValues` | 特定于 Provider 的选择                                   |
| `title`, `systemPrompt`                       | Agent 配置                                               |
| `providerOptions`                             | 不透明的、特定于 Provider 的选项                         |
| `mcpServers`, `toolPolicy`                    | MCP 配置和精确的工具预批准                               |
| `cwd`                                         | 不能修改                                                 |

### 修改已保存的配置 {#change-saved-configuration}

在 `contribute(server)` 中添加这个回调，为新的 Codex Agent 设置已保存的选项：

```ts
server.addHook("agent.create.before", async ({ request }) => {
  if (request.config.provider !== "codex") {
    return request;
  }

  return {
    ...request,
    config: {
      ...request.config,
      providerOptions: {
        ...request.config.providerOptions,
        sandbox_mode: "workspace-write",
        approval_policy: "on-request",
      },
    },
  };
});
```

| 输入                                        | 结果                |
| ------------------------------------------- | ------------------- |
| `providerOptions.sandbox_mode: "read-only"` | `"workspace-write"` |
| `providerOptions.web_search: "disabled"`    | 由展开运算保留      |

所选的 Provider 会校验 `providerOptions`。显式的 Codex 沙盒和批准选项会覆盖它的模式预设。

## agent.create.after {#agentcreateafter}

常规创建完成后触发，收到 `{ agent }`；导入和恢复不会触发。

```ts
server.addHook("agent.create.after", async ({ agent }) => {
  console.log(agent);
});
```

## agent.open.before {#agentopenbefore}

收到 `{ request }`，只能修改 `env`、`tools` 和 `mcpServers`，修改只用于此次启动。其他字段只读。抛出错误、无效返回或超时会让打开操作失败。

**`agent.open.before` 请求示例：**

```json
{
  "agentId": "agent-123",
  "workspaceId": "workspace-456",
  "provider": "codex",
  "cwd": "/projects/shop",
  "title": "Workspace notes",
  "labels": { "notes/enabled": "true" },
  "tools": [],
  "mcpServers": {},
  "reason": "resume",
  "purpose": "interactive",
  "env": { "COMPANY_ENV": "development" }
}
```

| 字段          | 取值                                                                                                     |
| ------------- | -------------------------------------------------------------------------------------------------------- |
| `workspaceId` | 字符串或 `null`                                                                                          |
| `title`       | 只读字符串或 `null`；创建时使用创建请求的标题，恢复时使用已保存的标题。                                  |
| `labels`      | 只读的字符串映射；创建时使用创建请求的标签，恢复时使用已保存的标签。                                     |
| `tools`       | 可编辑的 `AgentToolDefinition[]`，仅包含此插件的工具。参阅[为 Agent 附加工具](#attach-tools-to-agents)。 |
| `mcpServers`  | 可编辑的服务器映射，仅用于启动且仅包含本插件的条目。参阅[注入 MCP 服务器](#inject-mcp-servers)。         |
| `reason`      | `create`、`resume`、`refresh`、`import`                                                                  |
| `purpose`     | `interactive`、`history`                                                                                 |
| `env`         | 启动覆盖映射；不包括 daemon 继承的环境变量。替换整个映射即可添加、替换或移除覆盖项。                     |

### 在每次打开会话时注入环境变量 {#inject-environment-variables-on-every-session-opening}

```ts
server.addHook("agent.open.before", async ({ request }) => {
  return {
    ...request,
    env: {
      ...request.env,
      COMPANY_ENV: "development",
    },
  };
});
```

会话打开时的环境变量覆盖项会在每次打开时重新计算。`agent.create.before.env` 只作用于首次启动，不会写入 Agent 的已保存配置。

### 注入 MCP 服务器 {#inject-mcp-servers}

需要将服务器保存为 Agent 配置的一部分时，使用 `agent.create.before.config.mcpServers`。
插件拥有的服务器使用下面的回调，在 `contribute(server)` 中添加，并把占位 URL 替换为你的 MCP 端点：

```ts
server.addHook("agent.open.before", async ({ request }) => {
  if (request.provider !== "codex") return;

  return {
    ...request,
    mcpServers: {
      ...request.mcpServers,
      company: { type: "http", url: "https://tools.example.com/mcp" },
    },
  };
});
```

`request.mcpServers` 使用与 `AgentSessionConfig.mcpServers` 相同的服务器配置。
每个插件的映射从空开始，只在该插件自己的回调之间累积，不包含 Agent 已保存的服务器或其他插件的服务器。
替换映射即可添加、替换或移除本插件的条目；返回 `undefined` 会保持不变。
Paseo 按插件 ID 顺序合并各插件的最终映射。

名称与已保存的服务器、Paseo 内部的 `paseo` 服务器或其他插件的服务器冲突时，会话打开失败，错误会指出服务器名称及双方来源。
不支持 MCP 的 Provider 会报 `does not support MCP servers` 错误，并指出插件名称。
`purpose: "history"` 的会话打开会忽略这些服务器。安装、停用和重新加载插件后，现有 Agent
通过[会话重新打开行为](#attach-tools-to-agents)获取变化。

### 为 Agent 附加工具 {#attach-tools-to-agents}

在 `index.server.ts` 中定义工具，并在 `server.addHook("agent.open.before", async handler)` 中附加它。
此示例只向带有 `notes/enabled: "true"` 标签的 Agent 提供 `append_workspace_note`：

```ts
import { writeFile } from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import { defineAgentTool, type PluginServerContext } from "@getpaseo/plugin/server";

const appendWorkspaceNote = defineAgentTool({
  name: "append_workspace_note",
  description: "Append a note to AGENT_NOTES.md in this agent's working directory.",
  input: z.object({ note: z.string().min(1).describe("Note to append") }),
  async execute({ note }, { agent, signal }) {
    const notePath = path.join(agent.cwd, "AGENT_NOTES.md");
    await writeFile(notePath, `${note}\n`, { encoding: "utf8", flag: "a", signal });
    return {
      text: `Saved note to ${notePath}`,
      structuredContent: { path: notePath },
    };
  },
});

export default function contribute(server: PluginServerContext) {
  server.addHook("agent.open.before", async ({ request }) => {
    if (request.labels["notes/enabled"] !== "true") return;
    return { ...request, tools: [...request.tools, appendWorkspaceNote] };
  });
  return () => {};
}
```

通过 SDK 创建 Agent 时设置标签：

```ts
const agent = await paseo.agents.create({
  config: { provider: "codex/gpt-5.4" },
  cwd: "/projects/shop",
  labels: { "notes/enabled": "true" },
});
```

使用会话打开请求中的字段选择 Agent：

| 选择依据   | 回调中的条件                                 |
| ---------- | -------------------------------------------- |
| 标签       | `request.labels["notes/enabled"] === "true"` |
| Provider   | `request.provider === "codex"`               |
| 工作区     | `request.workspaceId === "workspace-456"`    |
| 单个 Agent | `request.agentId === chosenAgentId`          |

每个插件的 `request.tools` 都从空数组开始。同一插件中的后续回调会看到前面回调返回的工具；
替换数组即可添加或移除工具。返回 `undefined` 会保留请求。`purpose: "history"` 时会忽略工具。

从 `@getpaseo/plugin/server` 导入 `defineAgentTool`、`AgentToolDefinition`、`AgentToolContext`
和 `AgentToolResult`。辅助函数会根据 Zod schema 推断验证后的输入类型：

```ts
declare function defineAgentTool<Input extends z.ZodType>(tool: {
  name: string;
  title?: string;
  description: string;
  input: Input;
  execute(
    input: z.output<Input>,
    context: AgentToolContext,
  ): AgentToolResult | Promise<AgentToolResult>;
}): AgentToolDefinition;

interface AgentToolResult {
  text?: string;
  structuredContent?: unknown;
  isError?: boolean;
}
```

| 执行 context | 值                                                                                                  |
| ------------ | --------------------------------------------------------------------------------------------------- |
| `agent`      | 发起调用的 Agent，以 `PluginHookAgent` 表示，包括 `id`、`workspaceId`、`provider`、`cwd` 和 `title` |
| `paseo`      | 已连接到此 daemon 的 SDK                                                                            |
| `signal`     | 此次工具调用的中止信号；请把它传给文件操作或外部请求                                                |

`execute` 在插件进程中运行。闭包和原始 Zod schema 保留在该进程中；只有工具元数据和调用的
输入、结果会跨进程传递。Paseo 在调用 `execute` 前验证输入。无效输入会将 schema 问题返回给
Agent。抛出的错误会转换为 `{ isError: true, text: message }`，插件继续运行。取消调用或关闭
其传输连接会中止 `signal` 并丢弃结果。工具调用最长运行 10 分钟，与钩子的 30 秒超时分开。
关闭或重新打开 Agent 会话会移除旧定义。

在 `text` 中返回模型可见的文本。`structuredContent` 必须可序列化为 JSON；省略 `text` 时，
Paseo 会将该值格式化为供模型读取的文本。设置 `isError: true` 可报告工具错误。

Paseo 通过 Agent 工具传输提供附加的工具，按 Provider 的支持情况使用 MCP 或原生注册。
同一份定义和回调适用于这两种方式。附加的工具遵循[与 Paseo 工具相同的限制](/docs/mcp#limit-paseo-tools-by-provider)。
只有该 Agent 能看到这些工具。

名称必须匹配 `/^[a-z][a-z0-9_]*$/`，且不超过 64 个字符。如果名称与同一 Agent 的 Paseo
工具或另一插件的工具冲突，会话打开会失败，并指出双方。一个插件内的重复名称也会导致
会话打开失败。回调未选中的 Agent 不受影响。

| 变更                                                  | Agent 何时收到变更                                                                                                                                                                 |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 启动、停止或重新加载声明了 `agent.open.before` 的插件 | 已打开的会话会标记为过期。每个会话在下一次空闲提示词前重新打开；正在运行的轮次会继续，不会被中断。重新打开后的下一条提示词复用该会话。                                             |
| 修改回调选择的 Agent                                  | 编辑此示例后运行 `paseo plugin reload agent-tools`，然后在 Agent 空闲时发送提示词。                                                                                                |
| 立即应用                                              | 调用 `await paseo.agents.ref(id).reload({ requestId: "reload-notes" })`。`requestId` 可选。这会重新打开 Provider 会话，以 `reason: "refresh"` 再次运行钩子，并中断正在运行的轮次。 |
| 读取当前快照                                          | 调用 `await paseo.agents.ref(id).refresh()`。这只读取状态，不会重新打开会话。                                                                                                      |

如果自动重新打开失败，触发它的提示词会返回该错误。参阅
[完整 agent-tools 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools)。

## agent.close.after {#agentcloseafter}

运行中的 Agent 实例关闭后触发，收到 `{ agent }`。

```ts
server.addHook("agent.close.after", async ({ agent }) => {
  console.log(agent);
});
```

## agent.archive.after {#agentarchiveafter}

归档状态保存后触发，收到 `{ agent, archivedAt }`；运行时清理可能尚未完成。

```ts
server.addHook("agent.archive.after", async ({ agent, archivedAt }) => {
  console.log(agent, archivedAt);
});
```

## agent.turn.before {#agentturnbefore}

实时轮次开始后触发，收到 `{ agent, turnId }`。历史重放不会触发。

```ts
server.addHook("agent.turn.before", async ({ agent, turnId }) => {
  console.log(agent, turnId);
});
```

## agent.turn.after {#agentturnafter}

实时轮次完成、失败或取消后触发，收到 `{ agent, turnId, outcome, timeline }`。`outcome.kind` 为 `completed`、`failed` 或 `canceled`。`timeline` 已弃用且始终为空；请按下面的示例通过 SDK 获取历史。

### 在轮次结束时发送后续消息 {#send-a-follow-up-when-a-turn-ends}

把 [server/inspect.ts](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts) 复制到你的插件中。该辅助文件从 `@getpaseo/protocol/agent-types` 导入类型；请把与 daemon 版本相同的 `@getpaseo/protocol` 加入开发依赖，并在加载插件之前安装它们。`latestOutputText` 会拼接最近一条用户消息之后的文本片段。

```ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { latestOutputText } from "./server/inspect";

export default function contribute(server: PluginServerContext) {
  server.addHook("agent.turn.after", async (event, context) => {
    if (event.outcome.kind === "canceled") {
      return;
    }

    const agent = context.paseo.agents.ref(event.agent.id);
    const page = await agent.timeline.refetch({ direction: "tail", limit: 20 });
    const text = latestOutputText(page.entries.map(({ item }) => item));
    if (/out of credits/i.test(text)) {
      await agent.send("Try again.");
    }
  });

  return () => {};
}
```

```text
Turn ends: "out of credits"
  → plugin sends "Try again."
  → a new turn starts
```

该处理函数只获取最近 20 条时间线条目，不检查这一页之前的输出。插件需要更多历史时，请用 `cursor` 和 `direction` 继续翻页。

这会用现有的 SDK 发送一条新消息。持续匹配会不断发送后续消息；需要时请在插件中添加次数限制或延迟。附件和工具的效果不会被重放。

## agent.permission.before {#agentpermissionbefore}

权限或问题进入待处理状态后触发，收到 `{ agent, request }`；`request` 为 `AgentPermissionRequest`。通过 SDK 回应。

### 回应权限请求 {#answer-a-permission-request}

使用同一个[辅助文件](https://github.com/getpaseo/paseo/blob/main/plugin-examples/lifecycle-actions/server/inspect.ts)中的 `shellCommand`：

```ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { shellCommand } from "./server/inspect";

export default function contribute(server: PluginServerContext) {
  server.addHook("agent.permission.before", async (event, context) => {
    const command = shellCommand(event.request);
    if (command === null) {
      return;
    }

    const agent = context.paseo.agents.ref(event.agent.id);
    if (/\brm\s+-rf\b/.test(command)) {
      await agent.respondToPermission({
        requestId: event.request.id,
        response: { behavior: "deny", message: "Recursive deletion is blocked." },
      });
      return;
    }

    if (command.trim() === "git status") {
      await agent.respondToPermission({
        requestId: event.request.id,
        response: { behavior: "allow" },
      });
    }
  });

  return () => {};
}
```

| 请求             | 结果                 |
| ---------------- | -------------------- |
| `rm -rf build`   | 拒绝                 |
| `git status`     | 批准                 |
| 其他命令或请求   | 保持待处理，留给用户 |
| 已经处理过的请求 | SDK 响应失败         |

这个正则表达式只是一个示例策略，而不是 shell 解析器。权限请求也可能是问题、计划和模式变更；请求权限不会结束当前轮次。

## agent.permission.after {#agentpermissionafter}

待处理请求得到回应或清除后触发，收到 `{ agent, requestId, resolution }`；`resolution` 为 `AgentPermissionResponse`。

```ts
server.addHook("agent.permission.after", async ({ agent, requestId, resolution }) => {
  console.log(agent, requestId, resolution);
});
```

Agent 钩子不包括 daemon 的私有临时辅助 Agent。公开的后台工作区及其普通 Agent 使用相同钩子。

## 顺序与返回值 {#ordering-and-returned-values}

```text
Creation request
  → agent.create.before hooks (plugin-ID order; registration order within each plugin)
  → resolve defaults and validate provider configuration
  → derive launch configuration with Paseo runtime tools and daemon prompt
  → agent.open.before hooks (same ordering; env and plugin-local tools and MCP servers)
  → set PASEO_AGENT_ID and PASEO_AGENT_CWD
  → open provider session and save agent configuration
```

| 回调返回                                                | 下一个回调收到                    |
| ------------------------------------------------------- | --------------------------------- |
| `{ ...request, env: { ...request.env, REGION: "eu" } }` | 之前的请求，添加或替换了 `REGION` |
| `{ ...request, env: { REGION: "eu" } }`                 | 之前的请求，整个覆盖映射被替换    |
| `undefined`                                             | 未改变的请求                      |
| 抛出错误或返回无效数据                                  | 操作失败；后面的回调不会运行      |

没有自动的深度合并。`env` 在插件之间累积；`tools` 和 `mcpServers` 只在同一插件的回调之间累积。
返回替换的映射或数组会替换该范围内之前的值。

## context 与清理 {#context-and-cleanup}

| 契约                       | 行为                                                                               |
| -------------------------- | ---------------------------------------------------------------------------------- |
| `context.paseo`            | 已连接到此 daemon 的现有 SDK                                                       |
| `context.signal`           | 在调用超时或插件停止时中止；请把它传给外部请求                                     |
| 输入数据                   | 分离的快照；通过返回的请求或 SDK 命令改变状态                                      |
| 注册结果                   | 幂等的移除函数，例如 `const remove = server.addHook(...); remove();`               |
| 重新加载、停用、移除、关闭 | 移除剩余的注册                                                                     |
| 未知的钩子名称             | 注册失败                                                                           |
| 钩子超时                   | 30 秒；会中止 signal。等待型钩子会让待处理的操作失败；事件处理函数会记录一条错误。 |
| 事件处理函数出错           | 记录在插件名下；原操作继续进行                                                     |
| 事件投递                   | 实时、尽力而为；没有重放、持久化或自动重试                                         |
| 事件并发                   | 不同的事件可能重叠；不保证回调完成的顺序                                           |

## 完整示例 {#complete-examples}

| 插件                                                                                                   | 包含                                                      |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
| [lifecycle-logger](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-logger)       | Agent、工作区和 worktree 钩子；隐去环境变量值的 JSON 日志 |
| [lifecycle-actions](https://github.com/getpaseo/paseo/tree/main/plugin-examples/lifecycle-actions)     | 后续消息、权限、环境变量、切换 Provider、选择 worktree    |
| [agent-configuration](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-configuration) | MCP 注入以及 Codex 沙盒/批准选项                          |
| [agent-tools](https://github.com/getpaseo/paseo/tree/main/plugin-examples/agent-tools)                 | 按标签选择 Agent，并在插件进程中执行文件操作的工具        |

用 `paseo plugin logs lifecycle-logger` 或主机的 `daemon.log` 读取 logger 的输出。

## 已重命名的钩子 {#renamed-hooks}

旧注册方法已弃用，会在 2027-04-30 之后移除。现有插件继续按原来的方式运行。同一插件中混合使用新旧注册方法时，处理函数按注册顺序运行。`addHook` 只接受新名称。

| 旧注册方式                                         | 新注册方式                                                |
| -------------------------------------------------- | --------------------------------------------------------- |
| `server.before("workspace.create", handler)`       | `server.addHook("workspace.create.before", asyncHandler)` |
| `server.on("workspace.created", handler)`          | `server.addHook("workspace.create.after", asyncHandler)`  |
| `server.on("workspace.archived", handler)`         | `server.addHook("workspace.archive.after", asyncHandler)` |
| `server.before("agent.create", handler)`           | `server.addHook("agent.create.before", asyncHandler)`     |
| `server.on("agent.created", handler)`              | `server.addHook("agent.create.after", asyncHandler)`      |
| `server.before("agent.session_open", handler)`     | `server.addHook("agent.open.before", asyncHandler)`       |
| `server.on("agent.closed", handler)`               | `server.addHook("agent.close.after", asyncHandler)`       |
| `server.on("agent.archived", handler)`             | `server.addHook("agent.archive.after", asyncHandler)`     |
| `server.on("agent.turn_started", handler)`         | `server.addHook("agent.turn.before", asyncHandler)`       |
| `server.on("agent.turn_ended", handler)`           | `server.addHook("agent.turn.after", asyncHandler)`        |
| `server.on("agent.permission_requested", handler)` | `server.addHook("agent.permission.before", asyncHandler)` |
| `server.on("agent.permission_resolved", handler)`  | `server.addHook("agent.permission.after", asyncHandler)`  |
