헤드리스 AI CLI의 샌드박스는 "없음"이라고 거짓말한다
AI CLI를 헤드리스로 쓰면 샌드박스가 파일 접근을 막아도 에러 없이 빈 결과만 돌아와 모델이 "파일이 없다"고 오판하며, 종료 코드는 0으로 찍힙니다.
AI 코딩 CLI를 다른 에이전트의 하위 도구로 붙이는 구성이 늘고 있습니다. 무거운 코드베이스 스캔은 값싼 CLI에 헤드리스(-p, one-shot)로 던지고, 비싼 모델은 그 요약만 받아 실제 수정을 합니다. 컨텍스트 비용을 아끼는 데 효과가 큽니다.
이때 반사적으로 붙이는 플래그가 --sandbox 입니다. 읽기만 시킬 거니까 툴 권한을 조여두자는, 지극히 상식적인 판단입니다.
그 상식이 정확히 반대로 작동했습니다.
종료 코드 0, 완전히 틀린 답
샌드박스가 파일 접근을 막으면 CLI 안의 모델은 "접근이 거부됐다"는 신호를 받지 못합니다. 그저 결과가 비어 있을 뿐입니다. 그리고 모델은 빈 결과를 이렇게 해석합니다.
client/디렉터리가 존재하지 않거나, 해당 디렉터리 아래에 파일명에Preview가 포함된 파일이 존재하지 않습니다.
실제로는 그 경로에 6개 파일이 멀쩡히 있었습니다. 크래시도, 에러 코드도, 경고도 없습니다. 종료 코드는 0입니다.
거짓 음성이 거짓 양성보다 훨씬 늦게 발각되기 때문에 이게 위험합니다. 없는 파일을 있다고 하면 다음 단계에서 바로 깨집니다. 반대로 "없습니다"는 그럴듯하게 흘러가고, 그 위에 "그럼 새로 만들자"는 결정이 쌓입니다. 이미 있는 구현을 중복으로 짜고, 커버리지 스윕은 취약점을 놓친 채 통과합니다.
헤드리스는 툴 승인을 통째로 거부한다
시점 주의 (2026-09 갱신). 아래는 2026년 7월 기준 관찰이고, 그 사이 CLI 쪽이 고쳐졌습니다. 승인이 필요한 툴을 soft-deny 할 때 필요한 규칙 이름을 stderr 로 알려주게 됐고(1.1.3), 헤드리스 경로에서도 설정 파일의 권한 규칙을 따르게 됐습니다(1.1.5). 지금 최신 버전에서는 아래 증상이 그대로 재현되지 않습니다. 그래도 남겨 두는 건 실패가 조용해지는 구조 자체는 반복되기 때문입니다.
같은 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. 샌드박스를 켜기 전에 "차단이 모델에게 전달되는가"를 확인합니다. 차단이 빈 결과로 번역되는 도구라면 조여진 권한은 안전장치가 아니라 오답 생성기입니다. 반대로 차단 사실을 결과에 실어 돌려주는 도구(예: Claude Code 는 막힌 경로·호스트를 명시해 돌려줍니다)라면 켜는 쪽이 맞습니다. "샌드박스를 쓰지 마라"가 아니라 "확인하고 쓰라" 입니다 — 강제력을 OS 경계에서 프롬프트로 내리는 건 규약 3 이 경계하는 바로 그 실수입니다.
2. "못 찾았다"는 결론이 아니라 재확인 신호입니다. 부재를 근거로 무언가를 결정하기 전에 결정론적 도구(rg, find)로 교차 검증합니다. LLM의 부재 판정은 원래 신뢰도가 낮은데, 샌드박스는 그걸 체계적으로 더 낮춥니다.
3. 모드 플래그가 실제로 적용됐는지는 출력만 봐선 모릅니다. 계획 모드를 켰는데도 파일이 만들어졌습니다. 나중에 밝혀진 원인은 계획 모드가 무른 게 아니라 헤드리스에서 --mode 가 적용조차 되지 않던 버그였습니다(1.1.12 에서 수정). 교훈은 그래도 남습니다 — 쓰기 금지를 모드에 맡기지 말고 실행 뒤 git status 로 확인합니다. 진짜 방어선은 프롬프트의 명시적 금지와 실행 후 확인입니다.
# 스캔 결과는 파일로 떨구고, 레포 오염 여부를 매번 확인한다
some-ai-cli -p "코드를 수정하지 마라. <질문>" --print-timeout 5m > /tmp/brief.md
git status --porcelain실패가 실패처럼 보이는가
에이전트를 도구로 감쌀 때 검증해야 할 것은 "성공했을 때 잘 도는가"가 아닙니다. 실패가 실패처럼 보이는가입니다.
사람이 쓰는 CLI는 권한 거부를 붉은 에러로 보여줍니다. 그 CLI를 모델 뒤에 두는 순간, 같은 거부가 자연어 문장 한 줄로 번역되면서 에러라는 사실 자체가 사라집니다. 종료 코드 0, 그럴듯한 문장, 완전히 틀린 결론.
새 도구를 파이프라인에 넣기 전에 일부러 실패시켜 보는 스모크 테스트를 한 번 돌릴 가치가 여기에 있습니다. 있는 걸 찾게 해보고, 없는 걸 찾게 해보고, 두 응답이 구별되는지 봅니다. 구별되지 않으면 그 조합은 쓰면 안 됩니다.