OMC 가드레일은 AI가 도구를 쓰기 직전·직후에 끼어드는 두 층의 안전장치다. 1층(훅)은 진짜로 막고, 2층(워커 권한)은 부탁만 한다. 둘을 헷갈리면 “막힐 거라 믿고” 위험한 자동화를 풀어버리는 사고가 난다 — 그래서 배운다.
그림
flowchart TD
subgraph L1["1층 · 메커니컬 훅 · 진짜로 막는다"]
A[모델이 도구 호출] --> B{"PreToolUse<br/>pre-tool-enforcer"}
B -- deny --> C["실제 차단 + 사유 반환"]
B -- additionalContext --> D["조언만 주입 · 반복 억제"]
B -- 통과 --> E{"도구가 Bash인가?"}
E -- 예 --> F{"PermissionRequest<br/>permission-handler"}
F -- 안전 패턴 --> G[자동 승인]
F -- 그 외 --> H[사용자 승인 팝업]
E -- 아니오 --> I[도구 실행]
G --> I
I --> J{"PostToolUse<br/>post-tool-verifier"}
J --> K["실패 감지 · 경고 주입"]
end
subgraph L2["2층 · advisory 워커 권한 · 부탁만 한다"]
P[WorkerPermissions 규칙] --> Q[권한을 문장으로 변환]
Q --> R[워커 프롬프트에 텍스트로 삽입]
R --> S[워커가 full-auto로 실행]
S --> T[끝난 뒤 파일 비교로 위반 적발]
T -- enforce 모드 --> U["태스크 실패 처리 + 기록<br/>손은 못 막음"]
end
쉽게 풀기
가드레일을 건물 출입 통제로 비유하면 두 층이 선명해진다.
flowchart LR
subgraph 일층["1층 · 보안 게이트"]
direction TB
G1["차단봉 = Node 훅"] --> G2[우겨도 못 지나감]
end
subgraph 이층["2층 · 안내문"]
direction TB
N1[벽에 붙인 행동수칙] --> N2[무시하면 사후 적발만]
end
1층 = 진짜 보안 게이트(메커니컬, 훅). AI가 “파일을 쓰겠다”·“이 명령을 돌리겠다” 할 때마다 차단봉(작은 Node 스크립트)이 먼저 통과 여부를 결정한다.
pre-tool-enforcer — 도구 실행 직전. 규칙 위반(예: 잘못된 모델 라우팅)이면 deny로 실제 차단, 알리고 싶은 정도면 “조언 쪽지”만 끼운다.
permission-handler — 배시 전용 게이트. git status, npm run lint 같은 “안전 패턴”이면 팝업 없이 자동 통과. 단 ;&&| 같은 위험 기호가 하나라도 있으면 즉시 탈락.
post-tool-verifier — 실행 직후 결과를 보고 “실패했어, 고쳐”라고 일러준다.
1층은 진짜 차단봉이라 AI가 우겨도 못 지나간다.
2층 = 사내 행동수칙 안내문(advisory, 워커 권한). 여러 워커를 팀으로 띄울 때 “너는 이 폴더만 만져라” 규칙을 준다. 하지만 이건 차단봉이 아니라 벽에 붙인 안내문이다. 소스 주석에 명시돼 있다 — “MCP 워커는 full-auto라 기계적 제한 불가, 권한은 프롬프트 지시문으로 넣어 LLM이 따르도록 유도할 뿐.”
즉 워커가 다른 폴더를 건드려도 실시간으로 손을 못 잡는다. CLI 워커는 실행 전후 스냅샷을 비교해 끝난 뒤 “규칙 어겼네” 하고 사후 벌점만 줄 수 있다.
한 문장으로
“막는 가드레일은 훅(1층), 부탁하는 가드레일은 워커 권한(2층).” 진짜 차단이 필요하면 반드시 훅 층에서.
핵심 정리
구분
1층 · 훅 (메커니컬)
2층 · 워커 권한 (advisory)
막는 힘
진짜 차단 (deny)
못 막음, 부탁만
작동
도구 호출을 가로챔
프롬프트에 문장 삽입
위반 처리
실행 자체 차단
끝난 뒤 적발·기록
핵심 신호 3가지만 기억하면 된다.
permissionDecision: 'deny' — PreToolUse에서 실제로 막는 신호
additionalContext — 막지 않고 조언만 다음 턴에 주입
decision.behavior: 'allow' — PermissionRequest에서 배시 자동 승인
펼쳐보기: 호출자가 못 덮는 보안 기본 거부 목록
워커 권한이 advisory라도, 아래 경로는 코드가 항상 맨 앞에 prepend해 보호한다.
.git/**, .env*, **/.env*, **/secrets/**, **/.ssh/**, **/node_modules/.cache/**
펼쳐보기: 안전장치 만들 때 반드시 지킬 7가지
fail-open: 어떤 예외든 catch에서 {continue:true} (훅이 세션을 죽이면 안 됨)
차단은 permissionDecision:'deny'+사유, 조언은 additionalContext로 분리
이벤트별 훅 그룹 (PreToolUse, PermissionRequest, PostToolUse 등)
matcher
string
도구 필터. "*"=전체, "Bash"=배시만
hooks[].command
string
항상 node run.cjs <hook>.mjs (크로스플랫폼 런처 경유)
hooks[].timeout
number
초 단위 (PreToolUse=3, PermissionRequest=5)
형식 2 — 워커 권한 스키마 (WorkerPermissions)
필드
타입
설명
allowedPaths
string[]
수정 허용 glob. 빈 배열=전체 허용
deniedPaths
string[]
거부 glob (allowed보다 우선). 보안 기본값 항상 prepend
allowedCommands
string[]
허용 명령 접두사. 빈 배열=전체
maxFileSize
number
파일당 최대 바이트. 기본 Infinity
펼쳐보기: 강제 불가의 근거 (파일 최상단 주석 전문)
// src/team/permissions.ts/** * RBAC-compatible advisory permission scoping for workers. * * NOTE: This is an advisory layer only. MCP workers run in full-auto mode * and cannot be mechanically restricted. Permissions are injected into * prompts as instructions for the LLM to follow. */
워커 권한은 결국 워커 프롬프트에 붙는 ‘문장’으로 변환된다. 이 변환 자체가 “기계적 차단이 아니다”의 증거다.
// src/team/permissions.ts — 권한을 문장으로export function formatPermissionInstructions(permissions: WorkerPermissions): string { const lines: string[] = ['PERMISSION CONSTRAINTS:']; if (permissions.allowedPaths.length > 0) lines.push(`- You may ONLY modify files matching: ${permissions.allowedPaths.join(', ')}`); if (permissions.deniedPaths.length > 0) lines.push(`- You must NOT modify files matching: ${permissions.deniedPaths.join(', ')}`); // ...allowedCommands / maxFileSize 동일 패턴 return lines.join('\n'); // ← 결국 프롬프트에 붙는 '문장'일 뿐}
펼쳐보기: ReDoS-safe glob matcher 전문 + 보안 기본 거부값
정규식 대신 문자단위 백트래킹으로 ReDoS를 회피한다. 호출자가 못 덮는 SECURE_DENY_DEFAULTS와 함께 쓰인다.
/home/seunghyeong/harness-work/oh-my-claudecode/src/team/mcp-team-bridge.ts — buildEffectivePermissions, 프롬프트 빌드, 사후 snapshot diff → findPermissionViolations → enforce 모드 실패 처리