> ## Documentation Index
> Fetch the complete documentation index at: https://exosphere-auto-translate-docs-20260626-1023.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 아키텍처

> 훅 핸들러, 설정 로딩, 정책 평가의 내부 동작 방식

이 문서는 failproofai의 내부 동작 방식을 설명합니다. 훅 시스템이 에이전트 도구 호출을 가로채는 방법, 설정이 로드되고 병합되는 방법, 정책이 평가되는 방법, 그리고 대시보드가 에이전트 활동을 모니터링하는 방법을 다룹니다.

***

## 개요

failproofai는 두 개의 독립적인 서브시스템으로 구성됩니다:

1. **훅 핸들러** - Claude Code가 에이전트 도구 호출마다 실행하는 빠른 CLI 서브프로세스입니다. 정책을 평가하고 결정을 반환합니다.
2. **에이전트 모니터 (대시보드)** - 에이전트 세션을 모니터링하고 정책을 관리하기 위한 Next.js 웹 애플리케이션입니다.

두 서브시스템 모두 `~/.failproofai/`와 프로젝트의 `.failproofai/` 디렉토리에 있는 설정 파일을 공유하지만, 별도의 프로세스로 실행되며 파일 시스템을 통해서만 통신합니다.

***

## 훅 핸들러

### Claude Code와의 통합

`failproofai policies --install`을 실행하면 `~/.claude/settings.json`에 다음과 같은 항목이 작성됩니다:

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "failproofai --hook PreToolUse"
          }
        ]
      }
    ],
    "PostToolUse": [ ... ]
  }
}
```

이후 Claude Code는 각 도구 호출 전에 `failproofai --hook PreToolUse`를 서브프로세스로 실행하고, stdin으로 JSON 페이로드를 전달합니다.

### 페이로드 형식

```json theme={null}
{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/myproject/sessions/abc123.jsonl",
  "cwd": "/home/user/myproject",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "sudo apt install nodejs" }
}
```

`PostToolUse` 이벤트의 경우, 페이로드에 도구의 출력 결과가 담긴 `tool_result`도 포함됩니다.

핸들러는 stdin을 1 MB로 제한합니다. 이를 초과하는 페이로드는 무시되며, 모든 정책은 암묵적으로 허용됩니다.

### 응답 형식

**거부 (PreToolUse):**

```json theme={null}
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "Blocked by failproofai: sudo command blocked"
  }
}
```

**거부 (PostToolUse):**

```json theme={null}
{
  "hookSpecificOutput": {
    "additionalContext": "Blocked by failproofai because: API key detected in output"
  }
}
```

**지시 (Stop 이외의 모든 이벤트):**

```json theme={null}
{
  "hookSpecificOutput": {
    "additionalContext": "Instruction from failproofai: Verify tests pass before committing."
  }
}
```

**Stop 이벤트 지시:**

* 종료 코드: `2`
* 이유는 stdout이 아닌 stderr에 기록됩니다

**허용:**

* 종료 코드: `0`
* stdout은 비어 있습니다

**메시지와 함께 허용:**

`allow(message)`를 사용하면 작업이 허용된 경우에도 정책이 Claude에게 정보성 컨텍스트를 전달할 수 있습니다. 훅 핸들러는 다음 JSON을 **stdout**에 씁니다 (설정 파일이 아닌, 위의 거부 및 지시 응답과 동일하게 Claude Code에 대한 핸들러의 응답입니다):

```json theme={null}
// 훅 핸들러 프로세스가 stdout에 씁니다
{
  "hookSpecificOutput": {
    "additionalContext": "All CI checks passed on branch 'feat/my-feature'."
  }
}
```

* 종료 코드: `0` (작업이 허용됨)
* 여러 정책이 메시지와 함께 `allow`를 반환하는 경우, 해당 메시지들은 줄바꿈으로 연결되어 하나의 `additionalContext` 문자열이 됩니다
* 어떤 정책도 메시지를 제공하지 않으면 stdout은 비어 있습니다 (기존 동작과 동일)

### 처리 파이프라인

`src/hooks/handler.ts`가 전체 파이프라인을 구현합니다:

```text theme={null}
stdin JSON
  → 페이로드 파싱 (최대 1 MB)
  → 세션 메타데이터 추출 (session_id, cwd, tool_name, tool_input 등)
  → readMergedHooksConfig(cwd)    ← 프로젝트 + 로컬 + 글로벌 설정 병합
  → 활성화된 내장 정책을 해석된 파라미터와 함께 등록
  → customPoliciesPath에서 커스텀 정책 로드 (설정된 경우)
  → 커스텀 정책을 정책 레지스트리에 등록
  → 모든 정책 평가 (내장 정책 우선, 이후 커스텀)
      → 첫 번째 deny에서 즉시 중단
      → instruct 결정은 누적
      → allow 메시지는 누적
  → JSON 결정을 stdout에 씁니다
  → ~/.failproofai/hook-activity.jsonl에 이벤트 저장
  → 종료
```

전체 프로세스는 LLM 호출 없이 일반적인 페이로드 기준 100ms 이내에 완료됩니다.

***

## 설정 로딩

`src/hooks/hooks-config.ts`가 세 가지 범위의 설정 로딩을 구현합니다.

```text theme={null}
[1] {cwd}/.failproofai/policies-config.json        ← 프로젝트  (가장 높은 우선순위)
[2] {cwd}/.failproofai/policies-config.local.json  ← 로컬
[3] ~/.failproofai/policies-config.json             ← 글로벌   (가장 낮은 우선순위)
```

병합 로직:

* `enabledPolicies` - 세 파일 전체에서 중복을 제거한 합집합
* `policyParams` - 정책별 키 기준, 먼저 정의한 파일이 전체를 결정
* `customPoliciesPath` - 먼저 정의한 파일이 결정
* `llm` - 먼저 정의한 파일이 결정

웹 대시보드는 프로젝트 cwd 없이 실행되므로, 읽기와 쓰기 모두 `readHooksConfig()` (글로벌 전용)를 사용합니다.

***

## 정책 평가

`src/hooks/policy-evaluator.ts`가 순서대로 정책을 실행합니다.

각 정책에 대해:

1. 정책의 `params` 스키마를 조회합니다 (있는 경우).
2. 병합된 설정에서 `policyParams[policy.name]`을 읽습니다.
3. 사용자 제공 값을 스키마 기본값에 덮어써서 `ctx.params`를 생성합니다.
4. 해석된 컨텍스트와 함께 `policy.fn(ctx)`를 호출합니다.
5. 결과가 `deny`이면 즉시 중단하고 해당 결정을 반환합니다.
6. 결과가 `instruct`이면 메시지를 누적하고 계속 진행합니다.
7. 결과가 `allow`이면 다음 정책으로 넘어갑니다.

모든 정책 실행 후:

* `deny`가 반환된 경우, 거부 응답을 출력합니다.
* `instruct` 반환이 수집된 경우, 모든 메시지를 합쳐 단일 지시 응답을 출력합니다.
* 그 외에는 허용 응답을 출력합니다 (stdout 비움, 종료 코드 0).

***

## 내장 정책

`src/hooks/builtin-policies.ts`는 39개의 내장 정책을 `BuiltinPolicyDefinition` 객체로 정의합니다:

```typescript theme={null}
interface BuiltinPolicyDefinition {
  name: string;
  description: string;
  fn: (ctx: PolicyContext) => PolicyResult;
  match: {
    events: HookEventType[];
    tools?: string[];
  };
  defaultEnabled: boolean;
  category: string;
  beta?: boolean;
  params?: PolicyParamsSchema;
}
```

`params`를 허용하는 정책은 각 파라미터의 타입과 기본값이 담긴 `PolicyParamsSchema`를 선언합니다. 정책 평가기는 `fn`을 호출하기 전에 해석된 값을 `ctx.params`에 주입합니다. 기본값이 항상 먼저 적용되므로, 정책 함수는 null 검사 없이 `ctx.params`를 읽을 수 있습니다.

정책 내부의 패턴 매칭은 원시 문자열 매칭이 아닌 파싱된 명령어 토큰(argv)을 사용합니다. 이를 통해 셸 연산자 주입을 이용한 우회를 방지합니다 (예: `sudo systemctl status *` 패턴은 명령어에 `; rm -rf /`를 추가해도 우회할 수 없습니다).

***

## 커스텀 정책

`src/hooks/custom-hooks-registry.ts`는 `globalThis` 기반의 레지스트리를 구현합니다:

```typescript theme={null}
const REGISTRY_KEY = "__failproofai_custom_hooks__";

export const customPolicies = {
  add(hook: CustomHook): void { ... }
};

export function getCustomHooks(): CustomHook[] { ... }
export function clearCustomHooks(): void { ... }  // 테스트에서 사용
```

`src/hooks/custom-hooks-loader.ts`는 사용자의 정책 파일을 로드합니다:

1. 설정에서 `customPoliciesPath`를 읽고, 없으면 건너뜁니다.
2. 절대 경로로 변환하고 파일 존재 여부를 확인합니다.
3. `customPolicies`가 동일한 `globalThis` 레지스트리로 해석될 수 있도록 모든 `from "failproofai"` 임포트를 실제 dist 경로로 재작성합니다.
4. ESM 호환성을 보장하기 위해 전이적인 로컬 임포트도 재귀적으로 재작성합니다.
5. 임시 `.mjs` 파일을 작성하고 엔트리 파일을 `import()`합니다.
6. `getCustomHooks()`를 호출하여 등록된 훅을 가져옵니다.
7. `finally` 블록에서 모든 임시 파일을 정리합니다.

오류 발생 시 (파일 없음, 문법 오류, 임포트 실패), 오류는 `~/.failproofai/hook.log`에 기록되고 로더는 빈 배열을 반환합니다. 내장 정책은 영향을 받지 않습니다.

커스텀 정책은 모든 내장 정책이 실행된 후에 평가됩니다. 커스텀 정책의 `deny`는 이후 커스텀 정책을 즉시 중단하지만, 이 시점에서 내장 정책은 이미 모두 실행된 상태입니다.

***

## 활동 로깅

각 훅 이벤트 이후, 핸들러는 `~/.failproofai/hook-activity.jsonl`에 JSONL 한 줄을 추가합니다:

```json theme={null}
{
  "timestamp": "2026-04-06T12:34:56.789Z",
  "sessionId": "abc123",
  "eventType": "PreToolUse",
  "toolName": "Bash",
  "policyName": "block-sudo",
  "decision": "deny",
  "reason": "sudo command blocked by failproofai",
  "durationMs": 12
}
```

허용이 아닌 결정을 내린 정책당 한 줄씩 기록됩니다. 허용 결정은 파일 크기를 줄이기 위해 기록하지 않습니다.

***

## 대시보드 아키텍처

대시보드는 App Router를 사용하는 **Next.js 16** 애플리케이션으로, React Server Components와 Server Actions를 활용합니다.

```text theme={null}
app/
  layout.tsx                  ← 루트 레이아웃 (테마, 텔레메트리, 내비게이션)
  projects/page.tsx           ← 서버 컴포넌트: 모든 Claude 프로젝트 목록
  project/[name]/page.tsx     ← 서버 컴포넌트: 프로젝트 내 세션 목록
  project/[name]/session/
    [sessionId]/page.tsx      ← 서버 컴포넌트: 세션 뷰어 렌더링
  policies/page.tsx           ← 클라이언트 컴포넌트: 정책 관리 + 활동 로그
  actions/
    get-hooks-config.ts       ← 설정 + 정책 목록 읽기
    update-hooks-config.ts    ← 정책 활성화/비활성화 토글
    update-policy-params.ts   ← 정책 파라미터 업데이트
    get-hook-activity.ts      ← 활동 로그 페이지네이션/검색
    install-hooks-web.ts      ← 브라우저에서 훅 설치/제거
  api/
    download/[project]/[session]/route.ts   ← CLI 세션별 내보내기 (JSONL 또는 JSON)
```

**데이터 흐름:**

* 페이지 컴포넌트는 `lib/projects.ts`와 `lib/log-entries.ts`를 호출하여 파일 시스템에서 직접 프로젝트/세션 데이터를 읽습니다 (읽기에는 API 레이어 없음).
* 정책 페이지는 모든 변경 작업(토글, 파라미터 업데이트, 설치/제거)에 Server Actions를 사용합니다.
* 세션 뷰어는 Claude의 JSONL 트랜스크립트 형식을 파싱하고 메시지 및 도구 호출 타임라인을 렌더링합니다.

**주요 설계 결정:**

* 데이터베이스 없음 - 모든 영속 상태는 일반 파일(`~/.failproofai/`, `~/.claude/projects/`)에 저장됩니다.
* 변경 작업에 Server Actions 사용 - CRUD 작업에 REST API가 필요 없습니다.
* 읽기 페이지에 React Server Components 사용 - 초기 로딩이 빠르고 데이터 페칭을 위한 클라이언트 번들이 없습니다.
* 상호작용이 필요한 경우에만 클라이언트 컴포넌트 사용 (정책 토글, 활동 검색, 로그 뷰어).

***

## 파일 구조

```text theme={null}
failproofai/
├── bin/
│   └── failproofai.mjs           # CLI 라우터 (훅 / 대시보드 / 설치 등)
├── src/hooks/
│   ├── handler.ts                # 훅 이벤트 파이프라인
│   ├── builtin-policies.ts       # 39개 정책 정의
│   ├── policy-evaluator.ts       # 정책 실행 엔진
│   ├── policy-registry.ts        # 정책 등록 및 조회
│   ├── policy-types.ts           # TypeScript 인터페이스
│   ├── hooks-config.ts           # 다중 범위 설정 로딩
│   ├── custom-hooks-registry.ts  # globalThis 기반 훅 레지스트리
│   ├── custom-hooks-loader.ts    # 사용자 JS 훅을 위한 ESM 로더
│   ├── manager.ts                # 설치 / 제거 / 목록 작업
│   ├── install-prompt.ts         # 대화형 정책 선택 프롬프트
│   ├── hook-logger.ts            # hook.log에 로깅
│   ├── hook-activity-store.ts    # hook-activity.jsonl에 활동 저장
│   └── llm-client.ts             # LLM API 클라이언트 (AI 기반 정책용)
├── app/                          # Next.js 대시보드 (페이지 + 서버 액션)
├── lib/                          # 공유 유틸리티
│   ├── projects.ts               # 파일 시스템에서 Claude 프로젝트 열거
│   ├── log-entries.ts            # Claude 트랜스크립트 JSONL 형식 파싱
│   ├── paths.ts                  # 시스템 경로 해석
│   └── ...
├── components/                   # 공유 React UI 컴포넌트
├── contexts/                     # React 컨텍스트 프로바이더 (테마, 자동 새로고침, 텔레메트리)
├── examples/                     # 커스텀 훅 파일 예시
└── __tests__/                    # 단위 테스트 및 E2E 테스트
```
