Las políticas personalizadas te permiten escribir reglas para cualquier comportamiento del agente: aplicar convenciones del proyecto, prevenir derivas, controlar operaciones destructivas, detectar agentes bloqueados o integrarte con Slack, flujos de aprobación y más. Utilizan el mismo sistema de eventos de hook y las mismas decisiones allow, deny, instruct que las políticas integradas.
Ejemplo rápido
// 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();
},
});
Instálala:
failproofai policies --install --custom ./my-policies.js
Opción 1: Basada en convención (recomendada)
Coloca archivos *policies.{js,mjs,ts} en .failproofai/policies/ y se cargarán automáticamente, sin necesidad de flags ni cambios de configuración. Funciona como los git hooks: coloca el archivo y listo.
# Nivel de proyecto — incluido en git, compartido con el equipo
.failproofai/policies/security-policies.mjs
.failproofai/policies/workflow-policies.mjs
# Nivel de usuario — personal, se aplica a todos los proyectos
~/.failproofai/policies/my-policies.mjs
Cómo funciona:
- Se analizan tanto el directorio del proyecto como el del usuario (unión — no el primero que coincida por alcance)
- Los archivos se cargan alfabéticamente dentro de cada directorio. Usa prefijos
01-, 02- para controlar el orden
- Solo se cargan los archivos que coincidan con
*policies.{js,mjs,ts}; el resto se ignoran
- Cada archivo se carga de forma independiente (fail-open por archivo)
- Funciona junto con
--custom explícito y las políticas integradas
Las políticas por convención son la forma más sencilla de compartir políticas en un equipo. Incluye .failproofai/policies/ en git y todos los miembros del equipo las recibirán automáticamente.
Opción 2: Ruta de archivo explícita
# Instalar con un archivo de políticas personalizadas
failproofai policies --install --custom ./my-policies.js
# Reemplazar la ruta del archivo de políticas
failproofai policies --install --custom ./new-policies.js
# Eliminar la ruta de políticas personalizadas de la configuración
failproofai policies --uninstall --custom
La ruta absoluta resuelta se almacena en policies-config.json como customPoliciesPath. El archivo se carga de nuevo en cada evento de hook; no hay caché entre eventos.
Usar ambas juntas
Las políticas por convención y el archivo --custom explícito pueden coexistir. Orden de carga:
- Archivo explícito
customPoliciesPath (si está configurado)
- Archivos de convención del proyecto (
{cwd}/.failproofai/policies/, alfabético)
- Archivos de convención del usuario (
~/.failproofai/policies/, alfabético)
API
Importación
import { customPolicies, allow, deny, instruct } from "failproofai";
customPolicies.add(hook)
Registra una política. Llámalo las veces que necesites para definir múltiples políticas en el mismo archivo.
customPolicies.add({
name: string; // requerido - identificador único
description?: string; // se muestra en la salida de `failproofai policies`
match?: { events?: HookEventType[] }; // filtra por tipo de evento; omítelo para coincidir con todos
fn: (ctx: PolicyContext) => PolicyResult | Promise<PolicyResult>;
});
Funciones auxiliares de decisión
| Función | Efecto | Cuándo usarla |
|---|
allow() | Permite la operación silenciosamente | La acción es segura y no se necesita mensaje |
deny(message) | Bloquea la operación | El agente no debería realizar esta acción |
instruct(message) | Añade contexto sin bloquear | Proporciona contexto adicional al agente para mantenerlo en curso |
deny(message) — el mensaje aparece para Claude con el prefijo "Blocked by failproofai:". Un solo deny cortocircuita toda evaluación posterior.
instruct(message) — el mensaje se añade al contexto de Claude para la llamada de herramienta actual. Todos los mensajes instruct se acumulan y se entregan juntos.
Puedes añadir orientación adicional a cualquier mensaje deny o instruct incluyendo un campo hint en policyParams, sin necesidad de cambiar el código. Esto funciona también para políticas personalizadas (custom/), de convención de proyecto (.failproofai-project/) y de convención de usuario (.failproofai-user/). Consulta Configuración → hint para más detalles.
allow(message) permite la operación y envía un mensaje informativo a Claude. El mensaje se entrega como additionalContext en la respuesta stdout del manejador de hooks — el mismo mecanismo que usa instruct, pero con una semántica diferente: es una actualización de estado, no una advertencia.
| Función | Efecto | Cuándo usarla |
|---|
allow(message) | Permite y envía contexto a Claude | Confirmar que una verificación pasó, o explicar por qué se omitió |
Casos de uso:
- Confirmaciones de estado:
allow("All CI checks passed.") — le indica a Claude que todo está en orden
- Explicaciones de fail-open:
allow("GitHub CLI not installed, skipping CI check.") — le dice a Claude por qué se omitió una verificación para que tenga contexto completo
- Los mensajes se acumulan: si varias políticas devuelven
allow(message), todos los mensajes se unen con saltos de línea y se entregan juntos
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.");
},
});
Campos de PolicyContext
| Campo | Tipo | Descripción |
|---|
eventType | string | "PreToolUse", "PostToolUse", "Notification", "Stop" |
toolName | string | undefined | La herramienta que se está invocando (p. ej. "Bash", "Write", "Read") |
toolInput | Record<string, unknown> | undefined | Los parámetros de entrada de la herramienta |
payload | Record<string, unknown> | Payload completo del evento raw de Claude Code |
session | SessionMetadata | undefined | Contexto de sesión (ver más abajo) |
| Campo | Tipo | Descripción |
|---|
sessionId | string | Identificador de sesión de Claude Code |
cwd | string | Directorio de trabajo de la sesión de Claude Code |
transcriptPath | string | Ruta al archivo de transcripción JSONL de la sesión |
Tipos de eventos
| Evento | Cuándo se dispara | Contenido de toolInput |
|---|
PreToolUse | Antes de que Claude ejecute una herramienta | La entrada de la herramienta (p. ej. { command: "..." } para Bash) |
PostToolUse | Después de que una herramienta completa | La entrada de la herramienta + tool_result (la salida) |
Notification | Cuando Claude envía una notificación | { message: "...", notification_type: "idle" | "permission_prompt" | ... } — los hooks siempre deben devolver allow(), no pueden bloquear notificaciones |
Stop | Cuando la sesión de Claude finaliza | Vacío |
Orden de evaluación
Las políticas se evalúan en este orden:
- Políticas integradas (en orden de definición)
- Políticas personalizadas explícitas de
customPoliciesPath (en orden de .add())
- Políticas de convención del proyecto
.failproofai/policies/ (archivos en orden alfabético, orden de .add() dentro de cada archivo)
- Políticas de convención del usuario
~/.failproofai/policies/ (archivos en orden alfabético, orden de .add() dentro de cada archivo)
El primer deny cortocircuita todas las políticas siguientes. Todos los mensajes instruct se acumulan y se entregan juntos.
Importaciones transitivas
Los archivos de políticas personalizadas pueden importar módulos locales usando rutas relativas:
// 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");
},
});
Se resuelven todas las importaciones relativas accesibles desde el archivo de entrada. Esto se implementa reescribiendo las importaciones from "failproofai" a la ruta real de dist y creando archivos .mjs temporales para garantizar la compatibilidad con ESM.
Filtrado por tipo de evento
Usa match.events para limitar cuándo se dispara una política:
customPolicies.add({
name: "require-summary-on-stop",
match: { events: ["Stop"] },
fn: async (ctx) => {
// Solo se dispara cuando la sesión finaliza
// ctx.session.transcriptPath contiene el log completo de la sesión
return allow();
},
});
Omite match por completo para que se dispare en todos los tipos de eventos.
Manejo de errores y modos de fallo
Las políticas personalizadas son fail-open: los errores nunca bloquean las políticas integradas ni causan un crash en el manejador de hooks.
| Fallo | Comportamiento |
|---|
customPoliciesPath no configurado | No se ejecutan políticas personalizadas explícitas; las políticas por convención y las integradas continúan con normalidad |
| Archivo no encontrado | Se registra una advertencia en ~/.failproofai/hook.log; las políticas integradas continúan |
| Error de sintaxis/importación (explícito) | Error registrado en ~/.failproofai/hook.log; se omiten las políticas personalizadas explícitas |
| Error de sintaxis/importación (convención) | Error registrado; ese archivo se omite, los demás archivos de convención siguen cargándose |
fn lanza un error en tiempo de ejecución | Error registrado; ese hook se trata como allow; los demás hooks continúan |
fn tarda más de 10 segundos | Timeout registrado; se trata como allow |
| Directorio de convención inexistente | No se ejecutan políticas de convención; sin error |
Para depurar errores en políticas personalizadas, observa el archivo de log:tail -f ~/.failproofai/hook.log
Ejemplo completo: múltiples políticas
// 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 };
Ejemplos
El directorio examples/ contiene archivos de políticas listos para ejecutar:
| Archivo | Contenido |
|---|
examples/policies-basic.js | Cinco políticas de inicio que cubren los modos de fallo más comunes de los agentes |
examples/policies-advanced/index.js | Patrones avanzados: importaciones transitivas, llamadas asíncronas, limpieza de salidas y hooks de fin de sesión |
examples/convention-policies/security-policies.mjs | Políticas de seguridad por convención (bloquea escrituras en .env, previene reescritura del historial de git) |
examples/convention-policies/workflow-policies.mjs | Políticas de flujo de trabajo por convención (recordatorios de tests, auditoría de escrituras en archivos) |
Usar los ejemplos con archivo explícito
failproofai policies --install --custom ./examples/policies-basic.js
Usar los ejemplos basados en convención
# Copiar al nivel del proyecto
mkdir -p .failproofai/policies
cp examples/convention-policies/*.mjs .failproofai/policies/
# O copiar al nivel del usuario
mkdir -p ~/.failproofai/policies
cp examples/convention-policies/*.mjs ~/.failproofai/policies/
No se necesita ningún comando de instalación — los archivos se detectan automáticamente en el siguiente evento de hook.