
「에러가 나지 않는다」는 작동하고 있다는 증거가 되지 않는다 —— Claude Code의 hook이 침묵한 채 멈춰 있었던 이야기
요약
Claude Code의 hook 기능을 활용해 AI의 실수를 방지하는 자동 메모 제시 시스템을 구축했으나, 에러 없이 작동하지 않았던 사례를 다룹니다. 설계 의도대로 '일치하는 항목이 없을 때 침묵'하도록 만든 로직이 오히려 시스템의 중단을 감지하지 못하게 만든 과정을 설명합니다.
핵심 포인트
- Claude Code의 hook 기능을 이용한 자동 컨텍스트 주입 메커니즘 설명
- 에러가 발생하지 않는 것이 시스템의 정상 작동을 보장하지 않음
- 조건 불일치 시 '침묵'하도록 설계한 로직의 잠재적 위험성
- AI 에이전트 활용 시 모니터링 및 검증의 중요성
「그 감시자, 한 번이라도 울린 적 있습니까?」
얼마 전, 이런 질문을 받고 나서야 내가 만든 자동 체크가 2일 동안 전혀 작동하지 않았다는 사실을 깨달았다.
에러는 한 번도 발생하지 않았다. 로그에도 아무것도 남지 않았다. 테스트는 전부 통과했다. 그런데도 작동하고 있지 않았다.
게다가 그 2일 동안 나는 내내 「순조롭게 돌아가고 있다」고 믿고 있었다. 그렇게 믿을 만한 이유가 있었다 —— 이것이 이 기사의 이야기다.
우선 상황부터 (AI에 익숙하지 않은 분들을 위해)
나는 Claude Code라는, AI가 코드나 파일을 편집하도록 하는 도구를 일상적으로 사용하고 있다. 터미널 안에서 AI와 대화하며 작업을 진행하는 것이라고 생각하면 된다.
편리하지만, 한 가지 곤란한 점이 있다. AI는 「조심해줘」라고 말해도 그다지 조심해주지 않는다.
예를 들어 나는 「같은 실수를 반복하지 않기 위한 메모」를 모아두고 있다. 과거에 저질렀던 사고와 다음에 어떻게 해야 하는지를 한 줄씩 써 내려가는 실수 메모다. 이전에는 「작업 전에 이걸 읽어줘」라고 AI에게 부탁하곤 했다. 하지만 읽지 않을 때가 있다.
그렇다고 매번 전부 읽게 하는 것도 좋지 않다. 20건이나 쌓이면 관계없는 항목에 파묻혀, 정작 중요한 한 건까지 함께 읽히고 지나쳐 버린다.
그래서 이런 장치를 만들었다.
AI가 어떤 조작을 하려고 하는 순간, 그 조작과 관련된 메모만을 자동으로 눈앞에 제시하는 것이다.
예를 들어 AI가 「이제부터 공개 처리를 하겠습니다」라는 단계에 이르면, 과거에 공개 관련해서 저질렀던 사고의 교훈만이 슥 나타난다. 이미지만 만들고 있는 날에는 공개 관련 이야기는 전혀 나오지 않는다.
이를 실현하는 메커니즘이 **hook (후크)**다.
hook을 3줄로 요약하면
- Claude Code에는 「AI가 무언가를 하기 직전·직후에, 내가 작성한 프로그램을 끼워 넣는」 기능이 있다. 이것이 hook이다.
- 끼워 넣은 프로그램이 반환한 문장은 그대로 AI의 눈에 들어간다.
- 「어떤 조작 시에 끼워 넣을 것인가」는 조작의 이름을 써서 지정한다.
마지막 한 줄이 이번 사고의 무대가 된다.
hook 자체를 만드는 방법이나 함정은 이전에 다른 기사에 정리했다. 이 기사는 그 뒷이야기 —— 「만든 hook이 사실은 작동하고 있지 않았다」는 후일담에 해당한다.
설치한 메커니즘의 단 하나의 설계 방침
만든 것은 단순하다.
- 실수 메모의 각 줄에 「어떤 조작 시에 내보낼 것인가」의 태그를 붙인다 (공개할 때 / 삭제할 때 / 서브 에이전트(Sub-agent)를 기동할 때...)
- AI가 조작하려고 하는 순간, hook이 그 조작을 확인한다.
- 태그가 일치하는 줄만 제시한다. 일치하는 줄이 하나도 없다면 아무것도 내보내지 않는다.
서브 에이전트란, 본체 AI가 「이 조사 업무는 맡기겠다」며 또 다른 AI를 별도로 세워 일을 던지는 기능이다. 사람을 고용해 일부를 외주 주는 것과 비슷하다. 본체는 본체대로 작업을 계속할 수 있어 편리하지만, 던져진 곳에서 무슨 일이 일어나고 있는지는 본체에서 보기 어렵다. 그래서 「던질 때야말로 과거의 실수를 떠올리고 싶다」는 조작이기도 했다.
세 번째가 핵심이었다. 관계없는 것이 매번 나오면 사람도 AI도 읽지 않게 된다. 그래서 해당 사항이 없을 때는 완전히 침묵한다. 이것은 의도된 설계다.
테스트도 작성했다. 수동으로 「공개 조작이 들어왔다」는 가정의 데이터를 흘려보내면, 제대로 해당 줄이 반환된다. 전부 통과했다. 좋아, 완성이다, 라고 나는 생각했다.
2일 후
나는 이 프로젝트에서 가끔 콜드 스타트 (Cold Start) 테스트를 하고 있다.
오늘의 대화를 전혀 모르는 다른 AI를 세우고, 인수인계 메모만을 읽게 한다. 예열된 상태에서 작업을 이어가는 것이 아니라, 완전히 식어 있는 상태에서 바로 달려 나갈 수 있는지를 보는 것이다. 바로 달려 나갈 수 있다면 기록이 제대로 기능하고 있다는 증거이고, 막힌다면 기록 쪽에 구멍이 있다는 뜻이다.
그 테스트 도중 지적이 돌아왔다.
"서브 에이전트를 기동하는 조작에서는 이 메커니즘이 한 번도 발화하지 않았습니다."
반신반의하며 기록을 다시 확인했다. 과연 그 조작 때만 아무것도 나오지 않았다. 다른 조작(명령 실행, 파일 편집)에서는 정상적으로 나오고 있었다. 서브 에이전트의 기동만 계속 무반응이었다.
왜 2일 동안이나 눈치채지 못했는가
이 부분이 이 기사에서 가장 쓰고 싶었던 대목이다.
이 메커니즘은 「해당 사항이 없으면 아무것도 내보내지 않는다」는 설계였다.
다시 한번 화면에서 일어날 수 있는 상태를 나열해 보겠다.
| 실제로 일어나고 있는 일 | 화면에 보이는 것 |
|---|---|
| 정상적으로 작동하고 있으며, 우연히 해당되는 교훈이 없음 | 아무것도 나오지 않음 |
| 애초에 한 번도 호출되지 않음 (고장 남) | 아무것도 나오지 않음 |
똑같다. 구별할 실마리가 어디에도 없다.
에러가 발생하는 타입의 고장이라면 싫어도 알아챈다. 빨간 글씨가 나타나며 작업이 멈추기 때문이다. 하지만 이번 고장은, 정상적인 정적과 구별할 수 없다.
게다가 나는 다른 조작에서 정상적으로 발화하는 것을 매일 보고 있었다. 그래서 "이 메커니즘은 작동하고 있다"라고 굳게 믿고 있었다. 부분적으로 작동하는 것은 전체가 작동하는 것처럼 보인다. 이것이 2일간의 정체였다.
원인은 허탈할 정도로 단순했다
hook 설정에는 "어떤 조작 시에 개입할 것인가"를 조작의 이름을 나열하여 작성한다. 내가 작성했던 것은 이것이다.
{
"matcher": "Bash|Task",
"hooks": [{ "type": "command", "command": "sh ledger_inject_filter.sh" }]
...
matcher가 "이 이름의 조작이 오면 개입하라"는 지정이고, |는 "또는"이다. 즉, "커맨드 실행 또는 Task"라고 적고 있었다.
왜 Task라고 했는가. 서브 에이전트(Sub-agent)는 업무(Task)를 분리하여 다른 AI에게 던지는 기능이기에, 조작 이름도 Task일 것이라고 생각했다. 조사하지 않았다. 문서도 열어보지 않았다. 그럴듯한 단어를, 그럴듯하다는 이유만으로 적었다.
실제 이름은 Agent였다.
"matcher": "Bash|Task|Agent"
수정한 것은 이것뿐이다. 이름이 일치하지 않기 때문에, 개입 프로그램은 단 한 번도 호출되지 않았었다. 내용은 완전히 올바랐고 테스트도 정상적으로 통과했다. 호출되기만 하면 작동할 것이, 호출되지 않았을 뿐이다.
수정은 글자 한 자 수준의 일이었고, 5분 만에 끝났다. 수동으로 실제로 흘려 넣어 이번에는 제대로 3건의 교훈이 반환되는 것도 확인했다.
허탈했다. 동시에 약간 등골이 서늘해졌다. 이 5분의 수정을 찾아내기까지 2일이 걸렸기 때문이다.
일반화하자면, 아마 이런 내용이다
이번 실패를 Claude Code를 사용하지 않는 사람에게도 통하는 형태로 정리해 본다.
1. 이름을 추측으로 적지 마라
외부 도구나 프레임워크가 결정한 이름——설정 키, 이벤트 명, 식별자 종류——를 "아마 이럴 것이다"라는 추측으로 적으면, 조용히 빗나간다.
Task는 자연스러운 추측이었다. 자연스러웠기에 의심하지 않았다. 추측이 자연스러울수록, 틀렸을 때 알아차리기 어렵다.
작성하기 전에 실물을 확인하라. 확인할 수단이 없다면 다음 항목으로 보완하라.
2. "아무것도 없으면 침묵하는" 것은, 일부러 한 번 울려보기 전까지 완성된 것이 아니다
이것이 핵심이다.
조건부로 작동하는 것——알림, 필터, 모니터링, 검증(Validation)——은 작동하고 있다는 증거가 "아무 일도 일어나지 않는 것"이 되기 쉽다. 그리고 아무 일도 일어나지 않는 것은 고장 났다는 증거이기도 하다.
그러므로 만들었다면 일부러 조건을 충족시켜 한 번 울려라. 그것이 유일한 생존 확인이다.
내가 이번에 했던 것은 "수동으로 데이터를 흘려 넣는 테스트"까지였다. 그것은 내용의 검증은 될지언정, 실제 경로로 연결되어 있는지에 대한 검증은 되지 않는다. 내용이 올바르더라도 배선이 빠져 있다면 아무 일도 일어나지 않는다. 내가 확인하지 못했던 것은 바로 그 배선 쪽이었다.
3. 조용히 멈추는 메커니즘은 소란스럽게 멈추는 메커니즘보다 위험하다
소란스럽게 멈추는 메커니즘은 그 자리에서 수정된다. 조용히 멈추는 메커니즘은 멈춘 채로 며칠이고 방치된다.
게다가 까다로운 점은, 조용히 멈춘 메커니즘이 "지켜주고 있다"는 안도감만은 계속 제공한다는 것이다. 나는 이 2일 동안, 필요한 상황에서는 과거의 교훈이 자동으로 제시될 것이라 믿으며 작업했다. 실제로는 서브 에이전트를 기동하는 상황에서는 단 하나도 제시되지 않았다. 지켜지지 않고 있는데도, 지켜지고 있다고 믿고 있었다. 아무것도 없는 상태보다 훨씬 질이 나쁘다.
찾아낸 것은 문맥을 모르는 누군가였다
한 가지 더 적어두고 싶은 것이 있다.
이 결함을 찾아낸 것은 내가 아니었다. 오늘 작업의 경위를 전혀 모르는, 백지 상태의 AI였다.
나는 「작동하고 있다」는 선입견을 가지고 있었다. 직접 만든 사람이니 당연한 일이다. 하지만 아무런 정보가 없는 상태에서 바라보면, "이 메커니즘, 실행(trigger)된 기록이 하나도 없네요"라는 소박한 관찰이 된다. 선입견이 없는 만큼 사실이 그대로 보인다.
이것은 인간 사이에서도 마찬가지라고 생각한다. 만든 본인은 작동하고 있다는 것을 전제로 화면을 본다. 전제를 가지지 않은 사람만이 "이거, 정말로 작동하고 있나요?"라고 물을 수 있다.
정기적으로, 전제를 가지지 않은 눈을 거쳐라. 이번에는 그것이 효과가 있었다.
내일 할 수 있는 것
단 하나만 가져가실 수 있다면, 바로 이것이다.
자신이 설계한 「조건이 갖춰졌을 때만 작동하는 메커니즘」을, 일부러 조건을 충족시켜서 한 번 울려보는 것이다.
알림이든, 모니터링 알람(alert)이든, 입력 체크(input check)든, 자동 백업(automatic backup)이든 상관없다. 마지막으로 울린 게 언제인가요? 그 답이 "기억나지 않는다"라면, 그것은 조용히 작동하고 있다는 증거가 아니라, 침묵한 채 멈춰 있을 가능성이 있다.
울려보는 것은 대개 5분이면 끝난다. 나의 경우, 그 5분을 아끼려다 깨닫기까지 2일이 걸렸다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기