AI 개발 · 실무
Claude·Gemini·Codex 실전 백서 - 터미널 AI 코딩 도구를 일에 쓰는 법
권한과 승인 - 읽기 전용부터 자동 실행까지의 경계
Claude Code 의 --permission-mode·허용/차단 도구 목록, Codex 의 --sandbox 와 --ask-for-approval, 위험도별 명령 분류(읽기·편집·네트워크·삭제·배포), 모두 자동 승인의 위험, 완성 코드는 허용·차단 규칙 목록과 명령 문자열을 받아 허용·질문·거부를 판정하고 판정 이유를 출력하는 권한 규칙 판정기
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 지침 파일에 팀의 작업 방식과 검증 기준을 적었다. 그러나 “배포 전에 확인하라”라는 문장은 실행 권한을 제한하는 장치가 아니다. 도구가 문장을 잘 따르더라도 잘못 해석한 경로나 명령까지 막아 주지는 않는다. 팀에서 재현 가능한 작업 흐름을 만들려면 원하는 행동을 설명하는 지침과 실행 가능한 범위를 정하는 권한을 함께 설계해야 한다.
이 장에서는 작은 Python 서비스를 관리하는 팀을 예로 들어 읽기, 편집, 네트워크 접근, 삭제, 배포의 경계를 정한다. 제품 기능과 옵션에 관한 설명은 모두 2026년 10월 기준 예시다. 실제 적용 전에는 설치된 버전과 각 도구의 공식 문서를 확인한다. 완성 프로그램은 AI 도구나 셸을 실행하지 않고, 규칙과 명령 문자열을 받아 허용·질문·거부 중 하나와 그 이유를 출력한다.
- 샌드박스(sandbox)와 승인 정책이 서로 다른 경계를 다룬다는 점을 이해한다.
- 명령의 이름뿐 아니라 대상 경로와 외부 효과를 보고 위험을 분류한다.
- 허용·차단 규칙이 겹칠 때 차단을 우선하고, 모르는 명령은 질문으로 보낸다.
- 자동 승인 범위를 좁히고 실행 결과와 변경 차이로 판단을 검증한다.
문제 상황
세 명이 함께 관리하는 작은 서비스 저장소가 있다. 한 사람은 Claude Code로 테스트 수정을 맡기고, 다른 사람은 Gemini 계열 도구로 문서를 정리하며, 나머지 한 사람은 Codex CLI로 변경 사항을 검토한다. 어느 날 “실패하는 테스트를 고치고 작업 폴더를 정리하라”라는 요청을 보냈다. 여기서 정리는 임시 파일 제거일 수도 있고, 생성 파일 디렉터리를 통째로 지우는 일일 수도 있다.
문제는 요청문이 짧다는 데 있지 않다. 테스트 실행, 패키지 내려받기, 파일 삭제가 하나의 작업으로 묶였는데 각 행동의 권한이 구분되지 않았다는 데 있다. 테스트가 성공하더라도 원하지 않은 파일이 삭제되었다면 작업 전체를 성공으로 볼 수 없다. 저장소 밖의 설정 파일을 읽었다면 코드 변경 차이만으로는 그 사실을 알아내기 어렵다.
팀은 처음에 승인 창이 자주 뜬다는 이유로 모두 자동 승인하는 방법을 검토했다. 하지만 승인 횟수 감소는 안전성의 증거가 아니다. 넓은 권한을 주면 반복 확인은 줄어들지만 잘못된 명령도 같은 속도로 실행된다. 팀은 대신 자주 수행하는 읽기 작업만 좁게 허용하고, 편집과 테스트 실행은 별도로 판단하며, 삭제와 배포는 기본 차단하는 규칙을 만들기로 했다.
팀 요청문: 먼저 변경할 파일과 검증 방법을 설명하라. 저장소 안에서 필요한 수정만 수행하라. 외부 다운로드, 파일 삭제, 배포가 필요하면 대상과 이유를 제시하라. 작업 뒤에는 테스트 결과와 변경 차이를 함께 보고하라.
이 요청문은 협업의 기준이다. 실행을 막는 경계는 아래에서 정할 권한 설정과 규칙이 맡는다. 두 장치를 함께 사용해야 요청문을 잘못 해석한 경우에도 피해 범위를 줄일 수 있다.
실행 범위와 승인 여부를 나누어 생각한다
권한을 설계할 때는 두 질문을 분리한다. 첫째는 “이 실행 환경에서 무엇에 접근할 수 있는가”다. 읽을 수 있는 경로, 쓸 수 있는 경로, 네트워크 접근 가능 여부가 여기에 속한다. 둘째는 “가능한 행동 중 무엇을 사람에게 물어볼 것인가”다. 승인 정책은 두 번째 질문을 다룬다.
읽기 전용 환경에서도 민감한 파일을 읽는 문제는 남을 수 있다. 반대로 쓰기가 가능한 환경이라도 작업마다 확인을 받도록 운영할 수 있다. 승인 창이 없다는 사실만으로 쓰기가 허용되었다고 해석해서도 안 된다. 실행 제한에 걸리는 명령은 승인을 묻지 못한 채 실패할 수 있다. 따라서 팀 설정에는 실행 범위와 승인 방식을 각각 기록한다.
| 도구 | 확인된 제어 수단 | 팀에서 확인할 점 |
|---|---|---|
| Claude Code | --permission-mode, --allowedTools, --disallowedTools | 모드와 도구 허용·차단 목록을 함께 확인한다. |
| Gemini 계열의 Antigravity CLI | --mode, --sandbox, --dangerously-skip-permissions | 모드와 터미널 제한을 구분하고 전체 자동 승인을 피한다. |
| Gemini CLI | 이 장에서는 권한 옵션을 제시하지 않는다. | 설치된 도구의 공식 문서에서 권한 제어 방법을 확인한다. |
| Codex CLI | --sandbox, --ask-for-approval | 접근 범위와 승인 정책을 별도로 선택한다. |
2026년 10월 기준 예시로 Claude Code의 --permission-mode에는 acceptEdits, auto, bypassPermissions, manual, dontAsk, plan이 있다. 이름이 비슷해 보여도 같은 정책으로 취급해서는 안 된다. 특히 auto, dontAsk, bypassPermissions를 모두 “질문 없이 안전하게 처리”라는 하나의 의미로 묶으면 안 된다. 각 모드의 적용 조건과 예외는 설치된 버전의 공식 문서에서 확인한다.
2026년 10월 기준 예시로 Claude Code의 --allowedTools와 --disallowedTools는 도구 허용·차단에 사용한다. 목록의 패턴 문법을 일반 셸 문법이라고 가정하면 안 된다. 아래 Python 프로그램의 규칙 문법 역시 이 제품의 문법을 복제한 것이 아니다. 실제 목록을 작성할 때는 공식 문법을 확인하고, 허용 사례와 거부 사례를 각각 시험한다.
2026년 10월 기준 예시: claude --permission-mode manual
2026년 10월 기준 예시로 Codex CLI의 --sandbox 값에는 read-only, workspace-write, danger-full-access가 있다. --ask-for-approval에는 on-request, never 등의 값이 있다. never는 확인 요청을 하지 않는 정책이지 실행 환경의 제한을 풀어 주는 값이 아니다. 팀은 두 옵션을 따로 검토해야 한다.
2026년 10월 기준 예시: codex --sandbox read-only --ask-for-approval on-request
2026년 10월 기준 예시로 Antigravity CLI의 --mode에는 accept-edits, auto, plan이 있으며 --sandbox는 터미널 제한에 사용한다. --dangerously-skip-permissions는 모든 권한을 자동 승인하는 옵션이므로 편의를 위한 기본값으로 삼지 않는다. Gemini CLI의 권한 설정을 이 옵션으로 대신 설명해서도 안 된다. 실제 실행 중인 도구를 먼저 확인한다.
이 표는 기능의 우열을 비교하지 않는다. 같은 팀 규칙을 서로 다른 제어 수단에 옮길 때 확인할 위치를 보여 준다. 사실 확인의 출발점은 Claude Code 권한 문서, Codex 공식 문서, Gemini CLI 공식 저장소다.
명령 이름보다 대상과 효과를 먼저 본다
위험도는 명령 이름 하나로 결정되지 않는다. 파일을 읽는 명령도 비밀값을 출력할 수 있고, 테스트 명령도 프로젝트 코드를 실행하므로 파일 생성이나 외부 통신을 일으킬 수 있다. “테스트니까 읽기 작업”이라는 분류는 실제 효과를 놓친다. 읽기는 외부로 내용을 보내지 않는지까지, 편집은 어느 경로를 바꾸는지까지 살펴야 한다.
| 분류 | 명령 예시 | 초기 판단 | 확인할 내용 |
|---|---|---|---|
| 읽기 | git diff, git status | 좁은 형태만 허용 | 출력 내용과 추가 인자 |
| 편집·코드 실행 | 파일 수정, python3 main.py | 질문 | 변경 경로와 실행 코드 |
| 네트워크 | curl, git fetch | 질문 | 접속 대상과 보내는 데이터 |
| 삭제 | rm | 기본 거부 | 대상 경로와 복구 수단 |
| 배포·외부 반영 | git push, 배포 스크립트 | 기본 거부 | 원격 대상과 반영 범위 |
이 표의 초기 판단은 보편적인 제품 기본값이 아니라 이 장의 팀이 정한 운영 정책이다. 팀에 따라 테스트 전용 환경에서 특정 검증 명령을 허용할 수 있다. 다만 실행 파일, 인자, 작업 경로가 바뀌었다면 이전 승인을 그대로 재사용할 근거가 있는지 다시 살핀다.
허용 규칙은 짧을수록 편하지만 적용 범위는 넓어진다. git 전체를 허용하면 조회 명령뿐 아니라 원격 반영과 작업 상태를 바꾸는 명령까지 포함할 수 있다. 반면 git status라는 정확한 명령만 허용하면 검토할 범위가 작아진다. 일상적으로 필요한 명령부터 좁게 추가하는 편이 규칙의 의미를 설명하기 쉽다.
또 하나의 문제는 여러 행동을 한 문자열에 넣는 경우다. 조회 뒤에 삭제를 연결하거나, 명령 치환으로 다른 프로그램을 실행하면 앞부분만 검사하는 규칙을 빠져나갈 수 있다. 완성 프로그램은 셸 문법을 해석하려 하지 않고 이런 문자열을 거부한다. 실제 셸을 안전하게 흉내 내는 일보다 지원 범위를 제한하는 일이 이 예제의 목적에 맞는다.
거부를 먼저 확인하고 모르는 입력은 질문한다
판정 순서는 고정한다. 먼저 지원하지 않는 문법을 걸러 내고, 다음으로 차단 규칙을 검사한다. 차단되지 않은 입력에만 허용 규칙을 적용한다. 어느 규칙에도 맞지 않으면 질문으로 보낸다. 규칙을 찾지 못했다는 이유로 실행을 허용하면 새 명령이 추가될 때마다 권한이 조용히 넓어진다.
예제 규칙은 두 종류다. exact는 인자까지 같은 명령에만 일치한다. prefix는 앞쪽 인자들이 같으면 일치한다. 여기서 접두 일치는 문자열의 시작 부분 비교가 아니라 토큰 목록 비교다. rm 규칙은 rm으로 시작하는 실행 요청을 잡지만 rmdir라는 다른 명령까지 같은 것으로 취급하지 않는다.
명령 문자열은 Python 표준 라이브러리의 shlex.split으로 나눈다. 따옴표 안의 공백은 하나의 인자로 유지된다. 그러나 이 함수가 명령의 부작용을 분석하는 것은 아니다. 프로그램은 보수적으로 일부 문자를 입력 전체에서 거부하고, 경로·리다이렉션·명령 치환을 포함한 일반 셸 실행은 지원하지 않는다.
규칙을 의도적으로 충돌시키는 사례도 넣는다. rm을 허용 목록과 차단 목록 양쪽에 등록해도 차단이 이기는지 확인하기 위해서다. 실제 팀 설정에서는 불필요한 충돌을 제거하되, 판정기의 우선순위가 흔들리지 않는지는 이런 사례로 검사한다.
완성 코드
다음 내용을 main.py로 저장한다. 외부 패키지, 입력 파일, API 키가 필요하지 않다. 명령 문자열은 검사 대상 데이터이며 실제로 실행되지 않는다. 규칙 목록과 사례 목록이 프로그램의 입력이다. 실행할 때마다 같은 자체 검사와 같은 출력으로 끝난다.
import shlex
from dataclasses import dataclass
@dataclass(frozen=True)
class Rule:
text: str
mode: str
reason: str
def __post_init__(self) -> None:
if self.mode not in {"exact", "prefix"}:
raise ValueError("지원하지 않는 규칙 모드")
if not shlex.split(self.text):
raise ValueError("빈 규칙")
def matches(self, tokens: tuple[str, ...]) -> bool:
expected = tuple(shlex.split(self.text))
if self.mode == "exact":
return tokens == expected
return tokens[:len(expected)] == expected
@dataclass(frozen=True)
class Decision:
action: str
reason: str
UNSUPPORTED = frozenset(";&|<>`$\\\n\r*?[]{}()~#")
def decide(
command: str,
allowed: tuple[Rule, ...],
blocked: tuple[Rule, ...],
) -> Decision:
if not command.strip():
return Decision("거부", "빈 명령")
if any(char in UNSUPPORTED for char in command):
return Decision("거부", "지원하지 않는 셸 문법")
try:
tokens = tuple(shlex.split(command))
except ValueError:
return Decision("거부", "따옴표 구문 오류")
if not tokens or not tokens[0]:
return Decision("거부", "실행 파일 이름 없음")
for rule in blocked:
if rule.matches(tokens):
return Decision("거부", f"차단 규칙: {rule.reason}")
for rule in allowed:
if rule.matches(tokens):
return Decision("허용", f"허용 규칙: {rule.reason}")
return Decision("질문", "일치하는 규칙 없음")
def main() -> None:
allowed = (
Rule("git status", "exact", "작업 상태 조회"),
Rule("git diff", "exact", "변경 차이 조회"),
Rule("rm", "prefix", "충돌 검사용 허용"),
)
blocked = (
Rule("rm", "prefix", "파일 삭제"),
Rule("git push", "prefix", "원격 반영"),
)
cases = (
("git status", "허용", "허용 규칙: 작업 상태 조회"),
("git diff", "허용", "허용 규칙: 변경 차이 조회"),
("git status --short", "질문", "일치하는 규칙 없음"),
("python3 main.py", "질문", "일치하는 규칙 없음"),
("curl https://example.invalid", "질문", "일치하는 규칙 없음"),
("rm -rf build", "거부", "차단 규칙: 파일 삭제"),
("git push origin main", "거부", "차단 규칙: 원격 반영"),
("git diff; rm -rf build", "거부", "지원하지 않는 셸 문법"),
('git diff "unfinished', "거부", "따옴표 구문 오류"),
("", "거부", "빈 명령"),
("rmdir build", "질문", "일치하는 규칙 없음"),
("git diff README.md", "질문", "일치하는 규칙 없음"),
)
results = []
for command, expected_action, expected_reason in cases:
result = decide(command, allowed, blocked)
expected = Decision(expected_action, expected_reason)
if result != expected:
raise AssertionError(
f"{command!r}: 기대={expected!r}, 실제={result!r}"
)
results.append((command, result))
token_rule = Rule("cat 'team notes.txt'", "exact", "토큰 검사")
if not token_rule.matches(("cat", "team notes.txt")):
raise AssertionError("따옴표 토큰 검사 실패")
print("자체 검사: 13개 통과")
for command, result in results:
label = command if command else "(빈 명령)"
print(f"{result.action} | {label} | {result.reason}")
if __name__ == "__main__":
main()
줄별 해설
처음 두 import 문은 문자열 분리와 데이터 표현을 준비한다. shlex는 명령을 실행하지 않고 문자열을 토큰으로 나눈다. dataclass는 규칙과 판정 결과의 필드를 명시한다. 두 자료형에 frozen=True를 지정했으므로 생성한 뒤 필드를 실수로 바꾸기 어렵다.
Rule의 text는 비교할 명령, mode는 일치 방식, reason은 사람이 읽을 근거다. __post_init__은 규칙을 만드는 순간 모드와 빈 입력을 검사한다. 잘못된 규칙을 “일치하지 않는 규칙”으로 조용히 넘기지 않고 설정 오류로 드러내는 것이다. 규칙 안의 따옴표가 닫히지 않아도 생성 과정에서 오류가 발생한다.
matches에서는 규칙 문자열을 expected라는 튜플로 바꾼다. exact는 전체 튜플이 같아야 참이다. prefix는 규칙 길이만큼 앞부분을 잘라 비교한다. 문자열 startswith를 사용하지 않으므로 실행 파일 이름의 일부만 같다는 이유로 일치하지 않는다. 다만 prefix가 일치한 뒤의 인자는 검사하지 않는다는 점은 그대로 남는다.
Decision은 action과 reason을 묶는다. 허용 여부만 반환하면 나중에 왜 그런 판단을 했는지 알기 어렵다. 이유를 함께 반환하면 규칙 검토와 출력 검증이 쉬워진다. 이 예제에서는 결과 문장까지 예상값과 비교하므로 판정은 같지만 잘못된 이유를 붙이는 회귀도 발견할 수 있다.
UNSUPPORTED에는 연결 연산자, 리다이렉션, 치환 및 확장에 쓰이는 문자 등을 넣었다. 이 검사는 따옴표 안의 문자도 거부한다. 예를 들어 파일 이름에 별표가 들어 있어도 지원하지 않는다. 일반 셸보다 처리 범위가 좁다는 뜻이며, 문자열을 완전히 분석한 것처럼 보이게 하지 않기 위한 선택이다.
decide의 앞부분은 빈 문자열, 지원하지 않는 문자, 따옴표 오류, 빈 실행 파일 이름을 차례로 처리한다. 모두 거부하는 이유는 사용자의 승인을 받아도 프로그램이 그 입력을 정확히 해석할 수 없기 때문이다. 여기서 질문은 “해석은 가능하지만 정책 결정이 필요하다”는 결과이고, 구문 거부는 “이 판정기가 처리하지 않는다”는 결과다.
두 for 문은 정책의 핵심이다. blocked를 먼저 순회하고 일치하면 즉시 반환한다. 따라서 허용 목록에 rm이 있어도 삭제 차단이 우선한다. 같은 목록 안에서 여러 규칙이 맞으면 앞선 규칙의 이유가 출력된다. 운영 규칙에서는 목록 순서도 검토 대상이 된다.
마지막 반환문은 기본 질문 정책이다. python3 main.py와 curl 요청은 차단 규칙에 없지만 허용 규칙에도 없으므로 질문이 된다. rmdir도 마찬가지다. 이것은 삭제가 허용되었다는 뜻이 아니다. 자동 실행 권한을 주지 않은 상태로 사람이 대상과 효과를 확인하도록 남긴 것이다.
main의 cases에는 정상 조회, 추가 인자, 코드 실행, 네트워크, 삭제, 원격 반영, 명령 연결, 따옴표 오류, 빈 입력을 넣었다. 각 사례는 판정과 이유를 직접 적은 예상값을 가진다. 테스트가 규칙을 그대로 다시 계산하면 같은 오류를 공유할 수 있으므로 예상값을 별도로 작성했다.
results는 검사한 결과를 저장한다. 모든 자체 검사가 끝난 뒤 출력하므로 중간 실패를 정상 실행 결과와 혼동하지 않는다. 추가 토큰 검사는 따옴표로 감싼 파일 이름이 하나의 인자로 비교되는지 확인한다. 마지막 조건문은 main.py를 직접 실행했을 때만 main을 부른다.
실행 결과
터미널에서 다음 명령을 실행한다. 프로그램은 검사 대상에 포함된 삭제나 네트워크 명령을 실행하지 않고 즉시 종료한다.
python3 main.py
자체 검사: 13개 통과
허용 | git status | 허용 규칙: 작업 상태 조회
허용 | git diff | 허용 규칙: 변경 차이 조회
질문 | git status --short | 일치하는 규칙 없음
질문 | python3 main.py | 일치하는 규칙 없음
질문 | curl https://example.invalid | 일치하는 규칙 없음
거부 | rm -rf build | 차단 규칙: 파일 삭제
거부 | git push origin main | 차단 규칙: 원격 반영
거부 | git diff; rm -rf build | 지원하지 않는 셸 문법
거부 | git diff "unfinished | 따옴표 구문 오류
거부 | (빈 명령) | 빈 명령
질문 | rmdir build | 일치하는 규칙 없음
질문 | git diff README.md | 일치하는 규칙 없음
git status --short가 질문으로 나오는 것은 의도한 결과다. exact로 등록한 명령에 인자를 더하면 별도 판단을 받는다. 이 명령을 팀에서 반복 사용한다면 검토 후 정확한 형태를 추가할 수 있다. 편의를 위해 곧바로 git 전체를 접두 허용하는 방식으로 확장하지 않는다.
실행이 끝났다고 검증이 끝난 것은 아니다. 자체 검사는 미리 적은 사례만 확인한다. 파일을 추가하거나 수정한 뒤에는 git diff로 규칙, 예상값, 출력 문구를 함께 검토한다. 팀 저장소의 검증 절차에 맞춰 경고를 오류로 취급하는 컴파일 검사도 수행한다.
python3 -W error -m py_compile main.py
git diff -- main.py
이 장의 판정기는 실행 경계의 개념을 확인하는 모형이다. 실행 파일의 실제 위치, 심볼릭 링크, 환경 변수, 저장소 설정, 프로그램 내부 부작용은 분석하지 않는다. 따라서 이 코드를 실제 명령 실행기의 보안 경계로 연결하지 않는다. 현실의 실행 통제에는 도구가 제공하는 제한과 운영체제 수준의 격리가 함께 필요하다.
실무에서 자주 틀리는 것
허용을 먼저 반환한다
허용 목록부터 검사하면 넓은 허용 규칙이 좁은 차단 규칙을 가릴 수 있다. 다음은 잘못된 우선순위를 보여 주는 독립 실행 예다. 같은 명령이 두 목록에 있어도 허용한다.
def judge(command):
allowed = {"rm"}
blocked = {"rm"}
if command in allowed:
return "허용"
if command in blocked:
return "거부"
return "질문"
assert judge("rm") == "허용"
고친 코드는 차단부터 검사한다. 목록을 수정하는 사람과 순서를 수정하는 사람이 달라도 이 원칙이 유지되는지 회귀 사례로 확인한다.
def judge(command):
allowed = {"rm"}
blocked = {"rm"}
if command in blocked:
return "거부"
if command in allowed:
return "허용"
return "질문"
assert judge("rm") == "거부"
문자열의 앞부분만 보고 같은 명령으로 취급한다
문자열 비교는 실행 파일 이름과 인자 경계를 구분하지 않는다. 다음 잘못된 코드는 git status라는 글자로 시작하는 다른 문자열도 허용한다.
def matches(command):
return command.startswith("git status")
assert matches("git status-helper")
고친 코드는 토큰 전체를 비교한다. 이 코드는 일치 방식만 설명하며, 완성 코드의 구문 검사까지 대신하지 않는다.
import shlex
def matches(command):
return tuple(shlex.split(command)) == ("git", "status")
assert matches("git status")
assert not matches("git status-helper")
assert not matches("git status --short")
미등록 명령을 편의상 허용한다
새 명령이 들어올 때 질문이 생기는 것을 오류로 취급하면 기본값이 넓어진다. 다음 잘못된 코드는 차단 목록에 없는 모든 문자열을 허용한다. 차단 목록에 모든 위험한 명령을 열거할 수 있다는 가정을 깔고 있다.
def judge(command):
blocked = {"rm"}
return "거부" if command in blocked else "허용"
assert judge("unknown-task") == "허용"
고친 코드는 허용 근거가 없으면 질문한다. 실제 판정기는 여기에 구문 검사와 인자 비교를 더한다.
def judge(command):
blocked = {"rm"}
allowed = {"git status"}
if command in blocked:
return "거부"
if command in allowed:
return "허용"
return "질문"
assert judge("unknown-task") == "질문"
자동 승인을 검증 생략으로 연결한다
승인을 덜 묻도록 설정해도 실행 결과와 변경 차이 검토는 줄어들지 않는다. 다음 잘못된 코드는 승인 여부만으로 작업 완료를 판정한다. 명령이 허용되었다는 사실과 원하는 결과를 얻었다는 사실을 섞은 것이다.
def completed(approved, tests_passed, diff_reviewed):
return approved
assert completed(True, False, False)
고친 코드는 승인과 결과 검증을 모두 요구한다. 이 예에서는 테스트 통과와 변경 차이 검토를 완료 조건으로 정했다. 실제 작업에서는 산출물에 맞는 검증 항목을 사용한다.
def completed(approved, tests_passed, diff_reviewed):
return approved and tests_passed and diff_reviewed
assert not completed(True, False, False)
assert completed(True, True, True)
2026년 10월 기준 예시로 Claude Code의 bypassPermissions, Antigravity CLI의 --dangerously-skip-permissions, Codex CLI의 danger-full-access와 never는 같은 기능 이름이나 동일한 보안 경계를 뜻하지 않는다. 다만 접근 범위와 사람의 개입을 함께 넓히거나 줄일 때 검토 부담이 커진다는 공통점이 있다. 전체 자동 승인을 선택하기 전에 해당 작업에 필요한 좁은 규칙으로 해결할 수 있는지 확인한다.
한눈에 보기
| 대상 | 판단 원칙 | 검증 방법 |
|---|---|---|
| 실행 환경 | 읽기·쓰기·네트워크 범위를 구분한다. | 허용 경로와 차단 경로를 각각 확인한다. |
| 승인 정책 | 실행 가능 여부와 질문 여부를 구분한다. | 미등록 명령의 처리 결과를 확인한다. |
| 허용 목록 | 검토한 명령과 인자만 좁게 등록한다. | 추가 인자가 붙은 사례를 검사한다. |
| 차단 목록 | 허용보다 먼저 적용한다. | 양쪽에 일치하는 충돌 사례를 검사한다. |
| 복합 문자열 | 해석할 수 없는 문법을 거부한다. | 연결·치환·따옴표 오류를 검사한다. |
| 완료 판단 | 승인 후에도 결과를 확인한다. | 실행·테스트·변경 차이를 함께 검토한다. |
규칙의 품질은 허용 항목 수로 판단하지 않는다. 팀원이 각 규칙의 대상과 이유를 설명할 수 있고, 경계 사례를 실행해 같은 결과를 얻을 수 있어야 한다. 다음 장에서는 이 경계를 유지한 상태에서 Claude Code의 작업 흐름을 살펴본다.
연습 문제
- git status --short만 추가로 허용하라. 기존 git status와 새 명령은 허용하되 git status --short extra는 질문이 되도록 규칙과 자체 검사 사례를 추가하라.
- 네트워크 작업을 사람이 확인하도록 git fetch를 질문 상태로 유지하려 한다. 허용 목록에 git 접두 규칙을 추가하면 어떤 결과가 생기는지 설명하고 대안을 제시하라.
- 빈 문자열과 공백만 있는 문자열, 빈 실행 파일 이름을 구분하는 검사 사례를 추가하라. 각 입력이 어떤 이유로 거부되어야 하는지 쓰라.
- 팀원이 “허용 결과가 나왔으니 프로그램에 실제 실행 기능을 붙이자”고 제안했다. 이 판정기에서 다루지 않는 위험 세 가지와 실제 운영에서 필요한 조치를 설명하라.
정답과 해설
첫 번째 문제는 exact 규칙을 하나 추가하면 된다. 접두 규칙으로 바꾸지 않고 필요한 명령 형태만 등록한다. cases에도 새 명령과 추가 인자가 붙은 명령을 각각 넣고, 자체 검사 개수 표시를 함께 고친다.
import shlex
allowed = {
("git", "status"),
("git", "status", "--short"),
}
def judge(command):
tokens = tuple(shlex.split(command))
return "허용" if tokens in allowed else "질문"
assert judge("git status") == "허용"
assert judge("git status --short") == "허용"
assert judge("git status --short extra") == "질문"
두 번째 문제에서 git 접두 허용 규칙은 git fetch에도 일치한다. 별도 차단 규칙이 없다면 질문 대신 허용이 된다. 차단된 git push는 여전히 거부되지만, 차단 목록에 없는 다른 git 동작의 권한까지 넓어진다. 대안은 필요한 조회 명령을 exact로 등록하고 git fetch는 미등록 상태로 두는 것이다.
세 번째 문제에서 빈 문자열과 공백만 있는 문자열은 strip 검사에서 “빈 명령”으로 거부된다. 작은따옴표 두 개만 있는 문자열은 공백 제거 후에도 문자가 남지만 토큰으로 나누면 첫 항목이 빈 문자열이므로 “실행 파일 이름 없음”으로 거부된다. 아래 코드는 두 검사 위치의 차이를 확인한다.
import shlex
assert not "".strip()
assert not " ".strip()
assert "''".strip()
assert tuple(shlex.split("''")) == ("",)
네 번째 문제의 예로 실행 파일 바꿔치기, 경로의 심볼릭 링크, 실행 프로그램 내부의 외부 통신을 들 수 있다. 이 판정기는 이름과 인자만 비교하므로 같은 문자열 뒤에서 무엇이 실행되는지 보장하지 않는다. 실제 운영에서는 신뢰할 수 있는 실행 환경을 준비하고, 쓰기 경로와 네트워크 접근을 제한하며, 필요한 작업만 승인한다. 실행 이후에도 테스트 결과와 변경 차이를 확인한다. 허용 판정은 그 검증을 시작할 조건이지 완료 증거가 아니다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.