본문으로 건너뛰기
wooncloud

헤드리스 AI CLI의 샌드박스는 "없음"이라고 거짓말한다

AI 코딩 CLI를 헤드리스 모드로 사용할 때 샌드박스나 권한 제한이 에러를 빈 결과로 오인하게 만들어 모델이 잘못된 판단을 내리는 위험을 설명하며, 실패가 실패로 인식되도록 스모크 테스트를 통해 도구의 응답을 반드시 검증해야 함을 강조함

·6 min read· views·

닫힌 문 아래로 새어나오는 빛

AI 코딩 CLI를 다른 에이전트의 하위 도구로 붙이는 구성이 늘고 있다. 무거운 코드베이스 스캔은 값싼 CLI에 헤드리스(-p, one-shot)로 던지고, 비싼 모델은 그 요약만 받아 실제 수정을 한다. 컨텍스트 비용을 아끼는 데 효과가 크다.

이때 반사적으로 붙이는 플래그가 --sandbox 다. 읽기만 시킬 거니까 툴 권한을 조여두자는, 지극히 상식적인 판단이다.

그 상식이 정확히 반대로 작동했다.

종료 코드 0, 완전히 틀린 답

샌드박스가 파일 접근을 막으면 CLI 안의 모델은 "접근이 거부됐다"는 신호를 받지 못한다. 그저 결과가 비어 있을 뿐이다. 그리고 모델은 빈 결과를 이렇게 해석한다.

client/ 디렉터리가 존재하지 않거나, 해당 디렉터리 아래에 파일명에 Preview가 포함된 파일이 존재하지 않습니다.

실제로는 그 경로에 6개 파일이 멀쩡히 있었다. 크래시도, 에러 코드도, 경고도 없다. 종료 코드는 0이다.

거짓 음성이 거짓 양성보다 훨씬 늦게 발각되기 때문에 이게 위험하다. 없는 파일을 있다고 하면 다음 단계에서 바로 깨진다. 반대로 "없습니다"는 그럴듯하게 흘러가고, 그 위에 "그럼 새로 만들자"는 결정이 쌓인다. 이미 있는 구현을 중복으로 짜고, 커버리지 스윕은 취약점을 놓친 채 통과한다.

헤드리스는 툴 승인을 통째로 거부한다

같은 CLI에서 두 번째 함정도 나왔다. 헤드리스 모드는 툴 승인 프롬프트를 띄울 수 없으니, 모든 툴 호출을 조용히 거부한다. 설정 파일에 허용 규칙을 아무리 추가해도 그 경로에서는 무시된다.

증상은 빈 출력 한 줄이 전부였다. 로그를 열어보고서야 원인이 보였다.

tool_confirmation_manager.go:183] Print mode: soft-denying tool confirmation "Bash" at step 8
server.go:1886] Tool confirmation ... step 8 (type=Step_RunCommand approved=false)

인터랙티브로 같은 명령을 돌리면 잘 된다. 사람이 승인을 눌러주기 때문이다. 헤드리스로 넘어가는 순간에만 조용히 죽는 종류의 실패다.

세 가지 규약

1. 읽기 전용 목적이라도 샌드박스를 켜지 않는다. 접근 차단을 부재로 오역하는 모델에게 조여진 권한은 안전장치가 아니라 오답 생성기다. 권한을 줄이는 대신 프롬프트로 범위를 제한하고, 결과를 검증한다.

2. "못 찾았다"는 결론이 아니라 재확인 신호다. 부재를 근거로 무언가를 결정하기 전에 결정론적 도구(rg, find)로 교차 검증한다. LLM의 부재 판정은 원래 신뢰도가 낮은데, 샌드박스는 그걸 체계적으로 더 낮춘다.

3. "계획 모드"를 쓰기 방지책으로 믿지 않는다. 플래그 이름이 편집을 안 할 것처럼 생겼어도, 실제로 파일 생성을 지시하니 그대로 만들었다. 의도를 유도할 뿐 강제하지 않는다. 진짜 방어선은 프롬프트의 명시적 금지와 실행 후 확인이다.

# 스캔 결과는 파일로 떨구고, 레포 오염 여부를 매번 확인한다
some-ai-cli -p "코드를 수정하지 마라. <질문>" --print-timeout 5m > /tmp/brief.md
git status --porcelain

실패가 실패처럼 보이는가

에이전트를 도구로 감쌀 때 검증해야 할 것은 "성공했을 때 잘 도는가"가 아니다. 실패가 실패처럼 보이는가다.

사람이 쓰는 CLI는 권한 거부를 붉은 에러로 보여준다. 그 CLI를 모델 뒤에 두는 순간, 같은 거부가 자연어 문장 한 줄로 번역되면서 에러라는 사실 자체가 사라진다. 종료 코드 0, 그럴듯한 문장, 완전히 틀린 결론.

새 도구를 파이프라인에 넣기 전에 일부러 실패시켜 보는 스모크 테스트를 한 번 돌릴 가치가 여기에 있다. 있는 걸 찾게 해보고, 없는 걸 찾게 해보고, 두 응답이 구별되는지 본다. 구별되지 않으면 그 조합은 쓰면 안 된다.