자율 코딩 에이전트에 안전 가드레일(Safety Guardrails)을 추가한 방법: 5가지 교훈
요약
자율 코딩 에이전트를 실제 환경에서 운영하며 겪은 위험 요소와 이를 방지하기 위한 안전 가드레일 설계 방안을 다룹니다. 프롬프트 기반의 제약 대신 실행 환경(harness) 수준에서 도구의 가역성에 따라 권한을 계층화하는 전략을 제시합니다.
핵심 포인트
- 에이전트의 위험은 잘못된 코드가 아닌 되돌릴 수 없는 작업에서 발생함
- 프롬프트 기반 제약은 컨텍스트가 길어지면 효과가 급감함
- 도구를 기능이 아닌 가역성(reversibility) 기준으로 계층화해야 함
- 단순 승인 방식은 확인 피로를 유발하여 보안 무력화를 초래함
요약 (TL;DR)
자율 코딩 에이전트(autonomous coding agent)를 실제 저장소(repos)에서 몇 달 동안 실행해 보았는데, 가장 무서운 버그는 결코 "잘못된 코드"가 아니었습니다. 그것은 바로 "너무 빠르게 수행된 되돌릴 수 없는 작업(irreversible actions)"이었습니다. 여기에서 저는 확인 게이트(confirmation gates), 폭발 반경(blast-radius) 사고방식, 그리고 읽기 전용 대 변이(read-only-vs-mutating) 도구 분리를 중심으로 에이전트의 권한 모델을 어떻게 재설계했는지, 그리고 그 과정에서 얻은 5가지 교훈을 공유합니다.
문제점
제 에이전트가 3일 동안 커밋되지 않은 작업물을 force-push 했을 때, 저는 모델에게 화가 나지 않았습니다. 대신 제 자신에게 화가 났습니다. 왜냐하면 제가 git status와 정확히 동일한 권한 수준을 가진 도구(git push --force)를 에이전트에게 부여했기 때문입니다.
이것이 핵심 문제입니다. 대부분의 에이전트 설정은 "도구 호출(call a tool)"을 차별화되지 않은 하나의 동작으로 취급합니다. 하지만 ls와 rm -rf는 위험의 종류가 다르며, 만약 당신의 권한 시스템이 이를 구분하지 못한다면 에이전트 또한 구분하지 못합니다.
단순히 에이전트와 채팅하는 것이 아니라 실제로 에이전트를 자율적으로 실행하기 시작하자, 데모 단계에서는 겪지 못했던 세 가지 실패 유형이 나타났습니다:
- 기본적으로 파괴적인 도구들 (Destructive-by-default tools). 브랜치 삭제, 테이블 드롭(dropping tables),
git reset --hard— 이 모든 것들은 단 한 번의 도구 호출로 도달할 수 있으며, 단 하나의 잘못된 계획만으로 실행될 수 있습니다. - 조용한 폭발 반경 (Silent blast radius). "설정 파일을 정리해"라는 단 한 번의 지시가 다른 6개의 서비스가 의존하고 있는 파일을 건드렸습니다. 에이전트는 이를 알 방법이 없었고, 저 또한 작업이 완료될 때까지 알지 못했습니다.
- 확인 피로 (Confirmation fatigue). 저의 첫 번째 해결책은 "모든 쓰기 작업 전에 물어보기"였습니다. 하지만 그 방식은 약 하루 정도 지속되었고, 그 후 저는 프롬프트를 읽지도 않고 모든 승인을 마구 눌러버리는(rubber-stamping) 상태가 되었습니다. 이는 아예 게이트가 없는 것보다 더 나쁩니다.
이 중 어느 것도 모델 품질의 문제는 아닙니다. GPT-4급 모델과 Claude급 모델 모두 때때로 "기술적으로는 맞지만, 문맥적으로는 재앙적인" 결정을 내립니다. 해결책은 프롬프트가 아니라 하네스(harness, 실행 환경)에 존재해야 합니다.
이런 상황에서는 "그저 더 나은 시스템 프롬프트(system prompt)를 작성하자"라는 유혹에 빠지기 쉽습니다. 저도 처음에는 그렇게 시도해 보았습니다. 에이전트에게 "파괴적인 작업은 주의할 것" 그리고 "위험한 작업 전에는 반드시 물어볼 것"이라고 지시하는 긴 문단을 작성했습니다. 효과가 조금 있는 듯했으나, 긴 세션 동안 지시 사항이 컨텍스트 윈도우(context window) 밖으로 밀려나자마자 조용히 작동을 멈췄습니다. 프롬프트 수준의 주의력은 감퇴합니다. 하지만 하네스(harness, 실행 환경) 수준의 제약은 감퇴하지 않습니다. 하네스는 잊지 않기 때문입니다.
해결 방법
1. 카테고리가 아닌 가역성(reversibility)에 따라 도구를 계층화하기
도구를 수행하는 기능(파일 작업, git 작업, 셸 작업 등)에 따라 그룹화하는 대신, 되돌리기(undo)가 얼마나 어려운지에 따라 그룹화했습니다.
Tier 0 (자유): 파일 읽기, grep, 브랜치 목록 확인, 테스트 실행
Tier 1 (가역적): 파일 생성, 파일 수정, 브랜치 생성, PR(Pull Request) 오픈
Tier 2 (가역하기 어려움): force-push, 이미 게시된 커밋 rebase,
...
Tier 0는 아무런 마찰 없이 실행됩니다. Tier 1은 자동으로 실행되지만 눈에 띄게 로그가 남습니다. Tier 2와 3은 명시적인 확인 단계가 필요합니다. 여기서 결정적으로, 에이전트는 작업을 실행하기 전에 해당 작업이 어떤 계층에 속한다고 생각하는지 스스로 "명시"해야 합니다. 그래야 잘못된 분류가 도구 호출(tool call) 속에 숨겨지는 대신 트랜스크립트(transcript)에 드러나게 됩니다.
계층화할 때는 동사뿐만 아니라 범위(scope)도 고려해야 합니다. 에이전트가 2분 전에 생성한 파일에 대한 rm 명령은 저장소에 2년 동안 있었던 파일에 대한 rm 명령과는 완전히 다른 작업입니다. 동일한 도구 호출이지만, 파괴 범위(blast radius)는 판이하게 다릅니다. 저는 결국 대략적인 휴리스틱(heuristic)을 인코딩했습니다. 만약 에이전트가 현재 세션에서 무언가를 생성했거나 유일한 작성자라면, 그것을 삭제하거나 덮어쓰는 것은 계층을 한 단계 낮춥니다. 왜냐하면 "되돌리기 어려운" 비용이 거의 0에 가깝기 때문입니다. 기존에 존재하던 것은 원래의 계층을 그대로 유지합니다.
2. 파괴적인 작업을 하기 전 에이전트가 워킹 트리(working-tree) 상태를 확인하게 만들기
이 방법은 그 어떤 것보다 더 많은 실제 버그를 잡아냈습니다. 모든 Tier 2/3 git 작업을 수행하기 전에, 에이전트는 다음과 같은 가벼운 가드(guard)를 실행합니다.
git status --porcelain
만약 그 결과가 비어 있지 않고, 수행하려는 작업이 이를 폐기할 가능성이 있다면 (checkout, reset --hard, clean -f 등), 에이전트는 맹목적으로 진행하기 위해 허가를 구하는 대신 먼저 stash(임시 저장)를 하도록 지시받습니다. "삭제하기 전에 조사하라"는 원칙은 "삭제하기 전에 물어보라"는 원칙보다 훨씬 더 나은 기본 설정임이 밝혀졌습니다. 절반 정도의 경우에는 위험한 요소가 전혀 없었으며, 매번 물어보는 방식은 제가 무의식적으로 "네, 진행하세요"라고 대답하도록 훈련시키는 결과만 초래했을 것입니다.
3. Tier 2/3의 경우 "제안(propose)"과 "실행(execute)"을 분리할 것
에이전트의 도구 스키마(tool schema)는 상태를 변경하는(mutating) 동작을 두 개의 호출로 나눕니다. 하나는 diff/계획(plan)을 생성하는 호출이고, 다른 하나는 이를 적용하는 호출입니다. force-push의 경우, 이는 무엇이 변경될지 계산하고 이를 출력한 다음, 두 번째 명시적 호출을 통해 실제로 push를 수행함을 의미합니다. 이것만으로도 에이전트의 _계획(plan)_은 괜찮았지만, 실행(execution) 단계에서 잘못된 브랜치나 원격 저장소(remote)로 향해버리는 버그 카테고리 전체를 제거할 수 있었습니다.
flowchart LR
A[Agent decides on action] --> B{Tier?}
B -->|0-1| C[Execute directly]
...
4. 확인(confirmation) 범위를 세션이 아닌 요청(request) 단위로 한정할 것
초기에는 "사용자가 한 번 push를 승인했다"는 사실을 세션이 끝날 때까지의 포괄적인 승인으로 취급하는 실수를 저질렀습니다. 그 결과, 20분 뒤에 에이전트가 묻지도 않고 전혀 관련 없는 두 번째 브랜치에 push를 하는 상황이 발생했습니다. 기술적으로는 제가 push에 대해 "예"라고 말한 적이 있었지만, 바로 그 push에 대해 말한 것은 아니었습니다. 이제 모든 확인 절차는 특정 동작과 대상에 범위가 한정되며, 이전의 승인이 다른 브랜치, 저장소(repo), 또는 파괴적인 동사(destructive verb)로 조용히 확장되지 않습니다.
5. 실패뿐만 아니라 아차 했던 순간(near-misses)도 기록할 것
저는 에이전트가 _제안(proposed)_한 모든 Tier 2/3 동작에 대해, 그것이 실제로 실행되었는지 여부와 상관없이 실행 로그를 남기기 시작했습니다. 알고 보니 대부분의 가치는 실제로 실행된 작업이 아니라, 더 안전한 도구가 작동할 수 있는 상황에서 에이전트가 얼마나 자주 파괴적인 도구에 손을 뻗는지를 확인하는 데 있었습니다. 이 로그는 시간이 지남에 따라 도구 설명(tool descriptions)과 시스템 프롬프트(system prompt) 가이드를 강화하는 입력 데이터가 되었습니다.
해당 로그의 구체적인 사례를 하나 들자면, 2주간의 기간 동안 에이전트는 제가 유지하고 싶었던 추적되지 않은 파일들(untracked files) — 초안 노트, 스크래치 스크립트, 절반쯤 완료된 마이그레이션 등 — 이 포함된 워킹 트리(working tree)를 "정리"하기 위해 git clean -f를 네 번이나 별도로 제안했습니다. 이 네 번의 제안 중 실행된 것은 하나도 없었는데, 이는 Tier-2 게이트(gate)가 이를 포착했고 제가 매번 거절했기 때문입니다. 하지만 만약 제가 실행된 액션(executed actions)만을 추적하고 있었다면, "작업 공간 정리(clean the workspace)"에 대한 도구 설명(tool description)이 "추적되지 않은 모든 것을 삭제"하는 것으로 지속적으로 잘못 해석되고 있다는 신호를 전혀 받지 못했을 것이고, 에이전트가 직접 생성하지 않은 추적되지 않은 파일들을 명시적으로 제외하도록 도구 설명을 다시 작성하는 일도 결코 하지 못했을 것입니다.
잘못된 확신(false confidence)에 대한 참고 사항
설계하기 가장 어려운 실패 모드(failure mode)는 에이전트가 무모하게 행동하는 것이 아니라, 에이전트가 설득력 있게 조심스럽게 행동하는 것이었습니다. 몇 번의 경우, 에이전트는 신중해 보이는 계획을 서술(“먼저 워킹 트리를 확인한 다음 조심스럽게 진행하겠습니다”)하면서도, 서술 내용과 도구 호출(tool call)이 실제로 아무것과도 결합되어 있지 않았기 때문에 결국 Tier 3 액션을 실행하곤 했습니다. "조심하겠습니다"라고 말하는 것은 가드레일(guardrail)이 아닙니다. 그것은 단지 더 많은 토큰(tokens)일 뿐입니다. 해결책은 티어 체크(tier check)를 모델의 통제 밖에서 강제되는 엄격한 전제 조건(precondition)으로 만드는 것이었습니다. 즉, 에이전트 자신의 추론(reasoning)이 이미 무언가를 수행했다고 주장하더라도, 실제 게이트로부터의 확인 토큰(confirmation token)이 존재하지 않는 한 하네스(harness)가 Tier 2/3 도구 호출을 라우팅하는 것을 거부하도록 설정한 것입니다. 모델의 신중함에 대한 자기 보고(self-report)를 신뢰하는 것은 가드레일이 전혀 없는 것만큼이나 위험하다는 것이 밝혀졌습니다.
교훈 (Lessons Learned)
- 위험의 기준은 카테고리가 아니라 가역성(Reversibility)이어야 합니다. "Git 작업"은 그 자체로 위험 등급이 될 수 없습니다.
git log와git push --force는 실행 파일(binary) 외에는 공통점이 없습니다. - 확인 피로(Confirmation fatigue)는 실제적인 실패 모드이며, 이는 사용자의 잘못이 아니라 여러분의 잘못입니다. 모든 것에 차단벽(gate)을 설치하면 사람들은 프롬프트를 읽지 않게 됩니다. 덜 차단하되, 더 똑똑하게 차단하세요.
- 모호한 위험에 대해서는 "먼저 묻기"보다 "먼저 조사하기"가 더 효과적입니다. 비용이 적게 드는 읽기 전용 체크(
git status,SELECT COUNT(*))는 인간의 주의력을 소모하지 않고도 대부분의 불확실성을 해결할 수 있습니다. - 되돌리기 어려운 작업은 제안(propose)과 실행(execute)을 분리하세요. 이렇게 하면 "계획이 타당했는가"와 "실행이 계획과 일치했는가"를 각각 별도로 디버깅 가능한 두 가지 질문으로 분리할 수 있습니다.
- 승인 범위(Approval scope)는 좁고 명확해야 합니다. 세션 수준의 권한 상승(trust escalation)은 단 한 번의 "예"가 세 번의 도구 호출(tool calls) 후에 어떻게 사고로 이어지는지를 보여주는 전형적인 방식입니다.
향후 계획 (What's Next)
저는 등급 분류(tier classification) 자체를 학습 가능하게 만들기 위해 작업 중입니다. 현재는 제가 수동으로 유지하는 정적 목록이며, 새로운 도구(특히 제가 추가하는 MCP 서버들)가 자동으로 적절한 등급에 할당되지 않습니다. 다음 버전에서는 에이전트가 호출 시점에 스스로 분류하도록 맡기는 대신, 등록 시점에 도구의 위험도를 태깅하는 방식을 사용할 것입니다.
마무리 (Wrap-up)
데모 단계를 넘어 자율 코딩 에이전트를 운영하고 있다면, 권한 모델(permission model)은 있으면 좋은 기능(nice-to-have)이 아닙니다. 그것은 "에이전트가 잘못된 판단을 내렸다"와 "에이전트가 잘못된 판단을 내렸고 이제 복구 불가능하다" 사이를 가르는 결정적인 요소입니다. 프롬프트의 등급을 나누기 전에 도구의 등급부터 나누는 것부터 시작하세요.
이 내용이 유용했다면, AI 코딩 에이전트를 장기적으로 운영하는 현장의 더 많은 이야기를 위해 저를 팔로우해 주세요. 그리고 여러분의 환경에서는 파괴적인 작업(destructive-action)에 대한 차단(gating)을 어떻게 처리하고 있는지 댓글로 알려주세요. 진심으로 의견을 나누고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기