跳转到主要内容
本文档介绍 failproofai 的内部工作机制:钩子系统如何拦截 Agent 工具调用、配置如何加载与合并、策略如何评估,以及仪表板如何监控 Agent 活动。

概述

failproofai 包含两个独立的子系统:
  1. 钩子处理器 - 一个高速 CLI 子进程,Claude Code 在每次 Agent 工具调用时调用它。负责评估策略并返回决策结果。
  2. Agent 监控器(仪表板) - 一个 Next.js Web 应用,用于监控 Agent 会话和管理策略。
两个子系统共享 ~/.failproofai/ 目录和项目 .failproofai/ 目录中的配置文件,但它们作为独立进程运行,仅通过文件系统进行通信。

钩子处理器

与 Claude Code 的集成

运行 failproofai policies --install 时,它会将如下条目写入 ~/.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "failproofai --hook PreToolUse"
          }
        ]
      }
    ],
    "PostToolUse": [ ... ]
  }
}
之后,Claude Code 会在每次工具调用前将 failproofai --hook PreToolUse 作为子进程调用,并通过 stdin 传入 JSON 负载。

负载格式

{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/myproject/sessions/abc123.jsonl",
  "cwd": "/home/user/myproject",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "sudo apt install nodejs" }
}
对于 PostToolUse 事件,负载还包含工具输出结果 tool_result 处理器强制限制 stdin 最大为 1 MB。超出此限制的负载将被丢弃,所有策略隐式允许通过。

响应格式

拒绝(PreToolUse):
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "Blocked by failproofai: sudo command blocked"
  }
}
拒绝(PostToolUse):
{
  "hookSpecificOutput": {
    "additionalContext": "Blocked by failproofai because: API key detected in output"
  }
}
指令(除 Stop 事件外的任意事件):
{
  "hookSpecificOutput": {
    "additionalContext": "Instruction from failproofai: Verify tests pass before committing."
  }
}
Stop 事件的指令:
  • 退出码:2
  • 原因写入 stderr(而非 stdout)
允许:
  • 退出码:0
  • stdout 为空
允许并携带消息: allow(message) 允许策略在操作被允许时仍向 Claude 发送信息性上下文。钩子处理器将以下 JSON 写入 stdout(这不是配置文件——这是处理器对 Claude Code 的响应,与 deny 和 instruct 响应的方式相同):
// Written to stdout by the hook handler process
{
  "hookSpecificOutput": {
    "additionalContext": "All CI checks passed on branch 'feat/my-feature'."
  }
}
  • 退出码:0(操作被允许)
  • 当多个策略返回带消息的 allow 时,其消息将以换行符拼接为一个 additionalContext 字符串
  • 若没有策略提供消息,stdout 为空(与之前行为相同)

处理流水线

src/hooks/handler.ts 实现了完整的处理流水线:
stdin JSON
  → 解析负载(最大 1 MB)
  → 提取会话元数据(session_id、cwd、tool_name、tool_input 等)
  → readMergedHooksConfig(cwd)    ← 合并项目 + 本地 + 全局配置
  → 注册已启用的内置策略(含解析后的参数)
  → 从 customPoliciesPath 加载自定义策略(如已设置)
  → 将自定义策略注册到策略注册表
  → 评估所有策略(先内置,后自定义)
      → 首个 deny 立即短路
      → instruct 决策累积
      → allow 消息累积
  → 将 JSON 决策写入 stdout
  → 将事件持久化到 ~/.failproofai/hook-activity.jsonl
  → 退出
整个过程在典型负载下耗时不超过 100ms,且无需 LLM 调用。

配置加载

src/hooks/hooks-config.ts 实现了三级范围的配置加载。
[1] {cwd}/.failproofai/policies-config.json        ← 项目级(最高优先级)
[2] {cwd}/.failproofai/policies-config.local.json  ← 本地级
[3] ~/.failproofai/policies-config.json             ← 全局级(最低优先级)
合并逻辑:
  • enabledPolicies - 三个文件取去重并集
  • policyParams - 按策略名称为键,第一个定义该键的文件完全优先
  • customPoliciesPath - 第一个定义它的文件优先
  • llm - 第一个定义它的文件优先
由于 Web 仪表板不是通过项目 cwd 调用的,它使用 readHooksConfig()(仅全局)进行读写。

策略评估

src/hooks/policy-evaluator.ts 按顺序执行策略。 对每条策略:
  1. 查找策略的 params 模式(如果有)。
  2. 从合并后的配置中读取 policyParams[policy.name]
  3. 将用户提供的值覆盖到模式默认值上,生成 ctx.params
  4. 以解析后的上下文调用 policy.fn(ctx)
  5. 若结果为 deny,立即停止并返回该决策。
  6. 若结果为 instruct,累积消息并继续。
  7. 若结果为 allow,继续处理下一条策略。
所有策略执行完毕后:
  • 若存在任意 deny,输出拒绝响应。
  • 若收集到任意 instruct,输出单条指令响应(所有消息拼接)。
  • 否则,输出允许响应(stdout 为空,退出码 0)。

内置策略

src/hooks/builtin-policies.ts 将全部 26 条内置策略定义为 BuiltinPolicyDefinition 对象:
interface BuiltinPolicyDefinition {
  name: string;
  description: string;
  fn: (ctx: PolicyContext) => PolicyResult;
  match: {
    events: HookEventType[];
    tools?: string[];
  };
  defaultEnabled: boolean;
  category: string;
  beta?: boolean;
  params?: PolicyParamsSchema;
}
接受 params 的策略会声明一个 PolicyParamsSchema,为每个参数指定类型和默认值。策略评估器在调用 fn 前将解析后的值注入 ctx.params。策略函数读取 ctx.params 时无需空值判断,因为默认值始终会被预先应用。 策略内部的模式匹配使用已解析的命令 token(argv),而非原始字符串匹配。这可防止通过 shell 操作符注入绕过策略(例如,针对 sudo systemctl status * 的模式无法通过在命令后追加 ; rm -rf / 来绕过)。

自定义策略

src/hooks/custom-hooks-registry.ts 实现了一个基于 globalThis 的注册表:
const REGISTRY_KEY = "__failproofai_custom_hooks__";

export const customPolicies = {
  add(hook: CustomHook): void { ... }
};

export function getCustomHooks(): CustomHook[] { ... }
export function clearCustomHooks(): void { ... }  // used in tests
src/hooks/custom-hooks-loader.ts 负责加载用户的策略文件:
  1. 从配置中读取 customPoliciesPath;若不存在则跳过。
  2. 解析为绝对路径;检查文件是否存在。
  3. 将所有 from "failproofai" 导入重写为实际的 dist 路径,使 customPolicies 指向同一个 globalThis 注册表。
  4. 递归重写传递性本地导入,确保 ESM 兼容性。
  5. 写入临时 .mjs 文件并通过 import() 引入入口文件。
  6. 调用 getCustomHooks() 获取已注册的钩子。
  7. finally 块中清理所有临时文件。
遇到任何错误(文件未找到、语法错误、导入失败)时,错误信息会被记录到 ~/.failproofai/hook.log,加载器返回空数组。内置策略不受影响。 自定义策略在所有内置策略之后评估。自定义策略的 deny 仍会短路后续自定义策略(但此时所有内置策略已执行完毕)。

活动日志

每次钩子事件处理后,处理器会向 ~/.failproofai/hook-activity.jsonl 追加一行 JSONL 记录:
{
  "timestamp": "2026-04-06T12:34:56.789Z",
  "sessionId": "abc123",
  "eventType": "PreToolUse",
  "toolName": "Bash",
  "policyName": "block-sudo",
  "decision": "deny",
  "reason": "sudo command blocked by failproofai",
  "durationMs": 12
}
每条记录对应一条做出非允许决策的策略。允许决策不记录日志(以保持文件精简)。

仪表板架构

仪表板是一个 Next.js 16 应用,采用 App Router,使用 React Server Components 和 Server Actions。
app/
  layout.tsx                  ← 根布局(主题、遥测、导航)
  projects/page.tsx           ← 服务端组件:列出所有 Claude 项目
  project/[name]/page.tsx     ← 服务端组件:列出项目中的会话
  project/[name]/session/
    [sessionId]/page.tsx      ← 服务端组件:渲染会话查看器
  policies/page.tsx           ← 客户端组件:策略管理 + 活动日志
  actions/
    get-hooks-config.ts       ← 读取配置 + 策略列表
    update-hooks-config.ts    ← 开启/关闭策略
    update-policy-params.ts   ← 更新策略参数
    get-hook-activity.ts      ← 分页/搜索活动日志
    install-hooks-web.ts      ← 从浏览器安装/移除钩子
  api/
    download/[project]/[session]/route.ts   ← 将会话导出为 ZIP/JSONL
数据流:
  • 页面组件调用 lib/projects.tslib/log-entries.ts,直接从文件系统读取项目/会话数据(读取操作无需 API 层)。
  • 策略页面的所有变更操作(切换、参数更新、安装/移除)均通过 Server Actions 实现。
  • 会话查看器解析 Claude 的 JSONL 转录格式,并渲染消息和工具调用的时间线。
关键设计决策:
  • 无数据库——所有持久化状态存储在普通文件中(~/.failproofai/~/.claude/projects/)。
  • 变更操作使用 Server Actions——CRUD 操作无需 REST API。
  • 读取页面使用 React Server Components——加快首次加载速度,数据获取无需客户端包。
  • 仅在需要交互的地方使用客户端组件(策略切换、活动搜索、日志查看器)。

文件结构

failproofai/
├── bin/
│   └── failproofai.mjs           # CLI 路由器(hook / dashboard / install 等)
├── src/hooks/
│   ├── handler.ts                # 钩子事件流水线
│   ├── builtin-policies.ts       # 26 条策略定义
│   ├── policy-evaluator.ts       # 策略执行引擎
│   ├── policy-registry.ts        # 策略注册与查找
│   ├── policy-types.ts           # TypeScript 接口
│   ├── hooks-config.ts           # 多级范围配置加载
│   ├── custom-hooks-registry.ts  # 基于 globalThis 的钩子注册表
│   ├── custom-hooks-loader.ts    # 用户 JS 钩子的 ESM 加载器
│   ├── manager.ts                # 安装 / 移除 / 列出操作
│   ├── install-prompt.ts         # 交互式策略选择提示
│   ├── hook-logger.ts            # 日志写入 hook.log
│   ├── hook-activity-store.ts    # 将活动持久化到 hook-activity.jsonl
│   └── llm-client.ts             # LLM API 客户端(用于 AI 驱动的策略)
├── app/                          # Next.js 仪表板(页面 + 服务端 Actions)
├── lib/                          # 共享工具库
│   ├── projects.ts               # 从文件系统枚举 Claude 项目
│   ├── log-entries.ts            # 解析 Claude 转录 JSONL 格式
│   ├── paths.ts                  # 解析系统路径
│   └── ...
├── components/                   # 共享 React UI 组件
├── contexts/                     # React Context 提供者(主题、自动刷新、遥测)
├── examples/                     # 自定义钩子文件示例
└── __tests__/                    # 单元测试与 E2E 测试