인간은 세 번 중 한 번 위협을 놓쳤다 — AI 에이전트 승인이라는 착시

AI 에이전트가 명령을 실행하기 전 사람에게 “승인/거부”를 묻는 프롬프트는 안전장치인가, 아니면 사고가 났을 때 책임을 물릴 사람을 세워두는 장치인가? 40,000회의 게임 플레이가 남긴 데이터는 전자를 의심하게 한다.

도입

한 개발자가 브라우저 게임을 하나 만들었다. 플레이어는 AI 코딩 에이전트가 실행하려는 셸 명령을 하나씩 넘겨받아, 제한 시간 안에 “승인” 또는 “거부”를 누른다. 대부분은 git statusnpm test처럼 무해한 명령이지만, 일부는 자격증명을 빼돌리거나 백도어를 심는, 이미 탈취된 에이전트가 내리는 위협 명령이다. 게임은 지난 몇 달간 40,000회 넘게 플레이됐고, 40만 9천 건의 승인/거부 결정이 쌓였다. 제작자 Wirbelwind가 Hacker News에 공유한 통계에서 눈에 띄는 숫자는 하나다. 평균 정확도 66.3% — 플레이어는 화면에 뜬 위협 세 개 중 하나를 승인 버튼으로 통과시켰다. 게임을 시작하기 전에 “위협이 섞여 있다”는 경고를 명시적으로 봤는데도 그랬다. 이 글은 그 숫자가 게임 밖에서 무엇을 뜻하는지, 그리고 “사람이 최종 승인한다(human-in-the-loop)“는 설계가 실제로 안전을 만들어내는지를 따진다.

숫자가 말하는 것: 무엇을 놓쳤나

먼저 실험의 얼개부터 분리해 두자. 이것은 통제된 학술 실험이 아니라 인터넷에 공개된 게임이고, 플레이어는 자기 선택으로 참여한 익명의 표본이다. 그러니 절대 수치를 실무 현장에 그대로 옮길 수는 없다. 그럼에도 40만 건이라는 결정 규모는 개별 노이즈를 눌러 경향을 드러낸다. 제작자에 따르면 화면에 노출된 명령의 약 34%가 위협이었고, 플레이어의 평균 위협 적발 정확도는 66.3%였다. 뒤집으면, 위협 세 개 중 하나는 승인 버튼을 통과했다는 뜻이다.

흥미로운 것은 놓친 위협이 고르게 분포하지 않았다는 점이다. 통계는 위협을 성격별로 나눠 적발 실패율(miss rate)을 집계했다.

  • rm -rf /나 광범위한 chmod처럼 대놓고 파괴적인 명령: 11.7%
  • crontab 등록이나 git 후크 탈취처럼 지속성을 심는 변형: 23.8%
  • 코드 실행을 동반한 유출(exfiltration): 33.4%
  • 자격증명 접근 같은 권한 범위 위반: 35.0%

파괴적인 명령은 열에 하나꼴로 놓쳤지만, 권한 범위를 슬쩍 넘는 명령은 셋에 하나 이상 통과시켰다. 위협이 “위험해 보일수록” 사람은 잘 막았고, “평범해 보일수록” 잘 뚫렸다. 이 비대칭이 이 실험의 진짜 발견이다.

이 경향을 가장 극적으로 보여준 것이 npm 스크립트다. 통계에서 가장 많이 놓친 위협 1위는 npm run analyze로, 적발 실패율이 64.7%였다. npm run setup은 48.0%, npm run deploy는 44.9%였다. 이 세 명령을 묶으면 실패율 52.5%로, 다른 유출 공격의 28.4%보다 거의 두 배 높았다. 제작자의 표현을 빌리면 “익숙한 스크립트 이름 뒤에 페이로드를 숨기면 성공률이 대략 두 배가 된다.” 핵심은 명령 위에 표시되던 실행 이력(history log) — 그 스크립트가 실제로 무슨 일을 하는지 보여주는 로그 — 를 플레이어가 대체로 무시했다는 데 있다. npm run이라는 익숙한 접두사가 그 뒤에 붙은 임의의 셸을 가려버린 것이다. 사람은 명령 문자열의 앞부분만 읽고 신뢰 여부를 결정한다.

반대 방향의 실수도 만만치 않았다. 무해한 명령을 위협으로 오인해 거부한 비율(false rejection)도 높았다. npm config set registry는 59%가 거부됐고, 빌드 산출물을 지우는 rm -rf dist/는 45%, 3000번 포트를 점유한 프로세스를 죽이는 kill $(lsof -t -i:3000)는 43%가 거부됐다. 홈 디렉터리의 셸 설정을 읽는 cat ~/.zshrc는 54.1%가 거부됐다. 즉 사람들은 위협을 놓치는 동시에, 정상 작업의 절반 가까이를 공연히 틀어막았다. 정밀도와 재현율을 함께 잃은 것이다.

전체 그림은 더 어둡다. 세션의 32.9%가 마이너스 점수로 끝났다. 모든 위협을 하나도 빠뜨리지 않고 잡아낸 플레이어는 35.2%였지만, 그러면서도 안전한 명령의 20% 이하만 거부한 — 즉 오탐까지 함께 억제한 — 플레이어는 20.8%에 그쳤다. 그리고 7%는 아무것도 보지 않은 듯 모든 프롬프트를 승인했다. 게다가 적발 실패율은 세션 후반으로 갈수록 다시 올라갔다. 시간이 지나며 집중력이 닳는, 전형적인 경계 피로(vigilance fatigue) 곡선이다.

왜 사람은 클릭하는가: 승인 피로의 구조

이 숫자들이 놀랍지 않다면, 그것은 우리가 같은 실패를 30년째 반복해 왔기 때문이다. “위험할 수 있으니 사용자에게 물어보고 확인 버튼을 받는다”는 보안 모델은 Windows의 UAC 팝업, 브라우저의 인증서 경고, 쿠키 동의 배너, 모바일 앱의 권한 요청에서 이미 수없이 시험됐고, 그때마다 같은 결말을 맞았다. 사람은 흐름을 막는 대화상자를 읽지 않고 치운다. Hacker News 토론에서도 “사용자에게 끊임없이 권한을 묻고 그가 실수하지 않기를 바라는 보안 모델이 또 나왔다, 지금까지 수없이 시도됐지만 한 번도 통하지 않았다”는 지적이 나왔다.

왜 통하지 않는가. 구조적 원인은 몇 겹이다.

첫째, 신호 대 잡음비다. 승인 프롬프트의 압도적 다수는 무해하다. git status를 백 번 승인하는 동안 사람의 뇌는 “승인”을 기본값으로 학습한다. 백한 번째에 낀 위협은 이 자동화된 리듬에 그대로 실려 통과한다. 게임에서 익숙한 npm run 접두사가 유독 잘 뚫린 것은 이 잡음 학습의 교과서적 사례다. 위협의 기저율(base rate)이 낮을수록, 경보 하나하나에 대한 사람의 민감도는 떨어진다.

둘째, 맥락의 결핍이다. 승인을 누르는 사람은 그 명령이 건드릴 파일의 내용도, 그 스크립트가 내부에서 무엇을 호출하는지도 온전히 알지 못한 채 결정한다. npm run analyze 한 줄만 보고 그 뒤의 package.json에 무엇이 적혀 있는지 매번 열어보는 사람은 없다. 판단에 필요한 정보가 프롬프트 안에 없으니, 프롬프트는 판단이 아니라 요식(儀式)을 만든다. 사람에게 결정권을 주면서 결정에 필요한 재료는 주지 않는 셈이다. 이것은 게임 밖의 실제 공급망 공격이 작동하는 방식과 정확히 겹친다. 최근 몇 년간 npm 생태계를 흔든 사고들은 대부분 패키지의 postinstall 훅이나 스크립트 뒤에 페이로드를 숨겼다. 겉면의 명령 이름은 평범하고, 위험은 한 겹 아래에 있다. 게임 속 npm run analyze가 64.7%나 뚫린 것은 단순한 게임 특유의 함정이 아니라, 사람이 명령의 “겉이름”만으로 신뢰를 배분한다는 인지적 사실의 재현이다.

셋째, 피로다. 앞의 데이터가 보여주듯 경계는 시간과 함께 닳는다. 제작자는 HN에서 “피로 때문에 사람들은 오히려 승인 자체를 통째로 우회하는 쪽으로 도망친다”고 지적했다 — 매번 묻는 것이 귀찮으니 “전부 허용”을 켜버리는 것이다. 프롬프트를 늘릴수록 사람은 프롬프트를 끄는 법을 배운다. 안전을 위해 추가한 마찰이, 역설적으로 안전장치를 통째로 제거하도록 사람을 몰아간다.

그렇다면 애초에 이 프롬프트는 무엇을 위해 존재하는가. 토론에서 가장 날카로운 관점은 승인 버튼의 진짜 기능이 사고 방지가 아니라 책임 소재라는 것이었다. 한 참여자는 “human-in-the-loop의 목적은 재앙을 막는 것이 아니라 손해가 났을 때 책임질 사람을 두는 것”이라고 냉소했고, 다른 이들은 이를 “도덕적 크럼플 존(moral crumple zone)” — 자동차의 충격 흡수 구역처럼, 복잡한 자동 시스템이 오작동할 때 그 법적·도덕적 충격을 대신 흡수하도록 배치된 인간 — 이라는 논문 속 표현으로 불렀다. “직원이 그 명령을 승인했잖아, 그러니 우리 책임이 아니야”라는 알리바이 장치라는 것이다. 자율주행차가 사고 직전 자동 운전을 슬그머니 해제하더라는 냉소가 같은 맥락에서 오갔다.

물론 반론도 있었다. 어떤 참여자는 완벽하지 않더라도 나쁜 에이전트와 좋은 에이전트 사이에 약간의 마찰과 사람의 사고를 끼워 넣는 것은 그 자체로 가치가 있다고 했다. 문제는 그 마찰이 실제로 위협을 걸러낼 때만 방어라는 점이다. 걸러내지 못한 채 심리적 안심만 준다면 그것은 방어가 아니라 보안 극장(security theater)이다. 66.3%라는 숫자는 이 프롬프트가 어느 쪽에 가까운지를 묻고 있다.

하니스는 무엇을 해야 하나

이 실험이 코딩 에이전트를 실제로 굴리는 사람에게 주는 교훈은 역설적이다. 안전의 무게중심을 “사람의 승인”에서 “사람이 승인할 필요가 없는 구조”로 옮기라는 것이다. 제작자 본인도 결론을 여기에 뒀다. 넓은 권한을 내주기 전에 샌드박스와 권한 격리를 도구 차원에서 쉽게 만들어야 하며, HITL을 유효한 대안으로 내세워선 안 된다는 것이다.

토론에 등장한 실천안은 대체로 세 갈래였다. 하나는 격리다. 에이전트를 컨테이너나 VM 안에서, 작업 디렉터리만 마운트한 채 돌리면, 무언가 폭주해도 그 폭발 반경(blast radius)을 상자 안에 가둘 수 있다. 한 참여자는 에이전트를 권한 없는 유닉스 사용자로 세우고, 필요한 파일만 chmod로 열어주고, git은 pull만 허용하고 push는 코드 리뷰를 거치게 하고, 네트워크는 방화벽으로 필요한 요청만 통과시킨 뒤 — “그다음에야 그 끔찍한 권한 요청 극장을 끈다”고 했다. 다른 참여자는 Claude Code를 .config·.local 접근을 막도록 설정해 자격증명 유출 경로를 원천 차단한 사례를 들었다.

둘은 자동 분류다. 매 행동을 별도의 분류기(auto mode)나 문맥이 분리된 다른 에이전트가 심사하게 하는 방식이다. 실제로 일부 하니스(예: Codex)는 명령을 검토하는 별도 에이전트를 지원한다. 다만 여기엔 근본적인 반론이 따라붙었다. “가끔 실패하는 시스템이 실패하지 않도록, 역시 가끔 실패하는 시스템에게 검사시키는” 구조라는 것이다. 별도 모델의 심사는 유출 확률을 낮출 뿐 0으로 만들지 못하고, 사람이 놓친 것을 기계가 반드시 잡는다는 보장도 없다.

셋은 언어·능력(capability) 차원의 통제다. 코드가 접근할 수 있는 자원을 실행 전에 능력으로 못 박아, 파일시스템 접근 자체를 원천적으로 봉쇄하자는 오래된 아이디어(object-capability)가 다시 소환됐다. 다만 한 참여자가 지적했듯, 에이전트에게 웹 접근을 허용하는 순간 어떤 요청이든 사이드 채널로 정보를 흘릴 수 있어, “유용하면서 동시에 안전한” 지점은 생각보다 좁다. 유출은 LLM의 버그가 아니라 본질에 가깝다는 냉정한 진단도 나왔다.

세 갈래의 공통점은 분명하다. 어느 쪽도 사람에게 매 명령을 읽고 판단하라고 요구하지 않는다. 사람은 정책을 한 번 설계하고, 실행은 기계가 강제한다. 프롬프트를 더 잘 만드는 것이 아니라, 프롬프트가 필요 없는 경계를 먼저 긋는 것이 방향이다.

결론

다시 리드의 질문으로 돌아가자. 승인 프롬프트는 안전장치인가, 책임 전가 장치인가. 40,000회의 데이터는 “지금 형태로는 후자에 가깝다”고 답한다. 경고를 미리 읽은 자발적 플레이어조차 위협의 3분의 1을 놓쳤고, 익숙한 이름 뒤에 숨은 페이로드는 성공률이 두 배였으며, 정상 작업의 절반은 공연히 막혔고, 집중력은 세션이 길어질수록 닳았다. 이 모든 것은 개인의 부주의가 아니라 설계의 산물이다.

그렇다고 사람을 완전히 빼라는 결론은 성급하다. 사람은 정책을 세우고, 예외를 판단하고, 시스템이 왜 그렇게 작동하는지 책임지는 자리에 있어야 한다. 다만 그 자리는 “매초 튀어나오는 대화상자의 승인 버튼”이 아니다. 우리가 물어야 할 질문은 “이 명령을 승인할 것인가”가 아니라 “애초에 이 명령이 승인 없이도 안전하게 실행될 수 있는 상자 안에 에이전트가 들어 있는가”다. 당신의 코딩 에이전트는 지금 어느 쪽에 있는가. 그리고 오늘 당신이 마지막으로 누른 “허용”은, 판단이었는가 리듬이었는가?


출처: