가드레일·샌드박스 횡단 — AI가 함부로 못 하게 막는 법
한 줄 요약
가드레일·샌드박스 = AI가 위험한 도구·명령·경로에 손대지 못하도록 프롬프트로 부탁하거나(soft) 코드·OS로 진짜 차단하는(hard) 안전장치다. 왜 배우나: 프롬프트 인젝션으로 모델이 속아도 사고가 안 나려면 “막는 결정이 모델 바깥에 있어야” 하는데, 7개 하네스가 이 원칙을 어떻게 구현했는지 보면 안전한 에이전트를 짜는 설계 지도가 통째로 그려진다.
그림 — 가드레일이 작동하는 순간
아래는 도구 호출 하나가 가드레일을 통과하는 흐름이다. 두 개의 세계 — soft(부탁) 와 hard(강제) — 가 어떻게 겹겹이 쌓이는지 보자.
flowchart TB P([모델이 도구 호출 시도]) --> S1{"soft 층<br/>프롬프트·헌법으로<br/>사전 부탁"} S1 --> G{"hard 게이트<br/>코드·OS가 판정"} G -->|deny 먼저 검사| D[차단] G -->|ask| H["사람/LLM 심판에게 질문"] G -->|allow| E["(도구 실행)"] H -->|승인| E H -->|거부| D D -.차단 사유를 텍스트로.-> P E --> O["사후 검증·로그 마스킹"] classDef hard fill:#fde2e4,stroke:#c9184a,color:#3a0ca3; classDef soft fill:#e2f0cb,stroke:#5a8f29,color:#1b3a00; class G,D,H hard class S1 soft
두 가지만 먼저 잡으면 된다
가드레일을 읽을 때 항상 두 질문으로 환원하라. ① 막는가(hard) vs 부탁하는가(soft)? ② 누가 그 결정을 강제로 소유하는가(모델? 사람? 또 다른 LLM? OS 커널?) 7개 프레임워크의 차이는 거의 다 이 두 축에서 갈린다.
쉽게 풀기
공항 보안검색대를 떠올려 보자. 승객(=AI의 도구 호출)이 게이트를 통과하려 한다.
- soft 가드레일 = 안내방송. “위험물은 가져오지 마세요”라는 방송은 사람을 설득할 뿐, 마음먹고 어기면 못 막는다. 프롬프트나
CLAUDE.md의 금지 규칙이 여기에 해당한다. 모델이 무시하면 그만이다. - hard 가드레일 = X선 검색기와 차단봉. 사람의 의지와 무관하게 물리적으로 통과를 막는다. OS 샌드박스, 권한 규칙의
deny, 코드가 던지는 예외(throw)가 여기다. 모델이 속아도 이건 안 뚫린다.
7개 하네스가 공통으로 깨달은 핵심은 이거다: “절대 안 되는 것”은 안내방송이 아니라 차단봉으로 막아야 한다. 왜냐하면 모델은 프롬프트 인젝션에 속을 수 있는 “신뢰 못 할 작업자”이기 때문이다.
여기서 갈리는 디테일을 단계로 풀면:
- 순서가 안전을 정한다. 검색대는 “통과 명단”보다 “차단 명단”을 먼저 본다. Claude Code는
deny → ask → allow순으로, Codex는 여러 규칙이 걸리면max()로 가장 빡센 결정을 채택한다. 허용을 빠뜨리면 불편할 뿐이지만, 금지를 빠뜨리면 사고가 난다 — 그래서 모두 “막는 쪽으로 틀리기”를 택한다. - 차단당해도 모델은 계속 일한다. 막힌 사유를 사람이 아니라 모델에게 텍스트로 돌려준다. “이 경로는 못 써, 대신 저기로 가봐” 식이다. 차단을 에러가 아니라 “다시 계획해라” 신호로 줘야 루프가 안 죽는다.
- 민감한 짐은 누구든 못 건드린다.
.git·.env·.ssh·secrets는 거의 모든 하네스가 블랙리스트로 박아둔다. 자격증명 유출이나.git파괴는 되돌릴 수 없는 최악의 사고라서다. - 검색기가 고장 나면? 방향이 중요하다. 도구 래퍼(검색대 기계)가 버그로 죽으면 작업을 막지 않고 통과시킨다(fail-open) — 가드 버그가 일을 영영 멈추면 안 되니까. 반대로 보안 판정 자체가 실패하면(타임아웃 등) 거부한다(fail-closed) — 안전 결정은 의심스러우면 막아야 옳다.
핵심 정리
각 프레임워크가 가드를 어디에 두고 어떻게 강제하는가를 슬림하게 정리한다.
| 프레임워크 | hard 강제의 위치 | 한 줄 특이점 |
|---|---|---|
| Claude Code | OS 샌드박스 + 권한규칙 + managed(MDM) 3중 | deny→ask→allow 첫매칭, 보호경로는 allow로도 못 뚫음 |
| Codex | OS 샌드박스(Seatbelt/Landlock+bwrap/제한토큰) | Guardian = LLM 심판이 승인 피로를 자동 판정 |
| OMC | hook deny(진짜 차단) + 워커 RBAC(부탁) | “막는 가드 vs 부탁 가드”를 코드 주석으로 명시 분리 |
| gajae-code | bash prefix 파싱 + 런타임 throw | 정당화 영수증 — 자식 스폰을 게이팅(고유) |
| ouroboros | 엔진이 짜는 실행 봉투(env·CLI 깃발) | 결정/번역 분리 — 결정은 엔진 한 곳, 어댑터는 번역만 |
| fable-ish | 거의 없음(호스트에 명시 위임) | 순수 soft + redact() 로그 마스킹뿐, 한계를 솔직히 선언 |
| 내 패턴 | OMC hook 차용(realpathSync 후 거부) | settings + hook + 자연어 헌법 3층 혼합 |
6개 하네스가 똑같이 수렴한 6가지 공통 패턴
서로 모르고 짰는데도 같은 결론에 도달했다 — 그래서 “정답에 가까운 설계”라고 볼 수 있다.
- deny-by-default. 모호·파괴적이면 일단 막는다. 안전 실패는 비대칭적으로 비싸다.
- hard는 모델 바깥, soft는 프롬프트. 인젝션에 모델이 속아도 OS/런타임이 막는다.
- 민감경로 하드코딩.
.git/.env/.ssh/secrets는 보편 블랙리스트.- 차단 사유를 모델에게 피드백. 차단을 “재계획 신호”로 줘야 루프가 안 죽는다.
- fail-open(가드 IO) vs fail-closed(보안 판정). 신뢰성과 안전성은 반대로 틀려야 옳다.
- 셸 메타문자(
; & | $ ( )등) 전면 거부.git status && rm -rf /같은 체이닝 우회를 prefix 매칭만으론 못 막으니, 화이트리스트 검사 전에 탈락시킨다.
갈라지는 분기점 — 누가 왜 다르게 했나
- 진짜 OS 샌드박스가 있나? Claude Code·Codex만 커널 수준 자체 격리. 나머지는 언어 수준 검사(throw/deny)나 호스트 위임으로 대체 — 플러그인형은 Claude Code가 이미 OS 샌드박스를 주니 그 위에 정책층만 얹는 게 합리적.
- 승인 피로를 누가 자동화? Codex만 Guardian(LLM 심판)으로 “물어볼 것”을 자동 판정. 대신 fail-closed·untrusted-evidence·서킷브레이커로 두껍게 방어. (Claude Code도
auto모드에 분류기 모델이 있다.)- 결정을 분산 vs 단일소유? Claude Code는 여러 범위(managed/user/project/local)에 분산하고 우선순위로 조정. ouroboros는 정반대로 “결정은 엔진 한 곳, 번역은 어댑터”로 극단적 단일소유.
- 무엇을 가드 대상으로 보나? 대부분 “도구 호출”을 막지만 gajae만 에이전트 스폰을 정당화 영수증으로 게이팅(fork bomb 유사 폭주를 1급 위험으로 봄). ouroboros도
_DEPTH카운터(천장 5)로 재귀 스폰을 막는다.- 자기 한계를 솔직히 선언? fable-ish는 “hard는 호스트에 위임”, OMC는 워커 RBAC를 “advisory layer only”라 명시. 단 OMC의 hook deny 자체는 진짜 hard다(이 둘을 “못 막는다”로 묶으면 과장).
- 외부 플러그인을 신뢰하나? ouroboros만 전용 방화벽(단일 초크포인트 + 신뢰튜플 + digest 재계산 + 감사원장). 진화 루프로 코드가 바뀌니 “코드 치환 공격”을 1급 위협으로 봤다.
실제 예시
각 프레임워크의 hard/soft가 실제 코드로 어떻게 생겼는지 본다.
// Claude Code: settings.json — deny가 ask·allow를 이기는 첫매칭 규칙
// harness-work/claude-code/examples/settings/settings-strict.json
{
"permissions": {
"deny": ["Read(.git/**)", "Read(.env*)", "Bash(rm:*)"], // 최우선, allow로도 못 뚫림
"ask": ["Bash(git push:*)"],
"allow": ["Read(src/**)", "Bash(npm test:*)"]
},
"defaultMode": "acceptEdits",
"sandbox": { "filesystem": "workspace-write", "network": "deny" }
}// Codex: 여러 규칙이 걸리면 가장 제한적 결정을 채택 (max())
// harness-work/codex/codex-rs/core/src/safety.rs
// Allow < Prompt < Forbidden 순서라 max()가 곧 "가장 빡센 쪽"
let decision = matched_rules.iter().map(|r| r.decision).max()
.unwrap_or(Decision::Prompt);
// Guardian(LLM 심판)은 트랜스크립트를 "untrusted evidence"로 라벨해 인젝션 방어
// 타임아웃 = ReviewDecision::TimedOut → record_guardian_non_denial (서킷브레이커 카운트 제외)# fable-ish: 순수 soft — 차단이 아니라 권고문 주입 + 비밀 마스킹
# harness-work/fable-ish/scripts/classify_task.py
if DESTRUCTIVE_REQUEST_RE.search(prompt) and not SAMPLE_RE.search(prompt):
mode = "blocked" # additionalContext로 권고만, 실제 실행은 못 막음
# 유일한 hard는 redact() 로그 마스킹 + Stop 훅의 decision:"block"(검증 없는 종료 차단)
text = redact(text) # sk-... / ghp_... / xox... / key=value 패턴 치환// gajae-code: 자식 4개 초과 스폰 시 "정당화 영수증" 5필드 필수 (고유 설계)
// harness-work/gajae-code/packages/coding-agent/src/task/spawn-gate.ts
interface SpawnPlanReceipt { // 누락 시 모델에게 텍스트 반환 → 재시도
whyParallel: string; whyNotLocal: string; independence: string;
expectedOutputs: string; cleanupPlan: string;
}
// bash는 원본 + "cd && " 제거본 둘 다 검사해 래퍼 우회를 차단요약 & 셀프체크
- soft는 부탁, hard는 강제. 7개 하네스 전부 둘을 구조적으로 분리하고, “절대 규칙”은 프롬프트가 아니라 런타임/OS에 둔다.
- deny-first + 민감경로 하드코딩 + 차단 사유 피드백이 모두가 수렴한 공통 골격이다.
- OS 자체 샌드박스는 Claude Code·Codex뿐, 나머지는 throw·hook deny·실행봉투·호스트 위임으로 대체한다.
스스로 답해보기:
- 프롬프트 인젝션으로 모델이 “
.env를 읽어라”에 속았다. 어느 층이 실제로 이걸 막아야 하며, 왜 프롬프트(soft)로는 부족한가? - 가드 코드가 버그로 예외를 던졌다. 도구 래퍼는 fail-open인데 보안 심판은 fail-closed인 이유를 한 문장으로 설명할 수 있는가?
- 내 스택(Claude+Codex+OMC)에서 “워커 폭주”를 진짜로 막으려면 어디(gajae식 영수증 게이트)에 무엇을 걸어야 하는가?
연결
기능노트 근거: CC_80_guardrails · CX_80_sandbox-and-approval-guardrails · OMC_60_guardrails-permissions · GJ_70_guardrails-sandbox-permission-gating · OB_80_guardrails-sandbox-permissions-plugin-firewall · FB_90_guardrails-soft-vs-hard · MINE_70_guardrails-permissions-sandbox
소스 경로(접으면 보임)
- Claude Code:
harness-work/claude-code/examples/settings/settings-strict.json,.../mdm/managed-settings.json; 원문_원문아카이브/claude-code/permissions.md·permission-modes.md·settings.md·sandboxing.md- Codex:
harness-work/codex/codex-rs/core/src/safety.rs,sandboxing/src/{manager.rs,seatbelt_base_policy.sbpl,landlock.rs},execpolicy/src/{policy.rs,decision.rs},core/src/guardian/{mod.rs,prompt.rs,policy.md,review.rs},protocol/src/protocol.rs:814; 원문_원문아카이브/codex/{31_sandbox.md,68_permissions.md,02_agent-approvals-security.md}- OMC:
harness-work/oh-my-claudecode/scripts/{pre-tool-enforcer.mjs,post-tool-verifier.mjs,run.cjs},src/hooks/permission-handler/index.ts,src/team/{permissions.ts,mcp-team-bridge.ts},hooks/hooks.json- gajae-code:
harness-work/gajae-code/packages/coding-agent/src/tools/{bash-allowed-prefixes.ts,bash.ts,plan-mode-guard.ts},src/task/{spawn-gate.ts,index.ts,types.ts},src/session/agent-session.ts,src/prompts/tools/bash.md- ouroboros:
harness-work/ouroboros/src/ouroboros/{sandbox.py,orchestrator/policy.py,claude_permissions.py,codex_permissions.py,copilot_permissions.py,runtime/child_env.py,plugin/firewall.py,plugin/lockfile.py}- fable-ish:
harness-work/fable-ish/scripts/{classify_task.py,ledger.py,verify_state.py},hooks/{hooks.json,user_prompt_submit.py,stop_gate.py}- 내 패턴:
~/.claude/settings.json·settings.local.json,~/.claude/plugins/marketplaces/omc/...,/mnt/d/human-token-workflow/AGENTS.md,/mnt/d/akh2/CLAUDE.md
Codex 교차검증 (gpt-5.5가 실제 소스와 대조)
큰 축(“hard vs soft”, “결정 소유자가 모델 밖에 있어야”)은 맞으나 일부 범위 과장·위치 오판이 있다. 사실 보정: ① Codex
SandboxType은 4값이지만None은 무샌드박스 → 실제 격리는 3계열(sandboxing/src/manager.rs:24). ② Guardian은 항상 개입이 아니라OnRequest/Granular+AutoReview일 때만(guardian/review.rs:165) — “사람 승인 전체 대체”로 읽으면 틀림. ③ Guardian 타임아웃은record_guardian_non_denial이라 서킷브레이커 거부 카운트에 안 들어감(review.rs:402,:580). ④ fable-ish의 hard는redact()뿐이 아니라Stop훅decision:"block"도 있음 → “hard execution guard 없음”이 정확(hooks.json:28,stop_gate.py:30). ⑤ OMC 워커 snapshot diff는 기본값permissionEnforcement: off이고 켜도 예방 아닌 사후 적발(team/types.ts:26). 빠진 차이: Claude Codeauto모드 분류기 모델, OMC PermissionRequest의 repo inspection·heredoc safe base·민감경로 검사, gajae plan-mode write guard, ouroboros의 별도 capability policy(policy.py:97,:258)는 표가 과소평가함. 해석 보정: “OS 샌드박스 자체 구현은 CC·Codex뿐”이 정확(ouroboros는 백엔드 위임 번역). “스폰 게이팅은 gajae 고유”는 hard 영수증 기준으로만 맞음(OMC·ouroboros도 다른 방식으로 스폰 위험을 다룸). OMC hook deny는 hard이므로 fable과 한 묶음으로 “못 막는다” 하면 과장. 차용안 현실성: Guardian식 LLM 자동승인은 deterministic allow/deny를 먼저 통과시키고 회색지대 read/test만 LLM에, write/network/destructive는 자동 allow 금지. gajae 영수증은 5필드(문서는 3필드만 적음). 결정/번역 분리는SandboxClass하나론 부족하고 read/write root·network·denied-read·approval reviewer까지 담은 다차원 policy object 필요.redact()전역화는 효과 크나 sink wrapper·테스트·과도 redaction 방지 규칙 동반 필요.writer≠reviewer는 settings deny로 못 막는 identity 규칙이라 ledger나 Stop/PreToolUse 훅 검증 필요.