Skip to main content
本文档介绍 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 的响应,与拒绝和指令响应的方式相同):
  • 退出码:0(操作被放行)
  • 若多个策略均返回附带消息的 allow,其消息将以换行符连接,合并为单个 additionalContext 字符串
  • 若没有策略提供消息,则 stdout 为空(与之前的行为一致)

处理流水线

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

配置加载

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 结果,则输出拒绝响应。
  • 若收集到任意 instruct 结果,则输出单条指令响应,所有消息以换行符连接。
  • 否则,输出放行响应(stdout 为空,退出码为 0)。

内置策略

src/hooks/builtin-policies.ts 将全部 39 条内置策略定义为 BuiltinPolicyDefinition 对象:
接受 params 的策略会声明一个 PolicyParamsSchema,其中包含各参数的类型和默认值。策略评估器在调用 fn 之前,会将解析后的值注入 ctx.params。策略函数读取 ctx.params 时无需进行空值判断,因为默认值始终会被预先填充。 策略内部的模式匹配使用解析后的命令令牌(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 记录:
每条记录对应一个做出非放行决策的策略。放行决策不会被记录(以保持文件精简)。

仪表板架构

仪表板是一个 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——首次加载更快,无需为数据获取打包客户端代码。
  • 仅在需要交互性的地方使用客户端组件(策略切换、活动搜索、日志查看器)。

文件结构