This document explains how failproofai works internally: how the hook system intercepts agent tool calls, how configuration is loaded and merged, how policies are evaluated, and how the dashboard monitors agent activity.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.
Overview
failproofai has two independent subsystems:- Hook handler - A fast CLI subprocess that Claude Code invokes on every agent tool call. Evaluates policies and returns a decision.
- Agent Monitor (Dashboard) - A Next.js web application for monitoring agent sessions and managing policies.
~/.failproofai/ and the project’s .failproofai/ directory, but they run as separate processes and communicate only through the filesystem.
Hook handler
Integration with Claude Code
When you runfailproofai policies --install, it writes entries like this into ~/.claude/settings.json:
failproofai --hook PreToolUse as a subprocess before each tool call, passing a JSON payload on stdin.
Payload format
PostToolUse events, the payload also contains tool_result with the tool’s output.
The handler enforces a 1 MB stdin limit. Payloads exceeding this are discarded and all policies implicitly allow.
Response format
Deny (PreToolUse):- Exit code:
2 - Reason written to stderr (not stdout)
- Exit code:
0 - Empty stdout
allow(message) lets a policy send informational context back to Claude even when the operation is permitted. The hook handler writes the following JSON to stdout (not a config file — this is the handler’s response to Claude Code, just like deny and instruct responses above):
- Exit code:
0(operation is allowed) - When multiple policies return
allowwith a message, their messages are joined with newlines into a singleadditionalContextstring - If no policy provides a message, stdout is empty (same as before)
Processing pipeline
src/hooks/handler.ts implements the full pipeline:
Configuration loading
src/hooks/hooks-config.ts implements three-scope config loading.
enabledPolicies- deduplicated union across all three filespolicyParams- per-policy key, first file that defines it wins entirelycustomPoliciesPath- first file that defines it winsllm- first file that defines it wins
readHooksConfig() (global only) for reading and writing, since it is not invoked with a project cwd.
Policy evaluation
src/hooks/policy-evaluator.ts runs policies in order.
For each policy:
- Look up the policy’s
paramsschema (if it has one). - Read
policyParams[policy.name]from the merged config. - Merge user-provided values over schema defaults to produce
ctx.params. - Call
policy.fn(ctx)with the resolved context. - If the result is
deny, stop immediately and return that decision. - If the result is
instruct, accumulate the message and continue. - If the result is
allow, continue to the next policy.
- If any
denywas returned, emit the deny response. - If any
instructreturns were collected, emit a single instruct response with all messages joined. - Otherwise, emit an allow response (empty stdout, exit 0).
Builtin policies
src/hooks/builtin-policies.ts defines all 39 built-in policies as BuiltinPolicyDefinition objects:
params declare a PolicyParamsSchema with types and defaults for each parameter. The policy evaluator injects resolved values into ctx.params before calling fn. Policy functions read ctx.params without null-guarding because defaults are always applied first.
Pattern matching inside policies uses parsed command tokens (argv), not raw string matching. This prevents bypass via shell operator injection (e.g. a pattern for sudo systemctl status * cannot be bypassed by appending ; rm -rf / to the command).
Custom policies
src/hooks/custom-hooks-registry.ts implements a globalThis-backed registry:
src/hooks/custom-hooks-loader.ts loads the user’s policy file:
- Read
customPoliciesPathfrom config; skip if absent. - Resolve to absolute path; check file exists.
- Rewrite all
from "failproofai"imports to the actual dist path socustomPoliciesresolves to the sameglobalThisregistry. - Recursively rewrite transitive local imports to ensure ESM compatibility.
- Write temporary
.mjsfiles andimport()the entry file. - Call
getCustomHooks()to retrieve registered hooks. - Clean up all temp files in a
finallyblock.
~/.failproofai/hook.log and the loader returns an empty array. Built-in policies are unaffected.
Custom policies are evaluated after all built-in policies. A custom policy deny still short-circuits further custom policies (but all built-ins have already run by that point).
Activity logging
After each hook event, the handler appends a JSONL line to~/.failproofai/hook-activity.jsonl:
Dashboard architecture
The dashboard is a Next.js 16 application using the App Router with React Server Components and Server Actions.- Page components call
lib/projects.tsandlib/log-entries.tsto read project/session data directly from the filesystem (no API layer for reads). - The Policies page uses Server Actions for all mutations (toggle, params update, install/remove).
- The session viewer parses Claude’s JSONL transcript format and renders a timeline of messages and tool calls.
- No database - all persistent state is in plain files (
~/.failproofai/,~/.claude/projects/). - Server Actions for mutations - no REST API needed for CRUD operations.
- React Server Components for read pages - faster initial load, no client bundle for data fetching.
- Client components only where interactivity is needed (policy toggles, activity search, log viewer).

