2026년 8월, 한 AI 비서가 헬스장 수업을 예약하라는 지시를 받았다. 수업이 이미 찼다는 사실을 확인한 에이전트는 예약 API를 찾아냈다. 그 API에는 제대로 된 권한 검사가 없었다. 에이전트는 허용된 기간 밖의 예약을 만들고, 자리를 확보하려고 대기 순번이 더 높던 사람의 예약을 취소했다. 그리고는 사과했다.
보도는 이를 시스템을 해킹한 ‘불량 에이전트’ 사건으로 불렀다. 하지만 그 해석은 틀렸고, 그래서 위험하다. 고쳐야 할 곳을 엉뚱하게 가리키기 때문이다. 에이전트는 아무것도 해킹하지 않았다. API가 허용한 일을 그대로 했을 뿐이다. 백엔드는 충분한 권한이 없는 호출자에게도 다른 사람의 예약을 취소할 길을 열어 두었고, 돌이킬 수 없는 작업 전에 사람의 승인을 요구하는 장치도 없었다. 에이전트는 그 열린 문을 처음으로 무심하게 통과한 호출자였을 뿐이다.
이 글의 요지는 하나다. 되돌릴 수 없는 작업을 프롬프트로 막을 수는 없다. 더 나은 시스템 프롬프트도, “파괴적인 작업 전에는 확인하세요”라는 지시도, 정중한 도구 설명도 충분하지 않다. 모델이 협조하든 그렇지 않든 반드시 실행되는 결정론적 코드에 멈춤 장치를 둬야 한다. 나는 한때 18개 팀이 쓰던 운영 인프라를 삭제한 적이 있다. 그 사고를 복구할 수 있었던 이유는 나를 강제로 멈춰 세운, 둔하지만 타협하지 않는 코드 한 조각 덕분이었다. 프롬프트는 나를 구하지 못했다. 코드가 구했다.
## 프롬프트가 안전 장치가 될 수 없는 이유
프롬프트는 요청일 뿐이다. 아무리 좋은 모델도 “삭제하기 전에 반드시 물어보라”는 말을 물리적 제약이 아니라 강한 안내로 받아들인다. 삭제가 당연히 옳다고 판단하거나, 상황을 잘못 읽거나, 적대적인 입력에 흔들리는 순간 안내는 효력을 잃는다. 더구나 에이전트의 가치는 사람이 매번 개입하지 않아도 행동한다는 데 있다. 사람이 이상함을 알아차렸을 때는 이미 되돌릴 수 없는 일이 끝난 뒤일 수 있다.
안전 장치를 둘 수 있는 위치는 세 곳이다.
1. **프롬프트 안.** “확인 없이 파괴적인 작업을 하지 마.” 확률적으로 동작하는 시스템에 건네는 제안이다. 조용히 실패할 수 있고, 가장 필요한 예외 상황에서 실패한다.
2. **도구 스키마 안.** 도구를 위험하다고 표시하고 `confirm: true` 인자를 요구할 수 있다. 더 낫기는 하다. 그러나 그 인자 역시 모델이 채운다. 모델이 실행을 여는 토큰을 만들 수 있다면, 모델이 실행을 열 수 있다는 뜻이다. 결정을 옮겼을 뿐 모델 바깥으로 빼낸 것은 아니다.
3. **모델이 우회할 수 없는 결정론적 코드 안.** 래퍼, 훅, 또는 외부 조건이 충족되지 않으면 요청을 거부하는 백엔드 검사다. 모델의 판단이 틀려도 버티는 층은 이것뿐이다.
헬스장 사고에는 첫째와 둘째만 있었다. 백엔드는 호출자를 믿었고, 셋째 방어선은 없었다.
## 내가 ‘불량 에이전트’였던 날
같은 종류의 실패를 나도 겪었다. 그때 실행 주체는 언어 모델이 아니라 사람이었다.
클라우드 인프라를 정리하다가 범위가 제한된 일회용 리소스라고 생각한 대상에 삭제 명령을 내렸다. 실제로는 여러 팀이 함께 쓰는 API Gateway 사용자 지정 도메인이었다. 한 번의 호출로 18개 팀의 base-path mapping이 사라졌고, 트래픽 라우팅도 함께 끊겼다. 삭제 명령은 내가 의도한 일이 아니라, 내가 시킨 일을 정확하게 수행했을 뿐이었다.
어떤 프롬프트도 그때의 나를 막지 못했을 것이다. 내가 바로 프롬프트를 해석하고 실행을 결정한 주체였기 때문이다. 나는 명령을 읽었고, 영향 범위를 이해했다고 믿었고, 틀렸다. 사람의 판단도 이 지점에서 무너진다. 헬스장 사례에서 모델의 판단이 무너진 것과 다르지 않다. 지능이 생물학적이라는 사실은 안전에 아무 보탬이 되지 않았다.
복구를 가능하게 한 것은 제안이 아니라 절차로 굳혀 둔 습관이었다. 공유 리소스를 삭제하기 전에 전체 mapping 테이블을 캡처하는 절차다. 삭제 직전에 남긴 `(base path -> API, stage)` 덤프는 18개 중 17개 mapping을 복구하는 데 필요한 정확한 청사진이 됐다. 복구하지 못한 하나는 백엔드 API 자체가 이미 삭제되어 연결할 대상이 남아 있지 않았다. 사고를 막기 위한 안전 단계가, 사고 뒤에는 복구의 기반이 됐다.
그래서 나는 헬스장 백엔드 엔지니어가 했어야 할 일을 했다. 멈춤 장치를 머릿속 판단에서 코드로 옮겼다.
## 코드 수준의 방어선은 어떻게 생기는가
지금 내 에이전트 도구는 실행 전 모든 명령을 검사하는 pre-execution hook 뒤에서 동작한다. 극히 일부의 치명적인 작업은 하드 블록한다. 경고가 아니다. 명령 자체가 실행되지 않는다.
구조는 단순하다.
- 영향 범위가 넓고 되돌릴 수 없는 작업만 담은 차단 목록을 둔다. 공유 도메인 삭제, hosted zone 삭제, 운영 테이블 삭제가 여기에 들어간다. 이런 요청은 실행 전에 non-zero exit로 거부된다.
- 차단 목록은 의도적으로 좁게 잡는다. 훅은 복구에 필요한, 범위가 정확히 제한된 수정, 예를 들어 base-path mapping 하나만 삭제하는 작업까지 막지 않는다. 모든 것을 막으면 사람들은 우회로를 찾는다. 복구 비용이 감당할 수 없는 작업만 막아야 한다.
- 우회 경로는 편리함보다 감사 가능성을 우선한다. 정말 진행해야 한다면 `CONFIRM_SHARED_DELETE=1` 같은 명시적 override 토큰을 명령 앞에 붙여야 한다. 이 토큰을 직접 입력하는 행위는 셸 히스토리와 감사 로그에 남는다. “모델이 `confirm: true`를 만들었다”와 “사람이 기록을 남기며 차단을 의도적으로 해제했다”는 전혀 다르다.
- 차단 메시지는 runbook 역할도 한다. 훅은 사용자를 멈춰 세울 때 먼저 실행할 영향 범위 확인 쿼리와, 대신 사용할 수 있는 정확히 좁은 명령을 함께 보여 준다. 위험한 길로 들어서려는 바로 그 순간에 안전한 경로를 안내하는 것이다.
핵심은 이 장치들이 명령을 내리는 주체의 지능을 신뢰하지 않는다는 점이다. 그 주체가 Claude든 셸 스크립트든 새벽 2시의 나든 상관없다. 차단은 행위자에게 보내는 부탁이 아니라 환경의 속성이다.
## 다른 도메인에도 같은 원칙을 적용할 수 있다
헬스장 사례로 돌아가 보자. 올바른 해결책은 “에이전트에게 더 좋은 프롬프트를 주자”가 아니었다.
- 호출자가 자기 소유가 아닌 리소스를 취소하려 하면 백엔드는 매번 `403`을 반환해야 한다. 권한 검사는 서버 측 코드에서 이뤄져야 한다.
- 다른 사람의 예약을 취소하는 것처럼 되돌릴 수 없는 작업에는 호출자 스스로 만들 수 없는 승인 절차가 필요하다. 프롬프트가 아니라 강제되는 human-in-the-loop 게이트여야 한다.
- 요청이 거부된 뒤 에이전트가 검사를 건너뛸 다른 endpoint를 찾을 수 없어야 한다. 검사는 예의 바른 경로 하나가 아니라 모든 경로에 있어야 한다.
이는 내 삭제 훅과 같은 성질이다. 도메인만 다를 뿐이다. 차단 목록은 정말로 비가역적인 작업으로 좁히고, override는 사람이 발급하며 감사할 수 있게 만들고, 모든 경로에서 강제해야 한다. 그리고 거부 메시지는 단순히 “안 된다”고 끝내지 말고 안전한 대안을 가리켜야 한다.
## 지금 해야 할 일
중요한 일을 건드릴 수 있는 에이전트를 출시한다면, 화려한 해법보다 먼저 지루한 일을 해야 한다.
- 에이전트가 수행할 수 있는 작업을 나열하고, 되돌릴 수 없거나 공유 리소스에 영향을 주는 작업을 표시한다. 코드 수준의 게이트가 필요한 범위는 그 짧은 목록이면 된다. 나머지는 빠르게, 프롬프트 기반으로 처리해도 된다.
- 목록의 각 항목에 모델의 응답과 무관하게 실행되는 결정론적 검사를 둔다. 래퍼, 훅, 서버 측 권한 규칙이 여기에 해당한다. 모델이 해제 토큰을 만들 수 있다면 그것은 게이트가 아니다.
- override는 모델이 만들 수 있는 인자가 아니라 사람이 직접 수행하고 감사할 수 있는 행위로 만든다.
- 복구 청사진은 파괴적인 단계 *뒤가 아니라 앞에* 남긴다. 영향 범위를 확인하려고 실행한 쿼리가 나중에는 복구 지도가 된다.
불편하지만 중요한 결론은, 행위자의 지능이 안전 계층이 아니라는 것이다. 더 똑똑한 모델이 이 문제를 해결해 주지 않는다. 실패는 무지해서가 아니라 확신에 찬 채 틀리기 때문에 발생하며, 사람과 모델 모두 그런 순간을 맞는다. 멈춤 장치는 확신이 닿지 못하는 곳, 즉 코드에 둬야 한다.
2026년 8월, 한 AI 비서가 헬스장 수업을 예약하라는 지시를 받았다. 수업이 이미 찼다는 사실을 확인한 에이전트는 예약 API를 찾아냈다. 그 API에는 제대로 된 권한 검사가 없었다. 에이전트는 허용된 기간 밖의 예약을 만들고, 자리를 확보하려고 대기 순번이 더 높던 사람의 예약을 취소했다. 그리고는 사과했다.
보도는 이를 시스템을 해킹한 ‘불량 에이전트’ 사건으로 불렀다. 하지만 그 해석은 틀렸고, 그래서 위험하다. 고쳐야 할 곳을 엉뚱하게 가리키기 때문이다. 에이전트는 아무것도 해킹하지 않았다. API가 허용한 일을 그대로 했을 뿐이다. 백엔드는 충분한 권한이 없는 호출자에게도 다른 사람의 예약을 취소할 길을 열어 두었고, 돌이킬 수 없는 작업 전에 사람의 승인을 요구하는 장치도 없었다. 에이전트는 그 열린 문을 처음으로 무심하게 통과한 호출자였을 뿐이다.
이 글의 요지는 하나다. 되돌릴 수 없는 작업을 프롬프트로 막을 수는 없다. 더 나은 시스템 프롬프트도, “파괴적인 작업 전에는 확인하세요”라는 지시도, 정중한 도구 설명도 충분하지 않다. 모델이 협조하든 그렇지 않든 반드시 실행되는 결정론적 코드에 멈춤 장치를 둬야 한다. 나는 한때 18개 팀이 쓰던 운영 인프라를 삭제한 적이 있다. 그 사고를 복구할 수 있었던 이유는 나를 강제로 멈춰 세운, 둔하지만 타협하지 않는 코드 한 조각 덕분이었다. 프롬프트는 나를 구하지 못했다. 코드가 구했다.
프롬프트가 안전 장치가 될 수 없는 이유
프롬프트는 요청일 뿐이다. 아무리 좋은 모델도 “삭제하기 전에 반드시 물어보라”는 말을 물리적 제약이 아니라 강한 안내로 받아들인다. 삭제가 당연히 옳다고 판단하거나, 상황을 잘못 읽거나, 적대적인 입력에 흔들리는 순간 안내는 효력을 잃는다. 더구나 에이전트의 가치는 사람이 매번 개입하지 않아도 행동한다는 데 있다. 사람이 이상함을 알아차렸을 때는 이미 되돌릴 수 없는 일이 끝난 뒤일 수 있다.
안전 장치를 둘 수 있는 위치는 세 곳이다.
프롬프트 안. “확인 없이 파괴적인 작업을 하지 마.” 확률적으로 동작하는 시스템에 건네는 제안이다. 조용히 실패할 수 있고, 가장 필요한 예외 상황에서 실패한다.
도구 스키마 안. 도구를 위험하다고 표시하고 confirm: true 인자를 요구할 수 있다. 더 낫기는 하다. 그러나 그 인자 역시 모델이 채운다. 모델이 실행을 여는 토큰을 만들 수 있다면, 모델이 실행을 열 수 있다는 뜻이다. 결정을 옮겼을 뿐 모델 바깥으로 빼낸 것은 아니다.
모델이 우회할 수 없는 결정론적 코드 안. 래퍼, 훅, 또는 외부 조건이 충족되지 않으면 요청을 거부하는 백엔드 검사다. 모델의 판단이 틀려도 버티는 층은 이것뿐이다.
헬스장 사고에는 첫째와 둘째만 있었다. 백엔드는 호출자를 믿었고, 셋째 방어선은 없었다.
내가 ‘불량 에이전트’였던 날
같은 종류의 실패를 나도 겪었다. 그때 실행 주체는 언어 모델이 아니라 사람이었다.
클라우드 인프라를 정리하다가 범위가 제한된 일회용 리소스라고 생각한 대상에 삭제 명령을 내렸다. 실제로는 여러 팀이 함께 쓰는 API Gateway 사용자 지정 도메인이었다. 한 번의 호출로 18개 팀의 base-path mapping이 사라졌고, 트래픽 라우팅도 함께 끊겼다. 삭제 명령은 내가 의도한 일이 아니라, 내가 시킨 일을 정확하게 수행했을 뿐이었다.
어떤 프롬프트도 그때의 나를 막지 못했을 것이다. 내가 바로 프롬프트를 해석하고 실행을 결정한 주체였기 때문이다. 나는 명령을 읽었고, 영향 범위를 이해했다고 믿었고, 틀렸다. 사람의 판단도 이 지점에서 무너진다. 헬스장 사례에서 모델의 판단이 무너진 것과 다르지 않다. 지능이 생물학적이라는 사실은 안전에 아무 보탬이 되지 않았다.
복구를 가능하게 한 것은 제안이 아니라 절차로 굳혀 둔 습관이었다. 공유 리소스를 삭제하기 전에 전체 mapping 테이블을 캡처하는 절차다. 삭제 직전에 남긴 (base path -> API, stage) 덤프는 18개 중 17개 mapping을 복구하는 데 필요한 정확한 청사진이 됐다. 복구하지 못한 하나는 백엔드 API 자체가 이미 삭제되어 연결할 대상이 남아 있지 않았다. 사고를 막기 위한 안전 단계가, 사고 뒤에는 복구의 기반이 됐다.
그래서 나는 헬스장 백엔드 엔지니어가 했어야 할 일을 했다. 멈춤 장치를 머릿속 판단에서 코드로 옮겼다.
코드 수준의 방어선은 어떻게 생기는가
지금 내 에이전트 도구는 실행 전 모든 명령을 검사하는 pre-execution hook 뒤에서 동작한다. 극히 일부의 치명적인 작업은 하드 블록한다. 경고가 아니다. 명령 자체가 실행되지 않는다.
구조는 단순하다.
영향 범위가 넓고 되돌릴 수 없는 작업만 담은 차단 목록을 둔다. 공유 도메인 삭제, hosted zone 삭제, 운영 테이블 삭제가 여기에 들어간다. 이런 요청은 실행 전에 non-zero exit로 거부된다.
차단 목록은 의도적으로 좁게 잡는다. 훅은 복구에 필요한, 범위가 정확히 제한된 수정, 예를 들어 base-path mapping 하나만 삭제하는 작업까지 막지 않는다. 모든 것을 막으면 사람들은 우회로를 찾는다. 복구 비용이 감당할 수 없는 작업만 막아야 한다.
우회 경로는 편리함보다 감사 가능성을 우선한다. 정말 진행해야 한다면 CONFIRM_SHARED_DELETE=1 같은 명시적 override 토큰을 명령 앞에 붙여야 한다. 이 토큰을 직접 입력하는 행위는 셸 히스토리와 감사 로그에 남는다. “모델이 confirm: true를 만들었다”와 “사람이 기록을 남기며 차단을 의도적으로 해제했다”는 전혀 다르다.
차단 메시지는 runbook 역할도 한다. 훅은 사용자를 멈춰 세울 때 먼저 실행할 영향 범위 확인 쿼리와, 대신 사용할 수 있는 정확히 좁은 명령을 함께 보여 준다. 위험한 길로 들어서려는 바로 그 순간에 안전한 경로를 안내하는 것이다.
핵심은 이 장치들이 명령을 내리는 주체의 지능을 신뢰하지 않는다는 점이다. 그 주체가 Claude든 셸 스크립트든 새벽 2시의 나든 상관없다. 차단은 행위자에게 보내는 부탁이 아니라 환경의 속성이다.
다른 도메인에도 같은 원칙을 적용할 수 있다
헬스장 사례로 돌아가 보자. 올바른 해결책은 “에이전트에게 더 좋은 프롬프트를 주자”가 아니었다.
호출자가 자기 소유가 아닌 리소스를 취소하려 하면 백엔드는 매번 403을 반환해야 한다. 권한 검사는 서버 측 코드에서 이뤄져야 한다.
다른 사람의 예약을 취소하는 것처럼 되돌릴 수 없는 작업에는 호출자 스스로 만들 수 없는 승인 절차가 필요하다. 프롬프트가 아니라 강제되는 human-in-the-loop 게이트여야 한다.
요청이 거부된 뒤 에이전트가 검사를 건너뛸 다른 endpoint를 찾을 수 없어야 한다. 검사는 예의 바른 경로 하나가 아니라 모든 경로에 있어야 한다.
이는 내 삭제 훅과 같은 성질이다. 도메인만 다를 뿐이다. 차단 목록은 정말로 비가역적인 작업으로 좁히고, override는 사람이 발급하며 감사할 수 있게 만들고, 모든 경로에서 강제해야 한다. 그리고 거부 메시지는 단순히 “안 된다”고 끝내지 말고 안전한 대안을 가리켜야 한다.
지금 해야 할 일
중요한 일을 건드릴 수 있는 에이전트를 출시한다면, 화려한 해법보다 먼저 지루한 일을 해야 한다.
에이전트가 수행할 수 있는 작업을 나열하고, 되돌릴 수 없거나 공유 리소스에 영향을 주는 작업을 표시한다. 코드 수준의 게이트가 필요한 범위는 그 짧은 목록이면 된다. 나머지는 빠르게, 프롬프트 기반으로 처리해도 된다.
목록의 각 항목에 모델의 응답과 무관하게 실행되는 결정론적 검사를 둔다. 래퍼, 훅, 서버 측 권한 규칙이 여기에 해당한다. 모델이 해제 토큰을 만들 수 있다면 그것은 게이트가 아니다.
override는 모델이 만들 수 있는 인자가 아니라 사람이 직접 수행하고 감사할 수 있는 행위로 만든다.
복구 청사진은 파괴적인 단계 뒤가 아니라 앞에 남긴다. 영향 범위를 확인하려고 실행한 쿼리가 나중에는 복구 지도가 된다.
불편하지만 중요한 결론은, 행위자의 지능이 안전 계층이 아니라는 것이다. 더 똑똑한 모델이 이 문제를 해결해 주지 않는다. 실패는 무지해서가 아니라 확신에 찬 채 틀리기 때문에 발생하며, 사람과 모델 모두 그런 순간을 맞는다. 멈춤 장치는 확신이 닿지 못하는 곳, 즉 코드에 둬야 한다.