allow, deny, instruct decisions as built-in policies.
Quick example
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.
- 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
--customand built-in policies
Option 2: Explicit file path
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:
- Explicit
customPoliciesPathfile (if configured) - Project convention files (
{cwd}/.failproofai/policies/, alphabetical) - User convention files (
~/.failproofai/policies/, alphabetical)
API
Import
customPolicies.add(hook)
Registers a policy. Call this as many times as needed for multiple policies in the same file.
Decision helpers
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.
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.
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
PolicyContext fields
SessionMetadata fields
Event types
Evaluation order
Policies are evaluated in this order:- Built-in policies (in definition order)
- Explicit custom policies from
customPoliciesPath(in.add()order) - Convention policies from project
.failproofai/policies/(files alphabetical,.add()order within) - 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:from "failproofai" imports to the actual dist path and creating temporary .mjs files to ensure ESM compatibility.
Event type filtering
Usematch.events to limit when a policy fires:
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.Full example: multiple policies
Examples
Theexamples/ directory contains ready-to-run policy files:

