문제점 (Problem)
Claude Code는 6가지 권한 모드 (permission modes)를 제공합니다. Plan mode는 그중 하나입니다. 이 모드가 활성화되면, 마치 실제 가드레일 (guardrail)처럼 보이는 시스템 리마인더 (system reminder)가 주입됩니다:
Plan mode가 활성화되었습니다. 사용자가 아직 실행을 원하지 않는다고 표시했습니다.
-- 당신은 어떠한 수정도 해서는 안 되며, 읽기 전용이 아닌 도구(설정 변경 또는 커밋 포함)를 실행하거나, 그 외 시스템에 어떠한 변경도 가해서는 안 됩니다.
문장만 본다면 모델이 진정으로 제약받고 있다고 믿기 쉽습니다. 하지만 소스 코드를 읽어보면 그렇지 않습니다. 이 리마인더는 이름 그대로 컨텍스트 (context) 내에 존재하는 하나의 문자열일 뿐입니다. 도구 실행 경로 (tool execution path) 어디에도 도구 수준의 거부 목록 (deny list), 디스패치 단계의 권한 확인 (dispatch-stage permission check), 또는 모드 인지형 허용 목록 (mode-aware allow-list)이 존재하지 않습니다.
소스 코드상에 존재하는 격차 (Where the gap is, in source)
Claude Code 패키지에서 발견된 세 가지 증거입니다:
- **시스템 리마인더는
messages.ts:3227에 `
Plan이 Claude Code에서 유일한 프롬프트 전용 구조물은 아니지만, 대부분의 유사한 기능들이 실제 런타임 체크 (runtime checks)를 수반한다는 점에서 Plan은 눈에 띄게 다릅니다:
| 모드 | 메커니즘 | 실제 강제성 여부? |
|---|
default | 익숙하지 않은 작업에 대해 권한 승인 프롬프트(permission prompt)를 띄움 | 예 |
| ... | | |
표준 빌드에서의 Shift+Tab 사이클은 default → acceptEdits → plan → bypass → default로, 4단계 루프를 형성합니다. dontAsk가 존재하지만, 이는 플래그(flag)나 설정을 통해서만 도달할 수 있습니다.
흥미로운 클러스터는 bypassPermissions와 auto입니다. 두 기능 모두 실제적인 피해를 줄 수 있으므로, Plan 모드는 절대 호출하지 않는 정적 위험 탐지 (static danger detection) 계층과 함께 배포됩니다:
permissionSetup.ts:94-147의 isDangerousBashPermission() — 와일드카드나 인터프리터(interpreter)를 사용하는 Bash 규칙을 플래그(flag) 처리합니다.
permissionSetup.ts:157-233의 isDangerousPowerShellPermission() — iex, Start-Process 등을 플래그(flag) 처리합니다.
L295-342의 findDangerousClassifierPermissions() — auto 모드로 진입하기 전 모든 허용 규칙을 스캔합니다.
L510-553의 stripDangerousPermissionsForAutoMode() — auto 모드인 동안 위험한 규칙들을 strippedDangerousRules로 이동시킵니다.
L561-579의 restoreDangerousPermissions() — auto 모드를 벗어날 때 이를 복구합니다.
강제 실행 인프라(enforcement infrastructure)는 이미 존재합니다. 단지 Plan 모드가 그 중 어떤 것도 사용하지 않을 뿐입니다.
교훈 (Lessons learned)
-
권고(Advisory) vs. 강제 집행(Hard enforcement). 에이전트 기반 코딩 제품에서 "모델에게 X를 하지 말라고 말하는 것"과 "모델이 X를 할 수 없는 것"은 근본적으로 다른 두 가지 속성입니다. 전자는 희망 사항이며, 후자는 도구 계층(tool-layer)의 로직을 필요로 합니다. Plan 모드는 전자에 해당합니다.
-
프롬프트 우회(Prompt bypass)에는 악의가 필요하지 않습니다. 정교한 인젝션(injection)이 필요하지 않습니다. 긴 대화와 컨텍스트 드리프트(context drift)는 모델의 유효한 주의력(attention) 범위 밖으로 시스템 리마인더(system reminders)를 자연스럽게 밀어냅니다. 충분한 토큰, 충분한 도구 결과, 충분한 문답이 이어지면 리마인더는 효과적으로 사라질 때까지 희석됩니다.
-
진정한 해결책의 모습. Edit, Write, Bash를 Tool.isReadOnly()를 참조하고 부작용(side effects)이 발생하기 _전_에 Plan 모드에서의 호출을 거부하는 모드 인식 디스패처(mode-aware dispatcher)로 감싸야 합니다. 허용 목록(allow-list)은 산문(prose)이 아니라 데이터여야 합니다. 모델은 스스로에게 "이 쓰기 작업은 괜찮아"라고 설득할 수 있지만, return 문을 말로 설득해서 통과할 수는 없습니다.
-
표준 보안 패턴. 이것은 숙련되지 않은 보안 시스템이 아닌 모든 시스템이 사용하는 정책(policy)과 권고(advisory)의 분리입니다. 심층 방어(Defense in depth) 원칙에 따르면, 정책은 설득될 수 있는 계층보다 더 낮은 계층에 존재해야 합니다.
Claude Agent SDK에 미치는 영향
Agent SDK는 permission_mode를 노출합니다. coreSchemas.ts:339에 있는 열거형(enum)에는 'dontAsk'가 포함되어 있어, 다운스트림 개발자들이 실제 강제 집행을 선택할 수 있습니다. 하지만 그들은 또한 자신만의 Plan 모드 형태의 가드(guard)를 작성할 수도 있습니다: "강력한 시스템 프롬프트를 설정하고 희망하기"와 같은 방식 말입니다.
두 번째 경로를 선택하는 사람은 누구나 동일한 종류의 버그를 배포하게 됩니다. Agent-SDK 문서와 에이전트 설계 검토(design reviews) 시, 어떤 보장이 런타임(runtime)에서 오는지, 그리고 어떤 보장이 모델이 준수하도록 요청받은 문자열(string)에서 오는지 명시적으로 밝히는 것이 가치가 있습니다.