> ## 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.

# 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

<Tip>
  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.
</Tip>

### 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<PolicyResult>;
});
```

### 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.

<Tip>
  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.
</Tip>

### 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<string, unknown> \| undefined` | The tool's input parameters                                 |
| `payload`   | `Record<string, unknown>`              | 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)

<Note>
  The first `deny` short-circuits all subsequent policies. All `instruct` messages are accumulated and delivered together.
</Note>

***

## 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                                                 |

<Tip>
  To debug custom policy errors, watch the log file:

  ```bash theme={null}
  tail -f ~/.failproofai/hook.log
  ```
</Tip>

***

## 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.
