ClOr

ClOr

백엔드 실무 트러블슈팅과 AI 에이전트 구조 분석을 기록합니다.

Claude Code 해부학 (완결)

51만 줄 소스코드를 19편에 걸쳐 분석한 완결 시리즈

전체 시리즈 보기 →

백엔드 트러블슈팅

실무에서 겪은 장애와 해결 과정 기록

전체 시리즈 보기 →

최신 글

article thumbnail

시리즈 이전 편: [Part 9] 멀티에이전트 분석 — 혼자 일 못하면 팀을 짜는 AI

AI한테 터미널 권한을 줬습니다. "파일 좀 정리해줘"라고 했는데, rm -rf /를 치면 어떡하죠? 농담이 아닙니다. AI 에이전트에게 셸 접근 권한을 주는 순간, 이 질문은 현실이 됩니다. Claude Code는 이 문제를 어떻게 풀었을까요? 소스를 직접 까봤습니다.


목차

    1. 권한 모드 — 신뢰 수준을 단계로 나눈다
    1. 도구 실행 전 체크 흐름 — hasPermissionsToUseTool
    1. 위험 동작 분류 — 세 가지 리스크 레벨
    1. 권한 판정 결과의 세 가지 행동
    1. 사용자 승인 UX — 허용/거부/항상 허용
    1. 규칙의 출처 — 누가 이 규칙을 만들었나
    1. 그래서 뭐가 달라지나 — AI 에이전트 권한 설계의 교훈

1. 권한 모드 — 신뢰 수준을 단계로 나눈다

Claude Code는 모든 도구 실행 전에 "지금 어떤 모드인가"를 먼저 확인합니다. src/types/permissions.ts에 정의된 모드 목록을 보겠습니다.

// src/types/permissions.ts
export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',
  'bypassPermissions',
  'default',
  'dontAsk',
  'plan',
] as const

export type InternalPermissionMode =
  ExternalPermissionMode | 'auto' | 'bubble'

각 모드의 역할은 명확합니다. default는 매번 물어보고, plan은 계획만 세우고, auto는 AI 분류기가 판단하고, bypassPermissions는 전부 허용합니다. dontAsk는 반대로 물어볼 상황이 오면 그냥 거부해버립니다.

핵심은 모드가 "전부 허용" 아니면 "전부 차단"이 아니라는 점입니다. 각 모드 안에서도 규칙 기반으로 세밀한 제어가 들어갑니다.


2. 도구 실행 전 체크 흐름 — hasPermissionsToUseTool

모든 도구 호출은 useCanUseTool 훅을 거칩니다. 이 훅은 내부적으로 hasPermissionsToUseToolInner를 호출하는데, 여기서 단계별 체크가 진행됩니다.

// src/utils/permissions/permissions.ts (1158행~)
async function hasPermissionsToUseToolInner(
  tool: Tool,
  input: { [key: string]: unknown },
  context: ToolUseContext,
): Promise<PermissionDecision> {
  // 1a. 거부 규칙 확인
  const denyRule = getDenyRuleForTool(/*...*/)
  if (denyRule) return { behavior: 'deny', /*...*/ }

  // 1b. 항상 묻기 규칙 확인
  const askRule = getAskRuleForTool(/*...*/)

  // 1c. 도구 자체의 권한 검사
  toolPermissionResult = await tool.checkPermissions(parsedInput, context)

  // 2a. 모드 기반 허용 (bypassPermissions)
  // 2b. 항상 허용 규칙 확인
  // 3. passthrough → ask로 변환
}

순서가 중요합니다. 거부 규칙이 가장 먼저 적용되고, 그 다음 도구 자체 검사, 마지막으로 모드 기반 허용입니다. bypassPermissions 모드라 해도 safetyCheck 타입의 거부는 무시할 수 없습니다. .git/이나 .claude/ 같은 민감 경로는 어떤 모드에서든 확인을 요구합니다.


3. 위험 동작 분류 — 세 가지 리스크 레벨

Claude Code는 명령어의 위험도를 LOW, MEDIUM, HIGH 세 단계로 분류합니다. permissionExplainer.ts에서 AI가 직접 판단합니다.

// src/utils/permissions/permissionExplainer.ts
riskLevel: {
  type: 'string',
  enum: ['LOW', 'MEDIUM', 'HIGH'],
  description:
    'LOW (safe dev workflows), MEDIUM (recoverable changes), HIGH (dangerous/irreversible)',
},

LOW는 ls, cat 같은 읽기 전용 작업입니다. MEDIUM은 파일 수정처럼 되돌릴 수 있는 변경입니다. HIGH는 rm -rf처럼 되돌릴 수 없는 파괴적 작업입니다.

별도로 dangerousPatterns.ts에는 셸에서 임의 코드를 실행할 수 있는 위험 패턴 목록이 하드코딩되어 있습니다.

// src/utils/permissions/dangerousPatterns.ts
export const DANGEROUS_BASH_PATTERNS: readonly string[] = [
  'python', 'python3', 'node', 'deno', 'tsx',
  'ruby', 'perl', 'php', 'lua',
  'npx', 'bunx', 'bash', 'sh',
  'eval', 'exec', 'env', 'xargs', 'sudo',
  'ssh',
  // ...
]

이 패턴에 해당하는 명령이 "항상 허용" 규칙에 등록되어 있으면, auto 모드 진입 시 해당 규칙을 자동으로 제거합니다. Bash(python:*)처럼 인터프리터에 와일드카드 허용을 걸어두면 사실상 모든 코드를 실행할 수 있기 때문입니다.


4. 권한 판정 결과의 세 가지 행동

모든 권한 체크는 PermissionBehavior 세 가지 중 하나를 반환합니다.

// src/types/permissions.ts
export type PermissionBehavior = 'allow' | 'deny' | 'ask'

allow면 바로 실행, deny면 즉시 거부, ask면 사용자에게 물어봅니다. 여기에 내부적으로 passthrough라는 네 번째 상태가 있는데, 이건 "도구 자체는 판단을 안 했으니 상위 로직에서 결정해달라"는 의미입니다. 최종적으로 ask로 변환됩니다.


5. 사용자 승인 UX — 허용/거부/항상 허용

ask 판정이 나오면 handleInteractivePermission이 동작합니다. 이 함수는 사용자에게 확인 UI를 보여주면서, 동시에 여러 자동 승인 경로를 경쟁시킵니다.

// src/hooks/toolPermission/handlers/interactiveHandler.ts
ctx.pushToQueue({
  // ...
  onAbort() { /* 사용자가 취소 */ },
  async onAllow(updatedInput, permissionUpdates) {
    /* 허용 + 규칙 영구 저장 가능 */ },
  onReject(feedback?) { /* 거부 + 피드백 전달 */ },
  async recheckPermission() {
    /* 설정 변경 시 재검사 */ },
})

사용자가 "항상 허용"을 선택하면 persistPermissions를 통해 설정 파일에 규칙이 영구 저장됩니다. 그 규칙은 PermissionRuleSource에 따라 사용자 설정, 프로젝트 설정, 로컬 설정 등 다른 곳에 저장될 수 있습니다.

동시에 훅(hook), AI 분류기(classifier), 브릿지(claude.ai 웹)가 비동기로 경쟁합니다. 누가 먼저 응답하든 claim() 함수가 단 한 번만 resolve되도록 보장합니다. 레이스 컨디션을 원자적 체크로 막는 깔끔한 설계입니다.


6. 규칙의 출처 — 누가 이 규칙을 만들었나

권한 규칙은 출처(source)를 항상 기록합니다.

// src/types/permissions.ts
export type PermissionRuleSource =
  | 'userSettings'    // ~/.claude/settings.json
  | 'projectSettings' // 프로젝트 .claude/settings.json
  | 'localSettings'   // 로컬 전용 설정
  | 'flagSettings'    // 피처 플래그
  | 'policySettings'  // 조직 정책
  | 'cliArg'          // CLI 인수
  | 'command'         // 슬래시 커맨드
  | 'session'         // 현재 세션 한정

이게 왜 중요하냐면, policySettingsflagSettings에서 온 규칙은 사용자가 삭제할 수 없기 때문입니다. 조직 관리자가 "이 명령은 절대 안 돼"라고 설정하면, 개발자가 로컬에서 아무리 허용 규칙을 추가해도 거부가 우선합니다.


7. 그래서 뭐가 달라지나 — AI 에이전트 권한 설계의 교훈

Claude Code의 권한 시스템에서 배울 수 있는 설계 원칙을 정리하면 이렇습니다.

첫째, 거부가 허용보다 우선합니다. deny 규칙은 어떤 모드에서든 가장 먼저 체크됩니다. 안전성을 보장하는 가장 확실한 방법입니다.

둘째, 위험 패턴을 하드코딩합니다. AI 판단에만 의존하지 않고, DANGEROUS_BASH_PATTERNS 같은 정적 목록으로 알려진 위험을 차단합니다. AI가 아무리 "이건 안전해요"라고 해도 sudo가 들어간 와일드카드 허용은 자동으로 제거됩니다.

셋째, 판단 경로를 다중화합니다. 사용자 입력, 훅, AI 분류기, 원격 브릿지가 동시에 경쟁하되, 원자적 resolve로 중복 실행을 방지합니다.

넷째, 모든 결정을 기록합니다. permissionLogging.ts에서 모든 승인/거부를 analytics, OTel, 도구별 카운터로 팬아웃합니다. 사후 감사(audit)가 가능한 구조입니다.


다음 편 예고: [Claude Code 해부학 Part 11] 클로드 코드(Claude Code) MCP 구현 분석 — AI의 USB 포트를 직접 만들어봤다

profile

ClOr

@ClOr

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

ClOr · 백엔드 트러블슈팅과 AI 에이전트 구조 분석을 기록합니다.