하네스(harness — AI가 다 했다고 거짓말 못 하게 증거를 강제하는 감시 장치) 연구 · 2026-07-30

에이전트 스코프 훅은
이미 있었다

문제는 이것이었다. 내 하네스에는 사용자 단위·프로젝트 단위의 자동 검사 파이프라인은 있는데, 에이전트 하나하나에 따로 거는 검사는 없었다. 그래서 "플랫폼이 지원하지 않는다"는 통념이 정말 맞는지 직접 실측했다. 통념은 절반만 맞았다. Claude Code 플랫폼은 이미 세 가지 방식으로 지원하고 있었다. 없는 것은 하네스가 그걸 쓰지 않은 것뿐이었다. 89개 에이전트 중 채택은 0개였다. 용어를 짚어 두면 이렇다. 하네스는 AI가 일을 다 했다고 거짓말하지 못하게 증거를 강제하는 감시 장치다. 파이프라인(pipeline)은 그 검사가 순서대로 이어지는 흐름이다. 에이전트(agent)는 역할을 나눠 맡는 AI 작업자다. 이 글에서는 에이전트마다 따로 거는 그 검사를 에이전트 스코프 훅이라 부른다. 이 페이지는 그 원인분석과 실측, 그리고 만들어 본 파이프라인을 정리한다.

없는 건 기능이 아니라 채택이었다

먼저 용어 하나. 훅(hook)은 "특정 순간이 오면 자동으로 실행되는 작은 검사 스크립트"다. 예를 들어 파일을 저장할 때마다 문법을 검사하는 것, 서버에 코드를 올리기 직전 테스트가 통과했는지 확인하고 막아서는 관문이 훅이다. Claude Code에서 이 훅은 지금까지 "사용자 전체" 또는 "프로젝트 하나" 단위로만 거는 줄 알려져 있었다. 이렇게 설정이 적용되는 범위를 스코프(scope)라고 부른다. 그런데 "에이전트 하나하나마다 다른 훅을 건다"는 건 못 한다는 통념이 있었다. 이 글의 주제인 에이전트 스코프 훅이 바로 그것이다. 실측해 보니 플랫폼은 이미 세 가지 방법으로 이걸 지원하고 있었다. 진짜 문제는 우리 하네스가 89개 에이전트 중 단 하나도 그 기능을 쓰지 않았다는 것이었다.

0 / 89에이전트 채택률
3플랫폼 지원 방식
8격리 실측 실험
13Codex 적대 리뷰
플랫폼(Claude Code v2.1.219)이 이미 지원하는 3가지 — 전부 실측 확인 frontmatter 훅 에이전트 정의 파일(.md) 맨 위 설정칸에 훅을 직접 적음 그 에이전트가 도는 동안만 유효 ✓ 발화 확인 (debug 로그) 사용자 스코프는 신뢰승인 불필요 agent_type 식별자 훅에 들어오는 입력(stdin)에 "지금 어느 에이전트냐"가 담김 메인 대화엔 없고 서브에이전트만 ✓ 에이전트 3건 / 메인 0건 실측 = 분배기의 판단 근거가 됨 SubagentStart 필터 서브에이전트가 시작/종료할 때 에이전트 이름으로 훅을 매칭 matcher: "hookprobe" 정확 발화 ✓ 4/4 매칭, 오발화 0건 이벤트: Start · Stop · TaskDone… 플랫폼은 준비돼 있었다 — 하네스 채택만 0 / 89 user·project·plugin 에이전트 전부에서 훅 사용 0건
세 가지 지원 방식을 8개 격리 실험으로 하나씩 발화시켜 확인했다. 격리 실험이란 다른 훅을 전부 끈 상태에서 검사 대상 하나만 켜고 돌리는 실험이다. 초록 체크(✓)는 실제 로그·덤프 파일(훅에 들어온 입력을 그대로 남긴 파일)로 증거를 남긴 항목이다.
왜 아무도 안 썼나 — 두 층의 이야기

원인은 두 겹이다. 하나는 기능이 늦게 생긴 플랫폼의 역사다. 다른 하나는 그 기능이 생긴 것을 알아채지 못한 우리 하네스의 사각지대다. 진짜 원인은 오른쪽, 하네스 쪽이다.

플랫폼 — 역사적 이유

늦게 생긴 기능

"원래 훅은 설정 파일만 소유했다"

  • 초기 설계에서 훅은 사용자·프로젝트 설정 파일에만 존재했다. 에이전트 정의 파일(에이전트의 역할·도구·지시를 적어 둔 문서)은 훅을 가질 수 없었다
  • 훅에 들어오는 입력에 "어느 에이전트냐" 정보가 아예 없었다. 그래서 훅 하나가 에이전트별로 나눠 주는 분배 방식으로 우회하는 것조차 불가능했다
  • 에이전트 파일은 저장소로 유통되는 문서다. 거기에 실행 명령을 붙이면 악성 저장소가 내 컴퓨터에서 명령을 실행하는 공격 통로가 된다
  • 그래서 나중에 기능이 추가될 때도 프로젝트 에이전트에는 "신뢰 승인"(이 저장소의 에이전트 훅을 실행해도 좋다고 사용자가 한 번 허락하는 관문)이 함께 붙었다 (실측 재현)
하네스 — 진짜 원인

보이지 않던 사각지대

"기능이 생긴 걸 관측할 표면이 없었다"

  • 파이프라인이 기능 등장 전에 굳어졌다. 그 뒤로 다시 검토할 계기(트리거)가 없었다
  • 참조하던 문서 스냅샷(특정 시점에 저장해 둔 사본)이 낡았다. 에이전트 정의 파일 맨 위 설정칸에 적는 frontmatter 훅도, "어느 에이전트냐"를 알려주는 에이전트 종류 입력 필드도 그 문서에는 아예 없었다. 그 입력 필드의 이름은 agent_type(어느 에이전트 안에서 도는지 알려주는 값)이다.
  • 훅 입력에 실리는 그 에이전트 종류 필드를 텔레메트리(관측 로그 — 훅이 언제 어디서 돌았는지 남기는 기록)가 기록하지 않았다. 그래서 "훅이 에이전트 안에서 돈다"는 사실이 측정에 안 잡혔고, 갭이 갭으로 안 보였다
  • 도구 화이트리스트(에이전트가 쓸 수 있는 도구 목록)·종료 훅·문장 지시로 우회해 왔다. 그래서 기능 부재가 마찰로 드러나지 않았다
같은 명령이 위치에 따라 갈린다

방안의 심장은 분배기(dispatcher)다. 분배기란 설정 파일에 걸어 둔 훅 하나가 "지금 어느 에이전트냐"를 읽어 알맞은 검사로 넘겨주는 장치다. 그래서 89개 에이전트 파일을 하나도 안 건드리고 중앙에서 에이전트별 정책을 관리할 수 있다. 훅에 들어오는 입력에는 "어느 에이전트냐"를 알려주는 에이전트 종류 필드가 담겨 있다. 분배기는 그 값만 본다. 아래는 실제로 돌려 본 결과다. 그림에서 분배기가 읽는 그 필드의 이름은 agent_type이다. 똑같은 date 명령이 메인 대화에선 통과하고, 특정 에이전트 안에서는 막힌다.

agent_type 분배기 — 하나의 훅, 위치별 상이 판정 메인 대화가 실행: date 입력에 agent_type 없음 hookprobe 에이전트가 실행: date 입력에 agent_type = hookprobe 분배기 (설정 훅 1개) 입력의 agent_type을 읽어 알맞은 검사로 넘김 · 89개 에이전트 파일 무수정 에이전트 없음 hookprobe 규칙 적용 통과 · exit 0 메인엔 이 게이트가 없으므로 그대로 실행 차단 · exit 2 이 에이전트 안에선 echo만 허용 → date 거부 실측 e2e: MAIN_DATE=통과 · AGENT_ECHO=통과 · AGENT_DATE=차단
실제 Claude 세션으로 돌린 결과다. 훅은 단 하나인데, 입력에 담긴 에이전트 이름을 보고 같은 명령을 다르게 판정한다. 이것이 에이전트 스코프 훅의 핵심이다.
3층으로 짓는다 — 관측부터

권장 순서는 C → B → A다. 관측(C)을 먼저 깔아야 다음 두 층의 효과가 수치로 측정된다. 분배기(B)가 중앙 규약을 세운 뒤, 개별 훅(A)은 꼭 필요한 고위험 에이전트에만 붙인다. 이 순서가 유지비가 가장 낮다.

Tier C · 먼저

관측

텔레메트리(관측 로그)에 두 필드를 추가한다. 하나는 어느 에이전트 안에서 돌았는지 알려주는 에이전트 종류이고, 다른 하나는 그 실행 하나하나를 구분하는 에이전트 고유 번호다. 이게 없으면 아래 두 층을 깔아도 "훅이 에이전트 안에서 돈다"는 사실이 측정에 안 잡힌다. 몇 줄짜리 변경이다. 두 필드의 이름은 차례로 agent_type과 agent_id(실행마다 따로 붙는 번호)다.

2
추가 필드
최소
변경량
Tier B · 중심

분배기

설정 훅 하나가 훅 입력에 실리는 에이전트 종류 필드를 읽어 에이전트별 검사로 넘긴다. 89개 에이전트 파일을 안 건드리고 중앙에서 정책을 관리한다. "특정 에이전트 안에서만 위험 명령 차단" 같은 강제 게이트(gate — 조건을 못 채우면 실행 자체를 막는 관문)를 실증 완료했다. 분배기가 읽는 그 필드의 이름은 agent_type이다.

1
중앙 훅
0
에이전트 수정
Tier A · 선별

개별 훅

에이전트 정의 파일에 훅을 직접 붙인다. 사용자 스코프(내 컴퓨터 전체에 적용되는 범위)의 에이전트는 신뢰 승인 배관 없이 바로 동작한다(실측). 인프라를 되돌릴 수 없게 만지는 배포 담당 devops, 사용자 대신 결과를 검증하는 user-proxy 같은 고위험 에이전트에만 선별 적용한다.

고위험
선별 적용
즉시
user 스코프
지원 방식 플랫폼 상태 하네스 채택 실측 증거
frontmatter 훅 지원됨 — 그 에이전트가 도는 동안만 유효 0 / 89 debug 로그(개발자용 상세 실행 기록) Registered 1 frontmatter hook(s)
agent_type 입력(에이전트 종류 필드) 지원됨 — 서브에이전트(메인 대화가 일을 맡기려고 띄우는 하위 에이전트) 훅에만 존재 미사용 에이전트 3건 vs 메인 0건 (덤프)
SubagentStart 필터 지원됨 — 에이전트 이름으로 매칭 미사용 matcher(이름 필터) 4/4 발화, 오발화 0
신뢰 승인 관문 프로젝트 에이전트만 필요 · 사용자 스코프는 불필요 즉시 채택 가능 "not trusted"(신뢰 안 됨) 에러 재현
"헛소리라고 가정하라" — 13라운드

방안을 실제 코드로 만든 뒤, 다른 모델(Codex)에게 "이건 헛소리다"라고 가정하고 결함을 찾게 했다. 이런 검증을 적대 리뷰라고 부른다. 라운드마다 진짜 결함이 나왔다. 결함에는 등급을 매겼다. 핵심 로직의 심각 결함은 6라운드에서 사라졌다. 이후는 곁가지(로그 유틸리티)의 하드닝(hardening — 우회 경로를 하나씩 막아 단단하게 만드는 일)이 이어지다 13라운드에서 전부 통과(CLEAN)로 수렴했다. 아래 타임라인에서 위쪽 붉은 마커는 심각 등급, 아래 회색 마커는 높음 등급이다. 심각 등급은 반드시 고쳐야 배포할 수 있는 결함이다. 높음 등급은 심각 다음으로 위험한 결함이다. 리뷰 도구가 두 등급을 적는 원래 표기는 차례로 심각 등급(CRITICAL)과 높음 등급(HIGH)이다.

Codex 적대 리뷰 R1 → R13 수렴 CRITICAL HIGH R1 eval 우회 R2 개행 우회 R3 R4 R5 R6 TOCTOU·토큰화 R7 R9 R10 R11 디렉토리 R12 R13 CLEAN 핵심 로직 — 심각 결함 (R6에서 종료) 곁가지 로그 유틸 하드닝 → 수렴 각 수정마다 우회 벡터를 단위 테스트로 전수 방어 확인 · 공백 경로 e2e 재검증
라운드가 반복되며 같은 계열의 결함이 되풀이될 때가 있다. 문자열 필터의 무한 우회, 로그의 적대적 파일시스템 공격(공격자가 파일·폴더를 바꿔치기해 로그 쓰기를 속이는 수법)이 그랬다. 그럴 때는 6번째 패치 대신 설계·주장을 낮추는 것이 옳다. 완전 방어가 불가능한 부분은 OS 격리(운영체제가 프로세스를 가두는 방식)에 위임하고 그 한계를 문서로 남겼다.
핵심 통찰

"헛소리라고 가정하라"는 적대 검증이 실제로 결함을 잡았다. 그중 하나는 내가 놓칠 뻔한 진짜 버그였다. 로그 유틸(기록을 파일에 쓰는 작은 도구)을 파이썬으로 바꾸면서 입력 통로가 막혀 데이터가 하나도 안 써졌다. "실제로 기록됐는지" 검증하는 습관 덕에 잡았다. 결론은 단순하다. 에이전트 스코프 훅은 이미 있었고, 없던 건 채택뿐이다. 관측(C) → 분배기(B) → 개별 훅(A) 순으로 3층을 세우면 89개 에이전트를 안 건드리고도 에이전트별 강제 게이트를 얻는다.