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

# 커스텀 정책

> JavaScript로 직접 정책을 작성하세요 - 컨벤션 적용, 드리프트 방지, 실패 감지, 외부 시스템 연동

커스텀 정책을 사용하면 에이전트 동작에 관한 규칙을 직접 작성할 수 있습니다. 프로젝트 컨벤션 적용, 드리프트 방지, 위험 작업 차단, 멈춘 에이전트 감지, Slack이나 승인 워크플로 연동 등 다양하게 활용할 수 있습니다. 내장 정책과 동일한 훅 이벤트 시스템과 `allow`, `deny`, `instruct` 결정 방식을 사용합니다.

***

## 빠른 예시

```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();
  },
});
```

설치:

```bash theme={null}
failproofai policies --install --custom ./my-policies.js
```

***

## 커스텀 정책을 불러오는 두 가지 방법

### 방법 1: 컨벤션 기반 (권장)

`.failproofai/policies/` 디렉터리에 `*policies.{js,mjs,ts}` 파일을 넣으면 자동으로 불러옵니다 — 별도 플래그나 설정 변경이 필요 없습니다. git hooks처럼 파일만 넣으면 바로 동작합니다.

```
# 프로젝트 레벨 — 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` 옵션 및 내장 정책과 함께 동작합니다

<Tip>
  컨벤션 정책은 조직의 품질 기준을 세우는 가장 쉬운 방법입니다. `.failproofai/policies/`를 git에 커밋하면 모든 팀원이 별도 설정 없이 동일한 규칙을 자동으로 적용받습니다. 새로운 실패 패턴을 발견할 때마다 정책을 추가하고 푸시하세요. 시간이 지날수록 기여가 쌓이며 살아있는 품질 기준이 만들어집니다.
</Tip>

### 방법 2: 명시적 파일 경로

```bash theme={null}
# 커스텀 정책 파일과 함께 설치
failproofai policies --install --custom ./my-policies.js

# 정책 파일 경로 교체
failproofai policies --install --custom ./new-policies.js

# 설정에서 커스텀 정책 경로 제거
failproofai policies --uninstall --custom
```

변환된 절대 경로는 `policies-config.json`의 `customPoliciesPath`에 저장됩니다. 파일은 매 훅 이벤트마다 새로 불러오며 이벤트 간 캐싱은 없습니다.

### 두 방법을 함께 사용하기

컨벤션 정책과 명시적 `--custom` 파일은 공존할 수 있습니다. 불러오는 순서:

1. 명시적 `customPoliciesPath` 파일 (설정된 경우)
2. 프로젝트 컨벤션 파일 (`{cwd}/.failproofai/policies/`, 알파벳 순)
3. 사용자 컨벤션 파일 (`~/.failproofai/policies/`, 알파벳 순)

***

## API

### 가져오기

```js theme={null}
import { customPolicies, allow, deny, instruct } from "failproofai";
```

### `customPolicies.add(hook)`

정책을 등록합니다. 같은 파일 내에 여러 정책을 등록하려면 원하는 만큼 호출하세요.

```ts theme={null}
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` 메시지는 누적되어 함께 전달됩니다.

<Tip>
  `policyParams`의 `hint` 필드를 추가하면 코드 변경 없이 `deny`나 `instruct` 메시지에 추가 안내를 붙일 수 있습니다. 커스텀(`custom/`), 프로젝트 컨벤션(`.failproofai-project/`), 사용자 컨벤션(`.failproofai-user/`) 정책 모두에서 동작합니다. 자세한 내용은 [설정 → hint](/ko/configuration#hint-cross-cutting)를 참고하세요.
</Tip>

### 정보성 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)`를 반환하면, 모든 메시지가 줄바꿈으로 합쳐져 함께 전달됩니다

```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.");

    // ... 브랜치 상태 확인 ...
    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`         | 세션 컨텍스트 (아래 참조)                                             |

### `SessionMetadata` 필드

| 필드               | 타입       | 설명                      |
| ---------------- | -------- | ----------------------- |
| `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 세션이 종료될 때   | 비어 있음                                                                                                                   |

***

## 평가 순서

정책은 다음 순서로 평가됩니다:

1. 내장 정책 (정의된 순서)
2. `customPoliciesPath`의 명시적 커스텀 정책 (`.add()` 호출 순서)
3. 프로젝트 `.failproofai/policies/`의 컨벤션 정책 (파일 알파벳 순, 파일 내 `.add()` 순서)
4. 사용자 `~/.failproofai/policies/`의 컨벤션 정책 (파일 알파벳 순, 파일 내 `.add()` 순서)

<Note>
  첫 번째 `deny`가 발생하면 이후 모든 정책 평가가 생략됩니다. 모든 `instruct` 메시지는 누적되어 함께 전달됩니다.
</Note>

***

## 전이적 임포트

커스텀 정책 파일은 상대 경로를 사용해 로컬 모듈을 임포트할 수 있습니다:

```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");
  },
});
```

엔트리 파일에서 도달 가능한 모든 상대 임포트가 처리됩니다. 내부적으로 `from "failproofai"` 임포트를 실제 dist 경로로 재작성하고 ESM 호환성을 위해 임시 `.mjs` 파일을 생성하는 방식으로 구현됩니다.

***

## 이벤트 타입 필터링

`match.events`를 사용해 정책이 발동하는 시점을 제한할 수 있습니다:

```js theme={null}
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`로 처리                             |
| 컨벤션 디렉터리 없음              | 컨벤션 정책 실행 없음; 오류 없음                              |

<Tip>
  커스텀 정책 오류를 디버깅하려면 로그 파일을 실시간으로 확인하세요:

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

***

## 전체 예시: 여러 정책

```js theme={null}
// 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` | 컨벤션 기반 워크플로 정책 (테스트 알림, 파일 쓰기 감사)             |

### 명시적 파일 예시 사용

```bash theme={null}
failproofai policies --install --custom ./examples/policies-basic.js
```

### 컨벤션 기반 예시 사용

```bash theme={null}
# 프로젝트 레벨로 복사
mkdir -p .failproofai/policies
cp examples/convention-policies/*.mjs .failproofai/policies/

# 또는 사용자 레벨로 복사
mkdir -p ~/.failproofai/policies
cp examples/convention-policies/*.mjs ~/.failproofai/policies/
```

별도 설치 명령이 필요 없습니다 — 다음 훅 이벤트 시 파일이 자동으로 인식됩니다.
