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

개요

failproofai는 두 개의 독립적인 서브시스템으로 구성됩니다:
  1. 훅 핸들러 - Claude Code가 에이전트 도구 호출마다 실행하는 빠른 CLI 서브프로세스입니다. 정책을 평가하고 결정을 반환합니다.
  2. 에이전트 모니터 (대시보드) - 에이전트 세션을 모니터링하고 정책을 관리하기 위한 Next.js 웹 애플리케이션입니다.
두 서브시스템 모두 ~/.failproofai/와 프로젝트의 .failproofai/ 디렉토리에 있는 설정 파일을 공유하지만, 별도의 프로세스로 실행되며 파일 시스템을 통해서만 통신합니다.

훅 핸들러

Claude Code와의 통합

failproofai policies --install을 실행하면 ~/.claude/settings.json에 다음과 같은 항목이 작성됩니다:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "failproofai --hook PreToolUse"
          }
        ]
      }
    ],
    "PostToolUse": [ ... ]
  }
}
이후 Claude Code는 각 도구 호출 전에 failproofai --hook PreToolUse를 서브프로세스로 실행하고, stdin으로 JSON 페이로드를 전달합니다.

페이로드 형식

{
  "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):
{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "Blocked by failproofai: sudo command blocked"
  }
}
거부 (PostToolUse):
{
  "hookSpecificOutput": {
    "additionalContext": "Blocked by failproofai because: API key detected in output"
  }
}
지시 (Stop 이외의 모든 이벤트):
{
  "hookSpecificOutput": {
    "additionalContext": "Instruction from failproofai: Verify tests pass before committing."
  }
}
Stop 이벤트 지시:
  • 종료 코드: 2
  • 이유는 stdout이 아닌 stderr에 기록됩니다
허용:
  • 종료 코드: 0
  • stdout은 비어 있습니다
메시지와 함께 허용: allow(message)를 사용하면 작업이 허용된 경우에도 정책이 Claude에게 정보성 컨텍스트를 전달할 수 있습니다. 훅 핸들러는 다음 JSON을 stdout에 씁니다 (설정 파일이 아닌, 위의 거부 및 지시 응답과 동일하게 Claude Code에 대한 핸들러의 응답입니다):
// 훅 핸들러 프로세스가 stdout에 씁니다
{
  "hookSpecificOutput": {
    "additionalContext": "All CI checks passed on branch 'feat/my-feature'."
  }
}
  • 종료 코드: 0 (작업이 허용됨)
  • 여러 정책이 메시지와 함께 allow를 반환하는 경우, 해당 메시지들은 줄바꿈으로 연결되어 하나의 additionalContext 문자열이 됩니다
  • 어떤 정책도 메시지를 제공하지 않으면 stdout은 비어 있습니다 (기존 동작과 동일)

처리 파이프라인

src/hooks/handler.ts가 전체 파이프라인을 구현합니다:
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가 세 가지 범위의 설정 로딩을 구현합니다.
[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 객체로 정의합니다:
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.tsglobalThis 기반의 레지스트리를 구현합니다:
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 한 줄을 추가합니다:
{
  "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를 활용합니다.
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.tslib/log-entries.ts를 호출하여 파일 시스템에서 직접 프로젝트/세션 데이터를 읽습니다 (읽기에는 API 레이어 없음).
  • 정책 페이지는 모든 변경 작업(토글, 파라미터 업데이트, 설치/제거)에 Server Actions를 사용합니다.
  • 세션 뷰어는 Claude의 JSONL 트랜스크립트 형식을 파싱하고 메시지 및 도구 호출 타임라인을 렌더링합니다.
주요 설계 결정:
  • 데이터베이스 없음 - 모든 영속 상태는 일반 파일(~/.failproofai/, ~/.claude/projects/)에 저장됩니다.
  • 변경 작업에 Server Actions 사용 - CRUD 작업에 REST API가 필요 없습니다.
  • 읽기 페이지에 React Server Components 사용 - 초기 로딩이 빠르고 데이터 페칭을 위한 클라이언트 번들이 없습니다.
  • 상호작용이 필요한 경우에만 클라이언트 컴포넌트 사용 (정책 토글, 활동 검색, 로그 뷰어).

파일 구조

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 테스트