Harness · 관측 복구 · 2026-08-01

돌고 있다고 믿었다
아는 것은 아니었다

질문은 하나였다 — 에이전트·스킬 스코프 훅 파이프라인은 존재하는가, 그리고 실제로 도는가. 답은 "돈다"였다. 다만 그걸 아는 방법이 없었고, 조사 도중 프로젝트 스코프 605개 훅이 통째로 측정 밖이라는 게 드러났다. 관측을 복구하고 나서야 서브에이전트 안의 발화가 처음으로 보였다 — 누적 11,502건.

한 문장으로 — 돌고 있었다, 다만 볼 수 없었다

원래 질문은 "에이전트·스킬 스코프 훅 파이프라인이 존재하고 실제로 도는가"였다. 답은 둘로 갈린다 — 전역 훅은 서브에이전트 안에서 확실히 돈다(누적 발화 11,502건, 25개 에이전트, 실제 차단 18건). 반면 에이전트·스킬마다 다른 훅을 거는 "차등" 기능은 채택이 0이다. 플랫폼은 이미 세 가지 방식으로 지원하는데 하네스가 하나도 쓰지 않았다. 그리고 이 사실을 확인하려다 프로젝트 스코프 605개 훅이 통째로 측정 밖이라는 별개의 문제가 드러났다.

11,502서브에이전트 내 발화(누적)
25에이전트 종류
18에이전트 내 실제 차단
0 / 89 · 0 / 555agent · skill 차등 채택
605project 훅
0% → 100%관측 커버리지
11교차 리뷰 적발 결함
8h 03m소요
1단계 · 타당성

분석이 맞는지부터 다시 쟀다

문서를 모르는 blind 감사 2기가 디스크에서 독립 재실측. 코드 주장 24항목 전건 확인, 수치 정정 2건, 반증 1건.

2단계 · 복구

래퍼를 씌워 발화를 기록

정본 4파일 수정 + 19개 프로젝트 롤아웃. 훅 발화가 (태그, 스코프, 훅명, exit)으로 남는다.

3단계 · 검증

내 수정에서 결함 11건이 나왔다

Claude QA는 4라운드 내내 전건 PASS. 같은 코드에서 Codex가 CRITICAL 3·HIGH 4·MEDIUM 4를 적발.

에이전트·스킬 스코프는 존재하는가, 도는가

두 질문을 분리해야 답이 정확해진다. ①전역 훅이 에이전트 안에서도 도는가②에이전트·스킬마다 다른 훅을 걸 수 있는가는 다른 얘기다. 전자는 "돈다"이고, 후자는 "플랫폼은 되는데 우리가 안 썼다"이다. 그리고 ①을 증명할 수 있게 된 것은 이번 작업 이후다 — 그전까지 텔레메트리는 훅 이름만 남기고 어느 에이전트 안에서 돌았는지를 기록하지 않았다. 도는 걸 알 수 없으니 "없는 것"처럼 보였던 셈이다.

두 갈래 — 전역 훅의 에이전트 내 작동 vs 에이전트별 차등 ① 전역 훅이 에이전트 안에서 도는가 → 돈다 (실측 확인) settings.json 전역 훅 메인 대화 · 서브에이전트 동일 적용 서브에이전트 25종 내부 발화 11,502건 exhaustive-auditor · web-qa-tester · viz-recheck … 실제 차단 18건 (exit 2) 동적명령 7 · scaffold 위반 6 · 세션탈취 4 · 스킬스캔 1 10개 에이전트에 이름까지 귀속됨 SubagentStop 62건 기록 종료 이벤트도 발화 — 파이프라인 실재 “없다”가 아니라 “안 보였다” agent_type 도입(7/31 14:01Z)~8/1 04:27Z 누적 ② 에이전트·스킬마다 다른 훅 → 플랫폼은 지원, 채택 0 agent frontmatter hooks 0 / 89 에이전트 정의 파일에 훅 선언 — 사용 0 skill frontmatter hooks 0 / 555 1회용(once) 훅도 여기서만 동작 — 사용 0 Subagent 이벤트 agent-type matcher matcher 전부 빈 값 등록은 됐으나 전 에이전트 일괄 적용 결과 — 모든 에이전트가 같은 훅만 받는다 고위험 에이전트에만 더 조이는 것이 불가능 파손이 아니라 미채택 — 기회 손실 차등은 못 하지만 사각은 아니다 전역 훅이 이미 모든 에이전트를 덮고 있다
왼쪽은 이번에 실측으로 증명된 것, 오른쪽은 플랫폼 기능이 있는데 쓰지 않은 것. 두 질문을 섞으면 "파이프라인이 없다"는 잘못된 결론이 나온다.
차등 방식플랫폼 지원하네스 채택실작동 증거
전역 훅 (settings.json) 지원 — 메인·서브에이전트 동일 전면 사용 발화 11,502건 · 차단 18건(동적명령 7·scaffold 6·세션탈취 4·스킬스캔 1), 10개 에이전트에 귀속
agent frontmatter hooks: 지원 (프로젝트 에이전트는 신뢰 승인 관문 동반) 0 / 89 격리 실험으로 동작 확인, 운영 채택 없음
skill frontmatter hooks: 지원 — once: true(1회 후 자동 해제)는 여기서만 동작 0 / 555 격리 실험으로 동작 확인, 운영 채택 없음
Subagent 이벤트 agent-type matcher 지원 등록됨, 단 matcher 전부 빈 값 SubagentStop 62건 발화 — 다만 전 에이전트 일괄
왜 "없다"고 보였나

텔레메트리가 훅 이름만 기록하고 어느 에이전트 안이었는지는 남기지 않았다. 그래서 "에이전트 안에서 훅이 돈다"는 사실이 측정에 잡히지 않았고, 갭이 갭으로 보이지도 않았다. 이번에 agent_type·agent_id 필드를 넣고 나서야 위 수치가 처음 나왔다 — 측정을 만들자 질문이 답을 얻은 셈이다. 남은 과제는 "돌게 하는 것"이 아니라 고위험 에이전트에만 더 조이는 차등을 켜는 것이다.

같은 훅, 다른 가시성

user 스코프(~/.claude/settings.json) 훅 중 37개는 hook-run.sh 텔레메트리 래퍼를 경유해 발화 기록을 남긴다. 그런데 프로젝트 스코프 훅은 605개 전부가 래퍼를 거치지 않았다. 설치기가 처음부터 래퍼 없이 등록했기 때문이다 — grep -c 'hook-run.sh' 결과가 0이었다.

마이그레이션 전 — 훅 실행 경로 두 갈래 Claude Code 플랫폼 도구 호출 시 훅 이벤트 발생 user 스코프 · 54개 hook-run.sh (래퍼) · 37개 경유 실제 훅 실행 (exit 0 / 2) 기록 남음 → dead-hook 판정 가능 project 스코프 · 605개 래퍼 없음 · 0개 경유 실제 훅 실행 (정상 차단함) 기록 없음 → 죽어도 모름 실행은 양쪽 다 정상 — 갈라지는 것은 “그 실행을 아는가”뿐 하네스 강제 표면 659개 중 621개(94.2%)가 측정 밖이었다
발화 여부와 관측 여부는 다른 축이다. 오른쪽 경로는 차단도 하고 게이트도 지켰지만, 그 사실을 확인할 수단이 없었다.
분석을 믿기 전에 다시 쟀다

요청은 "타당성을 입증하고 개선안을 짜서 진행하라"였다. 그래서 구현 전에 문서를 읽지 않은 감사 에이전트 2기를 띄워 디스크에서 직접 세게 했다. 한쪽은 수치 전수(19개 프로젝트 settings.json 파싱), 다른 쪽은 코드 주장 24항목(설치기 라인·리포터 분모·매처 배선). 결론은 분석이 타당했다였지만, 그대로는 아니었다.

검증 항목분석 문서재실측판정
project 훅 래퍼 경유00 / 605확인 핵심 주장
중복 등록 분포33건 / 5개 프로젝트15+15+1+1+1 완전 일치확인
user 스코프 구성54 = 53 command + 1 http정확히 일치확인
설치기 코드 주장L224 래퍼 없음 외 24항목전건 일치확인
영향 프로젝트 수18개19개 (+1)정정 심층 중첩 누락
영향 훅 수582개605개 (+23)정정
"설치 프로젝트는 전부 git 저장소"손실 커버리지 03개는 자체 .git 없음반증 설계 보정
반증이 만든 실제 결함

자체 .git이 없는 3개 프로젝트는 상위 탐색이 전부 홈 디렉토리에서 멈춰 같은 태그를 받았다. 태그가 겹치면 한 프로젝트의 발화가 다른 프로젝트의 죽은 동명 훅을 살아있게 만든다 — 무음 오판이다. 정지 조건을 .claude/settings.json 마커 우선으로 바꿔 해소했고, 정상 프로젝트 태그는 그대로 유지됐다.

4파일을 함께 고쳐야 했다

"설치기 한 줄만 고치면 된다"는 초안은 적대 리뷰에서 무너졌다 — 래퍼가 자기 자신을 등록하고, 설치기가 append-only라 같은 훅이 두 번 실행되고, 리포터 분모에 project가 없어 다 고쳐도 리포트에 안 잡히는 구조였다. 그래서 배치·조립·마이그레이션·집계 네 지점을 한 단위로 바꿨다.

마이그레이션 후 — 배치 · 조립 · 치환 · 집계 ① 배치 hook-templates/_lib/ glob 밖 — 자기등록 구조적 차단 ② 조립 + 치환 install-project-hooks.sh 정규화 후 제거 → 래퍼 1건 삽입 (exact-once) 결과 settings.json × 19 중복 33 → 0 · 절대경로 0 ③ 래퍼 — hook-run.sh (투명 통과) stdin 그대로 전달 · exit code 그대로 전파 · stdout/stderr 무오염 project_tag (salt+sha256) scope (user/project) agent_type / agent_id hook-triggers.jsonl 한 줄 = 한 발화 · 전수 유효 JSON ④ 집계 — dead-hook-report.sh 동일성 키 = (scope, project_tag, 훅명) · 판정불가는 보류 버킷으로 분리 네트워크 호출 0 · 자유 입력 문자열 미재현
래퍼는 투명해야 한다 — 게이트가 차단하던 것은 래퍼를 씌운 뒤에도 그대로 차단해야 하고, 그 사실이 이제 기록으로 남는다.
초안 (리뷰 전)

설치기 두 줄

명령 문자열에 래퍼를 앞에 붙인다

그대로 실행했다면 훅이 이중 실행되거나, 래퍼가 자기 자신을 감싸거나, 다 고쳐도 리포트에 안 잡혔을 것이다.

확정 (37결함 수렴 후)

4파일 + 신규 디렉토리

배치 · 조립 · 치환 · 집계를 한 단위로

래퍼를 glob 밖에 두고, append가 아니라 치환하고, 리포터 분모와 동일성 키를 함께 바꿨다.

내 QA는 전건 PASS였다 — 그게 문제였다

Claude로 돌린 QA(결정론 TC 13건 + E2E 저니 6건)는 4라운드 내내 전건 PASS였다. 같은 코드를 Codex에 적대 리뷰시키자 CRITICAL 3 · HIGH 4 · MEDIUM 4, 합쳐 11건이 나왔다. 더 눈여겨볼 것은 그중 2건이 "수정이 만든 새 구멍"이었다는 점이다.

교차 리뷰 5라운드 — 위쪽은 Claude 판정, 아래쪽은 Codex 판정 13/13 · 6/6 PASS 10/10 PASS 6/6 PASS FIFO 재현 검증 blind 10/10 + 결정론 재현 R1 R2 R3 R4 R5 CRITICAL 2 게이트 우회 env 하드링크 truncate + HIGH 2 HIGH 1 · MED 2 설정 동시성 락 부재 제어문자 · 개행 경로 CRITICAL 1 R2 수정이 만든 락 파일 truncation + HIGH 1 (과잉주장) NO_CRITICAL + MED 1 락 자리 FIFO → 설치 무한 대기 CRITICAL·HIGH 0 FIFO 수정 재리뷰 MEDIUM 잔여: 회귀테스트 부재 같은 코드를 두 모델이 봤는데 판정이 갈렸다 Claude QA는 계약·회귀·저니를 검증했고, Codex는 “이 코드가 어떻게 악용되는가”를 물었다 R3의 CRITICAL은 R2에서 내가 추가한 락 코드에서 나왔다 — 완화책이 새 공격면이 된 전형
위쪽 초록 = Claude QA 판정, 아래쪽 = 같은 라운드의 Codex 판정. R1~R4 네 라운드 동안 위아래가 어긋나고, R5(FIFO 수정 재리뷰)에서 처음 함께 clean이 된다.
교차 리뷰가 잡은 것
라운드결함무엇이 문제였나수정
R1 CRITICAL 진단 모드가 게이트를 통째로 우회 태그 조회용 환경변수가 켜져 있으면 모든 훅이 태그만 찍고 exit 0 — 에러도 로그도 없이 강제 계층 정지 인자 개수로 교차검증 (무인자일 때만 진단)
R1 CRITICAL 설치기 cp가 링크 대상을 파괴 훅 자리를 피해자 파일 하드링크로 선점하면 설치 한 번에 그 파일이 0바이트 프로젝트 내 모든 쓰기를 temp + rename 원자 교체
R1 HIGH 훅명 JSON 인젝션 훅 파일명에 개행을 심으면 위조 텔레메트리 레코드를 주입해 판정을 속일 수 있다 필드 새니타이즈
R1 HIGH 공백 포함 경로가 쪼개짐 프로젝트 경로에 공백이 있으면 인자가 분할돼 그 프로젝트가 리포트에서 조용히 누락 마커 목록 배열화
R2 HIGH 설정 파일 동시 갱신 소실 원자적 rename은 크래시 안전성일 뿐 — 두 설치기가 겹치면 나중 쓰기가 앞선 등록을 조용히 삼킴 읽기~교체 전 구간 배타 락 (fail-closed)
R2 MEDIUM 제어문자 잔존 따옴표·개행 4종만 지우면 탭이 남아 그 줄이 파싱 실패 → 레코드 무음 소실 제어문자 클래스 전체 제거
R2 MEDIUM 개행 포함 경로 경로에 개행이 있으면 탐색 단계에서 분할 — 공백 수정으로도 안 닫힌 잔여 경로 NUL 구분 탐색
R3 CRITICAL 락 파일이 새 공격면이 됨 R2에서 추가한 락을 open("w")로 열어 락 획득 전에 이미 truncate — 심링크로 무기화 가능 쓰기 플래그 없는 open + 심링크/하드링크/일반파일 3검사
R3 HIGH 보호 범위 과잉 주장 락은 협조하는 설치기끼리만 직렬화하는데 주석은 "수동 편집까지 보호"라고 적혀 있었다 주장 범위를 코드 주석에서 축소
R4 MEDIUM 락 자리 FIFO → 설치 무한 대기 파이프를 심어두면 열기 단계에서 타임아웃 이전에 영원히 블록 논블록 + 일반파일 검사 (구버전 hang, 신버전 즉시 거부 실측)
R5 MEDIUM FIFO 거부에 회귀 테스트가 없다 수정은 수동 재현으로 확인했지만 영속 테스트가 없어 다음 변경에서 조용히 깨질 수 있다 미조치 — 후속 과제로 기록 (R5는 CRITICAL·HIGH 0)
반복된 패턴

R1의 하드링크 방어를 넣자 R2에서 동시성 축이 비어 있었고, 그걸 락으로 막자 R3에서 락 자신이 뚫렸고, 그걸 막자 R4에서 FIFO가 남았다. 완화책을 넣을 때마다 그 완화책이 지나가지 않는 경로가 남는다 — 원칙을 적어두는 것으로는 부족하고, 분기마다 확인해야 한다.

19개 프로젝트에 실제로 착지했는가

"고쳤다"와 "적용됐다"는 다르다. 설치는 19/19, 로컬 커밋은 4차례(마이그레이션 · 태그 수정 · 마커 갱신 · 보안 수정)에 걸쳐 16개 저장소에 반영했고, 각 커밋은 git show --name-only로 의도한 .claude 경로만 들어갔는지 확인했다. 홈 디렉토리가 저장소인 3개 프로젝트는 무관한 변경 230만 줄이 널려 있어 특히 중요했다.

착지
19설치 완료
16커밋 반영

나머지 3개는 저장소가 .claude/(또는 .claude/hooks/)를 gitignore로 제외 — 파일은 적용, 커밋은 정상 no-op.

정합
19/19고유 태그
0중복 등록

래퍼 지문은 정본·템플릿·설치본·미러 전부 동일. 설정 파일 19개 전수 유효 JSON, 절대 경로 0.

무해성
1.0게이트 강제율
0user 스코프 회귀

차단하던 훅은 래퍼를 거쳐도 그대로 차단(exit 2). 관측을 얻으면서 강제력을 잃지 않았다.

최종 재검증

구현자 서술을 받지 않은 blind 검증자가 기준 10개를 디스크에서 직접 실측해 전건 통과했다 — 실제 차단 저니(위반 입력 → exit 2 + 태그 레코드), FIFO/하드링크 공격 재현, 지문 4자 일치, 커밋된 내용 기준 확인까지. "돌 것이다"가 아니라 돌렸고 기록이 남았다가 판정 근거다.

닫지 않은 것과, 남은 교훈
의도적으로 남긴 것

범위 밖 5+16건

숨기지 않고 보류 버킷으로 노출

템플릿 관리 밖 커스텀 훅 5건과 user 스코프 미래핑 16건은 이번 범위가 아니다. 리포터가 판정 불가로 분리 집계해 "측정 못 한 것"이 "문제 없음"으로 둔갑하지 않게 했다.

후속 과제 — 원래 질문의 나머지 절반

차등을 실제로 켜는 일

측정은 열렸고, 채택은 아직 0

이번에 연 것은 관측이지 차등이 아니다. 그리고 FIFO 거부에 회귀 테스트가 없다(R5 MEDIUM, 미조치). 다음 순서는 ① agent_type으로 분배하는 훅 하나로 89개 에이전트 파일을 손대지 않고 per-agent 검사를 거는 것, ② 그다음 고위험 에이전트(배포·사용자 대리)에만 frontmatter 훅을 직접 부여하는 것이다. 매처 사각(노출 8종)과 로그 로테이션 미작동도 조사만 해둔 상태다.

측정이 없으면 이후 모든 변경의 효과를 검증할 수 없다. 그래서 관측 복구를 가장 먼저 했다 — 기능을 하나 더 얹는 것보다, 이미 있는 것들이 살아 있는지 아는 쪽이 먼저였다.
1
지표를 믿기 전에 그 지표가 무엇을 세는지 본다
dead 훅 0건은 “다 살아있다”가 아니라 “user 스코프만 셌다”였다
2
범위 측정은 탐색 조건이 결과를 만든다
문서가 한 번 교정했던 “깊이 과소평가”가 재실측에서 또 나왔다 (18 → 19)
3
고친 직후가 가장 위험하다
R2의 락 수정이 R3의 CRITICAL을 만들었다 — 완화책도 리뷰 대상이다
4
같은 모델의 QA는 같은 맹점을 공유한다
전건 PASS 4회 옆에서 다른 모델이 결함 11건을 집었다

이 작업의 발단이 된 분석 — 에이전트 스코프 훅은 이미 있었다 — 채택되지 않았을 뿐