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

概述

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

钩子处理器

与 Claude Code 的集成

运行 failproofai policies --install 后,它会将如下条目写入 ~/.claude/settings.json
Claude Code 随后会在每次工具调用前将 failproofai --hook PreToolUse 作为子进程调用,并通过 stdin 传递 JSON 载荷。

载荷格式

对于 PostToolUse 事件,载荷还会包含 tool_result 字段,其中包含工具的输出内容。 处理器对 stdin 强制执行 1 MB 的限制。超出此限制的载荷将被丢弃,所有策略隐式允许通过。

响应格式

拒绝(PreToolUse):
拒绝(PostToolUse):
指令(除 Stop 外的任何事件):
Stop 事件的指令:
  • 退出码:2
  • 原因写入 stderr(而非 stdout)
允许:
  • 退出码:0
  • stdout 为空
带消息的允许: allow(message) 允许策略在操作被允许时向 Claude 回传信息性上下文。钩子处理器将以下 JSON 写入 stdout(这不是配置文件——这是处理器向 Claude Code 的响应,与 deny 和 instruct 响应方式相同):
  • 退出码:0(操作被允许)
  • 当多个策略返回带消息的 allow 时,消息将以换行符拼接为单个 additionalContext 字符串
  • 若没有策略提供消息,stdout 为空(与之前相同)

处理流水线

src/hooks/handler.ts 实现了完整的处理流水线:
对于典型载荷,整个过程在 100ms 内完成,无需调用任何 LLM。

配置加载

src/hooks/hooks-config.ts 实现了三层配置加载。
合并逻辑:
  • enabledPolicies - 对三个文件的并集去重
  • policyParams - 按策略键名,第一个定义该键的文件完全优先
  • customPoliciesPath - 第一个定义该值的文件优先
  • llm - 第一个定义该值的文件优先
Web 控制台使用 readHooksConfig()(仅全局配置)进行读写,因为它不以项目 cwd 调用。

策略评估

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 返回,输出 deny 响应。
  • 若收集到任何 instruct 返回,输出单个 instruct 响应,所有消息拼接在一起。
  • 否则,输出 allow 响应(stdout 为空,退出码 0)。

内置策略

src/hooks/builtin-policies.ts 将全部 39 条内置策略定义为 BuiltinPolicyDefinition 对象:
接受 params 的策略会声明一个 PolicyParamsSchema,其中包含每个参数的类型和默认值。策略评估器在调用 fn 之前将解析后的值注入 ctx.params。策略函数读取 ctx.params 时无需空值保护,因为默认值始终优先应用。 策略内部的模式匹配使用解析后的命令 token(argv),而非原始字符串匹配。这可以防止通过 Shell 操作符注入绕过(例如,针对 sudo systemctl status * 的模式不会因在命令后追加 ; rm -rf / 而被绕过)。

自定义策略

src/hooks/custom-hooks-registry.ts 实现了一个基于 globalThis 的注册表:
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:
每条记录对应一条做出非 allow 决策的策略。Allow 决策不记录日志(以保持文件精简)。

控制台架构

控制台是一个 Next.js 16 应用,使用 App Router,结合 React Server Components 和 Server Actions。
数据流:
  • 页面组件调用 lib/projects.tslib/log-entries.ts 直接从文件系统读取项目/会话数据(读取操作无 API 层)。
  • 策略页面使用 Server Actions 处理所有变更操作(切换、参数更新、安装/移除)。
  • 会话查看器解析 Claude 的 JSONL 转录格式,并渲染消息和工具调用的时间线。
关键设计决策:
  • 无数据库——所有持久化状态存储在纯文本文件中(~/.failproofai/~/.claude/projects/)。
  • 变更操作使用 Server Actions——无需为 CRUD 操作构建 REST API。
  • 读取页面使用 React Server Components——更快的首屏加载,无需客户端数据获取 bundle。
  • 仅在需要交互性时使用 Client 组件(策略切换、活动搜索、日志查看器)。

文件结构