Chuyển đến nội dung chính

title: Kiến trúc description: “Cách hook handler, config loading, và policy evaluation hoạt động bên trong” icon: sitemap

Tài liệu này giải thích cách failproofai hoạt động bên trong: cách hệ thống hook chặn các lệnh gọi tool của agent, cách cấu hình được tải và hợp nhất, cách các chính sách được đánh giá, và cách dashboard giám sát hoạt động của agent.

Tổng quan

failproofai có hai subsystem độc lập:
  1. Hook handler - Một CLI subprocess nhanh mà Claude Code gọi trên mỗi lệnh gọi tool của agent. Đánh giá các chính sách và trả về một quyết định.
  2. Agent Monitor (Dashboard) - Một ứng dụng web Next.js để giám sát các session agent và quản lý các chính sách.
Cả hai subsystem chia sẻ các tệp cấu hình trong ~/.failproofai/ và thư mục .failproofai/ của dự án, nhưng chúng chạy dưới dạng các process riêng biệt và chỉ giao tiếp thông qua hệ thống tệp.

Hook handler

Tích hợp với Claude Code

Khi bạn chạy failproofai policies --install, nó sẽ ghi các mục như thế này vào ~/.claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "failproofai --hook PreToolUse"
          }
        ]
      }
    ],
    "PostToolUse": [ ... ]
  }
}
Claude Code sau đó gọi failproofai --hook PreToolUse dưới dạng một subprocess trước mỗi lệnh gọi tool, truyền một payload JSON trên stdin.

Định dạng payload

{
  "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" }
}
Đối với các sự kiện PostToolUse, payload cũng chứa tool_result với kết quả đầu ra của tool. Handler thực thi giới hạn 1 MB cho stdin. Các payload vượt quá giới hạn này bị loại bỏ và tất cả các chính sách được phép một cách ngầm.

Định dạng phản hồi

Deny (PreToolUse):
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "Blocked by failproofai: sudo command blocked"
  }
}
Deny (PostToolUse):
{
  "hookSpecificOutput": {
    "additionalContext": "Blocked by failproofai because: API key detected in output"
  }
}
Instruct (bất kỳ sự kiện nào ngoại trừ Stop):
{
  "hookSpecificOutput": {
    "additionalContext": "Instruction from failproofai: Verify tests pass before committing."
  }
}
Stop event instruct:
  • Mã thoát: 2
  • Lý do được ghi vào stderr (không phải stdout)
Allow:
  • Mã thoát: 0
  • Stdout trống
Allow with message: allow(message) cho phép một chính sách gửi context thông tin trở lại Claude ngay cả khi thao tác được cho phép. Hook handler ghi JSON sau đây vào stdout (không phải một tệp cấu hình — đây là phản hồi của handler process đối với Claude Code, giống như các phản hồi deny và instruct ở trên):
// Được ghi vào stdout bởi hook handler process
{
  "hookSpecificOutput": {
    "additionalContext": "All CI checks passed on branch 'feat/my-feature'."
  }
}
  • Mã thoát: 0 (thao tác được cho phép)
  • Khi nhiều chính sách trả về allow với một thông báo, các thông báo của chúng được nối với ký tự xuống dòng thành một chuỗi additionalContext duy nhất
  • Nếu không có chính sách nào cung cấp một thông báo, stdout trống (giống như trước đây)

Processing pipeline

src/hooks/handler.ts triển khai toàn bộ pipeline:
stdin JSON
  → parse payload (max 1 MB)
  → extract session metadata (session_id, cwd, tool_name, tool_input, etc.)
  → readMergedHooksConfig(cwd)    ← merges project + local + global config
  → register enabled builtin policies with resolved params
  → load custom policies from customPoliciesPath (if set)
  → register custom policies into policy registry
  → evaluate all policies (builtins first, then custom)
      → first deny short-circuits
      → instruct decisions accumulate
      → allow messages accumulate
  → write JSON decision to stdout
  → persist event to ~/.failproofai/hook-activity.jsonl
  → exit
Toàn bộ quá trình chạy trong dưới 100ms cho các payload điển hình mà không có lệnh gọi LLM.

Configuration loading

src/hooks/hooks-config.ts triển khai config loading ba phạm vi.
[1] {cwd}/.failproofai/policies-config.json        ← project  (highest priority)
[2] {cwd}/.failproofai/policies-config.local.json  ← local
[3] ~/.failproofai/policies-config.json             ← global   (lowest priority)
Merge logic:
  • enabledPolicies - union không trùng lặp trên cả ba tệp
  • policyParams - mỗi chính sách là một key, tệp đầu tiên xác định nó sẽ giành toàn bộ
  • customPoliciesPath - tệp đầu tiên xác định nó sẽ giành
  • llm - tệp đầu tiên xác định nó sẽ giành
Dashboard web sử dụng readHooksConfig() (chỉ global) để đọc và ghi, vì nó không được gọi với một project cwd.

Policy evaluation

src/hooks/policy-evaluator.ts chạy các chính sách theo thứ tự. Đối với mỗi chính sách:
  1. Tìm kiếm schema params của chính sách (nếu có).
  2. Đọc policyParams[policy.name] từ config đã hợp nhất.
  3. Hợp nhất các giá trị do người dùng cung cấp trên các giá trị mặc định của schema để tạo ra ctx.params.
  4. Gọi policy.fn(ctx) với context đã được giải quyết.
  5. Nếu kết quả là deny, dừng ngay lập tức và trả về quyết định đó.
  6. Nếu kết quả là instruct, tích lũy thông báo và tiếp tục.
  7. Nếu kết quả là allow, tiếp tục đến chính sách tiếp theo.
Sau khi tất cả các chính sách chạy:
  • Nếu bất kỳ deny nào được trả về, phát ra phản hồi deny.
  • Nếu bất kỳ kết quả instruct nào được thu thập, phát ra một phản hồi instruct duy nhất với tất cả các thông báo được nối.
  • Nếu không, phát ra một phản hồi allow (stdout trống, exit 0).

Builtin policies

src/hooks/builtin-policies.ts định nghĩa tất cả 26 chính sách tích hợp dưới dạng các đối tượng BuiltinPolicyDefinition:
interface BuiltinPolicyDefinition {
  name: string;
  description: string;
  fn: (ctx: PolicyContext) => PolicyResult;
  match: {
    events: HookEventType[];
    tools?: string[];
  };
  defaultEnabled: boolean;
  category: string;
  beta?: boolean;
  params?: PolicyParamsSchema;
}
Các chính sách chấp nhận params khai báo một PolicyParamsSchema với các kiểu và giá trị mặc định cho mỗi tham số. Policy evaluator tiêm các giá trị đã được giải quyết vào ctx.params trước khi gọi fn. Các hàm chính sách đọc ctx.params mà không cần null-guard vì các giá trị mặc định luôn được áp dụng trước. Khớp mẫu bên trong các chính sách sử dụng các command token được phân tích cú pháp (argv), không phải khớp chuỗi thô. Điều này ngăn chặn bypass thông qua shell operator injection (ví dụ: một mẫu cho sudo systemctl status * không thể bị bypass bằng cách thêm ; rm -rf / vào command).

Custom policies

src/hooks/custom-hooks-registry.ts triển khai một registry hỗ trợ bởi 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 tải tệp chính sách của người dùng:
  1. Đọc customPoliciesPath từ cấu hình; bỏ qua nếu không có.
  2. Giải quyết đến đường dẫn tuyệt đối; kiểm tra xem tệp có tồn tại.
  3. Viết lại tất cả các import from "failproofai" đến đường dẫn dist thực tế để customPolicies giải quyết đến cùng một registry globalThis.
  4. Viết lại đệ quy các import cục bộ chuyển tiếp để đảm bảo khả năng tương thích ESM.
  5. Ghi các tệp .mjs tạm thời và import() tệp entry.
  6. Gọi getCustomHooks() để truy xuất các hook đã đăng ký.
  7. Dọn sạch tất cả các tệp tạm thời trong một khối finally.
Đối với bất kỳ lỗi nào (tệp không tìm thấy, lỗi cú pháp, lỗi import), lỗi được ghi nhật ký vào ~/.failproofai/hook.log và loader trả về một mảng trống. Các chính sách tích hợp không bị ảnh hưởng. Các chính sách tùy chỉnh được đánh giá sau tất cả các chính sách tích hợp. Một chính sách tùy chỉnh deny vẫn short-circuit các chính sách tùy chỉnh tiếp theo (nhưng tất cả các tích hợp đã chạy ở thời điểm này).

Activity logging

Sau mỗi sự kiện hook, handler nối một dòng JSONL vào ~/.failproofai/hook-activity.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
}
Một dòng cho mỗi chính sách đã đưa ra quyết định không phải allow. Các quyết định Allow không được ghi nhật ký (để giữ tệp nhỏ).

Dashboard architecture

Dashboard là một ứng dụng Next.js 16 sử dụng App Router với React Server Components và Server Actions.
app/
  layout.tsx                  ← Root layout (theme, telemetry, nav)
  projects/page.tsx           ← Server component: list all Claude projects
  project/[name]/page.tsx     ← Server component: list sessions in a project
  project/[name]/session/
    [sessionId]/page.tsx      ← Server component: render session viewer
  policies/page.tsx           ← Client component: policy management + activity log
  actions/
    get-hooks-config.ts       ← Read config + policy list
    update-hooks-config.ts    ← Toggle policy on/off
    update-policy-params.ts   ← Update policy parameters
    get-hook-activity.ts      ← Paginate/search activity log
    install-hooks-web.ts      ← Install/remove hooks from the browser
  api/
    download/[project]/[session]/route.ts   ← Export session as ZIP/JSONL
Data flow:
  • Các trang component gọi lib/projects.tslib/log-entries.ts để đọc dữ liệu dự án/session trực tiếp từ hệ thống tệp (không có lớp API cho việc đọc).
  • Trang Policies sử dụng Server Actions cho tất cả các mutations (toggle, params update, install/remove).
  • Session viewer phân tích định dạng transcript JSONL của Claude và hiển thị một timeline của các thông báo và lệnh gọi tool.
Key design decisions:
  • Không có database - tất cả trạng thái persistent nằm trong các tệp thuần túy (~/.failproofai/, ~/.claude/projects/).
  • Server Actions cho mutations - không cần REST API cho các thao tác CRUD.
  • React Server Components cho các trang đọc - tải lần đầu nhanh hơn, không có client bundle cho việc tìm nạp dữ liệu.
  • Client components chỉ khi cần tương tác (policy toggles, activity search, log viewer).

File layout

failproofai/
├── bin/
│   └── failproofai.mjs           # CLI router (hook / dashboard / install / etc.)
├── src/hooks/
│   ├── handler.ts                # Hook event pipeline
│   ├── builtin-policies.ts       # 26 policy definitions
│   ├── policy-evaluator.ts       # Policy execution engine
│   ├── policy-registry.ts        # Policy registration and lookup
│   ├── policy-types.ts           # TypeScript interfaces
│   ├── hooks-config.ts           # Multi-scope config loading
│   ├── custom-hooks-registry.ts  # globalThis-backed hook registry
│   ├── custom-hooks-loader.ts    # ESM loader for user JS hooks
│   ├── manager.ts                # install / remove / list operations
│   ├── install-prompt.ts         # Interactive policy selection prompt
│   ├── hook-logger.ts            # Logging to hook.log
│   ├── hook-activity-store.ts    # Persist activity to hook-activity.jsonl
│   └── llm-client.ts             # LLM API client (for AI-powered policies)
├── app/                          # Next.js dashboard (pages + server actions)
├── lib/                          # Shared utilities
│   ├── projects.ts               # Enumerate Claude projects from filesystem
│   ├── log-entries.ts            # Parse Claude transcript JSONL format
│   ├── paths.ts                  # Resolve system paths
│   └── ...
├── components/                   # Shared React UI components
├── contexts/                     # React context providers (theme, auto-refresh, telemetry)
├── examples/                     # Example custom hook files
└── __tests__/                    # Unit and E2E tests