> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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<string, Record<string, unknown>>`

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 <path>` (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 <event>` on each tool use:
  * **Claude Code**: `~/.claude/settings.json` (user), `<cwd>/.claude/settings.json` (project), `<cwd>/.claude/settings.local.json` (local)
  * **OpenAI Codex**: `~/.codex/hooks.json` (user), `<cwd>/.codex/hooks.json` (project) — Codex doesn't have a `local` scope
  * **GitHub Copilot CLI *(beta)***: `~/.copilot/hooks/failproofai.json` (user), `<cwd>/.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), `<cwd>/.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), `<cwd>/.opencode/opencode.json` + `<cwd>/.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 <id>`. 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), `<cwd>/.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 <Event> --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), `<cwd>/.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.
