모든 Bash 명령이 SSH 키를 읽거나 파일을 유출하는 것을 OS 수준에서 방지.
Claude Code의 샌드박스 시스템은 AI가 실행하는 모든 Bash 명령을 OS 수준에서 격리합니다. SSH 키, API 토큰, 시스템 설정 같은 민감한 리소스에 대한 접근을 커널 수준 메커니즘으로 차단하며, 보안 저장소는 시크릿을 안전하게 관리합니다.
sandbox/index.ts → sandbox/seatbelt.ts → sandbox/bubblewrap.ts → sandbox/network.ts → sandbox/filesystem.ts → security/keychain.ts
핵심 구성 요소:
샌드박스는 OS별로 다른 커널 수준 격리 메커니즘을 사용합니다. macOS에서는 Apple의 Seatbelt(sandbox-exec)을, Linux에서는 bubblewrap(bwrap) + seccomp을 사용합니다.
// sandbox/seatbelt.ts — macOS 샌드박스 프로파일 생성
function buildSeatbeltProfile(config: SandboxConfig): string {
return `
(version 1)
(deny default)
;; 기본 읽기 허용 (프로젝트 디렉토리)
(allow file-read*
(subpath "${config.projectRoot}"))
;; 쓰기 허용 (프로젝트 디렉토리만)
(allow file-write*
(subpath "${config.projectRoot}"))
;; 민감 파일 항상 거부
(deny file-read*
(subpath "/Users/\${USER}/.ssh")
(subpath "/Users/\${USER}/.gnupg")
(subpath "/Users/\${USER}/.aws/credentials"))
;; 네트워크: 허용된 도메인만
(allow network-outbound
(remote tcp "${config.allowedDomains.join('" "')}"))`
}
// sandbox/bubblewrap.ts — Linux 샌드박스 구성
function buildBwrapArgs(config: SandboxConfig): string[] {
return [
'--ro-bind', '/usr', '/usr', // 시스템 바이너리 읽기 전용
'--ro-bind', '/bin', '/bin',
'--ro-bind', '/lib', '/lib',
'--bind', config.projectRoot, config.projectRoot, // 프로젝트만 RW
'--tmpfs', '/tmp', // 격리된 /tmp
'--unshare-net', // 기본 네트워크 격리
'--seccomp', seccompFilterFd, // syscall 필터
'--die-with-parent', // 부모 종료 시 함께 종료
]
}
프로세스 내 제한(예: Node.js의 --experimental-permission)은 네이티브 바이너리(git, gcc 등)에 적용되지 않습니다. Claude가 실행하는 Bash 명령은 시스템의 모든 바이너리를 호출할 수 있으므로, OS 커널 수준에서 격리해야만 모든 자식 프로세스에 대해 일관된 보안을 보장할 수 있습니다.
샌드박스는 사용자 환경과 보안 요구사항에 따라 3가지 모드로 운영됩니다.
// sandbox/index.ts — 3가지 샌드박스 모드
type SandboxMode = 'disabled' | 'regular' | 'auto-allow'
function resolveSandboxMode(): SandboxMode {
// 명시적 비활성화
if (config.sandbox === false) return 'disabled'
// auto-allow: 신뢰된 프로젝트에서 자동 승인
if (config.permissionMode === 'auto') return 'auto-allow'
// 기본: 모든 작업에 사용자 승인 필요
return 'regular'
}
disabled 모드에서도 권한 시스템은 여전히 동작합니다. 샌드박스 비활성화는 OS 수준 격리만 제거하며, Claude의 도구 사용 승인 프로세스는 유지됩니다.
네트워크 제어는 도메인 기반 허용/거부 목록으로 관리됩니다. 기본적으로 모든 외부 네트워크 접근이 차단되며, 명시적으로 허용된 도메인만 접근 가능합니다.
// sandbox/network.ts — 도메인 기반 네트워크 제어
const DEFAULT_ALLOWED_DOMAINS = [
'api.anthropic.com', // Claude API
'github.com', // git 작업
'registry.npmjs.org', // npm 패키지
'pypi.org', // pip 패키지
]
const ALWAYS_DENIED_DOMAINS = [
'*.onion', // Tor 네트워크
'metadata.google.internal', // 클라우드 메타데이터
'169.254.169.254', // AWS 메타데이터
]
169.254.169.254와 metadata.google.internal은 클라우드 인스턴스의 메타데이터 서비스입니다. 이 엔드포인트에 접근하면 인스턴스의 IAM 역할 자격 증명, 서비스 계정 토큰, 네트워크 설정 등을 획득할 수 있습니다. SSRF(Server-Side Request Forgery) 공격의 핵심 벡터이므로 항상 차단합니다.
특정 파일과 디렉토리는 샌드박스 모드와 관계없이 항상 접근이 거부됩니다.
// sandbox/filesystem.ts — 항상 거부 목록
const ALWAYS_DENIED: string[] = [
// Claude Code 내부 설정 (변조 방지)
'settings.json', // 권한 설정 변경 방지
'.claude/skills/*', // 스킬 주입 방지
// Git 보안
'.git/hooks/*', // git hook 변조 방지
'.bare-git-sentinel', // bare repo 감지 우회 방지
// 민감한 시크릿
'~/.ssh/*', // SSH 키
'~/.gnupg/*', // GPG 키
'~/.aws/credentials', // AWS 자격 증명
'~/.config/gcloud/*', // GCP 자격 증명
]
function isPathDenied(path: string): boolean {
return ALWAYS_DENIED.some(pattern =>
minimatch(normalizePath(path), pattern)
)
}
settings.json이 항상 거부 목록에 포함되는 이유는 특히 중요합니다. Claude가 자신의 권한 설정을 수정할 수 있다면, 보안 메커니즘 전체가 무력화될 수 있기 때문입니다.
.bare-git-sentinel은 bare git 레포지토리를 감지하는 센티널 파일입니다. bare 레포에서는 작업 트리가 없으므로 샌드박스의 파일시스템 제한이 다르게 적용됩니다. 이 파일을 조작하면 샌드박스 정책을 우회할 수 있어 접근이 차단됩니다.
보안 저장소는 API 키, OAuth 토큰 등의 시크릿을 안전하게 관리합니다. macOS에서는 시스템 Keychain을, 지원되지 않는 환경에서는 평문 폴백을 사용합니다.
// security/keychain.ts — macOS Keychain 통합
async function storeSecret(key: string, value: string): Promise<void> {
// hex 인코딩: Keychain이 특수 문자를 안전하게 처리하도록
const hexValue = Buffer.from(value).toString('hex')
await execFile('security', [
'add-generic-password',
'-a', 'claude-code',
'-s', key,
'-w', hexValue,
'-U', // 기존 항목 업데이트
])
}
async function readSecret(key: string): Promise<string | null> {
const hexValue = await execFile('security', [
'find-generic-password',
'-a', 'claude-code',
'-s', key,
'-w',
])
return Buffer.from(hexValue.trim(), 'hex').toString()
}
Keychain 접근은 서브프로세스 호출이 필요하므로 20~40ms가 소요됩니다. 빈번한 API 호출을 위해 30초 TTL(Time-to-Live) 인메모리 캐시를 사용합니다.
// 30초 TTL 캐시
const secretCache = new Map<string, { value: string; expires: number }>()
const TTL_MS = 30_000 // 30초
async function getCachedSecret(key: string): Promise<string | null> {
const cached = secretCache.get(key)
if (cached && cached.expires > Date.now()) {
return cached.value
}
const value = await readSecret(key)
if (value) {
secretCache.set(key, { value, expires: Date.now() + TTL_MS })
}
return value
}
Linux 또는 Keychain이 사용 불가한 환경에서는 ~/.claude/credentials에 평문으로 저장됩니다. 파일 권한은 0o600(소유자만 읽기/쓰기)으로 설정됩니다.
macOS Keychain의 security CLI는 일부 특수 문자(줄바꿈, 큰따옴표, 백슬래시)를 올바르게 처리하지 못합니다. API 키나 OAuth 토큰에 이런 문자가 포함될 수 있으므로, 저장 전에 hex로 인코딩하고 읽기 시 디코딩합니다. Base64 대신 hex를 사용하는 이유는 hex가 +나 / 같은 CLI에서 문제가 될 수 있는 문자를 포함하지 않기 때문입니다.
샌드박스 위반이 발생하면 사용자에게 명확한 피드백을 제공하고, 위반 이벤트를 분석 시스템에 기록합니다.
// 위반 감지 & 보고
function handleSandboxViolation(violation: Violation): void {
// UI에 위반 사항 표시
displayWarning({
type: violation.type, // 'file_access' | 'network' | 'syscall'
path: violation.path, // 접근 시도된 경로/도메인
command: violation.cmd, // 실행된 명령
})
// 분석 이벤트 기록 (PII 제외)
logEvent('sandbox_violation', {
type: violation.type,
// 경로는 해싱하여 기록 (PII 방지)
pathHash: hashPath(violation.path),
})
}
settings.json, .claude/skills, .bare-git-sentinel은 항상 접근이 거부되어 보안 메커니즘 우회를 방지합니다0o600 권한)이 적용됩니다Q1. OS 수준 샌드박스가 Node.js 프로세스 내 제한보다 필요한 이유는?
Q2. 169.254.169.254가 항상 거부 도메인에 포함되는 이유는?
169.254.169.254는 AWS, GCP 등 클라우드 프로바이더의 인스턴스 메타데이터 서비스(IMDS)입니다. 이 엔드포인트에 접근하면 IAM 역할 임시 자격 증명을 획득할 수 있어 SSRF 공격의 핵심 벡터입니다.Q3. settings.json이 항상 거부 목록에 포함되는 이유는?
settings.json을 수정할 수 있다면, 샌드박스 모드를 비활성화하거나 모든 파일 접근을 허용하는 등 보안 정책을 자체적으로 변경할 수 있습니다. 이는 "자물쇠가 열쇠를 만들 수 없어야 한다"는 원칙입니다.Q4. macOS Keychain에 시크릿을 저장할 때 hex 인코딩을 사용하는 이유는?
security CLI 도구는 셸을 통해 값을 전달하므로, 줄바꿈, 큰따옴표, 백슬래시 등의 특수 문자가 포함된 값이 손상될 수 있습니다. hex 인코딩은 모든 바이트를 안전한 16진수 문자로 변환합니다.Q5. 보안 저장소의 30초 TTL 캐시가 필요한 이유는?
security 바이너리를 서브프로세스로 실행해야 하므로 20~40ms가 소요됩니다. API 호출마다 이 비용이 발생하면 체감 지연이 증가하므로, 30초 TTL 캐시로 반복 호출의 비용을 제거합니다.