# Architecture Source: https://docs.befailproof.ai/architecture How the hook handler, config loading, and policy evaluation work internally 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. *** ## Overview failproofai has two independent subsystems: 1. **Hook handler** - A fast CLI subprocess that Claude Code invokes on every agent tool call. Evaluates policies and returns a decision. 2. **Agent Monitor (Dashboard)** - A Next.js web application for monitoring agent sessions and managing policies. Both subsystems share configuration files in `~/.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 run `failproofai policies --install`, it writes entries like this into `~/.claude/settings.json`: ```json theme={null} { "hooks": { "PreToolUse": [ { "matcher": "", "hooks": [ { "type": "command", "command": "failproofai --hook PreToolUse" } ] } ], "PostToolUse": [ ... ] } } ``` Claude Code then invokes `failproofai --hook PreToolUse` as a subprocess before each tool call, passing a JSON payload on stdin. ### Payload format ```json theme={null} { "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" } } ``` For `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):** ```json theme={null} { "hookSpecificOutput": { "permissionDecision": "deny", "permissionDecisionReason": "Blocked by failproofai: sudo command blocked" } } ``` **Deny (PostToolUse):** ```json theme={null} { "hookSpecificOutput": { "additionalContext": "Blocked by failproofai because: API key detected in output" } } ``` **Instruct (any event except Stop):** ```json theme={null} { "hookSpecificOutput": { "additionalContext": "Instruction from failproofai: Verify tests pass before committing." } } ``` **Stop event instruct:** * Exit code: `2` * Reason written to stderr (not stdout) **Allow:** * Exit code: `0` * Empty stdout **Allow with message:** `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): ```json theme={null} // Written to stdout by the hook handler process { "hookSpecificOutput": { "additionalContext": "All CI checks passed on branch 'feat/my-feature'." } } ``` * Exit code: `0` (operation is allowed) * When multiple policies return `allow` with a message, their messages are joined with newlines into a single `additionalContext` string * If no policy provides a message, stdout is empty (same as before) ### Processing pipeline `src/hooks/handler.ts` implements the full pipeline: ```text theme={null} 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 ``` The entire process runs in under 100ms for typical payloads with no LLM calls. *** ## Configuration loading `src/hooks/hooks-config.ts` implements three-scope config loading. ```text theme={null} [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` - deduplicated union across all three files * `policyParams` - per-policy key, first file that defines it wins entirely * `customPoliciesPath` - first file that defines it wins * `llm` - first file that defines it wins The web dashboard uses `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: 1. Look up the policy's `params` schema (if it has one). 2. Read `policyParams[policy.name]` from the merged config. 3. Merge user-provided values over schema defaults to produce `ctx.params`. 4. Call `policy.fn(ctx)` with the resolved context. 5. If the result is `deny`, stop immediately and return that decision. 6. If the result is `instruct`, accumulate the message and continue. 7. If the result is `allow`, continue to the next policy. After all policies run: * If any `deny` was returned, emit the deny response. * If any `instruct` returns 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: ```typescript theme={null} interface BuiltinPolicyDefinition { name: string; description: string; fn: (ctx: PolicyContext) => PolicyResult; match: { events: HookEventType[]; tools?: string[]; }; defaultEnabled: boolean; category: string; beta?: boolean; params?: PolicyParamsSchema; } ``` Policies that accept `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: ```typescript theme={null} 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` loads the user's policy file: 1. Read `customPoliciesPath` from config; skip if absent. 2. Resolve to absolute path; check file exists. 3. Rewrite all `from "failproofai"` imports to the actual dist path so `customPolicies` resolves to the same `globalThis` registry. 4. Recursively rewrite transitive local imports to ensure ESM compatibility. 5. Write temporary `.mjs` files and `import()` the entry file. 6. Call `getCustomHooks()` to retrieve registered hooks. 7. Clean up all temp files in a `finally` block. On any error (file not found, syntax error, import failure), the error is logged to `~/.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`: ```json theme={null} { "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 } ``` One line per policy that made a non-allow decision. Allow decisions are not logged (to keep the file small). *** ## Dashboard architecture The dashboard is a **Next.js 16** application using the App Router with React Server Components and Server Actions. ```text theme={null} 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 ← Per-CLI session export (JSONL or JSON) ``` **Data flow:** * Page components call `lib/projects.ts` and `lib/log-entries.ts` to 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. **Key design decisions:** * 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). *** ## File layout ```text theme={null} failproofai/ ├── bin/ │ └── failproofai.mjs # CLI router (hook / dashboard / install / etc.) ├── src/hooks/ │ ├── handler.ts # Hook event pipeline │ ├── builtin-policies.ts # 39 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 ``` # Built-in Policies Source: https://docs.befailproof.ai/built-in-policies All 39 built-in policies that catch common agent failure modes failproofai ships with 39 built-in policies that catch common agent failure modes. Each policy fires on a specific hook event type and tool name. Nineteen policies accept parameters that let you tune their behavior without writing code. Five workflow policies enforce a commit → push → PR → CI pipeline before Claude stops. *** ## Overview Policies are grouped into categories: | Category | Policies | Hook type | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | [Dangerous commands](#dangerous-commands) | block-sudo, block-rm-rf, block-curl-pipe-sh, block-failproofai-commands | PreToolUse | | [Infra commands](#infra-commands) | block-kubectl, block-terraform, block-aws-cli, block-gcloud, block-az-cli, block-helm, block-gh-pipeline | PreToolUse | | [Secrets (sanitizers)](#secrets-sanitizers) | sanitize-jwt, sanitize-api-keys, sanitize-connection-strings, sanitize-private-key-content, sanitize-bearer-tokens | PostToolUse | | [Environment](#environment) | block-env-files, protect-env-vars | PreToolUse | | [File access](#file-access) | block-read-outside-cwd, block-secrets-write | PreToolUse | | [Git](#git) | block-push-master, block-work-on-main, block-force-push, warn-git-amend, warn-git-stash-drop, warn-all-files-staged | PreToolUse | | [Database](#database) | warn-destructive-sql, warn-schema-alteration | PreToolUse | | [Warnings](#warnings) | warn-large-file-write, warn-package-publish, warn-background-process, warn-global-package-install | PreToolUse | | [Package managers](#package-managers) | prefer-package-manager | PreToolUse | | [Workflow](#workflow) | require-commit-before-stop, require-push-before-stop, require-pr-before-stop, require-no-conflicts-before-stop, require-ci-green-before-stop | Stop | * **`block-`** — stop the agent from proceeding. * **`warn-`** — give the agent additional context so it can self-correct. * **`sanitize-`** — scrub sensitive data from tool output before the agent sees it. ### Namespaces Every policy lives in a `/` slot. Built-in policies belong to the **`failproofai/`** namespace — for example, `failproofai/sanitize-jwt`. The namespace prevents collisions when you also load custom or third-party policies with similar short names. In your config you can refer to a built-in by either its short name or its qualified name; both forms resolve to the same policy: ```json theme={null} { "enabledPolicies": [ "sanitize-jwt", "failproofai/block-rm-rf" ] } ``` If a name has no `/`, failproofai treats it as belonging to the default namespace `failproofai`. Names that already contain a `/` (e.g. `myorg/foo`, `custom/my-hook`) are kept as-is. * **`require-`** — block the Stop event until conditions are met. *** Every policy supports an optional `hint` field in `policyParams`. The hint is appended to the deny or instruct message Claude sees, giving actionable guidance without modifying policy code. Works with built-in, custom, and convention policies. See [Configuration → hint](/configuration#hint-cross-cutting) for details. *** ## Dangerous commands Prevent agents from running operations that are hard to undo or that could damage the host system. ### `block-sudo` **Event:** PreToolUse (Bash)\ **Default:** Denies any `sudo` command. Blocks invocations that include the `sudo` keyword. Pattern matching is done on parsed command tokens, not the raw string, to prevent bypass via shell operator injection. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------ | | `allowPatterns` | `string[]` | `[]` | Exact command prefixes that are permitted. Each entry is matched against the parsed argv tokens. | **Example:** ```json theme={null} { "policyParams": { "block-sudo": { "allowPatterns": ["sudo systemctl status", "sudo journalctl"] } } } ``` With this config, `sudo systemctl status nginx` is allowed, but `sudo rm /etc/hosts` is denied. Patterns are matched against parsed tokens, not the raw command string. This prevents bypass via appended shell operators (e.g. `sudo systemctl status x; rm -rf /` does not match `sudo systemctl status *`). *** ### `block-rm-rf` **Event:** PreToolUse (Bash)\ **Default:** Denies `rm -rf`, `rm -fr`, and similar recursive deletion forms. **Parameters:** | Param | Type | Default | Description | | ------------ | ---------- | ------- | -------------------------------------------------------- | | `allowPaths` | `string[]` | `[]` | Paths that are safe to recursively delete (e.g. `/tmp`). | **Example:** ```json theme={null} { "policyParams": { "block-rm-rf": { "allowPaths": ["/tmp", "/var/cache"] } } } ``` *** ### `block-curl-pipe-sh` **Event:** PreToolUse (Bash)\ **Default:** Denies `curl | bash`, `curl | sh`, `wget | bash`, and similar patterns. No parameters. *** ### `block-failproofai-commands` **Event:** PreToolUse (Bash)\ **Default:** Denies commands that would uninstall or disable failproofai itself (e.g. `npm uninstall failproofai`, `failproofai policies --uninstall`). No parameters. *** ## Infra commands Stop coding agents from running infrastructure CLIs or triggering CI/CD pipelines. All policies in this category are **opt-in** (`defaultEnabled: false`) — agents that legitimately need to call `kubectl`, `terraform`, etc. will not be disrupted unless you enable the policy. When enabled, every invocation of the matched CLI is denied unless the command matches an entry in `allowPatterns`. The pattern grammar is the same as [`block-sudo`](#block-sudo): tokens are matched against parsed argv, `*` is a wildcard for one token, and any command containing a standalone shell operator (`&&`, `||`, `|`, `;`) or a token with embedded shell metacharacters is rejected before allowlist matching to prevent injection bypasses. ### `block-kubectl` **Event:** PreToolUse (Bash)\ **Default:** Denies any `kubectl` invocation. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | -------------------------------------------- | | `allowPatterns` | `string[]` | `[]` | kubectl command prefixes that are permitted. | **Example:** ```json theme={null} { "policyParams": { "block-kubectl": { "allowPatterns": ["kubectl get *", "kubectl describe *", "kubectl logs *"] } } } ``` With this config, `kubectl get pods` is allowed but `kubectl apply -f deploy.yaml` is denied. *** ### `block-terraform` **Event:** PreToolUse (Bash)\ **Default:** Denies any `terraform` or `tofu` (OpenTofu) invocation. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | --------------------------------------------------- | | `allowPatterns` | `string[]` | `[]` | terraform/tofu command prefixes that are permitted. | **Example:** ```json theme={null} { "policyParams": { "block-terraform": { "allowPatterns": ["terraform plan", "terraform validate", "terraform show *"] } } } ``` *** ### `block-aws-cli` **Event:** PreToolUse (Bash)\ **Default:** Denies any `aws` CLI invocation. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | -------------------------------------------- | | `allowPatterns` | `string[]` | `[]` | aws CLI command prefixes that are permitted. | **Example:** ```json theme={null} { "policyParams": { "block-aws-cli": { "allowPatterns": ["aws s3 ls *", "aws sts get-caller-identity"] } } } ``` *** ### `block-gcloud` **Event:** PreToolUse (Bash)\ **Default:** Denies any `gcloud` (Google Cloud) CLI invocation. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | ------------------------------------------- | | `allowPatterns` | `string[]` | `[]` | gcloud command prefixes that are permitted. | **Example:** ```json theme={null} { "policyParams": { "block-gcloud": { "allowPatterns": ["gcloud auth list", "gcloud config list"] } } } ``` *** ### `block-az-cli` **Event:** PreToolUse (Bash)\ **Default:** Denies any `az` (Azure) CLI invocation. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | ------------------------------------------- | | `allowPatterns` | `string[]` | `[]` | az CLI command prefixes that are permitted. | **Example:** ```json theme={null} { "policyParams": { "block-az-cli": { "allowPatterns": ["az account show", "az group list"] } } } ``` *** ### `block-helm` **Event:** PreToolUse (Bash)\ **Default:** Denies any `helm` invocation. **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | ----------------------------------------- | | `allowPatterns` | `string[]` | `[]` | helm command prefixes that are permitted. | **Example:** ```json theme={null} { "policyParams": { "block-helm": { "allowPatterns": ["helm list", "helm status *"] } } } ``` *** ### `block-gh-pipeline` **Event:** PreToolUse (Bash)\ **Default:** Denies the following `gh` CLI subcommands that mutate state or trigger pipelines: * `gh workflow run`, `gh workflow enable`, `gh workflow disable` * `gh run rerun`, `gh run cancel` * `gh pr merge` * `gh release create`, `gh release delete` * `gh cache delete` * `gh secret set`, `gh secret delete` Read-only `gh` subcommands such as `gh pr view`, `gh pr list`, `gh run list`, `gh release view`, and `gh api repos/.../...` are **not** matched by this policy — they are routinely needed for workflow checks (including failproofai's own `require-ci-green-before-stop`). **Parameters:** | Param | Type | Default | Description | | --------------- | ---------- | ------- | ---------------------------------------------------------------------------------- | | `allowPatterns` | `string[]` | `[]` | Specific scripted invocations to allow even though they would otherwise be denied. | **Example:** ```json theme={null} { "policyParams": { "block-gh-pipeline": { "allowPatterns": ["gh run rerun *"] } } } ``` *** ## Secrets (sanitizers) Stop agents from leaking credentials into their context or output. Sanitizer policies fire on **PostToolUse** events. When Claude runs a Bash command, reads a file, or calls any tool, these policies inspect the output before it is returned to Claude. If a secret pattern is detected, the policy returns a deny decision that prevents the output from being passed back. ### `sanitize-jwt` **Event:** PostToolUse (all tools)\ **Default:** Redacts JWT tokens (three base64url segments separated by `.`). No parameters. *** ### `sanitize-api-keys` **Event:** PostToolUse (all tools)\ **Default:** Redacts common API key formats: Anthropic (`sk-ant-`), OpenAI (`sk-`), GitHub PATs (`ghp_`), AWS access keys (`AKIA`), Stripe keys (`sk_live_`, `sk_test_`), and Google API keys (`AIza`). **Parameters:** | Param | Type | Default | Description | | -------------------- | ------------------------------------ | ------- | ---------------------------------------------- | | `additionalPatterns` | `{ regex: string; label: string }[]` | `[]` | Additional regex patterns to treat as secrets. | **Example:** ```json theme={null} { "policyParams": { "sanitize-api-keys": { "additionalPatterns": [ { "regex": "myco_[A-Za-z0-9]{32}", "label": "MyCo internal API key" }, { "regex": "pat_[0-9a-f]{40}", "label": "Internal PAT" } ] } } } ``` *** ### `sanitize-connection-strings` **Event:** PostToolUse (all tools)\ **Default:** Redacts database connection strings that contain embedded credentials (e.g. `postgresql://user:password@host/db`). No parameters. *** ### `sanitize-private-key-content` **Event:** PostToolUse (all tools)\ **Default:** Redacts PEM blocks (`-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----`, etc.). No parameters. *** ### `sanitize-bearer-tokens` **Event:** PostToolUse (all tools)\ **Default:** Redacts `Authorization: Bearer ` headers where the token is 20 or more characters. No parameters. *** ## Environment Protect sensitive environment configuration from being read or exposed by agents. ### `block-env-files` **Event:** PreToolUse (Bash, Read)\ **Default:** Denies reading `.env` files via `cat .env`, `Read` tool calls with `.env` as the file path, etc. Does not block `.envrc` or other environment-adjacent files - only files named exactly `.env`. No parameters. *** ### `protect-env-vars` **Event:** PreToolUse (Bash)\ **Default:** Denies commands that print environment variables: `printenv`, `env`, `echo $VAR`. No parameters. *** ## File access Keep agents working inside project boundaries and away from sensitive files. ### `block-read-outside-cwd` **Event:** PreToolUse (Read, Bash)\ **Default:** Denies reading files outside the project root. The boundary is `CLAUDE_PROJECT_DIR` (set once per session by Claude Code), with a fallback to the session's current working directory when that variable is unset. Using the project root rather than the live `cwd` means the boundary stays stable even after Claude `cd`s into a subdirectory. **Parameters:** | Param | Type | Default | Description | | ------------ | ---------- | ------- | --------------------------------------------------------------------------- | | `allowPaths` | `string[]` | `[]` | Absolute path prefixes that are permitted even if outside the project root. | **Example:** ```json theme={null} { "policyParams": { "block-read-outside-cwd": { "allowPaths": ["/shared/data", "/opt/company/config"] } } } ``` *** ### `block-secrets-write` **Event:** PreToolUse (Write, Edit)\ **Default:** Denies writes to files commonly used for private keys and certificates: `id_rsa`, `id_ed25519`, `*.key`, `*.pem`, `*.p12`, `*.pfx`. **Parameters:** | Param | Type | Default | Description | | -------------------- | ---------- | ------- | --------------------------------------------------- | | `additionalPatterns` | `string[]` | `[]` | Additional filename patterns (glob-style) to block. | **Example:** ```json theme={null} { "policyParams": { "block-secrets-write": { "additionalPatterns": [".token", ".secret"] } } } ``` *** ## Git Prevent accidental pushes, force-pushes, and branch mistakes that are hard to undo. ### `block-push-master` **Event:** PreToolUse (Bash)\ **Default:** Denies `git push origin main` and `git push origin master`. **Parameters:** | Param | Type | Default | Description | | ------------------- | ---------- | -------------------- | ----------------------------------------------- | | `protectedBranches` | `string[]` | `["main", "master"]` | Branch names that cannot be pushed to directly. | **Example:** ```json theme={null} { "policyParams": { "block-push-master": { "protectedBranches": ["main", "master", "release", "prod"] } } } ``` To allow pushing to all branches (effectively disabling this policy without removing it from `enabledPolicies`), set `protectedBranches: []`. *** ### `block-work-on-main` **Event:** PreToolUse (Bash)\ **Default:** Denies `git commit`, `git merge`, `git rebase`, and `git cherry-pick` while the working tree is on `main` or `master`. Branch creation and switching (`git checkout`, `git checkout -b`, `git switch`, `git switch -c`) are not affected. **Parameters:** | Param | Type | Default | Description | | ------------------- | ---------- | -------------------- | ---------------------------------------------------------------- | | `protectedBranches` | `string[]` | `["main", "master"]` | Branch names on which commit/merge/rebase/cherry-pick is denied. | *** ### `block-force-push` **Event:** PreToolUse (Bash)\ **Default:** Denies `git push --force` and `git push -f`. No policy-specific parameters. Use the cross-cutting [`hint`](/configuration#hint-cross-cutting) to suggest alternatives: ```json theme={null} { "policyParams": { "block-force-push": { "hint": "Create a new branch from your current HEAD (e.g. `git checkout -b `) and push that instead." } } } ``` *** ### `warn-git-amend` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to proceed carefully when running `git commit --amend`. Does not block the command. No parameters. *** ### `warn-git-stash-drop` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to confirm before running `git stash drop`. Does not block the command. No parameters. *** ### `warn-all-files-staged` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to review what it is staging when it runs `git add -A` or `git add .`. Does not block the command. No parameters. *** ## Database Catch destructive SQL operations before they execute against your database. ### `warn-destructive-sql` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to confirm before running SQL containing `DROP TABLE`, `DROP DATABASE`, or `DELETE` without a `WHERE` clause. No parameters. *** ### `warn-schema-alteration` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to confirm before running `ALTER TABLE` statements. No parameters. *** ## Warnings Give agents extra context before potentially risky but non-destructive operations. ### `warn-large-file-write` **Event:** PreToolUse (Write)\ **Default:** Instructs Claude to confirm before writing files larger than 1024 KB. **Parameters:** | Param | Type | Default | Description | | ------------- | -------- | ------- | ----------------------------------------------------------------- | | `thresholdKb` | `number` | `1024` | File size threshold in kilobytes above which a warning is issued. | **Example:** ```json theme={null} { "policyParams": { "warn-large-file-write": { "thresholdKb": 256 } } } ``` The hook handler enforces a 1 MB stdin limit on payloads. To test this policy with small content, set `thresholdKb` to a value well below 1024. *** ### `warn-package-publish` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to confirm before running `npm publish`. No parameters. *** ### `warn-background-process` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to be careful when launching background processes via `nohup`, `&`, `disown`, or `screen`. No parameters. *** ### `warn-global-package-install` **Event:** PreToolUse (Bash)\ **Default:** Instructs Claude to confirm before running `npm install -g`, `yarn global add`, or `pip install` without a virtual environment. No parameters. *** ## Package managers Enforce which package managers the agent is allowed to use. ### `prefer-package-manager` **Event:** PreToolUse (Bash)\ **Default:** Disabled. When enabled, blocks any package manager command not in the `allowed` list and tells Claude to rewrite the command using an allowed manager. Detects: pip, pip3, python -m pip, npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, poetry, pipenv, conda, cargo. | Parameter | Type | Default | Description | | --------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | `allowed` | string\[] | `[]` | Allowed package manager names. Any detected manager not in this list is blocked. When empty, the policy is a no-op. | | `blocked` | string\[] | `[]` | Additional manager names to block beyond the built-in list (e.g. `['pdm', 'pipx']`). | The built-in block list covers: pip, pip3, npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, poetry, pipenv, conda, cargo. Use `blocked` to append managers not in this list. **Example configuration:** ```json theme={null} { "enabledPolicies": ["prefer-package-manager"], "policyParams": { "prefer-package-manager": { "allowed": ["uv", "bun"], "blocked": ["pdm", "pipx"] } } } ``` With this config, `pip install flask` and `pdm install flask` are both denied with a message telling Claude to use `uv` or `bun` instead. Commands like `uv pip install flask` are allowed because `uv` is in the allowlist and is checked first. *** ## AI behavior Detect when agents get stuck or behave unexpectedly. ### `warn-repeated-tool-calls` **Event:** PreToolUse (all tools)\ **Default:** Instructs Claude to reconsider when the same tool is called 3+ times with identical parameters - a common sign the agent is stuck in a loop. No parameters. *** ## Workflow Enforce a disciplined end-of-session workflow. These policies fire on the **Stop** event and deny the agent from stopping until each condition is met. They follow a natural dependency chain: commit → push → PR → CI. If a policy denies, later policies in the chain are skipped (deny short-circuits). All workflow policies are **fail-open**: if the required tool is not available (e.g. `gh` not installed, no git remote), the policy allows with an informational message explaining why the check was skipped. ### Per-CLI Stop semantics Stop enforcement looks slightly different across the seven supported CLIs because each one exposes a different "agent finished" hook contract. The **outcome** is the same — the agent doesn't get away with stopping while a workflow gate is failing — but the **mechanics** differ. The table below summarizes; only Pi has a user-visible quirk worth understanding before you enable a `require-*-before-stop` policy. | CLI | When the gate fires | What you see | | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Claude Code | Same agent loop, immediately | Claude continues working — fixes the issue, then attempts to finish again. No interruption visible to you. | | Codex | Same agent loop, immediately | Same as Claude. | | GitHub Copilot CLI | Same agent loop, immediately | Same as Claude (uses Copilot's `{decision:"block", reason}` retry channel — verified empirically against Copilot CLI 1.0.41). | | Cursor Agent | Same agent loop, immediately | Same as Claude (uses Cursor's `{followup_message}` channel — capped at `loop_limit`, default 5 retries). | | Gemini CLI | Same agent loop, immediately | Same as Claude (uses Gemini's `{decision:"block", reason}` channel on `AfterAgent`). | | OpenCode | Same agent loop, immediately | Same as Claude (uses OpenCode's `client.session.prompt(...)` SDK call routed through `hookSpecificOutput.additionalContext`). | | **Pi (pi-coding-agent)** | **Next user turn** | **Pi visibly stops** when the gate fires — its agent loop exits and you're returned to the prompt. The gate then fires the next time you submit a prompt: failproofai prepends a `MANDATORY ACTION REQUIRED` directive to that turn's system prompt, instructing the LLM to complete the workflow step (commit, push, etc.) before doing whatever you asked. | **Pi limitation.** Pi's `AgentEndEvent` (the upstream equivalent of Claude's `Stop` hook) has no Result type — by the time it fires, Pi's agent loop has already exited. Pi cannot be forced to retry the same loop the way Claude / Copilot / Cursor / Gemini / OpenCode can. failproofai shifts the gate to Pi's `before_agent_start` event (which fires after the next user prompt) so the workflow check still enforces, just on the next turn rather than the current one. **What this means in practice:** * After Pi stops, the deny reason is captured in-memory keyed by Pi session id. The very next prompt you submit in the same Pi process drains it: the LLM sees the `MANDATORY ACTION REQUIRED` directive at the top of its system prompt, commits (or pushes / opens the PR / waits for CI), and only then continues with your request. The captured deny reason is one-shot — once drained, the gate is clear. * The gate is bounded by Pi's process lifetime. If you `Ctrl+C` Pi or quit between turns, the in-memory entry is dropped along with the process and the gate is missed. Claude, Copilot, Cursor, Gemini, and OpenCode have the same bound (kill the agent and the gate is missed) — Pi just makes it more visible because the agent visibly exits before the gate fires. * A pending deny is also cleared on `session_shutdown` for any reason (`new` / `resume` / `fork` / `quit`), so a stale gate from a prior session cannot leak into a fresh session started in the same Pi process. If you need Claude-style same-loop retry, run your `Stop` policies under any of the other six supported CLIs. We are tracking Pi upstream for a future Result type on `AgentEndEvent` that would let us close this gap. ### `require-commit-before-stop` **Event:** Stop\ **Default:** Denies stopping when there are uncommitted changes (modified, staged, or untracked files). Returns an informational message when the working directory is clean. No parameters. *** ### `require-push-before-stop` **Event:** Stop\ **Default:** Denies stopping when there are unpushed commits or when the current branch has no remote tracking branch. Suggests `git push -u` to create a tracking branch if needed. Fails open if no remote is configured. **Parameters:** | Param | Type | Default | Description | | -------- | -------- | ---------- | ----------------------- | | `remote` | `string` | `"origin"` | Remote name to push to. | **Example:** ```json theme={null} { "policyParams": { "require-push-before-stop": { "remote": "upstream" } } } ``` *** ### `require-pr-before-stop` **Event:** Stop\ **Default:** Denies stopping when no pull request exists for the current branch, or when the existing PR is closed without merging. Instructs Claude to create a PR with `gh pr create`. When the PR is **merged**, the policy allows (the work has shipped) and the message hints to switch off the branch (`git checkout main && git pull`). No parameters. This policy requires [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated. Run `gh auth login` with a personal access token that has `repo` scope for read access to pull requests. If `gh` is not installed or not authenticated, the policy fails open and reports the reason to Claude. *** ### `require-no-conflicts-before-stop` **Event:** Stop\ **Default:** Denies stopping when the current branch cannot cleanly merge into the base branch. The policy first confirms there is an `OPEN` PR on GitHub for the branch — without one, there is no merge target to enforce, so the entire policy short-circuits to allow. Once an `OPEN` PR is confirmed, two independent probes run: 1. **Local** — `git merge-tree --write-tree --name-only origin/ HEAD`. On conflict, the deny message names the conflicted files so Claude knows exactly what to resolve. 2. **GitHub** — reuses the `gh pr view --json mergeable,state` result already fetched in the precheck. Catches conflicts that a stale local `origin/` would miss (e.g. someone landed a conflicting PR on `main` since the last fetch). A `CONFLICTING` result denies. An `UNKNOWN` result also denies and instructs Claude to wait \~10 seconds and re-check before attempting to stop again — this prevents false negatives while GitHub recomputes. Skips entirely (allows) when: `gh` is not installed, no PR exists for the branch, the PR's state is not `OPEN` (e.g. `MERGED`, `CLOSED`), or `gh pr view` returns unparseable output. Also fails open when `origin/` is missing locally or when no commits are ahead of base — those Layer 1 fall-throughs still consult the cached PR mergeability before allowing. **Parameters:** | Param | Type | Default | Description | | ------------ | -------- | -------- | ------------------------------------------- | | `baseBranch` | `string` | `"main"` | Base branch to check for conflicts against. | GitHub CLI (`gh`) is required for this policy. The policy uses `gh pr view` to confirm an `OPEN` PR exists before running any conflict probe — without `gh`, the policy short-circuits to allow. Run `gh auth login` with a personal access token that has `repo` scope for read access to pull requests. *** ### `require-ci-green-before-stop` **Event:** Stop\ **Default:** Denies stopping when CI checks are failing or still running on the current branch. Checks both GitHub Actions workflow runs and third-party bot checks (e.g. CodeRabbit, SonarCloud, Codecov). Treats `skipped`, `cancelled`, and `neutral` conclusions as non-failing (the latter covers e.g. Socket Security alerts on outside contributor PRs, where the app intentionally reports neutral rather than success/failure). Returns an informational message when all checks pass. No parameters. This policy requires [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated. Run `gh auth login` with a personal access token that has `repo` scope for read access to Actions workflow runs and the Checks API. If `gh` is not installed or not authenticated, the policy fails open and reports the reason to Claude. *** *** ## Disabling individual policies Remove a specific policy from `enabledPolicies` in your config, or toggle it off in the dashboard's Policies tab. ```json theme={null} { "enabledPolicies": [ "block-rm-rf", "sanitize-api-keys" ] } ``` Policies not listed in `enabledPolicies` do not run, even if `policyParams` entries exist for them. # Audit past sessions (beta) Source: https://docs.befailproof.ai/cli/audit Count how often the agent did wasteful or risky things across past transcripts **Beta feature.** The audit ships as beta while we collect early feedback. The detector catalog and report format may change before the next stable cut. Please open an issue if anything looks off. The audit is now exposed as the **/audit dashboard page**, not a CLI subcommand. Open it from the dashboard navbar (between Policies and Projects), or visit `http://localhost:8020/audit` directly when running `failproofai` locally. ```bash theme={null} failproofai # open the dashboard, then click "Audit" ``` The dashboard scans past agent CLI transcripts on this machine (Claude Code, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) and reports how often the agent did things failproofai is built to stop — env-var checks, force pushes, redundant `cd ` prefixes, sleep-polling loops, re-reading files just edited, and more. For each transcript, every tool-use event is replayed through the 39 builtin policies **and** through 8 audit-only detectors that catch patterns not yet covered by runtime policies. Counts are aggregated per policy / detector across all sessions. ## What you get The `/audit` page composes six sections: 1. **Identity** — your agent classified into one of 8 archetypes (`optimist`, `cowboy`, `explorer`, `goldfish`, `paranoid architect`, `precision builder`, `hammer`, `ghost`) based on the weighted signal across every audited transcript. 2. **Strengths** — real numbers derived from the scan (clean-call %, "0 credential leaks", etc.) gated on the relevant sanitize policies actually firing. 3. **Score** — 0-100 with S/A/B/C/D/F bands and a projected uplift if every recommended policy were enabled. 4. **Findings** — per-policy cards with what happened, cost, captured evidence, and the exact `failproofai policy add ` to enable the live-time builtin that would have caught it. 5. **Prescribed policies** — aggregated install list with a one-shot `failproofai policies --install` command. 6. **Re-audit reminder** — "come back better." Set a 7-day email reminder via the api-server (requires sign-in; see [`failproofai auth`](/cli/auth)). ## Audit-only detectors These detect "stupid behavior" patterns not (yet) enforced in real time. They run only during the audit and never block a live tool call. | Detector | What it counts | | --------------------------- | -------------------------------------------------------------------------------------- | | `redundant-cd-cwd` | Bash commands starting with `cd && …` even though commands already run in `cwd`. | | `prefer-edit-over-read-cat` | `cat`/`head`/`tail`/`less`/`more` on a single source file — use the `Read` tool. | | `prefer-edit-over-sed-awk` | `sed -i` / `awk … > file` in-place edits — use the `Edit` tool. | | `prefer-write-over-heredoc` | Heredoc / multi-line `echo > file` writing files — use the `Write` tool. | | `sleep-polling-loop` | Long `sleep N` (≥ 30s) or `while …; sleep …; done` polling loops. | | `find-from-root` | `find /`, `find /home`, `find /usr`, etc. — scope to `cwd` instead. | | `git-commit-no-verify` | `git commit … --no-verify` / `-n`, skipping hooks. | | `reread-after-edit` | `Read` of a file that was just `Edit`/`Write` in the same session. | ## Caches * **Per-transcript cache** at `~/.failproofai/cache/audit/.json` keyed by `(mtime, size, engineVersion, detectorVersion)` — invalidates automatically when the transcript or the policy/detector code changes. Each entry also stores a `cachedAt` timestamp as **TTL metadata** (not part of the cache key); entries older than **7 days** are rejected on read so long-lived results don't outlive evolving detector intent. * **Whole-result cache** at `~/.failproofai/audit-dashboard.json` (mode 0600). Lets the dashboard render instantly on navigation without re-running. Also rejected on read past the **7-day TTL** — `/audit` then falls through to its empty state and prompts a fresh run. Click `[ re-audit now ]` near the bottom of the report to refresh — re-audit sends `noCache: true`, so it bypasses the per-transcript cache and re-scans every transcript instead of returning the cached result; the run streams progress via a sticky top strip and swaps the result in place on success (no page reload; a failed re-audit keeps the previous report). ## Notes * **No mutation.** The audit replays in read-only mode. `warn-repeated-tool-calls` is skipped because its per-session sidecar would otherwise be modified. * **Workflow policies skipped.** `require-*-before-stop` policies fire only on `Stop` events and `execSync` against the live git state — they have no meaningful "what would have happened in 2025" interpretation, so they don't appear in audit counts. * **Custom policies skipped.** User-supplied custom hooks are not replayed (they may have changed since the original session). # Sign in Source: https://docs.befailproof.ai/cli/auth Sign in to FailproofAI from the CLI to enable reminders and personalized features ```bash theme={null} failproofai auth login # email + one-time code failproofai auth logout # revoke this session failproofai auth whoami # print the signed-in identity ``` The legacy `--login` / `--logout` / `--whoami` flag form is still accepted as an alias for back-compat. Authentication is opt-in. Policies, the dashboard, the `/audit` page, and every other local feature work exactly the same whether you're signed in or not. The login surface exists so that features that **need** a stable identity (re-audit reminders today, more in the future) have somewhere to anchor. ## Sign-in flow ```bash theme={null} failproofai auth login ``` Prompts for your email, sends a 6-digit one-time code to that address, prompts for the code, and on success writes `~/.failproofai/auth.json` (mode `0600`). The same session is then visible to the in-app dashboard — clicking `[ set a reminder ]` on `/audit` will see you as signed in. The dashboard exposes the same flow as a modal dialog on `/audit` for users who never touch the CLI. ## Sign-out ```bash theme={null} failproofai auth logout ``` Revokes the current session on the server and deletes `~/.failproofai/auth.json`. If the api-server is unreachable, the local file is removed regardless — local intent to log out always wins. ## Identity check ```bash theme={null} failproofai auth whoami ``` Prints ` ()` and exits 0 when a valid session exists, or `not signed in` and exits 1 otherwise. Silently refreshes the access token in the background if it's within a minute of expiring. ## Persistent re-audit reminder When you click **`[ set a reminder ]`** on the `/audit` page (or sign in via the modal that the button gates on), the dashboard writes a small companion file at `~/.failproofai/next-audit.json`: ```json theme={null} { "next_audit_at": 1780765200, "user_email": "you@example.com", "set_at": 1780160574 } ``` This file is scoped to the email it was set for — switching the CLI session to a different account hides any reminder that belongs to the previous user. Default offset is **7 days**, configurable later when the scheduler lands. Created with `0600` perms like `auth.json`. The dashboard's `/api/auth/reminder` endpoint exposes `GET` (read), `POST` (set / reschedule), and `DELETE` (clear) and requires an active session. ## What's in `~/.failproofai/auth.json` ```json theme={null} { "access_token": "eyJhbGc…", "refresh_token": "9ede3e…", "access_expires_at": 1780160574, "refresh_expires_at": 1782748974, "user": { "id": "", "email": "you@example.com" } } ``` Created with `0600` perms (owner-only read/write). The access token is a 1-hour HS256 JWT; the refresh token is an opaque 256-bit random string that the server stores as `SHA-256(token)`. Refresh-token replay is detected server-side and revokes every session for the user. ## Environment variables | Variable | Default | Purpose | | ---------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------ | | `FAILPROOF_API_URL` | `https://api.befailproof.ai` | Override the api-server base URL. Useful for local development against a self-hosted api-server. | | `FAILPROOFAI_AUTH_DIR` | `~/.failproofai` | Override where `auth.json` is stored. Mostly for tests. | See [Environment variables](/cli/environment-variables) for the full list. ## Troubleshooting **"Could not reach the api-server"** — the CLI can't open a TCP connection to `FAILPROOF_API_URL`. Check your network, or set `FAILPROOF_API_URL` if you're running a self-hosted api-server. **"Rate limited"** — too many login attempts in a 15-minute window for that email (5/email) or IP (20/IP), or a 30-second resend cooldown after the previous request for the same email. The error message includes the retry-after window in seconds. **Code rejected** — the OTP was wrong, expired, or the row hit its 5-wrong-guess lockout. Run `failproofai auth login` again to request a fresh code. # View sessions Source: https://docs.befailproof.ai/cli/dashboard Launch the dashboard to browse agent sessions and manage policies ```bash theme={null} failproofai ``` Starts the web dashboard at `http://localhost:8020`. ## Options | Flag | Description | | ----------------------------- | --------------------------------------------------------- | | `--port ` | Port to listen on (default: `8020`) | | `--allowed-origins ` | Comma-separated hosts/IPs allowed to access dev resources | To point the dashboard at a non-default Claude project folder, set the `CLAUDE_PROJECTS_PATH` environment variable when launching. ## Examples ```bash theme={null} # Launch on a different port failproofai --port 9000 # Use a custom Claude projects path via environment variable CLAUDE_PROJECTS_PATH=~/my-claude-projects failproofai ``` # Environment variables Source: https://docs.befailproof.ai/cli/environment-variables Configure failproofai behavior with environment variables ## Dashboard | Variable | Description | | --------------------------------------------- | ----------------------------------------------------------------------- | | `PORT` | Dashboard port (default: `8020`) | | `CLAUDE_PROJECTS_PATH` | Override where Claude Code project folders are found | | `FAILPROOFAI_DISABLE_PAGES=policies,projects` | Comma-separated dashboard pages to hide | | `FAILPROOFAI_ALLOWED_DEV_ORIGINS` | Hosts/IPs allowed to access dev resources. Same as `--allowed-origins`. | ## Logging | Variable | Description | | ----------------------------------------- | ---------------------------------------------------------------------------------- | | `FAILPROOFAI_LOG_LEVEL=info\|warn\|error` | Server log level (default: `warn`) | | `FAILPROOFAI_HOOK_LOG_FILE` | Custom hook log file path, or `true` for default (`~/.failproofai/logs/hooks.log`) | ## Telemetry | Variable | Description | | ---------------------------------- | --------------------------------- | | `FAILPROOFAI_TELEMETRY_DISABLED=1` | Disable anonymous usage telemetry | ## Authentication | Variable | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `FAILPROOF_API_URL` | Override the api-server base URL used by `failproofai auth` and the dashboard auth dialog. Defaults to `https://api.befailproof.ai`; set to `http://localhost:8080` (or wherever) when running a local api-server. | | `FAILPROOFAI_AUTH_DIR` | Override where `auth.json` is stored (default: `~/.failproofai`). Mostly useful for isolated tests. | ## First-run prompt | Variable | Description | | ---------------------------- | ------------------------------------------------------------------------------------------ | | `FAILPROOFAI_NO_FIRST_RUN=1` | Skip the prompt that offers to install policies on the first bare `failproofai` invocation | ## LLM (for policy evaluation) | Variable | Description | | -------------------------- | ------------------------------------------------------- | | `FAILPROOFAI_LLM_BASE_URL` | LLM API endpoint (default: `https://api.openai.com/v1`) | | `FAILPROOFAI_LLM_API_KEY` | API key for LLM-powered policies | | `FAILPROOFAI_LLM_MODEL` | Model name (default: `gpt-4o-mini`) | # Hook handler (internal) Source: https://docs.befailproof.ai/cli/hook The subprocess Claude Code calls on each tool event ```bash theme={null} failproofai --hook ``` This is the command registered in Claude Code's `settings.json` by `failproofai policies --install`. You don't normally call this directly. Reads a JSON payload from stdin, evaluates all enabled policies, and exits with a code indicating the decision: | Exit code | Decision | Effect | | --------- | ---------- | ------------------------------------------------ | | `0` | `allow` | Permit the action | | `1` | `deny` | Block the action - Claude sees the denial reason | | `2` | `instruct` | Inject guidance into Claude's context | ### Supported event types | Category | Events | | --------------------- | ------------------------------------------------------------------------------------------ | | **Tool execution** | `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied` | | **Session lifecycle** | `SessionStart`, `SessionEnd`, `Stop`, `StopFailure` | | **User interaction** | `UserPromptSubmit`, `Notification`, `Elicitation`, `ElicitationResult` | | **Subagents & tasks** | `SubagentStart`, `SubagentStop`, `TaskCreated`, `TaskCompleted`, `TeammateIdle` | | **Configuration** | `InstructionsLoaded`, `ConfigChange`, `CwdChanged` | | **File system** | `FileChanged`, `WorktreeCreate`, `WorktreeRemove` | | **Context** | `PreCompact`, `PostCompact` | # Install policies Source: https://docs.befailproof.ai/cli/install-policies Enable policies so they run on every agent tool call ```bash theme={null} failproofai policies --install [policy-names...] [options] ``` Writes hook entries into your installed agent CLI's settings file (Claude Code, OpenAI Codex, or GitHub Copilot CLI *(beta)*) so failproofai intercepts tool calls. Aliases: `failproofai p -i` ## Options | Flag | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--cli claude\|codex\|copilot` | Agent CLI(s) to install for; space-separated (e.g. `--cli claude codex copilot`) or repeated. Omit to detect installed CLIs and prompt. | | `--scope user` | Install into the user-scope settings file (Claude: `~/.claude/settings.json`; Codex: `~/.codex/hooks.json`; Copilot: `~/.copilot/hooks/failproofai.json`). Default. | | `--scope project` | Install into the project-scope settings file (Claude: `/.claude/settings.json`; Codex: `/.codex/hooks.json`; Copilot: `/.github/hooks/failproofai.json`). | | `--scope local` | Claude only — installs into `/.claude/settings.local.json`. Codex and Copilot do not have a `local` scope. | | `--custom ` / `-c` | Path to a JS file containing custom hook policies | ## Behavior * **No policy names** - opens an interactive prompt to select policies * **Specific names** - enables those policies (added to any already enabled) * **`all`** - enables every available policy Installation is additive: running `--install` again adds new policies without removing existing ones. ## Examples ```bash theme={null} # Install all default policies globally (interactive) failproofai policies --install # Install specific policies for the current project failproofai policies --install block-sudo sanitize-api-keys --scope project # Enable all policies at once failproofai policies --install all # Install with a custom policies file failproofai policies --install --custom ./my-policies.js # Install for OpenAI Codex (project scope) failproofai policies --install --cli codex --scope project # Install for GitHub Copilot CLI (beta) for the current project failproofai policies --install --cli copilot --scope project # Install for all three CLIs at once failproofai policies --install --cli claude codex copilot ``` When `--custom ` is provided, the file is validated immediately - it must call `customPolicies.add()` at least once. The resolved path is saved to `policies-config.json` as `customPoliciesPath`. # List policies Source: https://docs.befailproof.ai/cli/list-policies See which policies are enabled, their parameters, and custom policies ```bash theme={null} failproofai policies ``` Shows all policies with their status, configured parameters, and custom policies. ## Sample output ```text theme={null} Failproof AI Hook Policies (user) Status Name Description ────── ────────────────────────────────────────────────────────────── ✓ block-sudo Block sudo commands allowPatterns: ["sudo systemctl status"] ✓ block-rm-rf Block recursive deletions ✗ block-curl-pipe-sh Block curl|bash patterns ✓ sanitize-api-keys Redact API keys from output additionalPatterns: [{ regex: "MY_TOKEN_...", label: "..." }] ── Custom Policies (/home/alice/myproject/my-policies.js) ────────────── ✓ require-jira-ticket Block commits without ticket ✓ approval-gate Approval gate for destructive ops ``` Unknown keys in `policyParams` are flagged here so you can catch typos early. # Uninstall policies Source: https://docs.befailproof.ai/cli/remove-policies Remove hook entries from Claude Code's settings ```bash theme={null} failproofai policies --uninstall [policy-names...] [options] ``` Removes failproofai hook entries from Claude Code's `settings.json`. Aliases: `failproofai p -u` ## Options | Flag | Description | | ----------------- | ------------------------------------------ | | `--scope user` | Remove from global settings (default) | | `--scope project` | Remove from project settings | | `--scope local` | Remove from local settings | | `--scope all` | Remove from all scopes at once | | `--custom` / `-c` | Clear the `customPoliciesPath` from config | ## Behavior * **No policy names** - removes all failproofai hook entries from the settings file * **Specific names** - disables those policies but keeps hooks installed ## Examples ```bash theme={null} # Remove all hooks globally failproofai policies --uninstall # Disable a specific policy (keeps hooks installed) failproofai policies --uninstall block-sudo # Remove hooks from every scope failproofai policies --uninstall --scope all # Clear custom policies path failproofai policies --uninstall --custom ``` # Check version Source: https://docs.befailproof.ai/cli/version Print the installed failproofai version ```bash theme={null} failproofai --version # or failproofai -v ``` Prints the installed version number. # Configuration Source: https://docs.befailproof.ai/configuration Config file format, three-scope system, and merge rules failproofai uses JSON configuration files to control which policies are active, how they behave, and where custom policies are loaded from. Configuration is designed to be easy to share with your team - commit it to your repo and every developer gets the same agent safety net. *** ## Configuration scopes There are three configuration scopes, evaluated in priority order: | Scope | File path | Purpose | | ----------- | ----------------------------------------- | ----------------------------------------------- | | **project** | `.failproofai/policies-config.json` | Per-repo settings, committed to version control | | **local** | `.failproofai/policies-config.local.json` | Personal per-repo overrides, gitignored | | **global** | `~/.failproofai/policies-config.json` | User-level defaults across all projects | When failproofai receives a hook event, it loads and merges all three files that exist for the current working directory. ### Merge rules **`enabledPolicies`** - the union of all three scopes. A policy enabled at any level is active. ```text theme={null} project: ["block-sudo"] local: ["block-rm-rf"] global: ["block-sudo", "sanitize-api-keys"] resolved: ["block-sudo", "block-rm-rf", "sanitize-api-keys"] ← deduplicated union ``` **`policyParams`** - first scope that defines params for a given policy wins entirely. There is no deep merging of values within a policy's params. ```text theme={null} project: block-sudo → { allowPatterns: ["sudo apt-get update"] } global: block-sudo → { allowPatterns: ["sudo systemctl status"] } resolved: { allowPatterns: ["sudo apt-get update"] } ← project wins, global ignored ``` ```text theme={null} project: (no block-sudo entry) local: (no block-sudo entry) global: block-sudo → { allowPatterns: ["sudo systemctl status"] } resolved: { allowPatterns: ["sudo systemctl status"] } ← falls through to global ``` **`customPoliciesPath`** - first scope that defines it wins. **`llm`** - first scope that defines it wins. *** ## Config file format ```json theme={null} { "enabledPolicies": [ "block-sudo", "block-rm-rf", "block-push-master", "sanitize-api-keys", "sanitize-jwt", "block-env-files", "block-read-outside-cwd" ], "policyParams": { "block-sudo": { "allowPatterns": ["sudo systemctl status", "sudo journalctl"] }, "block-push-master": { "protectedBranches": ["main", "release", "prod"] }, "block-rm-rf": { "allowPaths": ["/tmp"] }, "block-read-outside-cwd": { "allowPaths": ["/shared/data", "/opt/company"] }, "sanitize-api-keys": { "additionalPatterns": [ { "regex": "myco_[A-Za-z0-9]{32}", "label": "MyCo API key" } ] }, "warn-large-file-write": { "thresholdKb": 512 } }, "customPoliciesPath": "/home/alice/myproject/my-policies.js" } ``` *** ## Field reference ### `enabledPolicies` Type: `string[]` List of policy names to enable. Names must match exactly the policy identifiers shown by `failproofai policies`. See [Built-in Policies](/built-in-policies) for the full list. Policies not in `enabledPolicies` are inactive, even if they have entries in `policyParams`. ### `policyParams` Type: `Record>` Per-policy parameter overrides. The outer key is the policy name; the inner keys are policy-specific. Each policy documents its available parameters in [Built-in Policies](/built-in-policies). If a policy has parameters but you don't specify them, the policy's built-in defaults are used. Users who do not configure `policyParams` at all get identical behavior to previous versions. Unknown keys inside a policy's params block are silently ignored at hook-fire time but flagged as warnings when you run `failproofai policies`. #### `hint` (cross-cutting) Type: `string` (optional) A message appended to the reason when a policy returns `deny` or `instruct`. Use it to give Claude actionable guidance without modifying the policy itself. Works with any policy type — built-in, custom (`custom/`), project convention (`.failproofai-project/`), or user convention (`.failproofai-user/`). ```json theme={null} { "policyParams": { "block-force-push": { "hint": "Try creating a fresh branch instead." }, "block-sudo": { "allowPatterns": ["sudo apt-get"], "hint": "Use apt-get directly without sudo." }, "custom/my-policy": { "hint": "Ask the user for approval first." } } } ``` When `block-force-push` denies, Claude sees: *"Force-pushing is blocked. Try creating a fresh branch instead."* Non-string values and empty strings are silently ignored. If `hint` is not set, behavior is unchanged (backward-compatible). ### `customPoliciesPath` Type: `string` (absolute path) Path to a JavaScript file containing custom hook policies. This is set automatically by `failproofai policies --install --custom ` (the path is resolved to absolute before being stored). The file is loaded fresh on every hook event - there is no caching. See [Custom Policies](/custom-policies) for authoring details. ### Convention-based policies In addition to the explicit `customPoliciesPath`, failproofai automatically discovers and loads policy files from `.failproofai/policies/` directories: | Level | Directory | Scope | | ------- | -------------------------- | ------------------------------------ | | Project | `.failproofai/policies/` | Shared with team via version control | | User | `~/.failproofai/policies/` | Personal, applies to all projects | **File matching:** Only files matching `*policies.{js,mjs,ts}` are loaded (e.g. `security-policies.mjs`, `workflow-policies.js`). Other files in the directory are ignored. **No config needed:** Convention policies require no entries in `policies-config.json`. Just drop files into the directory and they're picked up on the next hook event. **Union loading:** Both project and user convention directories are scanned. All matching files from both levels are loaded (unlike `customPoliciesPath` which uses first-scope-wins). See [Custom Policies](/custom-policies) for more details and examples. ### `llm` Type: `object` (optional) LLM client configuration for policies that make AI calls. Not required for most setups. ```json theme={null} { "llm": { "model": "claude-sonnet-4-6", "apiKey": "sk-ant-..." } } ``` *** ## Managing configuration from the CLI The `policies --install` and `policies --uninstall` commands write to your agent CLI's hook settings file (the hook entry points), while `policies-config.json` is the file you manage directly. The two are separate: * **Agent CLI settings** — tells the agent to call `failproofai --hook ` on each tool use: * **Claude Code**: `~/.claude/settings.json` (user), `/.claude/settings.json` (project), `/.claude/settings.local.json` (local) * **OpenAI Codex**: `~/.codex/hooks.json` (user), `/.codex/hooks.json` (project) — Codex doesn't have a `local` scope * **GitHub Copilot CLI *(beta)***: `~/.copilot/hooks/failproofai.json` (user), `/.github/hooks/failproofai.json` (project) — Copilot has no `local` scope. Hook entries use Copilot's OS-keyed `bash`/`powershell` command fields with `timeoutSec`; the file carries a top-level `version: 1` marker. Copilot CLI support is **beta** while we verify the `events.jsonl` record schema (which the public docs do not specify) against more real-world sessions. * **Cursor Agent *(beta)***: `~/.cursor/hooks.json` (user), `/.cursor/hooks.json` (project) — Cursor has no `local` scope. Hook entries use the Claude-shaped `{type, command, timeout}` form (no `bash`/`powershell` split), but stored under camelCase event keys (`preToolUse`, `beforeSubmitPrompt`, …) in a flat array per Cursor's [hooks schema](https://cursor.com/docs/hooks); the file carries a top-level `version: 1` marker. The handler canonicalizes camelCase → PascalCase via `CURSOR_EVENT_MAP` so existing builtin policies fire unchanged. Cursor Agent support is **beta** while we verify Cursor's transcript on-disk format (not specified in the public docs) against more real-world installs. * **OpenCode *(beta)***: `~/.config/opencode/opencode.json` + `~/.config/opencode/plugins/failproofai.mjs` (user), `/.opencode/opencode.json` + `/.opencode/plugins/failproofai.mjs` (project) — OpenCode has no `local` scope. Unlike the other six CLIs, OpenCode has **no external-command hook system**: it loads in-process JS/TS plugins explicitly registered via the `plugin: []` array in `opencode.json` (auto-discovery from `.opencode/plugins/` is **not** how plugins load on opencode v1.14.33). Install drops a small generated plugin shim that subprocess-calls the failproofai binary and translates the binary's Claude-shape JSON response back into plugin semantics: `throw new Error()` for tool-event deny (cancels the tool call), `client.session.prompt(...)` for instruct AND for `Stop` / `SubagentStop` deny (submits the deny reason as the next user message — the only force-retry channel since `session.idle` is notification-only and throwing from it is a no-op), and no-op for allow. The shim canonicalizes both tool names (lowercase → PascalCase via `OPENCODE_TOOL_MAP`) and tool-input arg keys (camelCase → snake\_case via `OPENCODE_TOOL_INPUT_MAP` for `Read` / `Write` / `Edit`, e.g. `filePath` → `file_path`, `oldString` → `old_string`) before forwarding to the binary, so path-checking builtins like `block-read-outside-cwd`, `block-env-files`, and `block-secrets-write` fire unchanged on OpenCode tool calls. Sessions live in opencode's SQLite DB at `~/.local/share/opencode/opencode.db`; the dashboard's session viewer reads them via `opencode db --format json` and `opencode export `. OpenCode support is **beta** while we verify behavior across versions and against more real-world sessions. See the [OpenCode plugins docs](https://opencode.ai/docs/plugins/). * **Pi *(beta)***: `~/.pi/agent/settings.json` (user), `/.pi/settings.json` (project) — Pi has no `local` scope. Pi loads TypeScript extension packages at startup; the settings file is a flat string array `{"packages": ["./relative/path", …]}`. failproofai writes a single packages-array entry pointing at its bundled `pi-extension/` directory. The extension internally subscribes to Pi's `tool_call` / `user_bash` / `input` / `session_start` events and shells out to `failproofai --hook --cli pi`; the handler canonicalizes underscore\_lower\_snake\_case → PascalCase via `PI_EVENT_MAP` so existing builtin policies fire unchanged. Tool input args are also canonicalized via `PI_TOOL_INPUT_MAP` (Pi's Read / Write / Edit deliver `path` rather than `file_path`; mapping the top-level key lets `block-env-files` and `block-secrets-write` fire — `block-read-outside-cwd` already had a `path` fallback). Pi support is **beta** while Pi's extension API and session-log layout stabilize. * **Gemini CLI *(beta)***: `~/.gemini/settings.json` (user), `/.gemini/settings.json` (project) — Gemini has no `local` scope (it documents a `system` scope at `/etc/gemini-cli/settings.json` which failproofai does not expose). Hook entries use Claude's `{type, command, timeout}` form wrapped in Gemini's `{matcher, hooks: [...]}` matcher schema with `matcher: "*"` by default. Events are PascalCase (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, `Notification`, `SessionEnd`); the handler maps to Claude canonical names via `GEMINI_EVENT_MAP`. Tool names are snake\_case (`run_shell_command`, `read_file`, `write_file`, `replace`, …) — the handler canonicalizes via `GEMINI_TOOL_MAP` so existing builtin policies fire unchanged. The policy evaluator emits Gemini's flat `{decision: "deny", reason}` shape (preferred per Gemini's "Golden Rule" exit-0 contract), `{hookSpecificOutput: {hookEventName, additionalContext}}` for context injection on BeforeAgent / AfterTool / SessionStart, and `{decision: "block", reason}` on AfterAgent for force-retry semantics. Gemini CLI support is **beta** while we widen real-world coverage. See the [Gemini CLI hooks docs](https://geminicli.com/docs/hooks/). * **`policies-config.json`** — tells failproofai which policies to evaluate and with what params (shared across all agent CLIs) Pass `--cli claude|codex|copilot|cursor|opencode|pi|gemini` to target a specific agent (space-separated or repeated for any subset): ```bash theme={null} failproofai policies --install --cli codex --scope project failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project failproofai policies --install --cli gemini --scope project failproofai policies --install --cli claude codex copilot cursor opencode pi gemini ``` When `--cli` is omitted, `failproofai` detects which agent CLIs are installed (`which claude` / `which codex` / `which copilot` / `which cursor-agent` / `which opencode` / `which pi` / `which gemini`): * **One CLI detected** — auto-selects that CLI without prompting. * **Multiple CLIs detected** in an interactive terminal — shows an arrow-key single-select prompt grouped into a `Detected (N)` section (with an `Install for all N detected` aggregate row + each detected CLI individually) and a `Not installed (M) · install hooks ahead of time` section listing every undetected supported CLI as a forward-install option (↑↓ to move, Enter to select, ^C to quit). The uninstall flow shows only the Detected section. * **Multiple CLIs detected** in a non-interactive run (CI, no TTY) — installs for all detected CLIs without prompting. * **None detected** — falls back to `claude`, with a warning that no agent binary was found in PATH; the hook command is still written so it activates as soon as you install one. You can edit `policies-config.json` directly at any time; changes take effect immediately on the next hook event with no restart needed. *** ## Example: project-level config with team defaults Commit `.failproofai/policies-config.json` to your repo: ```json theme={null} { "enabledPolicies": [ "block-sudo", "block-rm-rf", "block-push-master", "sanitize-api-keys", "block-env-files" ], "policyParams": { "block-push-master": { "protectedBranches": ["main", "release", "hotfix"] } } } ``` Each developer can then create `.failproofai/policies-config.local.json` (gitignored) for personal overrides without affecting teammates. # Custom Policies Source: https://docs.befailproof.ai/custom-policies Write your own policies in JavaScript - enforce conventions, prevent drift, detect failures, integrate with external systems Custom policies let you write rules for any agent behavior: enforce project conventions, prevent drift, gate destructive operations, detect stuck agents, or integrate with Slack, approval workflows, and more. They use the same hook event system and `allow`, `deny`, `instruct` decisions as built-in policies. *** ## Quick example ```js theme={null} // my-policies.js import { customPolicies, allow, deny, instruct } from "failproofai"; customPolicies.add({ name: "no-production-writes", description: "Block writes to paths containing 'production'", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Write" && ctx.toolName !== "Edit") return allow(); const path = ctx.toolInput?.file_path ?? ""; if (path.includes("production")) { return deny("Writes to production paths are blocked"); } return allow(); }, }); ``` Install it: ```bash theme={null} failproofai policies --install --custom ./my-policies.js ``` *** ## Two ways to load custom policies ### Option 1: Convention-based (recommended) Drop `*policies.{js,mjs,ts}` files into `.failproofai/policies/` and they're automatically loaded — no flags or config changes needed. This works like git hooks: drop a file, it just works. ``` # Project level — committed to git, shared with the team .failproofai/policies/security-policies.mjs .failproofai/policies/workflow-policies.mjs # User level — personal, applies to all projects ~/.failproofai/policies/my-policies.mjs ``` **How it works:** * Both project and user directories are scanned (union — not first-scope-wins) * Files are loaded alphabetically within each directory. Prefix with `01-`, `02-` to control order * Only files matching `*policies.{js,mjs,ts}` are loaded; other files are ignored * Each file is loaded independently (fail-open per file) * Works alongside explicit `--custom` and built-in policies Convention policies are the easiest way to build a quality standard for your org. Commit `.failproofai/policies/` to git and every team member gets the same rules automatically — no per-developer setup needed. As your team discovers new failure modes, add a policy and push. Over time these become a living quality standard that keeps improving with every contribution. ### Option 2: Explicit file path ```bash theme={null} # Install with a custom policies file failproofai policies --install --custom ./my-policies.js # Replace the policies file path failproofai policies --install --custom ./new-policies.js # Remove the custom policies path from config failproofai policies --uninstall --custom ``` The resolved absolute path is stored in `policies-config.json` as `customPoliciesPath`. The file is loaded fresh on every hook event - there is no caching between events. ### Using both together Convention policies and the explicit `--custom` file can coexist. Load order: 1. Explicit `customPoliciesPath` file (if configured) 2. Project convention files (`{cwd}/.failproofai/policies/`, alphabetical) 3. User convention files (`~/.failproofai/policies/`, alphabetical) *** ## API ### Import ```js theme={null} import { customPolicies, allow, deny, instruct } from "failproofai"; ``` ### `customPolicies.add(hook)` Registers a policy. Call this as many times as needed for multiple policies in the same file. ```ts theme={null} customPolicies.add({ name: string; // required - unique identifier description?: string; // shown in `failproofai policies` output match?: { events?: HookEventType[] }; // filter by event type; omit to match all fn: (ctx: PolicyContext) => PolicyResult | Promise; }); ``` ### Decision helpers | Function | Effect | Use when | | ------------------- | ----------------------------- | --------------------------------------------- | | `allow()` | Permit the operation silently | The action is safe, no message needed | | `deny(message)` | Block the operation | The agent should not take this action | | `instruct(message)` | Add context without blocking | Give the agent extra context to stay on track | `deny(message)` - the message appears to Claude prefixed with `"Blocked by failproofai:"`. A single `deny` short-circuits all further evaluation. `instruct(message)` - the message is appended to Claude's context for the current tool call. All `instruct` messages are accumulated and delivered together. You can append extra guidance to any `deny` or `instruct` message by adding a `hint` field in `policyParams` — no code change needed. This works for custom (`custom/`), project convention (`.failproofai-project/`), and user convention (`.failproofai-user/`) policies too. See [Configuration → hint](/configuration#hint-cross-cutting) for details. ### Informational allow messages `allow(message)` permits the operation **and** sends an informational message back to Claude. The message is delivered as `additionalContext` in the hook handler's stdout response — the same mechanism used by `instruct`, but semantically different: it's a status update, not a warning. | Function | Effect | Use when | | ---------------- | --------------------------------- | ---------------------------------------------------------- | | `allow(message)` | Permit and send context to Claude | Confirm a check passed, or explain why a check was skipped | Use cases: * **Status confirmations:** `allow("All CI checks passed.")` — tells Claude everything is green * **Fail-open explanations:** `allow("GitHub CLI not installed, skipping CI check.")` — tells Claude why a check was skipped so it has full context * **Multiple messages accumulate:** if several policies each return `allow(message)`, all messages are joined with newlines and delivered together ```js theme={null} customPolicies.add({ name: "confirm-branch-status", match: { events: ["Stop"] }, fn: async (ctx) => { const cwd = ctx.session?.cwd; if (!cwd) return allow("No working directory, skipping branch check."); // ... check branch status ... if (allPushed) { return allow("Branch is up to date with remote."); } return deny("Unpushed changes detected."); }, }); ``` ### `PolicyContext` fields | Field | Type | Description | | ----------- | -------------------------------------- | ----------------------------------------------------------- | | `eventType` | `string` | `"PreToolUse"`, `"PostToolUse"`, `"Notification"`, `"Stop"` | | `toolName` | `string \| undefined` | The tool being called (e.g. `"Bash"`, `"Write"`, `"Read"`) | | `toolInput` | `Record \| undefined` | The tool's input parameters | | `payload` | `Record` | Full raw event payload from Claude Code | | `session` | `SessionMetadata \| undefined` | Session context (see below) | ### `SessionMetadata` fields | Field | Type | Description | | ---------------- | -------- | -------------------------------------------- | | `sessionId` | `string` | Claude Code session identifier | | `cwd` | `string` | Working directory of the Claude Code session | | `transcriptPath` | `string` | Path to the session's JSONL transcript file | ### Event types | Event | When it fires | `toolInput` contents | | -------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `PreToolUse` | Before Claude runs a tool | The tool's input (e.g. `{ command: "..." }` for Bash) | | `PostToolUse` | After a tool completes | The tool's input + `tool_result` (the output) | | `Notification` | When Claude sends a notification | `{ message: "...", notification_type: "idle" \| "permission_prompt" \| ... }` - hooks must always return `allow()`, they cannot block notifications | | `Stop` | When the Claude session ends | Empty | *** ## Evaluation order Policies are evaluated in this order: 1. Built-in policies (in definition order) 2. Explicit custom policies from `customPoliciesPath` (in `.add()` order) 3. Convention policies from project `.failproofai/policies/` (files alphabetical, `.add()` order within) 4. Convention policies from user `~/.failproofai/policies/` (files alphabetical, `.add()` order within) The first `deny` short-circuits all subsequent policies. All `instruct` messages are accumulated and delivered together. *** ## Transitive imports Custom policy files can import local modules using relative paths: ```js theme={null} // my-policies.js import { isBlockedPath } from "./utils.js"; import { checkApproval } from "./approval-client.js"; customPolicies.add({ name: "approval-gate", fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const approved = await checkApproval(ctx.toolInput?.command, ctx.session?.sessionId); return approved ? allow() : deny("Approval required for this command"); }, }); ``` All relative imports reachable from the entry file are resolved. This is implemented by rewriting `from "failproofai"` imports to the actual dist path and creating temporary `.mjs` files to ensure ESM compatibility. *** ## Event type filtering Use `match.events` to limit when a policy fires: ```js theme={null} customPolicies.add({ name: "require-summary-on-stop", match: { events: ["Stop"] }, fn: async (ctx) => { // Only fires when the session ends // ctx.session.transcriptPath contains the full session log return allow(); }, }); ``` Omit `match` entirely to fire on every event type. *** ## Error handling and failure modes Custom policies are **fail-open**: errors never block built-in policies or crash the hook handler. | Failure | Behavior | | -------------------------------- | ------------------------------------------------------------------------------------ | | `customPoliciesPath` not set | No explicit custom policies run; convention policies and built-ins continue normally | | File not found | Warning logged to `~/.failproofai/hook.log`; built-ins continue | | Syntax/import error (explicit) | Error logged to `~/.failproofai/hook.log`; explicit custom policies skipped | | Syntax/import error (convention) | Error logged; that file skipped, other convention files still load | | `fn` throws at runtime | Error logged; that hook treated as `allow`; other hooks continue | | `fn` takes longer than 10s | Timeout logged; treated as `allow` | | Convention directory missing | No convention policies run; no error | To debug custom policy errors, watch the log file: ```bash theme={null} tail -f ~/.failproofai/hook.log ``` *** ## Full example: multiple policies ```js theme={null} // my-policies.js import { customPolicies, allow, deny, instruct } from "failproofai"; // Prevent agent from writing to secrets/ directory customPolicies.add({ name: "block-secrets-dir", description: "Prevent agent from writing to secrets/ directory", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (!["Write", "Edit"].includes(ctx.toolName ?? "")) return allow(); const path = ctx.toolInput?.file_path ?? ""; if (path.includes("secrets/")) return deny("Writing to secrets/ is not permitted"); return allow(); }, }); // Keep the agent on track: verify tests before committing customPolicies.add({ name: "remind-test-before-commit", description: "Keep the agent on track: verify tests pass before committing", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = ctx.toolInput?.command ?? ""; if (/git\s+commit/.test(cmd)) { return instruct("Verify all tests pass before committing. Run `bun test` if you haven't already."); } return allow(); }, }); // Prevent unplanned dependency changes during freeze customPolicies.add({ name: "dependency-freeze", description: "Prevent unplanned dependency changes during freeze period", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = ctx.toolInput?.command ?? ""; const isInstall = /^(npm install|yarn add|bun add|pnpm add)\s+\S/.test(cmd); if (isInstall && process.env.DEPENDENCY_FREEZE === "1") { return deny("Package installs are frozen. Unset DEPENDENCY_FREEZE to allow."); } return allow(); }, }); export { customPolicies }; ``` *** ## Examples The `examples/` directory contains ready-to-run policy files: | File | Contents | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `examples/policies-basic.js` | Five starter policies covering common agent failure modes | | `examples/policies-advanced/index.js` | Advanced patterns: transitive imports, async calls, output scrubbing, and session-end hooks | | `examples/convention-policies/security-policies.mjs` | Convention-based security policies (block .env writes, prevent git history rewriting) | | `examples/convention-policies/workflow-policies.mjs` | Convention-based workflow policies (test reminders, audit file writes) | ### Using explicit file examples ```bash theme={null} failproofai policies --install --custom ./examples/policies-basic.js ``` ### Using convention-based examples ```bash theme={null} # Copy to project level mkdir -p .failproofai/policies cp examples/convention-policies/*.mjs .failproofai/policies/ # Or copy to user level mkdir -p ~/.failproofai/policies cp examples/convention-policies/*.mjs ~/.failproofai/policies/ ``` No install command needed — the files are picked up automatically on the next hook event. # Dashboard Source: https://docs.befailproof.ai/dashboard Monitor agent sessions, review tool calls, and manage policies The failproofai dashboard is a local web application for monitoring your AI agent sessions and managing policies. See what your agents did while you were away. *** ## Starting the dashboard ```bash theme={null} failproofai ``` Opens at `http://localhost:8020`. The dashboard reads directly from the filesystem - your Claude Code project folders and the failproofai config files. Nothing is written to a remote service. *** ## Pages ### Projects Lists all Claude Code, OpenAI Codex, GitHub Copilot CLI *(beta)*, Cursor Agent *(beta)*, OpenCode *(beta)*, Pi *(beta)*, and Gemini CLI *(beta)* projects found on your machine. Claude projects are discovered from `~/.claude/projects/` (or the path set by `CLAUDE_PROJECTS_PATH`); Codex projects are discovered by scanning every transcript under `~/.codex/sessions///
/*.jsonl` and grouping by the `cwd` recorded in each session's first record; Copilot CLI projects are discovered by scanning each `~/.copilot/session-state//workspace.yaml` (configurable via `COPILOT_HOME`) and grouping by its `cwd` field; Cursor Agent projects are discovered by scanning per-session metadata under `~/.cursor/agent-sessions//` (configurable via `CURSOR_HOME`, with `conversations/` and `sessions/` probed as fallbacks) for a `cwd` scalar in `meta.json` / `session.json` / `workspace.yaml`; OpenCode projects are discovered by querying its SQLite DB at `~/.local/share/opencode/opencode.db` via `opencode db --format json` (we read the `session` and `project` tables and group by `project_id`); Pi projects are discovered by scanning per-session JSONL transcripts under `~/.pi/agent/sessions//_.jsonl` (configurable via `PI_SESSIONS_DIR`) and pulling the `cwd` from each session's first record; Gemini CLI projects are discovered by scanning `~/.gemini/tmp//chats/session--.jsonl` (configurable via `GEMINI_SESSIONS_DIR`) and recovering the canonical cwd from the sibling `.project_root` text marker. A project that has been used by multiple CLIs renders as a single row with all matching badges. Use the **CLI** dropdown above the table to filter by a specific agent CLI; the URL preserves your selection as `?cli=claude|codex|copilot|cursor|opencode|pi|gemini`. Each project shows: * Project name (derived from the folder path) * A CLI badge — `Claude Code` (orange), `OpenAI Codex` (purple), `GitHub Copilot` (blue), `Cursor Agent` (emerald), `OpenCode` (amber), `Pi` (pink), and/or `Gemini CLI` (sky) * Date of most recent session activity Click a project to see its sessions. ### Sessions Lists all sessions within a project. Each session shows: * Session ID * Start and end timestamps * Number of tool calls * Hook activity count (policies that fired) Use the date range filter and session ID search to narrow the list. Sessions are paginated. Click a session to open the session viewer. ### Session viewer The session viewer answers the key question for autonomous agents: what did the agent do, and did it stay on track? A CLI badge beside the header indicates whether the session is a Claude Code, OpenAI Codex, GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, or Gemini CLI transcript. It shows a timeline of everything that happened in a session: * **Messages** - Claude's text responses and user prompts * **Tool calls** - Every tool Claude invoked, with its input and output * **Policy activity** - For each tool call, which policies fired and what decision they returned The stats bar at the top shows session duration, total tool calls, and a summary of hook decisions (allow / deny / instruct counts). Click the **Download Logs** button to export the session. For Claude Code, Codex, Copilot, Cursor, Pi, and Gemini sessions you get the original on-disk JSONL transcript byte-for-byte; for OpenCode (whose sessions live in SQLite, not on disk) you get a JSON document mirroring the underlying `session` / `messages` / `parts` tables. ### Audit A personality-driven report of how your agent has actually been behaving across past sessions. Runs the same scan as the `failproofai audit` CLI but renders it as a six-section dashboard: 1. **Identity** — classifies your agent into one of 8 archetypes (`the optimist`, `the cowboy`, `the explorer`, `the goldfish`, `the paranoid architect`, `the precision builder`, `the hammer`, `the ghost`) based on which detectors + policies fired and how heavily. Renders an 8×8 pixel sigil, the archetype tagline, "common in" / "primary risk" framing, and the closing one-liner. 2. **Show off your agent** — captures the identity card as a 1200×630 PNG suitable for posting to X / LinkedIn (click `make poster`). 3. **Strengths** — green-checked behaviors your agent already does right, derived from the live audit data (clean tool-call rate, average session length, zero credential leaks, zero retry storms, etc.). 4. **Score + leaderboard** — 0–100 score with letter grade (S/A/B/C/D/F), a distribution histogram showing where you fall in the cohort, prose ("a B starts at 71. you're 13 points away."), and a leaderboard table with your row highlighted. 5. **Findings** — per-finding cards ranked by impact. Each card surfaces what happened, what it costs, an evidence sample with real captured commands, and the failproofai policy that would catch the same pattern (`$ failproofai policy add `, click-to-copy). 6. **Prescribed policies + return loop** — a grid of every unenabled builtin policy that would close a gap, with a projected-score callout, plus a "re-audit in 7 days" CTA. Driven by the `failproofai audit` runtime — see [Audit CLI](/cli/audit) for the underlying scan engine, supported flags, and per-transcript cache invariants. The dashboard caches the latest result at `~/.failproofai/audit-dashboard.json` (mode `0600`, single slot, new runs overwrite) so revisits are instant; **both the per-transcript and whole-result caches are rejected on read once they're older than 7 days** so the dashboard never silently serves a week-old result — past the TTL `/audit` falls through to its empty state and prompts a fresh run. Clicking `[ re-audit now ]` near the bottom of the report POSTs `/api/audit/run` with `noCache: true` — re-audit bypasses the per-transcript cache and re-scans every transcript from scratch rather than silently returning the cached result — and the dashboard polls `/api/audit/status` at 1Hz until the run finishes; a sticky pink progress strip pins to the top of the viewport during the run with an elapsed timer, and the fresh result swaps in place on success (no full-page reload; a failed re-audit leaves the prior report intact). On failure the strip turns red with copy keyed off the `RerunError.kind` (`timeout` / `network` / `post_failed`). Empty state (no cache or expired) and zero-sessions state (cache exists but the scan found no transcripts) are surfaced separately. ### Policies A two-tab page for managing policies and reviewing activity. * Multi-select which agent CLIs failproofai protects from a single panel — Claude Code, OpenAI Codex, GitHub Copilot, Cursor Agent, OpenCode, Pi, and Gemini CLI all have a row with install status (`Active` / `Detected` / `Inactive`), the user-scope settings path, and a brand-colored accent. Check or uncheck the CLIs you want and click `Apply changes` to install/uninstall the diff in one step. CLIs whose binary is detected on PATH are pre-checked. * Toggle individual policies on or off with a single click (writes to `~/.failproofai/policies-config.json` — shared across every installed CLI) * Expand a policy to configure its parameters (for policies that support `policyParams`) * Set a custom policies file path * Full paginated history of every hook event that has fired across all sessions * Filter by decision, event type, CLI (Claude Code / OpenAI Codex / GitHub Copilot *(beta)* / Cursor Agent *(beta)* / OpenCode *(beta)* / Pi *(beta)* / Gemini CLI *(beta)*), policy name, or session ID * Each row shows: timestamp, policy name, decision, CLI badge (orange = Claude Code, purple = OpenAI Codex, blue = GitHub Copilot, emerald = Cursor Agent, amber = OpenCode, pink = Pi, sky = Gemini CLI), tool name, session ID, and the reason for deny/instruct decisions * Click a session ID to open its transcript — the viewer auto-detects which CLI fired the hook (Claude `~/.claude/projects/…`, Codex `~/.codex/sessions/…`, Copilot CLI `~/.copilot/session-state//events.jsonl`, Cursor Agent `~/.cursor/agent-sessions//events.jsonl`, OpenCode `~/.local/share/opencode/opencode.db`, Pi `~/.pi/agent/sessions//.jsonl`, Gemini CLI `~/.gemini/tmp//chats/.jsonl`) and renders the matching CLI badge in the header *** ## Auto-refresh The dashboard has an auto-refresh toggle in the top navigation. When enabled, the current page refreshes periodically to show new sessions and policy activity as they appear. Essential for monitoring long-running autonomous agent sessions. *** ## Disabling pages If you only need some parts of the dashboard, set `FAILPROOFAI_DISABLE_PAGES` to a comma-separated list of page names: ```bash theme={null} FAILPROOFAI_DISABLE_PAGES=policies failproofai ``` Valid values: `policies`, `projects`, `audit`. *** ## Configuring the projects path By default, the dashboard reads from the standard Claude Code projects directory. Override it for custom setups: ```bash theme={null} CLAUDE_PROJECTS_PATH=/custom/path/to/projects failproofai ``` *** ## Accessing from a non-localhost host When running the dashboard in **dev mode** (`npm run dev`) and accessing it from a hostname other than `localhost` - for example, a custom domain, a remote IP, or a tunneled URL - you may see a warning like: ```text theme={null} ⚠ Blocked cross-origin request to Next.js dev resource /_next/webpack-hmr from "dashboard.example.com". ``` This is Next.js blocking cross-origin access to its HMR (hot module reload) websocket, which is a dev-only feature. To allow your host, use the `--allowed-origins` flag: ```bash theme={null} npm run dev -- --allowed-origins dashboard.example.com ``` For multiple hosts or IPs, pass a comma-separated list: ```bash theme={null} npm run dev -- --allowed-origins dashboard.example.com,192.168.1.5 ``` You can also set the `FAILPROOFAI_ALLOWED_DEV_ORIGINS` environment variable instead: ```bash theme={null} FAILPROOFAI_ALLOWED_DEV_ORIGINS=dashboard.example.com npm run dev ``` This only applies to dev mode. When running `failproofai` (production mode), there is no HMR websocket and no cross-origin dev resource issue. # Examples Source: https://docs.befailproof.ai/examples How to set up hooks for Claude Code and the Agents SDK Ready-to-use examples for common scenarios. Each one shows how to install and what to expect. *** ## Setting up hooks for Claude Code Failproof AI integrates with Claude Code via its [hooks system](https://docs.anthropic.com/en/docs/claude-code/hooks). When you run `failproofai policies --install`, it registers hook commands in Claude Code's `settings.json` that fire on every tool call. ```bash theme={null} npm install -g failproofai ``` ```bash theme={null} failproofai policies --install ``` ```bash theme={null} cat ~/.claude/settings.json | grep failproofai ``` You should see hook entries for `PreToolUse`, `PostToolUse`, `Notification`, and `Stop` events. ```bash theme={null} claude ``` Policies now run automatically on every tool call. Try asking Claude to run `sudo rm -rf /` - it will be blocked. *** ## Setting up hooks for the Agents SDK If you're building with the [Agents SDK](https://docs.anthropic.com/en/docs/agents-sdk), you can use the same hook system programmatically. ```bash theme={null} npm install failproofai ``` Pass hook commands when creating your agent process. The hooks fire the same way as in Claude Code - via stdin/stdout JSON: ```bash theme={null} failproofai --hook PreToolUse # called before each tool failproofai --hook PostToolUse # called after each tool ``` ```javascript theme={null} import { customPolicies, allow, deny } from "failproofai"; customPolicies.add({ name: "limit-to-project-dir", description: "Keep the agent inside the project directory", match: { events: ["PreToolUse"] }, fn: async (ctx) => { const path = String(ctx.toolInput?.file_path ?? ""); if (path.startsWith("/") && !path.startsWith(ctx.session?.cwd ?? "")) { return deny("Agent is restricted to the project directory"); } return allow(); }, }); ``` ```bash theme={null} failproofai policies --install --custom ./my-agent-policies.js ``` *** ## Block destructive commands The most common setup - prevent agents from doing irreversible damage. ```bash theme={null} failproofai policies --install block-sudo block-rm-rf block-force-push block-curl-pipe-sh ``` What this does: * `block-sudo` - blocks all `sudo` commands * `block-rm-rf` - blocks recursive file deletion * `block-force-push` - blocks `git push --force` * `block-curl-pipe-sh` - blocks piping remote scripts to shell *** ## Prevent secret leakage Stop agents from seeing or leaking credentials in tool output. ```bash theme={null} failproofai policies --install sanitize-api-keys sanitize-jwt sanitize-connection-strings sanitize-bearer-tokens ``` These fire on `PostToolUse` - after a tool runs, they scrub the output before the agent sees it. *** ## Get Slack alerts when agents need attention Use the notification hook to forward idle alerts to Slack. ```javascript theme={null} import { customPolicies, allow, instruct } from "failproofai"; customPolicies.add({ name: "slack-on-idle", description: "Alert Slack when the agent is waiting for input", match: { events: ["Notification"] }, fn: async (ctx) => { const webhookUrl = process.env.SLACK_WEBHOOK_URL; if (!webhookUrl) return allow(); const message = String(ctx.payload?.message ?? "Agent is waiting"); const project = ctx.session?.cwd ?? "unknown"; try { await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `*${message}*\nProject: \`${project}\``, }), signal: AbortSignal.timeout(5000), }); } catch { // never block the agent if Slack is unreachable } return allow(); }, }); ``` Install it: ```bash theme={null} SLACK_WEBHOOK_URL=https://hooks.slack.com/... failproofai policies --install --custom ./slack-alerts.js ``` *** ## Keep agents on a branch Prevent agents from switching branches or pushing to protected ones. ```javascript theme={null} import { customPolicies, allow, deny } from "failproofai"; customPolicies.add({ name: "stay-on-branch", description: "Prevent the agent from checking out other branches", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); if (/git\s+checkout\s+(?!-b)/.test(cmd)) { return deny("Stay on the current branch. Create a new branch with -b if needed."); } return allow(); }, }); ``` *** ## Require tests before commits Remind agents to run tests before committing. ```javascript theme={null} import { customPolicies, allow, instruct } from "failproofai"; customPolicies.add({ name: "test-before-commit", description: "Remind the agent to run tests before committing", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); if (/git\s+commit/.test(cmd)) { return instruct("Run tests before committing. Use `npm test` or `bun test` first."); } return allow(); }, }); ``` *** ## Lock down a production repo Commit a project-level config so every developer on your team gets the same policies. Create `.failproofai/policies-config.json` in your repo: ```json theme={null} { "enabledPolicies": [ "block-sudo", "block-rm-rf", "block-force-push", "block-push-master", "block-env-files", "sanitize-api-keys", "sanitize-jwt" ], "policyParams": { "block-push-master": { "protectedBranches": ["main", "release", "production"] } } } ``` Then commit it: ```bash theme={null} git add .failproofai/policies-config.json git commit -m "Add failproofai team policies" ``` Every team member who has failproofai installed will automatically pick up these rules. *** ## Build an org-wide quality standard with convention policies The most impactful setup: commit `.failproofai/policies/` to your repo with policies tailored to your project. Every team member gets them automatically — no install commands, no config changes. ```bash theme={null} mkdir -p .failproofai/policies ``` ```js theme={null} // .failproofai/policies/team-policies.mjs import { customPolicies, allow, deny, instruct } from "failproofai"; // Enforce your team's preferred package manager // (or enable the built-in prefer-package-manager policy instead) customPolicies.add({ name: "enforce-bun", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); const cmd = String(ctx.toolInput?.command ?? ""); if (/\bnpm\b/.test(cmd)) return deny("Use bun instead of npm."); return allow(); }, }); // Remind the agent to run tests before committing customPolicies.add({ name: "test-before-commit", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); if (/git\s+commit/.test(ctx.toolInput?.command ?? "")) { return instruct("Run tests before committing."); } return allow(); }, }); ``` ```bash theme={null} git add .failproofai/policies/ git commit -m "Add team quality policies" ``` As your team hits new failure modes, add policies and push. Everyone gets the update on their next `git pull`. These policies become a living quality standard that grows with your team. *** ## More examples The [`examples/`](https://github.com/failproofai/failproofai/tree/main/examples) directory in the repo contains: | File | What it shows | | ---------------------------- | ---------------------------------------------------------------------------------- | | `policies-basic.js` | Starter policies - block production writes, force-push, piped scripts | | `policies-notification.js` | Slack alerts for idle notifications and session end | | `policies-advanced/index.js` | Transitive imports, async hooks, PostToolUse output scrubbing, Stop event handling | # For agents Source: https://docs.befailproof.ai/for-agents Add Failproof AI knowledge to your coding agent in one command. Works with Claude Code, Cursor, Windsurf, and more. Add the full Failproof AI reference to your coding agent in one command. Works with Claude Code, Cursor, Windsurf, and any other agent that supports skills. ```bash theme={null} npx skills add https://docs.befailproof.ai ``` `npx skills` detects which agents you have installed and adds the skill in the right format for each one automatically. ## What the skill covers | Area | What's included | | --------------- | -------------------------------------------------------------------- | | Policies | Built-in policy names, event types, parameters, enable/disable | | Custom policies | `customPolicies.add()`, match filters, `allow`/`deny`/`instruct` API | | Context object | `ctx.eventType`, `ctx.toolName`, `ctx.toolInput`, `ctx.session` | | Configuration | `policies-config.json` structure, scope merging, `policyParams` | | CLI | `failproofai policies --install`, `--uninstall`, `--custom`, scopes | | Dashboard | Session viewer, policy activity, environment variables | | Architecture | Hook handler flow, exit codes, stdin/stdout contract | ## Is the skill complete? Mintlify generates `llms.txt` from all pages in the navigation. The Failproof AI docs cover the full API - every policy, option, and example is included. If you find something missing, the source is at `https://docs.befailproof.ai/llms-full.txt`. For targeted context, link directly to a specific page: ```bash theme={null} # Just the custom policies API npx skills add https://docs.befailproof.ai/custom-policies # Just the built-in policies npx skills add https://docs.befailproof.ai/built-in-policies ``` # Getting started Source: https://docs.befailproof.ai/getting-started Install failproofai, enable policies, and let your agents run reliably ## Requirements * **Node.js** >= 20.9.0 * **Bun** >= 1.3.0 (optional - only needed for building from source) *** ## Installation ```bash npm theme={null} npm install -g failproofai ``` ```bash bun theme={null} bun add -g failproofai ``` *** ## Quick start Policies are rules that run before and after every agent tool call. They catch destructive commands, secret leakage, and other failure modes before they cause damage. ```bash theme={null} failproofai policies --install ``` This writes hook entries into your installed agent CLIs (Claude Code's `~/.claude/settings.json`, OpenAI Codex's `~/.codex/hooks.json`, GitHub Copilot CLI's `~/.copilot/hooks/failproofai.json`, Cursor Agent's `~/.cursor/hooks.json`, OpenCode's generated plugin shim at `~/.config/opencode/plugins/failproofai.mjs` plus a registration entry in `~/.config/opencode/opencode.json`'s `plugin` array, Pi's `~/.pi/agent/settings.json`, or Gemini CLI's `~/.gemini/settings.json`). When more than one is present you'll be prompted; pass `--cli claude codex copilot cursor opencode pi gemini` (any subset) to skip the prompt. GitHub Copilot CLI, Cursor Agent, OpenCode, Pi, and Gemini CLI support are **beta** — install with `--cli copilot`, `--cli cursor`, `--cli opencode`, `--cli pi`, or `--cli gemini`. ```bash theme={null} failproofai policies --install --scope project failproofai policies --install --cli codex --scope project failproofai policies --install --cli copilot --scope project failproofai policies --install --cli cursor --scope project failproofai policies --install --cli opencode --scope project failproofai policies --install --cli pi --scope project failproofai policies --install --cli gemini --scope project failproofai policies --install block-sudo block-rm-rf sanitize-api-keys ``` ```bash theme={null} failproofai policies ``` Shows every policy, whether it's enabled, and any configured parameters. ```bash theme={null} failproofai ``` Opens a local dashboard at `http://localhost:8020` where you can browse sessions, inspect tool calls, and manage policies. Start Claude Code as usual. If the agent tries something risky, failproofai intercepts it automatically. Leave it running unattended and review what happened in the dashboard. *** ## How policies work Every time an agent runs a tool, Claude Code calls failproofai as a subprocess: ```text theme={null} Claude Code → failproofai --hook PreToolUse → reads stdin JSON evaluates policies writes decision to stdout ``` Each policy returns one of three decisions: * **allow** - the agent proceeds normally * **deny** - the action is blocked, the agent is told why * **instruct** - extra context is added to the agent's prompt Policies run in your local process. Nothing is sent to a remote service. *** ## Set up team policies with convention-based policies The fastest way to establish quality standards across your team is the `.failproofai/policies/` convention. Drop policy files into this directory and they're loaded automatically — no flags, no config changes, no install commands. ```bash theme={null} mkdir -p .failproofai/policies ``` Copy the starter examples or write your own: ```bash theme={null} cp node_modules/failproofai/examples/convention-policies/*.mjs .failproofai/policies/ ``` Or create a new one: ```js theme={null} // .failproofai/policies/team-policies.mjs import { customPolicies, allow, deny, instruct } from "failproofai"; customPolicies.add({ name: "test-before-commit", match: { events: ["PreToolUse"] }, fn: async (ctx) => { if (ctx.toolName !== "Bash") return allow(); if (/git\s+commit/.test(ctx.toolInput?.command ?? "")) { return instruct("Run tests before committing."); } return allow(); }, }); ``` ```bash theme={null} git add .failproofai/policies/ git commit -m "Add team quality policies" ``` Every team member who has failproofai installed picks up these policies automatically. No per-developer setup needed. Commit `.failproofai/policies/` to your repo so the whole team shares the same standards. As your team discovers new failure modes, add policies and push — everyone gets the update on their next `git pull`. Over time these policies become a living quality standard that keeps improving. *** ## Data storage All configuration and logs stay on your machine: | Path | What it stores | | ----------------------------------------- | -------------------------------- | | `~/.failproofai/policies-config.json` | Global policy config | | `~/.failproofai/hook-activity.jsonl` | Hook execution history | | `~/.failproofai/hook.log` | Debug log for custom hook errors | | `.failproofai/policies-config.json` | Per-project config (committed) | | `.failproofai/policies-config.local.json` | Personal overrides (gitignored) | *** ## Uninstalling ```bash theme={null} failproofai policies --uninstall ``` Removes hook entries from `~/.claude/settings.json`. Config files in `~/.failproofai/` are kept. *** ## Next steps Scopes and config file format All 26 policies with parameters Write your own policies in JavaScript Monitor sessions and review policy activity # Failproof AI Source: https://docs.befailproof.ai/introduction FailproofAI gives AI agents 39 built-in failure policies that catch loops, secret leaks, destructive tool calls, and more in a single install. [![npm weekly downloads](https://img.shields.io/npm/dw/failproofai?style=flat-square\&color=2ea44f)](https://www.npmjs.com/package/failproofai) Hooks and policies for **AI failure handling**, **error recovery**, and **LLM reliability**. Keep your AI agents reliable and running autonomously across **Claude Code**, **OpenAI Codex**, **GitHub Copilot**, **Cursor Agent**, **OpenCode**, **Pi**, **Gemini CLI**, and the **Agents SDK**. AI agents fail in predictable ways. They run destructive commands, leak secrets, drift off-task, get stuck in loops, or push directly to main. Left unattended, small failures cascade into outages, leaked credentials, and lost work. FailproofAI solves this with **policies**. These rules hook into every agent tool call to **detect failures**, **mitigate them** (block, instruct, sanitize), and **alert you** when something needs attention. A local dashboard lets you review every tool call, agent failure, and recovery action afterward. No data leaves your machine. ## Get started Block destructive commands, prevent secret leakage, keep agents inside project boundaries, and more. All out of the box. Write your own rules in JavaScript with a simple allow / deny / instruct API. See what your agents did while you were away. Browse sessions, inspect tool calls, review where policies fired. Tune any policy without code. Set allowlists, protected branches, or thresholds per-project or globally. ## Quick start ```bash npm theme={null} npm install -g failproofai ``` ```bash bun theme={null} bun add -g failproofai ``` ```bash theme={null} failproofai policies --install # enable policies (or skip — `failproofai` will offer to set them up on first run) failproofai # launch the dashboard ``` See the [Getting started](/getting-started) guide for the full walkthrough. # Package Aliases Source: https://docs.befailproof.ai/package-aliases Registered typosquat-prevention aliases and how they work ## Official package The canonical npm package is **`failproofai`**: ```bash theme={null} npm install -g failproofai # or bun add -g failproofai ``` *** ## Why we own the alias names Typosquatting is a common supply-chain attack where a malicious actor registers a package name that is one keystroke away from a popular package. Unsuspecting users who mistype the install command end up running attacker-controlled code with full system access - exactly the kind of threat Failproof AI is designed to defend against. To eliminate this surface, **we pre-emptively own all common misspellings and formatting variants** of `failproofai` on npm. None of these names can be registered by a third party. Each one is a thin proxy that installs and delegates to the real `failproofai` package. *** ## Registered aliases **Formatting variants** - different ways to write "failproof ai": | Package | Status | | --------------- | --------------------- | | `failproof` | ✅ Published | | `failproof-ai` | ⏳ Pending npm support | | `fail-proof-ai` | ⏳ Pending npm support | | `failproof_ai` | ⏳ Pending npm support | | `fail_proof_ai` | ⏳ Pending npm support | | `fail-proofai` | ⏳ Pending npm support | **`failprof*` typos** - missing one `o` from "proof": | Package | Status | | -------------- | --------------------- | | `failprof` | ✅ Published | | `failprof-ai` | ✅ Published | | `failprofai` | ⏳ Pending npm support | | `fail-prof-ai` | ⏳ Pending npm support | | `failprof_ai` | ⏳ Pending npm support | **`faliproof*` typos** - transposed `a` and `i`: | Package | Status | | -------------- | --------------------- | | `faliproof` | ✅ Published | | `faliproof-ai` | ✅ Published | | `faliproofai` | ⏳ Pending npm support | > **Why pending?** npm's spam-prevention policy blocks names that normalize to the same string as an existing package after stripping punctuation and running similarity checks. We have contacted npm support to reserve these names for anti-squatting purposes. They will be activated once approved. You can verify any published alias is owned by us: ```bash theme={null} npm info failproof # Look for: "ExosphereHost Inc." in the maintainers field ``` *** ## How the aliases work Each alias package: 1. Lists `failproofai` as a dependency - so the real package (including its `postinstall` hook setup) runs on install 2. Exposes a binary matching its own name (e.g. `failprof-ai`) that proxies all arguments to the `failproofai` binary The proxy is a two-line Node script; there is no logic, no network calls, and no data collection beyond what `failproofai` itself does. *** ## If you find a name we missed Open an issue at [failproofai/failproofai](https://github.com/failproofai/failproofai/issues) and we will register it. # Testing Source: https://docs.befailproof.ai/testing Unit tests, E2E tests, and test helpers failproofai has two test suites: **unit tests** (fast, mocked) and **end-to-end tests** (real subprocess invocations). *** ## Running tests ```bash theme={null} # Run all unit tests once bun run test:run # Run unit tests in watch mode bun run test # Run E2E tests (requires setup - see below) bun run test:e2e # Type-check without building bunx tsc --noEmit # Lint bun run lint ``` *** ## Unit tests Unit tests live in `__tests__/` and use [Vitest](https://vitest.dev) with `jsdom`. ```text theme={null} __tests__/ hooks/ builtin-policies.test.ts # Policy logic for each builtin hooks-config.test.ts # Config loading and scope merging policy-evaluator.test.ts # Param injection and evaluation order custom-hooks-registry.test.ts # globalThis registry add/get/clear custom-hooks-loader.test.ts # ESM loader, transitive imports, error handling manager.test.ts # install/remove/list operations components/ sessions-list.test.tsx # Session list component project-list.test.tsx # Project list component ... lib/ logger.test.ts paths.test.ts date-filters.test.ts telemetry.test.ts ... actions/ get-hooks-config.test.ts get-hook-activity.test.ts ... contexts/ ThemeContext.test.tsx AutoRefreshContext.test.tsx ``` ### Writing a policy unit test ```typescript theme={null} import { describe, it, expect, beforeEach } from "vitest"; import { getBuiltinPolicies } from "../../src/hooks/builtin-policies"; import { allow, deny } from "../../src/hooks/policy-types"; describe("block-sudo", () => { const policy = getBuiltinPolicies().find((p) => p.name === "block-sudo")!; it("denies sudo commands", () => { const ctx = { eventType: "PreToolUse" as const, payload: {}, toolName: "Bash", toolInput: { command: "sudo apt install nodejs" }, params: { allowPatterns: [] }, }; expect(policy.fn(ctx)).toEqual(deny("sudo command blocked by failproofai")); }); it("allows non-sudo commands", () => { const ctx = { eventType: "PreToolUse" as const, payload: {}, toolName: "Bash", toolInput: { command: "ls -la" }, params: { allowPatterns: [] }, }; expect(policy.fn(ctx)).toEqual(allow()); }); it("allows patterns in allowPatterns", () => { const ctx = { eventType: "PreToolUse" as const, payload: {}, toolName: "Bash", toolInput: { command: "sudo systemctl status nginx" }, params: { allowPatterns: ["sudo systemctl status"] }, }; expect(policy.fn(ctx)).toEqual(allow()); }); }); ``` *** ## End-to-end tests E2E tests invoke the real `failproofai` binary as a subprocess, pipe a JSON payload to stdin, and assert on the stdout output and exit code. This tests the complete integration path that Claude Code uses. ### Setup E2E tests run the binary directly from the repo source. Before the first run, build the CJS bundle that custom hook files use when they import from `'failproofai'`: ```bash theme={null} bun build src/index.ts --outdir dist --target node --format cjs ``` Then run the tests: ```bash theme={null} bun run test:e2e ``` Rebuild `dist/` whenever you change the public hook API (`src/hooks/custom-hooks-registry.ts`, `src/hooks/policy-helpers.ts`, or `src/hooks/policy-types.ts`). ### E2E test structure ```text theme={null} __tests__/e2e/ helpers/ hook-runner.ts # Spawn the binary, pipe payload JSON, capture exit code + stdout + stderr fixture-env.ts # Per-test isolated temp directories with config files payloads.ts # Claude-accurate payload factories for each event type hooks/ builtin-policies.e2e.test.ts # Each builtin policy with real subprocess custom-hooks.e2e.test.ts # Custom hook loading and evaluation config-scopes.e2e.test.ts # Config merging across project/local/global policy-params.e2e.test.ts # Parameter injection for each parameterized policy ``` ### Using the E2E helpers **`FixtureEnv`** - isolated per-test environment: ```typescript theme={null} import { createFixtureEnv } from "../helpers/fixture-env"; const env = createFixtureEnv(); // env.cwd - temp dir; pass as payload.cwd to pick up .failproofai/policies-config.json // env.home - isolated home dir; no real ~/.failproofai leaks in env.writeConfig({ enabledPolicies: ["block-sudo"], policyParams: { "block-sudo": { allowPatterns: ["sudo systemctl status"] }, }, }); ``` `createFixtureEnv()` registers `afterEach` cleanup automatically. **`runHook`** - invoke the binary: ```typescript theme={null} import { runHook } from "../helpers/hook-runner"; import { Payloads } from "../helpers/payloads"; const result = await runHook( "PreToolUse", Payloads.preToolUse.bash("sudo apt install nodejs", env.cwd), { homeDir: env.home } ); expect(result.exitCode).toBe(0); expect(result.parsed?.hookSpecificOutput?.permissionDecision).toBe("deny"); ``` **`Payloads`** - ready-made payload factories: ```typescript theme={null} Payloads.preToolUse.bash(command, cwd) Payloads.preToolUse.write(filePath, content, cwd) Payloads.preToolUse.read(filePath, cwd) Payloads.postToolUse.bash(command, output, cwd) Payloads.postToolUse.read(filePath, content, cwd) Payloads.notification(message, cwd) Payloads.stop(cwd) ``` ### Writing an E2E test ```typescript theme={null} import { describe, it, expect } from "vitest"; import { createFixtureEnv } from "../helpers/fixture-env"; import { runHook } from "../helpers/hook-runner"; import { Payloads } from "../helpers/payloads"; describe("block-rm-rf (E2E)", () => { it("denies rm -rf", async () => { const env = createFixtureEnv(); env.writeConfig({ enabledPolicies: ["block-rm-rf"] }); const result = await runHook( "PreToolUse", Payloads.preToolUse.bash("rm -rf /", env.cwd), { homeDir: env.home } ); expect(result.exitCode).toBe(0); expect(result.parsed?.hookSpecificOutput?.permissionDecision).toBe("deny"); }); it("allows non-recursive rm", async () => { const env = createFixtureEnv(); env.writeConfig({ enabledPolicies: ["block-rm-rf"] }); const result = await runHook( "PreToolUse", Payloads.preToolUse.bash("rm /tmp/file.txt", env.cwd), { homeDir: env.home } ); expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); // allow → empty stdout }); }); ``` ### E2E response shapes | Decision | Exit code | stdout | | ------------------- | --------- | --------------------------------------------------------------------------------------- | | `PreToolUse` deny | `0` | `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` | | `PostToolUse` deny | `0` | `{"hookSpecificOutput":{"additionalContext":"Blocked ... because: ..."}}` | | Instruct (non-Stop) | `0` | `{"hookSpecificOutput":{"additionalContext":"Instruction from failproofai: ..."}}` | | Stop instruct | `2` | empty stdout; reason in stderr | | Allow | `0` | empty string | ### Vitest config E2E tests use `vitest.config.e2e.mts` with: * `environment: "node"` - no browser globals needed * `pool: "forks"` - true process isolation (tests spawn subprocesses) * `testTimeout: 20_000` - 20s per test (binary startup + hook eval) The `forks` pool is important: thread-based workers share `globalThis`, which can interfere with subprocess-spawning tests. Process-based forks avoid this. *** ## CI The full CI run (`bun run lint && bunx tsc --noEmit && bun run test:run && bun run build`) is required to pass before merging. The E2E suite runs as a separate CI job in parallel. See [Contributing](../CONTRIBUTING.md) for the complete pre-merge checklist.