프롬프트는 제안일 뿐이지만, 훅(Hook)은 강제적인 제동 장치입니다.

몇 달 동안 저는 Claude Code를 단순히 명확한 규칙이 필요한 주니어 개발자처럼 대했습니다. 제 프로젝트 지침은 명시적이었습니다: 절대 force-push 하지 말 것, 브랜치를 삭제하지 말 것, 파괴적인 명령을 실행하지 말 것. 대부분의 저녁에는 그것이 잘 작동했습니다. 에이전트는 테스트를 작성하고, 함수를 리팩터링하며, git 히스토리에 손을 대지 않았습니다. 그러다 한 번의 rebase가 잘못되었습니다.

컨텍스트 윈도우가 git 에러 출력으로 가득 찼습니다. 충돌 마커(conflict markers), detached HEAD 메시지, 브랜치 분기 경고가 토큰 하나하나 쌓여갔습니다. 그 소음 아래에는 force-push를 피하라는 저의 정중한 지침이 묻혀 있었습니다. 모델에게 스레드 내에서 가장 최근에 나타난 두드러진 텍스트는 에러 스트림이었습니다. 통계적 주의(statistical attention)가 정책(policy)을 이겼습니다. 에이전트는 2시간 동안 커밋하지 않은 로컬 변경 사항을 모두 날려버리는 명령을 실행했습니다. 악의적인 행동은 아니었습니다. 그저 주의가 분산되었을 뿐입니다. 이 차이가 중요합니다. LLM은 악의를 가지고 규칙을 어기는 것이 아닙니다. 컨텍스트 윈도우 내의 더 강력한 패턴이 이전의 지침을 일시적으로 압도하기 때문에 규칙을 어기는 것입니다.

그 사건은 에이전트의 안전성에 대한 제 생각을 바꾸어 놓았습니다. 99%의 시간 동안 작동하는 가드레일은 오히려 위험 요소입니다. 실패 모드가 시간, 비용 또는 운영 데이터를 앗아간다면, 그것을 프롬프트 안에만 두어서는 안 됩니다. 모델의 추론 루프 외부에서 강제 집행(enforcement)할 수 있는 수단이 필요합니다.

Claude Code 훅은 바로 이 문제를 해결합니다. 훅은 세 가지 특정 시점에서 도구 호출을 가로채는 작은 스크립트입니다: 도구가 실행되기 전(PreToolUse), 도구가 완료된 후(PostToolUse), 그리고 에이전트가 작업이 끝났다고 판단할 때(Stop). 훅은 외부 코드로 실행되기 때문에 모델의 메모리, 기분 또는 컨텍스트 압박에 의존하지 않습니다. 모델이 당신이 준 모든 지침을 잊어버하더라도, 훅은 여전히 "안 돼"라고 말할 것입니다.

그 허망한 저녁을 보낸 후 제가 구축한 하네스(harness)는 다음과 같습니다.

가드 훅(Guard Hook): 피해가 발생하기 전에 가로채기

저의 PreToolUse 훅은 셸(shell)이 명령을 건드리기 전에 모든 Bash 명령을 검사합니다. 저는 파괴적인 패턴에 대해 엄격한 차단 목록(denylist)을 유지합니다. 명령 문자열이 위험한 패턴과 일치하면, 훅은 실행을 중단하고 에이전트에게 직접 에러를 반환합니다.

제가 차단하는 패턴은 단순하고 명확합니다:

  • git push --force 또는 아직 신뢰하지 않는 모든 force-with-lease 변형
  • git reset --hard
  • rm -rf

이것은 정교한 보안 연구가 아닙니다. 일종의 안전벨트입니다. 하지만 중요한 디테일은 차단된 이후에 어떤 일이 일어나느냐 하는 것입니다.

저는 결코 "차단됨(Blocked)"이라는 무미건조한 답변만 보내지 않습니다. 단호한 거절은 에이전트를 혼란스럽게 만들고, 동일한 파괴적 명령의 변형을 계속 시도하는 루프에 빠뜨릴 수 있습니다. 대신, 에러 메시지에 탈출구를 포함합니다. 훅이 강제 리셋(hard reset)을 감지하면 에이전트에게 다음과 같이 말합니다: "이 명령은 커밋되지 않은 작업을 보호하기 위해 차단되었습니다. 먼저 체크포인트를 커밋한 다음 다시 시도하세요." 이 추가된 한 문장이 에이전트의 행동을 완전히 바꿉니다. 에이전트는 피해를 수습하려던 시도에서 안전을 확보하는 방향으로 전환합니다. 훅은 단순한 벽이 아니라 교통 관제 시스템입니다.

또한 셸 명령에 대해 허용 목록(allowlist) 대신 차단 목록(denylist)을 선택했습니다. 처음에는 명시적으로 지정된 안전한 git 서브 명령 세트만 허용하는 방안을 고려했습니다. 하지만 이는 금방 실패했습니다. 에이전트는 창의적일 정도로 문자 그대로 행동합니다. 그들은 상태를 확인하기 위해 git stash push -m "wip" 또는 git branch --show-current와 같이 정당하지만 예상치 못한 명령을 실행하기도 합니다. 허용 목록은 모델이 유효하지만 목록에 없는 명령을 만들어내는 순간 정상적인 워크플로우를 깨뜨립니다. 진정으로 파괴적인 패턴에 대한 짧고 정제된 차단 목록은 경계선을 보호하면서도 에이전트에게 움직일 수 있는 여유를 제공합니다.

포맷터 훅(Formatter Hook): 단순 반복 작업 자동화

예전에는 에이전트에게 "파일을 편집한 후에는 항상 포맷터를 실행하라"고 말하느라 프롬프트 토큰을 낭비하곤 했습니다. 에이전트는 절반 정도는 이를 잊어버렸습니다. 나머지 절반은 포맷팅을 할지 말지 물어보며 멈춰 섰고, 단 하나의 정답만 있는 결정에 도구 호출(tool call)을 낭비했습니다.

이제는 PostToolUse 훅으로 이를 처리합니다. 에이전트가 파일을 편집한 후, 훅은 파일 확장자를 확인합니다. Python 파일이면 Ruff를 실행합니다. JavaScript 또는 TypeScript 파일이면 Prettier를 실행합니다. Go 파일이면 gofmt를 실행합니다. 에이전트는 포맷터의 존재를 알 필요가 없습니다. 알 필요조차 없습니다.

이를 프롬프트 밖으로 옮긴 것은 두 가지 효과를 가져왔습니다. 첫째, 모델에 인지적 부하를 주지 않으면서도 코드가 일관되게 깔끔하게 유지됩니다. 둘째, 프로젝트 지침이 짧아졌습니다. 프롬프트에서 "항상(always)"과 "절대(never)"를 제거할 때마다, 모델은 그 토큰을 실제 문제 해결에 사용할 수 있습니다. 훅은 불변의 규칙(invariant)을 담당하고, 프롬프트는 의도(intent)를 담당합니다.

품질 게이트(Quality Gate): "완료"의 재정의

Stop hook은 에이전트가 작업을 마쳤다고 판단하고 세션을 종료하려고 시도할 때 실행됩니다. 저는 이를 허용하지 않습니다. 대신, 이 hook은 전체 테스트 스위트를 실행합니다. 만약 테스트 중 하나라도 실패하면, hook은 중지 명령을 차단하고 실패 결과(output)를 에이전트에게 반환합니다.

이는 완료의 정의를 바꿉니다. "완료(Done)"는 더 이상 모델이 느끼는 감정이 아닙니다. 그것은 측정 가능한 관문입니다. 에이전트는 harness가 코드가 작동함을 확인했을 때만 종료할 수 있습니다. 실제로 이는 긴밀한 피드백 루프를 생성합니다. 에이전트는 코드를 작성하고, 작업이 끝났다고 생각하여 중지 버튼을 누르지만, 즉시 pytest traceback을 마주하게 됩니다. 그러면 에이전트는 스스로 수정하여 import 에러나 깨진 assertion을 고치고 다시 중지를 시도합니다. 저는 인간의 개입 없이 이 루프 안에서 에이전트가 서너 번 반복하는 것을 지켜보았습니다. harness는 품질을 강제하고, 모델은 패치를 제공합니다.

이것이 에이전트 엔지니어링에 대해 가르쳐 주는 것

신뢰할 수 있는 자율 시스템을 구축하려면 사고방식의 전환이 필요합니다. 더 긴 프롬프트를 작성하는 것에서 더 정교한 harness를 구축하는 것으로 옮겨가야 합니다.

강제(enforcement)를 위해서는 hook을 사용하고, 정책(policy)을 위해서는 프롬프트를 사용하십시오. 규칙이 100% 항상 지켜져야 한다면, 그것은 자연어가 아닌 코드에 있어야 합니다. 프롬프트는 모호함, 취향, 아키텍처에는 뛰어나지만, 불변성(invariants)을 다루는 데는 형편없습니다. 만약 실수가 오후 내내 복구 시간을 잡아먹거나, 더 나아가 운영 환경의 가동 시간(uptime)에 영향을 줄 정도라면, hook을 작성하십시오.

짧은 프롬프트가 더 나은 결과를 만듭니다. 기계적인 규칙을 스크립트로 옮기면, 모델이 기억해야 할 것이 줄어들고 모순이 생길 여지도 줄어듭니다. 에이전트의 컨텍스트 윈도우(context window)는 희소한 자원입니다. 포맷팅 안내 문구로 이를 채우지 마십시오.

마지막으로, 여러분의 역할이 변하고 있음을 받아들이십시오. 에이전트가 자율성을 얻음에 따라, 인간의 업무는 콘텐츠 생성에서 가드레일(guardrails) 설계로 전환됩니다. 여러분은 모델이 무엇을 건드릴 수 있는지, 언제 종료할 수 있는지, 그리고 문제가 발생했을 때 어떻게 행동해야 하는지를 결정하는 harness를 구축하고 있는 것입니다. 그것이 바로 엔지니어링이지, 프롬프팅이 아닙니다.

이 접근 방식에 영감을 준 소스와 추가적인 구현 세부 사항은 여기에서 확인할 수 있습니다.

AI 에이전트로 무언가를 구축하고 있으며 다른 실무자들과 의견을 나누고 싶다면, GyaanSetu 학습 커뮤니티를 여기에서 찾을 수 있습니다.