> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 自定义策略

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

自定义策略让你可以为任何 Agent 行为编写规则：强制执行项目规范、防止配置漂移、拦截破坏性操作、检测卡死的 Agent，或与 Slack、审批工作流等外部系统集成。它们使用与内置策略相同的 Hook 事件系统以及 `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` 中。每次 Hook 事件触发时都会重新加载该文件，事件之间不存在缓存。

### 两种方式同时使用

基于约定的策略与显式 `--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 发送一条信息性消息。该消息通过 Hook 处理器 stdout 响应中的 `additionalContext` 传递——与 `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" \| ... }` —— Hook 必须始终返回 `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` 则对所有事件类型触发。

***

## 错误处理与故障模式

自定义策略采用**失败开放**原则：错误不会阻止内置策略运行，也不会导致 Hook 处理器崩溃。

| 故障情况                     | 行为                                        |
| ------------------------ | ----------------------------------------- |
| 未设置 `customPoliciesPath` | 不运行显式自定义策略；约定策略和内置策略正常继续                  |
| 文件未找到                    | 警告记录到 `~/.failproofai/hook.log`；内置策略继续运行  |
| 语法/导入错误（显式）              | 错误记录到 `~/.failproofai/hook.log`；跳过显式自定义策略 |
| 语法/导入错误（约定）              | 错误记录；跳过该文件，其他约定文件继续加载                     |
| `fn` 运行时抛出异常             | 错误记录；该 Hook 视为 `allow`；其他 Hook 继续运行       |
| `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`                | 高级模式：传递性导入、异步调用、输出脱敏、会话结束 Hook    |
| `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/
```

无需安装命令——文件在下次 Hook 事件触发时会被自动加载。
