> ## Documentation Index
> Fetch the complete documentation index at: https://exosphere-auto-translate-docs-20260626-1023.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 自定义策略

> 用 JavaScript 编写自己的策略——强制执行规范、防止偏移、检测故障、与外部系统集成

自定义策略让你能够为任何 Agent 行为编写规则：强制执行项目规范、防止偏移、拦截破坏性操作、检测卡死的 Agent，或与 Slack、审批工作流等系统集成。它们使用与内置策略相同的钩子事件系统和 `allow`、`deny`、`instruct` 决策机制。

***

## 快速示例

```js theme={null}
// my-policies.js
import { customPolicies, allow, deny, instruct } from "failproofai";

customPolicies.add({
  name: "no-production-writes",
  description: "Block writes to paths containing 'production'",
  match: { events: ["PreToolUse"] },
  fn: async (ctx) => {
    if (ctx.toolName !== "Write" && ctx.toolName !== "Edit") return allow();
    const path = ctx.toolInput?.file_path ?? "";
    if (path.includes("production")) {
      return deny("Writes to production paths are blocked");
    }
    return allow();
  },
});
```

安装：

```bash theme={null}
failproofai policies --install --custom ./my-policies.js
```

***

## 加载自定义策略的两种方式

### 方式一：基于约定（推荐）

将 `*policies.{js,mjs,ts}` 文件放入 `.failproofai/policies/` 目录，它们会被自动加载——无需任何命令行参数或配置更改。这与 git hooks 的工作方式类似：放入文件，直接生效。

```
# 项目级——提交到 git，与团队共享
.failproofai/policies/security-policies.mjs
.failproofai/policies/workflow-policies.mjs

# 用户级——个人使用，适用于所有项目
~/.failproofai/policies/my-policies.mjs
```

**工作原理：**

* 项目目录和用户目录均会被扫描（取并集，而非先匹配者优先）
* 每个目录内的文件按字母顺序加载；可用 `01-`、`02-` 前缀控制顺序
* 仅加载匹配 `*policies.{js,mjs,ts}` 的文件，其他文件会被忽略
* 每个文件独立加载（单个文件失败时采用宽松策略）
* 可与显式 `--custom` 参数及内置策略共存

<Tip>
  基于约定的策略是为团队建立质量标准的最便捷方式。将 `.failproofai/policies/` 提交到 git，每位团队成员就能自动获得相同的规则——无需逐个开发者配置。随着团队发现新的故障模式，只需添加一个策略并推送。随着时间推移，这些策略将成为一套随每次贡献不断完善的活质量标准。
</Tip>

### 方式二：显式文件路径

```bash theme={null}
# 安装时指定自定义策略文件
failproofai policies --install --custom ./my-policies.js

# 替换策略文件路径
failproofai policies --install --custom ./new-policies.js

# 从配置中移除自定义策略路径
failproofai policies --uninstall --custom
```

解析后的绝对路径会以 `customPoliciesPath` 字段存储在 `policies-config.json` 中。每次钩子事件触发时都会重新加载该文件，事件之间不存在缓存。

### 两种方式同时使用

基于约定的策略与显式 `--custom` 文件可以共存。加载顺序如下：

1. 显式 `customPoliciesPath` 文件（如已配置）
2. 项目约定文件（`{cwd}/.failproofai/policies/`，按字母顺序）
3. 用户约定文件（`~/.failproofai/policies/`，按字母顺序）

***

## API

### 导入

```js theme={null}
import { customPolicies, allow, deny, instruct } from "failproofai";
```

### `customPolicies.add(hook)`

注册一个策略。同一文件中可多次调用以注册多个策略。

```ts theme={null}
customPolicies.add({
  name: string;                         // 必填——唯一标识符
  description?: string;                 // 显示在 `failproofai policies` 输出中
  match?: { events?: HookEventType[] }; // 按事件类型过滤；省略则匹配所有事件
  fn: (ctx: PolicyContext) => PolicyResult | Promise<PolicyResult>;
});
```

### 决策辅助函数

| 函数                  | 效果        | 适用场景                 |
| ------------------- | --------- | -------------------- |
| `allow()`           | 静默允许操作    | 操作安全，无需消息提示          |
| `deny(message)`     | 阻止操作      | Agent 不应执行此操作        |
| `instruct(message)` | 添加上下文而不阻止 | 为 Agent 提供额外上下文以保持方向 |

`deny(message)` ——消息会以 `"Blocked by failproofai:"` 为前缀显示给 Claude。单个 `deny` 会短路所有后续评估。

`instruct(message)` ——消息会附加到 Claude 当前工具调用的上下文中。所有 `instruct` 消息会被累积并一起传递。

<Tip>
  你可以通过在 `policyParams` 中添加 `hint` 字段，为任何 `deny` 或 `instruct` 消息附加额外的指引——无需修改代码。这同样适用于自定义（`custom/`）、项目约定（`.failproofai-project/`）和用户约定（`.failproofai-user/`）策略。详见 [配置 → hint](/zh/configuration#hint-cross-cutting)。
</Tip>

### 信息性 allow 消息

`allow(message)` 允许操作**并**向 Claude 发送一条信息性消息。该消息以 `additionalContext` 的形式通过钩子处理器的 stdout 响应传递——与 `instruct` 使用相同的机制，但语义不同：它是一个状态更新，而非警告。

| 函数               | 效果                | 适用场景                |
| ---------------- | ----------------- | ------------------- |
| `allow(message)` | 允许并向 Claude 发送上下文 | 确认检查已通过，或说明为何跳过某项检查 |

使用场景：

* **状态确认：** `allow("All CI checks passed.")` ——告知 Claude 一切正常
* **宽松失败说明：** `allow("GitHub CLI not installed, skipping CI check.")` ——告知 Claude 为何跳过某项检查，使其获得完整上下文
* **多条消息累积：** 若多个策略各自返回 `allow(message)`，所有消息将以换行符连接后一并传递

```js theme={null}
customPolicies.add({
  name: "confirm-branch-status",
  match: { events: ["Stop"] },
  fn: async (ctx) => {
    const cwd = ctx.session?.cwd;
    if (!cwd) return allow("No working directory, skipping branch check.");

    // ... check branch status ...
    if (allPushed) {
      return allow("Branch is up to date with remote.");
    }
    return deny("Unpushed changes detected.");
  },
});
```

### `PolicyContext` 字段

| 字段          | 类型                                     | 描述                                                       |
| ----------- | -------------------------------------- | -------------------------------------------------------- |
| `eventType` | `string`                               | `"PreToolUse"`、`"PostToolUse"`、`"Notification"`、`"Stop"` |
| `toolName`  | `string \| undefined`                  | 被调用的工具（例如 `"Bash"`、`"Write"`、`"Read"`）                   |
| `toolInput` | `Record<string, unknown> \| undefined` | 工具的输入参数                                                  |
| `payload`   | `Record<string, unknown>`              | 来自 Claude Code 的完整原始事件载荷                                 |
| `session`   | `SessionMetadata \| undefined`         | 会话上下文（见下文）                                               |

### `SessionMetadata` 字段

| 字段               | 类型       | 描述                  |
| ---------------- | -------- | ------------------- |
| `sessionId`      | `string` | Claude Code 会话标识符   |
| `cwd`            | `string` | Claude Code 会话的工作目录 |
| `transcriptPath` | `string` | 会话 JSONL 记录文件的路径    |

### 事件类型

| 事件             | 触发时机          | `toolInput` 内容                                                                                            |
| -------------- | ------------- | --------------------------------------------------------------------------------------------------------- |
| `PreToolUse`   | Claude 运行工具之前 | 工具的输入（例如 Bash 对应 `{ command: "..." }`）                                                                    |
| `PostToolUse`  | 工具执行完成之后      | 工具的输入 + `tool_result`（输出结果）                                                                               |
| `Notification` | Claude 发送通知时  | `{ message: "...", notification_type: "idle" \| "permission_prompt" \| ... }` ——钩子必须始终返回 `allow()`，不能阻止通知 |
| `Stop`         | Claude 会话结束时  | 为空                                                                                                        |

***

## 评估顺序

策略按以下顺序评估：

1. 内置策略（按定义顺序）
2. 来自 `customPoliciesPath` 的显式自定义策略（按 `.add()` 调用顺序）
3. 项目 `.failproofai/policies/` 中的约定策略（文件按字母顺序，文件内按 `.add()` 调用顺序）
4. 用户 `~/.failproofai/policies/` 中的约定策略（文件按字母顺序，文件内按 `.add()` 调用顺序）

<Note>
  第一个 `deny` 会短路所有后续策略。所有 `instruct` 消息会被累积并一起传递。
</Note>

***

## 传递性导入

自定义策略文件可以使用相对路径导入本地模块：

```js theme={null}
// my-policies.js
import { isBlockedPath } from "./utils.js";
import { checkApproval } from "./approval-client.js";

customPolicies.add({
  name: "approval-gate",
  fn: async (ctx) => {
    if (ctx.toolName !== "Bash") return allow();
    const approved = await checkApproval(ctx.toolInput?.command, ctx.session?.sessionId);
    return approved ? allow() : deny("Approval required for this command");
  },
});
```

所有从入口文件可达的相对导入均会被解析。实现方式是将 `from "failproofai"` 的导入重写为实际的 dist 路径，并创建临时 `.mjs` 文件以确保 ESM 兼容性。

***

## 事件类型过滤

使用 `match.events` 限制策略的触发时机：

```js theme={null}
customPolicies.add({
  name: "require-summary-on-stop",
  match: { events: ["Stop"] },
  fn: async (ctx) => {
    // 仅在会话结束时触发
    // ctx.session.transcriptPath 包含完整的会话日志
    return allow();
  },
});
```

完全省略 `match` 则在每种事件类型上都触发。

***

## 错误处理与失败模式

自定义策略采用**宽松失败**策略：错误不会阻止内置策略运行，也不会导致钩子处理器崩溃。

| 故障情况                     | 行为                                        |
| ------------------------ | ----------------------------------------- |
| 未设置 `customPoliciesPath` | 不运行显式自定义策略；约定策略和内置策略正常继续                  |
| 文件未找到                    | 警告记录到 `~/.failproofai/hook.log`；内置策略继续运行  |
| 语法/导入错误（显式）              | 错误记录到 `~/.failproofai/hook.log`；跳过显式自定义策略 |
| 语法/导入错误（约定）              | 错误记录；跳过该文件，其他约定文件仍正常加载                    |
| `fn` 运行时抛出异常             | 错误记录；该钩子视为 `allow`；其他钩子继续运行               |
| `fn` 执行超过 10 秒           | 超时记录；视为 `allow`                           |
| 约定目录不存在                  | 不运行约定策略；不报错                               |

<Tip>
  要调试自定义策略错误，可监听日志文件：

  ```bash theme={null}
  tail -f ~/.failproofai/hook.log
  ```
</Tip>

***

## 完整示例：多个策略

```js theme={null}
// my-policies.js
import { customPolicies, allow, deny, instruct } from "failproofai";

// 防止 Agent 向 secrets/ 目录写入
customPolicies.add({
  name: "block-secrets-dir",
  description: "Prevent agent from writing to secrets/ directory",
  match: { events: ["PreToolUse"] },
  fn: async (ctx) => {
    if (!["Write", "Edit"].includes(ctx.toolName ?? "")) return allow();
    const path = ctx.toolInput?.file_path ?? "";
    if (path.includes("secrets/")) return deny("Writing to secrets/ is not permitted");
    return allow();
  },
});

// 保持 Agent 方向正确：提交前验证测试
customPolicies.add({
  name: "remind-test-before-commit",
  description: "Keep the agent on track: verify tests pass before committing",
  match: { events: ["PreToolUse"] },
  fn: async (ctx) => {
    if (ctx.toolName !== "Bash") return allow();
    const cmd = ctx.toolInput?.command ?? "";
    if (/git\s+commit/.test(cmd)) {
      return instruct("Verify all tests pass before committing. Run `bun test` if you haven't already.");
    }
    return allow();
  },
});

// 在冻结期间防止计划外的依赖变更
customPolicies.add({
  name: "dependency-freeze",
  description: "Prevent unplanned dependency changes during freeze period",
  match: { events: ["PreToolUse"] },
  fn: async (ctx) => {
    if (ctx.toolName !== "Bash") return allow();
    const cmd = ctx.toolInput?.command ?? "";
    const isInstall = /^(npm install|yarn add|bun add|pnpm add)\s+\S/.test(cmd);
    if (isInstall && process.env.DEPENDENCY_FREEZE === "1") {
      return deny("Package installs are frozen. Unset DEPENDENCY_FREEZE to allow.");
    }
    return allow();
  },
});

export { customPolicies };
```

***

## 示例

`examples/` 目录包含可直接运行的策略文件：

| 文件                                                   | 内容                                |
| ---------------------------------------------------- | --------------------------------- |
| `examples/policies-basic.js`                         | 涵盖常见 Agent 故障模式的五个入门策略            |
| `examples/policies-advanced/index.js`                | 高级模式：传递性导入、异步调用、输出脱敏和会话结束钩子       |
| `examples/convention-policies/security-policies.mjs` | 基于约定的安全策略（阻止 .env 写入、防止 git 历史改写） |
| `examples/convention-policies/workflow-policies.mjs` | 基于约定的工作流策略（测试提醒、审计文件写入）           |

### 使用显式文件示例

```bash theme={null}
failproofai policies --install --custom ./examples/policies-basic.js
```

### 使用基于约定的示例

```bash theme={null}
# 复制到项目级
mkdir -p .failproofai/policies
cp examples/convention-policies/*.mjs .failproofai/policies/

# 或复制到用户级
mkdir -p ~/.failproofai/policies
cp examples/convention-policies/*.mjs ~/.failproofai/policies/
```

无需执行安装命令——文件会在下次钩子事件触发时自动被识别加载。
