- Fix "Anthropics" → "Anthropic" typo in query-engine.md Mermaid diagram - Fix permission-system.md "이전" nav link to point to command-system.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
권한 시스템: Permission Model 분석
1. 개요
Permission System (권한 시스템)은 Claude Code에서 Tool (도구)을 실행하기 전 사용자의 승인을 관리하는 보안 레이어다. 단순한 yes/no 프롬프트가 아니라, 다계층 규칙 기반(rule-based) 평가 파이프라인으로 설계되어 있으며, 현재 모드(mode)·규칙(rule)·분류기(classifier)·훅(hook)이 순차적으로 적용된다.
핵심 위치
| 관심사 | 경로 |
|---|---|
| 타입 정의 | src/types/permissions.ts |
| Context 타입 | src/Tool.ts (ToolPermissionContext) |
| 모드 정의 | src/utils/permissions/PermissionMode.ts |
| 권한 체크 로직 | src/utils/permissions/permissions.ts |
| 초기화 및 모드 전환 | src/utils/permissions/permissionSetup.ts |
| 거부 추적 | src/utils/permissions/denialTracking.ts |
| Auto 모드 분류기 | src/utils/permissions/yoloClassifier.ts |
| 모드 순환 | src/utils/permissions/getNextPermissionMode.ts |
2. 아키텍처 다이어그램
flowchart TD
A[Tool 실행 요청] --> B{Permission Mode 확인}
B -->|bypassPermissions| Z[즉시 허용]
B -->|plan| P[쓰기 도구 차단 / 읽기만 허용]
B -->|default / acceptEdits / dontAsk / auto| C[규칙 매칭 단계]
C --> D{alwaysAllowRules 매칭?}
D -->|일치| Z
D -->|불일치| E{alwaysDenyRules 매칭?}
E -->|일치| DENY[거부 — PermissionDenyDecision]
E -->|불일치| F{alwaysAskRules 매칭?}
F -->|일치| ASK
F -->|불일치| G{Tool.checkPermissions 호출}
G -->|allow| Z
G -->|deny| DENY
G -->|ask| ASK
ASK --> H{shouldAvoidPermissionPrompts?}
H -->|true — 헤드리스 에이전트| HK[PermissionRequest Hook 실행]
HK -->|Hook 결정 있음| HOOKRESULT[Hook 결과 반환]
HK -->|Hook 결정 없음| DENY
H -->|false| I{auto 모드?}
I -->|yes| J[YOLO Classifier 실행]
J -->|shouldBlock=false| Z
J -->|shouldBlock=true| ASK2[사용자 프롬프트 표시]
I -->|no| ASK2
ASK2 --> K[사용자 결정]
K -->|허용| Z
K -->|거부| L[거부 기록 — DenialTracking]
L --> M{Denial 한계 도달?}
M -->|yes| N[프롬프트 폴백으로 전환]
M -->|no| DENY
Z --> EXEC[Tool 실행]
3. Permission Mode 타입
Permission Mode (권한 모드)는 시스템 전체의 기본 동작 방침을 결정한다. 외부(External)에서 사용자가 설정할 수 있는 모드와 내부(Internal) 전용 모드로 구분된다.
3.1 External Permission Mode
src/types/permissions.ts에서 EXTERNAL_PERMISSION_MODES로 선언되며, 사용자 설정(settings.json의 defaultMode)과 CLI 플래그(--permission-mode)에서 사용할 수 있다.
| 모드 | 심볼 | 색상 키 | 설명 |
|---|---|---|---|
default |
(없음) | text |
표준 동작. Tool별 checkPermissions 로직에 따라 ask/allow 결정 |
plan |
⏸ | planMode |
읽기 전용 Plan Mode. 파일 쓰기·명령 실행 등 상태 변경 Tool 차단 |
acceptEdits |
⏵⏵ | autoAccept |
파일 편집 자동 수락. Bash 실행은 여전히 확인 요구 |
bypassPermissions |
⏵⏵ | error |
모든 권한 체크 우회. isBypassPermissionsModeAvailable이 true일 때만 활성화 가능 |
dontAsk |
⏵⏵ | error |
권한 프롬프트 없이 거부 대신 자동 허용. bypassPermissions와 유사하나 의미론적으로 구분 |
3.2 Internal Permission Mode
auto 모드는 TRANSCRIPT_CLASSIFIER feature flag가 활성화된 ant(내부) 빌드에서만 사용 가능하다. 외부 공개 모드 목록(EXTERNAL_PERMISSION_MODES)에는 포함되지 않으며, toExternalPermissionMode()를 통해 'default'로 매핑된다.
bubble 모드는 타입 정의에만 존재하며 현재 UI 순환에 노출되지 않는다.
3.3 모드 순환 (Shift+Tab)
getNextPermissionMode() 함수가 Shift+Tab 입력 시 다음 모드를 결정한다.
일반 사용자: default → acceptEdits → plan → [bypassPermissions →] [auto →] default
ant 내부: default → [bypassPermissions →] [auto →] default
bypassPermissions는 isBypassPermissionsModeAvailable이 true일 때만 순환에 포함되고, auto는 isAutoModeAvailable과 런타임 feature gate가 모두 활성화되어야 포함된다.
4. ToolPermissionContext 분석
ToolPermissionContext는 src/Tool.ts에서 DeepImmutable<{...}>로 정의된다. DeepImmutable (깊은 불변 타입) 래퍼가 적용되어 런타임 중 권한 컨텍스트가 외부에서 변조되는 것을 타입 수준에서 방지한다.
export type ToolPermissionContext = DeepImmutable<{
mode: PermissionMode
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
alwaysAllowRules: ToolPermissionRulesBySource
alwaysDenyRules: ToolPermissionRulesBySource
alwaysAskRules: ToolPermissionRulesBySource
isBypassPermissionsModeAvailable: boolean
isAutoModeAvailable?: boolean
strippedDangerousRules?: ToolPermissionRulesBySource
shouldAvoidPermissionPrompts?: boolean
awaitAutomatedChecksBeforeDialog?: boolean
prePlanMode?: PermissionMode
}>
4.1 각 필드 설명
mode: PermissionMode
현재 활성 모드. 권한 평가의 첫 번째 분기점이다.
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
기본 작업 디렉터리 외에 파일 접근을 허용할 추가 경로 목록. 각 항목은 path와 source(어떤 설정에서 추가되었는지)를 포함한다.
alwaysAllowRules / alwaysDenyRules / alwaysAskRules: ToolPermissionRulesBySource
세 가지 규칙 목록. 모두 ToolPermissionRulesBySource 타입으로, 규칙의 출처(source)별로 문자열 배열을 저장한다.
type ToolPermissionRulesBySource = {
[T in PermissionRuleSource]?: string[]
}
출처(PermissionRuleSource)는 userSettings, projectSettings, localSettings, flagSettings, policySettings, cliArg, command, session 중 하나다. 이 출처 정보는 createPermissionRequestMessage()에서 사용자에게 "어느 설정 파일의 규칙이 이 요청을 차단했는지" 설명할 때 사용된다.
isBypassPermissionsModeAvailable: boolean
bypassPermissions 모드로 전환 가능한지 여부. 보안 민감 환경에서 false로 설정해 모드 접근 자체를 차단한다.
isAutoModeAvailable?: boolean
Auto 모드 게이트(Statsig feature gate 등)를 통과했는지 캐시된 값. 런타임에 live check(isAutoModeGateEnabled())와 함께 canCycleToAuto()에서 이중 검증된다.
strippedDangerousRules?: ToolPermissionRulesBySource
Auto 모드 진입 시 제거된 위험 규칙의 원본 보관소. isDangerousBashPermission() 등이 탐지한 규칙들이 이 필드에 저장되며, 모드 이탈 시 복원에 사용된다.
shouldAvoidPermissionPrompts?: boolean
true이면 사용자 인터페이스를 띄울 수 없는 헤드리스(headless) 에이전트 환경임을 의미한다. 이 경우 ask 결정은 프롬프트 대신 PermissionRequest 훅을 거쳐 자동 거부된다.
awaitAutomatedChecksBeforeDialog?: boolean
true이면 사용자 다이얼로그 표시 전에 분류기·훅 등 자동화 체크가 완료될 때까지 대기한다. 코디네이터(coordinator) 워커 환경에서 활성화된다.
prePlanMode?: PermissionMode
모델이 자체적으로 Plan Mode에 진입하기 전의 원래 모드를 저장한다. Plan Mode 이탈 시 이 값으로 복원한다.
4.2 빈 컨텍스트 초기값
getEmptyToolPermissionContext()는 다음 기본값을 반환한다.
{
mode: 'default',
additionalWorkingDirectories: new Map(),
alwaysAllowRules: {},
alwaysDenyRules: {},
alwaysAskRules: {},
isBypassPermissionsModeAvailable: false,
}
5. 권한 체크 흐름
5.1 규칙 매칭 단계
permissions.ts의 getAllowRules(), getDenyRules(), getAskRules()는 각각 PERMISSION_RULE_SOURCES 순서(userSettings → projectSettings → localSettings → flagSettings → policySettings → cliArg → command → session)로 규칙을 평탄화(flatMap)하여 반환한다.
Tool 수준 매칭 (toolMatchesRule)
규칙에 ruleContent가 없으면 Tool 전체에 대한 매칭이다. MCP (Model Context Protocol) 도구는 mcp__serverName__toolName 형식으로 식별되며, 서버 수준 규칙(mcp__server1)은 해당 서버의 모든 도구에 적용된다.
콘텐츠 수준 매칭 (getRuleByContentsForTool)
ruleContent가 있는 경우 도구별 커스텀 매칭 로직(Tool.checkPermissions())이 사용된다. 예를 들어 BashTool은 Bash(git commit:*) 형식으로 특정 명령 패턴에만 규칙을 적용한다.
5.2 복합 명령 처리
Bash 등 복합 명령을 실행하는 도구는 subcommandResults 타입의 PermissionDecisionReason을 반환할 수 있다. 하나의 명령 라인에 여러 서브커맨드가 있을 때, 각각을 독립적으로 평가한 결과를 맵으로 반환한다. 어느 하나라도 ask 또는 passthrough이면 전체가 사용자 프롬프트로 에스컬레이션된다.
5.3 PermissionDecision 타입 계층
PermissionResult
├── PermissionDecision
│ ├── PermissionAllowDecision — behavior: 'allow'
│ ├── PermissionAskDecision — behavior: 'ask', message, suggestions?, pendingClassifierCheck?
│ └── PermissionDenyDecision — behavior: 'deny', message, decisionReason (필수)
└── passthrough — 상위 처리기로 위임
PermissionAskDecision의 suggestions 필드에는 사용자가 규칙을 영구 저장할 수 있는 PermissionUpdate 목록이 포함될 수 있다. 예를 들어 "이 명령을 항상 허용"을 클릭하면 {type: 'addRules', destination: 'projectSettings', ...} 형태의 업데이트가 실행된다.
6. 자동 모드 (Auto Mode)
6.1 게이트 접근 검증
Auto 모드는 다중 게이트 구조로 보호된다.
- Feature flag:
TRANSCRIPT_CLASSIFIER빌드 플래그가 활성화된 빌드에서만 컴파일됨 - 사용자 타입:
process.env.USER_TYPE === 'ant'인 내부 사용자 전용 - Statsig gate: 런타임 feature gate 통과 필요 (
isAutoModeGateEnabled()) - 모델 지원:
modelSupportsAutoMode()로 현재 메인 루프 모델이 auto 모드를 지원하는지 확인 - 보안 제한 게이트:
checkSecurityRestrictionGate()
verifyAutoModeGateAccess() 함수가 세션 시작 시 위 조건들을 확인하고 결과를 isAutoModeAvailable에 캐시한다.
6.2 YOLO Classifier
yoloClassifier.ts의 YOLO (You Only Live Once) Classifier (분류기)는 Auto 모드에서 사용자 프롬프트 없이 도구 실행 여부를 자동 판단하는 LLM 기반 분류기다.
YoloClassifierResult 타입은 다음 주요 필드를 포함한다.
| 필드 | 설명 |
|---|---|
shouldBlock |
true이면 실행 차단 (사용자 프롬프트로 에스컬레이션) |
reason |
판단 근거 |
unavailable |
API 오류 등으로 분류기 자체가 불가용한 경우 |
transcriptTooLong |
컨텍스트 창 초과 — 동일 입력은 재시도해도 동일 결과이므로 즉시 폴백 |
model |
분류에 사용된 모델 ID |
stage |
2단계 XML 분류기에서 최종 결정을 낸 단계 ('fast' or 'thinking') |
분류기는 세션의 대화 기록(transcript)을 컨텍스트로 사용하며, getLastClassifierRequests()로 이전 요청을 캐싱하여 반복 오버헤드를 줄인다.
6.3 위험 권한 제거 (Dangerous Permission Stripping)
Auto 모드 진입 시 isDangerousBashPermission() 함수가 alwaysAllowRules를 스캔해 분류기를 우회할 수 있는 위험 패턴을 제거한다.
위험으로 판단하는 패턴:
Bash — 도구 전체 허용 (모든 명령 실행 가능)
Bash(*) — 와일드카드 전체 허용
Bash(python:*) — 스크립트 인터프리터 prefix 허용
Bash(python*) — 인터프리터 와일드카드
Bash(node -*) — 플래그 포함 패턴
제거된 규칙은 strippedDangerousRules에 보존되며, Auto 모드 이탈 시 복원된다. 동일 로직이 isDangerousPowerShellPermission() 함수로 PowerShell에도 적용된다.
6.4 Classifier Fail-Closed
분류기가 오류로 불가용 상태가 된 경우, 30분 새로고침 주기(CLASSIFIER_FAIL_CLOSED_REFRESH_MS)가 적용된다. 분류기 불가용 시 buildClassifierUnavailableMessage()가 메시지를 생성하고 ask 결정으로 폴백한다.
7. 거부 추적 (Denial Tracking)
denialTracking.ts는 분류기 또는 자동화된 체크가 반복적으로 거부할 때 사용자 프롬프트 폴백으로 전환하는 안전망이다.
7.1 상태 구조
type DenialTrackingState = {
consecutiveDenials: number // 연속 거부 횟수
totalDenials: number // 총 거부 횟수
}
const DENIAL_LIMITS = {
maxConsecutive: 3, // 연속 3회 거부 시 폴백
maxTotal: 20, // 총 20회 거부 시 폴백
}
7.2 상태 전환
recordDenial(state): 거부 발생 시 두 카운터 모두 증가 (순수 함수, 새 객체 반환)recordSuccess(state): 성공 시consecutiveDenials를 0으로 리셋 (총 거부 수는 유지)shouldFallbackToPrompting(state): 두 한계 중 하나라도 도달하면true반환
이 설계는 분류기가 특정 도구 요청을 지속적으로 잘못 판단하거나 과도하게 차단할 때 시스템이 무한 거부 루프에 빠지는 것을 방지한다.
8. 주요 설계 결정
8.1 DeepImmutable을 통한 불변성 보장
ToolPermissionContext에 DeepImmutable 래퍼를 적용하는 이유는 권한 컨텍스트가 여러 계층의 도구 실행 코드에 전달될 때 의도치 않은 변경을 타입 수준에서 차단하기 위해서다. TypeScript의 표준 readonly는 최상위 속성만 보호하지만, DeepImmutable은 중첩 구조까지 재귀적으로 읽기 전용으로 만든다.
src/types/permissions.ts에는 import cycle (순환 임포트) 문제를 피하기 위해 단순화된 readonly 버전의 ToolPermissionContext가 별도로 정의되어 있다.
8.2 출처 추적 규칙 시스템
규칙을 단순 문자열 배열로 저장하지 않고 ToolPermissionRulesBySource 구조를 사용해 출처를 보존한다. 이를 통해:
- 사용자에게 "어느 설정 파일의 어떤 규칙이 이 요청을 제어하는지" 명시적으로 안내할 수 있다
- 프로젝트 설정과 사용자 설정의 규칙이 충돌할 때 우선순위를 관리할 수 있다
- 규칙 삭제 시 올바른 파일에서 제거할 수 있다 (
deletePermissionRuleFromSettings)
8.3 Plan Mode 복원 메커니즘
모델이 대화 중 자체적으로 Plan Mode를 요청할 수 있다 (prePlanMode 필드). 이 경우 진입 전 모드를 prePlanMode에 저장하고 Plan Mode 이탈 시 복원한다. 이는 사용자가 수동으로 설정한 모드(acceptEdits 등)가 모델의 일시적 Plan Mode 진입으로 인해 유실되는 것을 방지한다.
8.4 헤드리스 에이전트를 위한 안전한 폴백
shouldAvoidPermissionPrompts가 true인 환경(백그라운드 에이전트, CI 파이프라인 등)에서는 ask 결정이 사용자 UI를 띄우는 대신 executePermissionRequestHooks()를 거친다. 훅이 명시적 결정을 내리지 않으면 자동 거부(AUTO_REJECT_MESSAGE)로 처리된다. 이는 UI가 없는 환경에서 시스템이 무한 대기하거나 의도치 않게 위험한 작업을 승인하는 것을 방지하는 설계다.
9. 주요 타입 요약
// 권한 행동 — 규칙이 취하는 조치
type PermissionBehavior = 'allow' | 'deny' | 'ask'
// 규칙 값 — 어떤 도구의 어떤 명령에 적용하는지
type PermissionRuleValue = {
toolName: string
ruleContent?: string // e.g. "git commit:*"
}
// 규칙 — 행동 + 값 + 출처
type PermissionRule = {
source: PermissionRuleSource
ruleBehavior: PermissionBehavior
ruleValue: PermissionRuleValue
}
// 업데이트 — 규칙 추가/제거/변경 명령
type PermissionUpdate =
| { type: 'addRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior }
| { type: 'removeRules'; ... }
| { type: 'replaceRules'; ... }
| { type: 'setMode'; destination: ...; mode: ExternalPermissionMode }
| { type: 'addDirectories'; ... }
| { type: 'removeDirectories'; ... }
// 결정 이유 — 왜 이 결정이 내려졌는지
type PermissionDecisionReason =
| { type: 'rule'; rule: PermissionRule }
| { type: 'mode'; mode: PermissionMode }
| { type: 'hook'; hookName: string; reason?: string }
| { type: 'classifier'; classifier: string; reason: string }
| { type: 'subcommandResults'; reasons: Map<string, PermissionResult> }
| { type: 'workingDir'; reason: string }
| { type: 'safetyCheck'; reason: string; classifierApprovable: boolean }
| ...
Navigation
- 이전: 커맨드 시스템
- 다음: 에이전트 오케스트레이션
- 상위: 목차