커스텀 정책을 사용하면 에이전트의 모든 동작에 대한 규칙을 직접 작성할 수 있습니다. 프로젝트 컨벤션 적용, 드리프트 방지, 위험한 작업 차단, 응답 없는 에이전트 감지, Slack 연동, 승인 워크플로우 등을 구현할 수 있습니다. 내장 정책과 동일한 훅 이벤트 시스템과 allow, deny, instruct 결정 방식을 사용합니다.
빠른 예시
// 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();
},
});
설치:
failproofai policies --install --custom ./my-policies.js
커스텀 정책을 로드하는 두 가지 방법
방법 1: 컨벤션 기반 (권장)
*policies.{js,mjs,ts} 파일을 .failproofai/policies/ 디렉토리에 넣으면 별도의 플래그나 설정 변경 없이 자동으로 로드됩니다. git 훅처럼 파일만 넣으면 바로 동작합니다.
# 프로젝트 레벨 — git에 커밋되어 팀 전체와 공유됨
.failproofai/policies/security-policies.mjs
.failproofai/policies/workflow-policies.mjs
# 사용자 레벨 — 개인용, 모든 프로젝트에 적용됨
~/.failproofai/policies/my-policies.mjs
동작 방식:
- 프로젝트와 사용자 디렉토리 모두 스캔됩니다 (합집합 방식 — 첫 번째 스코프 우선 방식이 아님)
- 각 디렉토리 내에서 파일은 알파벳 순으로 로드됩니다.
01-, 02- 접두사를 붙여 순서를 제어할 수 있습니다
*policies.{js,mjs,ts} 패턴에 맞는 파일만 로드되며, 그 외 파일은 무시됩니다
- 각 파일은 독립적으로 로드됩니다 (파일 단위로 fail-open)
- 명시적
--custom 및 내장 정책과 함께 동작합니다
컨벤션 정책은 조직의 품질 표준을 구축하는 가장 쉬운 방법입니다. .failproofai/policies/를 git에 커밋하면 모든 팀원이 별도 설정 없이 동일한 규칙을 자동으로 적용받습니다. 팀이 새로운 실패 패턴을 발견할 때마다 정책을 추가하고 푸시하세요. 시간이 지날수록 기여를 통해 계속 발전하는 살아있는 품질 표준이 됩니다.
방법 2: 명시적 파일 경로
# 커스텀 정책 파일과 함께 설치
failproofai policies --install --custom ./my-policies.js
# 정책 파일 경로 교체
failproofai policies --install --custom ./new-policies.js
# 설정에서 커스텀 정책 경로 제거
failproofai policies --uninstall --custom
변환된 절대 경로는 policies-config.json의 customPoliciesPath에 저장됩니다. 파일은 매 훅 이벤트마다 새로 로드되며, 이벤트 간 캐싱은 없습니다.
두 방법을 함께 사용하기
컨벤션 정책과 명시적 --custom 파일은 함께 사용할 수 있습니다. 로드 순서:
- 명시적
customPoliciesPath 파일 (설정된 경우)
- 프로젝트 컨벤션 파일 (
{cwd}/.failproofai/policies/, 알파벳 순)
- 사용자 컨벤션 파일 (
~/.failproofai/policies/, 알파벳 순)
API
임포트
import { customPolicies, allow, deny, instruct } from "failproofai";
customPolicies.add(hook)
정책을 등록합니다. 하나의 파일에 여러 정책을 등록하려면 필요한 만큼 반복 호출하세요.
customPolicies.add({
name: string; // 필수 - 고유 식별자
description?: string; // `failproofai policies` 출력에 표시됨
match?: { events?: HookEventType[] }; // 이벤트 타입으로 필터링; 생략 시 모든 이벤트에 매칭
fn: (ctx: PolicyContext) => PolicyResult | Promise<PolicyResult>;
});
결정 헬퍼 함수
| 함수 | 효과 | 사용 시점 |
|---|
allow() | 작업을 조용히 허용 | 해당 동작이 안전하고 메시지가 필요 없을 때 |
deny(message) | 작업을 차단 | 에이전트가 해당 동작을 수행해서는 안 될 때 |
instruct(message) | 차단 없이 컨텍스트 추가 | 에이전트가 올바른 방향을 유지하도록 추가 컨텍스트를 제공할 때 |
deny(message) - 메시지는 "Blocked by failproofai:" 접두사와 함께 Claude에게 표시됩니다. 하나의 deny가 발생하면 이후 모든 평가가 단락됩니다.
instruct(message) - 메시지는 현재 도구 호출에 대한 Claude의 컨텍스트에 추가됩니다. 모든 instruct 메시지는 누적되어 함께 전달됩니다.
deny 또는 instruct 메시지에 추가 가이던스를 덧붙이려면 policyParams의 hint 필드를 사용하세요 — 코드 변경이 필요 없습니다. 커스텀(custom/), 프로젝트 컨벤션(.failproofai-project/), 사용자 컨벤션(.failproofai-user/) 정책 모두에서 동작합니다. 자세한 내용은 설정 → hint를 참고하세요.
정보성 allow 메시지
allow(message)는 작업을 허용하면서 동시에 Claude에게 정보 메시지를 전달합니다. 메시지는 훅 핸들러의 stdout 응답에서 additionalContext로 전달됩니다 — instruct와 동일한 메커니즘이지만 의미적으로 다릅니다. 경고가 아닌 상태 업데이트입니다.
| 함수 | 효과 | 사용 시점 |
|---|
allow(message) | 허용하고 Claude에게 컨텍스트 전달 | 검사가 통과됐음을 확인하거나, 검사를 건너뛴 이유를 설명할 때 |
사용 사례:
- 상태 확인:
allow("All CI checks passed.") — Claude에게 모든 것이 정상임을 알림
- Fail-open 설명:
allow("GitHub CLI not installed, skipping CI check.") — 검사를 건너뛴 이유를 Claude에게 알려 전체 컨텍스트를 유지
- 다중 메시지 누적: 여러 정책이 각각
allow(message)를 반환하면 모든 메시지가 줄바꿈으로 합쳐져 함께 전달됨
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 필드
| 필드 | 타입 | 설명 |
|---|
eventType | string | "PreToolUse", "PostToolUse", "Notification", "Stop" |
toolName | string | undefined | 호출되는 도구 이름 (예: "Bash", "Write", "Read") |
toolInput | Record<string, unknown> | undefined | 도구의 입력 파라미터 |
payload | Record<string, unknown> | Claude Code로부터 받은 원시 이벤트 페이로드 전체 |
session | SessionMetadata | undefined | 세션 컨텍스트 (아래 참조) |
| 필드 | 타입 | 설명 |
|---|
sessionId | string | Claude Code 세션 식별자 |
cwd | string | Claude Code 세션의 작업 디렉토리 |
transcriptPath | string | 세션의 JSONL 트랜스크립트 파일 경로 |
이벤트 타입
| 이벤트 | 발생 시점 | toolInput 내용 |
|---|
PreToolUse | Claude가 도구를 실행하기 전 | 도구의 입력값 (예: Bash의 경우 { command: "..." }) |
PostToolUse | 도구 실행 완료 후 | 도구의 입력값 + tool_result (출력값) |
Notification | Claude가 알림을 보낼 때 | { message: "...", notification_type: "idle" | "permission_prompt" | ... } - 훅은 반드시 allow()를 반환해야 하며, 알림을 차단할 수 없음 |
Stop | Claude 세션이 종료될 때 | 비어 있음 |
평가 순서
정책은 다음 순서로 평가됩니다:
- 내장 정책 (정의된 순서)
customPoliciesPath의 명시적 커스텀 정책 (.add() 호출 순서)
- 프로젝트
.failproofai/policies/의 컨벤션 정책 (파일은 알파벳 순, 파일 내에서는 .add() 순서)
- 사용자
~/.failproofai/policies/의 컨벤션 정책 (파일은 알파벳 순, 파일 내에서는 .add() 순서)
첫 번째 deny가 발생하면 이후 모든 정책 평가가 단락됩니다. 모든 instruct 메시지는 누적되어 함께 전달됩니다.
전이적 임포트
커스텀 정책 파일은 상대 경로를 사용해 로컬 모듈을 임포트할 수 있습니다:
// 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");
},
});
엔트리 파일에서 도달 가능한 모든 상대 임포트가 처리됩니다. from "failproofai" 임포트를 실제 dist 경로로 재작성하고 임시 .mjs 파일을 생성하여 ESM 호환성을 보장하는 방식으로 구현되어 있습니다.
이벤트 타입 필터링
match.events를 사용해 정책이 발동되는 시점을 제한할 수 있습니다:
customPolicies.add({
name: "require-summary-on-stop",
match: { events: ["Stop"] },
fn: async (ctx) => {
// 세션이 종료될 때만 발동됩니다
// ctx.session.transcriptPath에 전체 세션 로그가 담겨 있습니다
return allow();
},
});
match를 완전히 생략하면 모든 이벤트 타입에서 발동됩니다.
오류 처리 및 실패 모드
커스텀 정책은 fail-open 방식입니다. 오류가 발생해도 내장 정책을 차단하거나 훅 핸들러를 중단시키지 않습니다.
| 실패 상황 | 동작 |
|---|
customPoliciesPath 미설정 | 명시적 커스텀 정책이 실행되지 않음; 컨벤션 정책과 내장 정책은 정상 동작 |
| 파일을 찾을 수 없음 | ~/.failproofai/hook.log에 경고 기록; 내장 정책은 계속 실행 |
| 문법/임포트 오류 (명시적) | ~/.failproofai/hook.log에 오류 기록; 명시적 커스텀 정책 건너뜀 |
| 문법/임포트 오류 (컨벤션) | 오류 기록; 해당 파일 건너뜀, 다른 컨벤션 파일은 계속 로드 |
fn 런타임 오류 | 오류 기록; 해당 훅은 allow로 처리; 다른 훅은 계속 실행 |
fn 10초 초과 | 타임아웃 기록; allow로 처리 |
| 컨벤션 디렉토리 없음 | 컨벤션 정책이 실행되지 않음; 오류 없음 |
커스텀 정책 오류를 디버깅하려면 로그 파일을 실시간으로 확인하세요:tail -f ~/.failproofai/hook.log
전체 예시: 다중 정책
// my-policies.js
import { customPolicies, allow, deny, instruct } from "failproofai";
// 에이전트가 secrets/ 디렉토리에 쓰지 못하도록 방지
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();
},
});
// 에이전트가 올바른 방향 유지: 커밋 전 테스트 검증
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();
},
});
// 프리즈 기간 중 계획되지 않은 의존성 변경 방지
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/ 디렉토리에는 바로 실행 가능한 정책 파일이 포함되어 있습니다:
| 파일 | 내용 |
|---|
examples/policies-basic.js | 일반적인 에이전트 실패 패턴을 다루는 5가지 기본 정책 |
examples/policies-advanced/index.js | 고급 패턴: 전이적 임포트, 비동기 호출, 출력 스크러빙, 세션 종료 훅 |
examples/convention-policies/security-policies.mjs | 컨벤션 기반 보안 정책 (.env 파일 쓰기 차단, git 히스토리 재작성 방지) |
examples/convention-policies/workflow-policies.mjs | 컨벤션 기반 워크플로우 정책 (테스트 리마인더, 파일 쓰기 감사) |
명시적 파일 예시 사용
failproofai policies --install --custom ./examples/policies-basic.js
컨벤션 기반 예시 사용
# 프로젝트 레벨로 복사
mkdir -p .failproofai/policies
cp examples/convention-policies/*.mjs .failproofai/policies/
# 또는 사용자 레벨로 복사
mkdir -p ~/.failproofai/policies
cp examples/convention-policies/*.mjs ~/.failproofai/policies/
별도의 설치 명령이 필요 없습니다 — 다음 훅 이벤트 시 파일이 자동으로 인식됩니다.