방구석컴퍼니
← 이야기 목록
AI 자동화2026-08-04

클로드 코드 훅은 왜 한 갈래로 못 믿는가 — 관측 레이어를 3중 폴백으로 설계한 이유

서브에이전트 관측을 전용 이벤트 하나에 걸면 42%가 유실된다. 이벤트 신뢰도를 실측으로 나누고, 이름 확보·상태 갱신·복원을 서로 다른 소스에 분산한 설계 근거를 정리했다.

방구석컴퍼니

혼자 일하는 사람 · 작업 기록

에이전트를 병렬로 굴리면 관측이 필요해집니다. 문제는 관측 레이어를 어디에 붙이느냐입니다.

이 글은 훅 기반 관측을 직접 설계하려다 이벤트가 오다 말다 해서 막힌 분을 위한 정리입니다. 훅은 이벤트에 프로그램을 걸어두는 것입니다.

클로드 코드는 SubagentStart / SubagentStop이라는 전용 이벤트를 제공합니다. 명세만 읽으면 선택지가 하나로 좁혀집니다. 저도 거기 걸었고, 화면은 조용히 비어 있었습니다.

이 글이 다루는 건 그 실패에서 나온 설계 결정입니다. 손을 어떻게 움직이느냐가 아니라 왜 그렇게 나눴는지가 내용입니다.

관측 대상이 스스로 상태를 말해준다

먼저 전제 하나를 분명히 합니다. 이 문제는 상태를 추론하는 문제가 아닙니다.

클로드 코드는 작업할 때마다 훅 이벤트를 밖으로 내보냅니다. 파일을 쓸 때, 검색할 때, 응답을 끝냈을 때 각각 신호가 나갑니다. 대략 30종 안팎이고 관측에 쓸 것은 그중 몇 개뿐입니다.

즉 프로세스를 폴링하거나 로그를 파싱해 추측할 필요가 없습니다. 대상이 자기 상태를 직접 알려줍니다. 설계 난이도는 여기서 크게 내려갑니다.

남는 문제는 하나입니다. 그 신호가 항상 오는가.

전용 이벤트의 실측 신뢰도

SubagentStart는 서브에이전트가 시작될 때 발화하고 agent_type을 payload에 담습니다. 문서상 이것 하나면 충분합니다.

한 사용자가 이 이벤트를 370건 추적했습니다. 결과는 이렇습니다.

항목실측
추적 건수370건
시작 이벤트 미발화42%
발화했으나 이름 필드 비어 있음발생함
종료 이벤트유실 잦음

해당 이슈는 not planned로 닫혔습니다. 수정 예정이 없다는 뜻이므로, 42% 유실은 버그가 아니라 현재 사양으로 취급해야 합니다.

여기서 설계 방향이 갈립니다. 유실을 예외로 두고 재시도를 붙일 것인가, 아니면 유실을 정상으로 가정하고 다른 소스를 함께 쓸 것인가.

재시도는 답이 아닙니다. 발화하지 않은 이벤트는 재시도할 대상 자체가 없습니다.

조용한 실패가 더 비싼 이유

두 번째 실패는 성격이 달랐습니다.

v2.1.63에서 훅 payload의 도구 식별 문자열이 조용히 교체됐습니다. 공지에 없었고, 더 까다로운 건 도구 필터 계층은 구 값도 계속 받아준다는 점입니다.

그래서 이런 상태가 됩니다.

계층동작
훅 필터통과 (Task 매칭됨)
훅 실행정상 발화
페이로드 문자열 비교실패 (AgentTask)
사용자에게 보이는 것아무것도 없음

예외가 발생하지 않습니다. 조건에 안 걸려서 그냥 지나가기 때문입니다. 에러 메시지가 있으면 최소한 검색이라도 하는데, 여기서는 검색할 문자열조차 없습니다.

관측 레이어를 설계할 때 이게 중요한 이유는, 관측 레이어의 실패는 정의상 조용하기 때문입니다. 감시하는 쪽이 고장 나면 감시 대상이 아니라 감시자가 침묵합니다. 그래서 이 계층에는 "실패하면 시끄럽게 만드는" 장치가 아니라 "실패해도 다른 경로로 메우는" 구조가 맞습니다.

이벤트별로 역할을 다르게 준다

핵심 결정은 이겁니다. 모든 이벤트를 같은 용도로 쓰지 않습니다.

이벤트마다 신뢰도와 담고 있는 정보가 다릅니다. 그러면 각자 잘하는 것만 시키면 됩니다.

담당 역할소스왜 이 소스인가
이름 확보도구 호출 직전 훅의 인자호출 자체가 있어야 에이전트가 뜨므로 발화가 보장됨
상태 갱신서브에이전트 전용 이벤트정확하지만 58%만 도착
상태 복원세션 기록 파일사후적이나 유실이 없음
자리 정리이벤트 부재 + 경과 시간종료 신호 유실을 흡수

이름을 전용 이벤트가 아니라 도구 호출 직전 훅에서 먼저 챙기는 게 이 설계의 중심입니다. 일을 넘기는 순간의 호출에는 어떤 종류의 에이전트를 부르는지가 인자로 들어 있습니다. 이름표를 붙이려고 따로 등록할 게 없었던 이유가 여기 있습니다 — 원래 실려 오던 정보를 받지 않고 있었을 뿐입니다.

식별자를 잘못 고르면 병렬에서만 틀린다

이 설계에서 실제로 밟은 함정 하나를 적어둡니다.

처음에는 subagent_type을 식별자로 썼습니다. 단일 실행에서는 완벽하게 동작했습니다. 문제는 같은 종류를 여러 개 동시에 띄웠을 때 나타났습니다.

상황기대실제
Explore 1개 실행캐릭터 1명캐릭터 1명 ✅
Explore 6개 병렬캐릭터 6명캐릭터 1명

subagent_type은 종류이지 개체가 아닙니다. 같은 종류가 전부 한 사람으로 합쳐졌습니다.

고약한 점은 하필 병렬일 때만 틀린다는 겁니다. 그런데 이 도구를 쓰는 이유가 정확히 병렬 상황이었습니다. 가장 필요한 순간에만 거짓말을 하는 화면이었던 셈입니다.

해결은 tool_use_id를 식별자로 쓰는 것입니다. 호출마다 고유하므로 개체가 분리됩니다.

관측 레이어의 실패는 왜 검출이 어려운가

일반적인 서비스 장애는 요청이 실패하면서 드러납니다. 사용자가 먼저 알고, 에러율이 튀고, 알림이 갑니다.

관측 레이어는 그 반대입니다. 여기가 죽으면 화면이 조용해질 뿐 아무것도 안 터집니다. 그리고 조용한 화면은 "지금 일이 없는 상태"와 육안으로 구분되지 않습니다.

그래서 이 계층에는 자기 진단이 하나 필요합니다. 저는 신호를 한 발 쏴서 캐릭터가 반응하는지 확인하는 절차를 넣었습니다. 실제 작업을 맡기기 전에 파이프가 살아 있는지부터 보는 것입니다.

지표 하나가 화면 전체의 신뢰를 결정한다

유휴 판정 시간도 같은 종류의 실수였습니다.

처음엔 12초 동안 신호가 없으면 노는 상태로 그렸습니다. 짧을수록 반응이 빠르다고 생각했습니다.

그런데 검색을 오래 하거나 빌드를 돌리는 동안에는 원래 신호가 안 나옵니다. 결과적으로 실제로 일하는 중인 13명이 전부 쉬는 것으로 표시됐습니다.

판정 시간결과
12초장시간 작업 중인 에이전트가 유휴로 오인됨
45초실제 유휴만 잡힘

여기서 얻은 원칙은 이렇습니다. 감시 화면이 거짓말을 하면 없느니만 못합니다. 지표의 임계값은 반응 속도가 아니라 관측 대상의 작업 리듬에서 나와야 합니다.

같은 이유로 반대 방향 보정도 필요했습니다. 몇 초 만에 끝나는 작업은 들어왔다 나가서 사람 눈이 못 따라갑니다. 그래서 퇴장 신호가 와도 최소 시간은 자리를 지키게 했습니다. 기록에는 남았는데 화면에는 아무도 없던 세 번째 사례가 이것이었습니다.

두 보정이 서로 반대 방향인 이유

유휴 판정은 늘리고 퇴장은 늦췄습니다. 방향이 반대인데 목적은 같습니다.

보정방향막으려는 오류
유휴 판정 12초 → 45초늦춤일하는 사람을 노는 걸로 표시
퇴장 후 자리 유지늦춤있었던 사람을 없던 걸로 표시

둘 다 화면이 실제보다 비어 보이는 쪽을 막습니다. 관측 도구에서 위양성(없는데 있다고 함)보다 위음성(있는데 없다고 함)이 더 비싼 이유는, 화면이 비어 있으면 사람이 확인을 멈추기 때문입니다.

포트 충돌은 왜 오진을 부르는가

세 가지 실패 중 첫 번째는 포트였습니다. 서버는 떴다고 나오는데 브라우저에는 전혀 다른 화면이 나왔습니다.

다른 프로그램이 같은 번호를 이미 점유하고 있었고, 요청이 그쪽으로 갔습니다. 번호만 바꾸면 끝나는 문제인데 화면에 뜬 메시지는 "찾을 수 없음"이라 원인이 거기 있는 줄 몰랐습니다.

교훈은 이렇습니다. 로컬 서버를 띄우는 도구는 포트를 고정하면 안 됩니다. 사용 중이면 다음 번호를 자동으로 찾게 만드는 편이, 사용자가 원인을 오진하는 비용보다 훨씬 쌉니다.

파서는 관대해야 정확하다

조직도 소스인 .claude/agents/*.md도 설계 판단이 필요한 지점이었습니다.

공식 스펙상 필수 프론트매터는 namedescription 둘뿐입니다. tools는 생략 가능하고, 인라인 콤마와 YAML 리스트가 모두 유효합니다. 그래서 같은 저장소 안에서도 파일 형식이 갈립니다.

엄격한 파서를 쓰면 명단이 반만 나옵니다. 실제로 그랬습니다. 설명 없이 저장해둔 파일들이 통째로 빠졌습니다.

폴백 체인은 선언된 설명 → 문서 제목 → 파일 이름 순으로 내려갑니다. 마지막 단계까지 가도 그 항목은 결과에 남습니다. 관대하게 만드는 것과 부실하게 만드는 것은 다릅니다. 형식이 제각각인 게 정상인 입력에서는 관대한 쪽이 더 정확합니다.

자동 배정에 예외 규칙을 더하지 않는다

역할별 자리 배정은 이름과 설명의 키워드로 결정합니다. 29명 중 28명은 알아서 맞는 자리로 갔고, 1명이 엉뚱한 곳에 앉았습니다.

여기서 규칙을 하나 더 붙이고 싶어집니다. 그 유혹을 눌러야 합니다.

키워드 규칙은 전역으로 적용되므로, 예외 하나를 잡으려고 조건을 추가하면 이미 잘 맞던 나머지가 흔들립니다. 28명을 위험에 빠뜨려 1명을 구하는 거래입니다.

수동 오버라이드를 남기는 쪽이 낫습니다. 단, 여기에 조건이 하나 붙습니다 — 손으로 고친 값은 재실행에서 살아남아야 합니다. 스캔은 실행할 때마다 다시 도는데 결과를 그대로 덮으면 수정이 매번 사라집니다. 이전 결과에서 자동 배정과 다른 값만 골라 되살리는 방식으로 해결했습니다.

자동 배정에서 수동 오버라이드로 넘어가는 손익분기

규칙 기반 자동화는 정확도가 90%대에 들어서면 나머지를 규칙으로 메우는 비용이 급격히 오릅니다.

29명 중 28명이 맞았으니 96.5%입니다. 남은 1명을 위해 조건을 추가하면 이미 맞은 28명의 판정이 흔들릴 위험을 감수해야 합니다. 반면 수동 오버라이드는 그 1명에게만 영향을 줍니다.

판단 기준은 이겁니다 — 예외를 규칙으로 흡수했을 때 기존 정답이 재검증돼야 하는가. 재검증이 필요하면 오버라이드가 싸고, 필요 없으면 규칙 추가가 낫습니다.

이 설계가 해주지 않는 것

관측 레이어의 경계를 분명히 긋습니다. 이 구조가 제공하는 것은 두 가지뿐입니다.

  1. 지금 어떤 에이전트가 무엇을 하는지 한 화면에서 확인
  2. 승인 대기 상태가 생기면 즉시 인지

제공하지 않는 것은 이렇습니다.

  • 위험한 명령 차단 — 훅으로 가능한 영역이지만 이 레이어의 책임이 아닙니다
  • 실행 전 정적 검사
  • 진행 중 작업의 중단
  • 비용 추적

한 매체는 이 부류의 도구를 두고 "멈출 수도 없는 에이전트를, 그래픽만 좋아진 상태로 여전히 지켜보고 있는 것"이라고 썼습니다. 반박할 게 없습니다. 완전 자동화를 목표로 잡는 순간 이 설계는 실패작이 됩니다.

애초에 목표가 달랐습니다. 관측이 필요한 이유는 아직 위임 대상을 완전히 신뢰하지 않기 때문이고, 그걸 인정하는 게 출발점입니다.

직접 만들어야 했던 이유

성능 때문이 아닙니다.

같은 소재의 오픈소스 도구가 이미 있습니다. 깃허브 별 8,800개, 설치 78,459회, 2026년 2월 24일 공개. 혼자 만들어 무료로 푼 프로젝트입니다. 저도 이걸로 시작했습니다.

그 저장소에도 서브에이전트가 안 보인다는 이슈가 올라와 있었고, 상위에서 고쳐야 한다는 이유로 닫혔습니다. 여기서 더 손대지 않겠다는 뜻입니다.

외부 프로젝트에 의존하고 있었다면 그 시점에 남는 선택지는 대기뿐입니다. 자체 구현의 값어치는 성능이 아니라 대응 지연이 0에 가깝다는 데 있습니다. 위 폴백 체인을 전부 쌓는 데 든 시간은 하룻저녁이었습니다.

정리

  • 전용 이벤트가 있어도 신뢰도를 실측하기 전에는 단독으로 걸지 않습니다.
  • 이벤트마다 역할을 나눕니다. 가장 확실한 소스에서 가장 중요한 정보(이름)를 챙깁니다.
  • 식별자는 종류가 아니라 개체 단위로 잡습니다. 종류로 잡으면 병렬에서만 틀립니다.
  • 임계값은 반응 속도가 아니라 관측 대상의 작업 리듬에서 도출합니다.
  • 입력 형식이 제각각인 게 정상이면 파서는 관대한 쪽이 정확합니다.
  • 자동 규칙의 예외는 규칙 추가가 아니라 수동 오버라이드로 처리하되, 재실행에서 살아남게 만듭니다.

비슷한 구조를 다른 에이전트 런타임에 붙여보신 분이 있다면, 그쪽 이벤트 신뢰도는 어땠는지 궁금합니다.

전체 구현 과정과 실패 지점은 유튜브 채널 방구석컴퍼니에 영상으로 정리해뒀습니다.

새 글 알림

이런 작업 기록, 계속 받아보실래요?

네이버 블로그 이웃추가하면 새 글이 피드로 바로 떠요. 광고 없이 진짜 이야기만.