매번 성공인데 원장은 0행 — 훅 네 개가 조용히 죽어 있던 이유
기록 장치가 "죽은 상태"인지 "기록할 일이 없던 상태"인지 구분하는 프로브를 자기 하네스 훅에 직접 던져볼 수 있어요.
공개일 · 갱신일
증상 원문
규칙은 있는데 데이터가 없었어요.
AI가 만진 문서에 출처를 각인하는 훅(hook — 특정 도구 사용 전후에 자동 실행되는 셸 스크립트)이 걸려 있었고, 그 결과가 쌓이는 provenance 원장 파일(vault-ai-touched.tsv)도 자리에 있었어요. 그런데 원장은 0행이었어요. 기록상 몇 달 동안요.
더 헷갈리는 건 훅이 매번 exit 0(종료 코드 0 = 정상 종료)으로 끝났다는 점이에요. 경고도, 오류도 한 줄 없었어요. 로그만 보면 모든 게 잘 돌아가는 시스템이었어요.
초기 진단 — 0행이 나오는 경우는 셋이에요
0행이라는 사실 하나로는 아무것도 못 정해요. 원인 후보가 최소 셋이거든요.
- 미실행 — 훅이 아예 안 불렸어요.
- 대상 없음 — 훅은 돌았는데 조건에 맞는 파일이 없었어요.
- 기록 생략·실패 — 훅도 돌았고 파일도 있었는데, 기록이 안 남았어요.
셋은 겉보기가 똑같아요. 전부 "원장 0행 + exit 0"이에요. 그래서 먼저 규칙 쪽을 확인했어요. 하네스 CLAUDE.md에는 "복귀 시 폴백 산출물 스팟체크" 같은 조항이 멀쩡히 살아 있었어요. 규칙은 있었고, 데이터만 없었어요.
결정적 프로브 — 정상 페이로드를 직접 주입했어요
세 후보를 가르는 가장 싼 방법은 정상 입력을 손으로 넣어보는 것이었어요. 실제 파일 편집이 일어났을 때와 똑같은 모양의 Write 페이로드(훅이 받는 입력 JSON)를 만들어 훅에 직접 넣었어요.
결과는 여전히 0행이었어요. 그래서 훅 내부의 처리 경로를 조사할 이유가 생겼어요. 이후 추적에서 의도한 파일이 경로 필터에 걸려 제외되는 것을 확인했고요. 정확히 말하면 "쓰기 실패"가 아니라 "대상 판정 오류로 기록이 생략됨"이에요.
다만 이 프로브가 말해 주지 않는 것도 분명해요. 운영 환경에서 훅이 자동 호출됐는지, 그 기간에 실제 대상 작업이 있었는지는 수동 프로브만으로 확인할 수 없어요. 제가 직접 실행한 사실은 자동 호출이 작동했다는 증거가 아니니까요.
자기 하네스에서 따라 하는 프로브
경로만 자기 환경에 맞게 바꿔 그대로 돌려 보세요.
HOOK='/path/to/hooks/provenance-record.sh'
LEDGER='/path/to/state/vault-ai-touched.tsv'
wc -l < "$LEDGER"
printf '%s' '{"tool_name":"Write","tool_input":{"file_path":"<VAULT>/notes/probe.md"}}' | bash "$HOOK"; printf 'exit=%s\n' "$?"
wc -l < "$LEDGER"
# 행이 안 늘면: 실행 경로 추적
printf '%s' '{"tool_name":"Write","tool_input":{"file_path":"<VAULT>/notes/probe.md"}}' | bash -x "$HOOK" 2>&1 | tail -20
<VAULT>는 자기 볼트의 정본 경로로 바꿔요. 입력 형식과 실행 환경은 버전에 따라 달라요. 검증 후 프로브가 만든 행은 식별해 제거해요.
원인 — 경로 분기가 이관 잔재를 먼저 집었어요
bash -x(셸이 실행하는 모든 줄을 화면에 찍어주는 추적 모드)로 훅을 다시 돌렸어요. 볼트 경로를 판정하는 지점에서 조기 종료하는 게 그대로 보였어요.
문제의 줄은 이거였어요.
elif [ -d "$HOME/Vaults/Obsidian-0.1" ]
홈 아래 Vaults/Obsidian-0.1은 이관하다 남은 잔재 디렉터리였어요. 2026-04-02자 .md 파일 2,546개가 들어 있었고, 디렉터리로서는 멀쩡히 존재했어요. 그래서 이 분기가 먼저 걸렸고, 진짜 볼트인 Documents/Obsidian-0.1(2,598개)의 파일은 전부 "볼트 밖 파일"로 필터링됐어요. 기준점 역할을 해야 할 OBSIDIAN_VAULT 환경변수는 설정돼 있지 않았고요.
훅 입장에서는 "볼트 밖 파일이니 기록 안 함"이 정상 동작이에요. 그래서 exit 0이었어요. fail-open(실패해도 막지 않고 통과시키는 설계)이라 아무것도 멈추지 않았고, 그래서 아무도 몰랐어요.
영향 범위 — 훅 하나가 아니라 네 개였어요
여기서 예상이 빗나갔어요. 같은 경로 분기를 복사해 쓰는 훅이 네 개였어요.
- provenance-record (출처 각인)
- cmds-pre-check / cmds-post-validate (문서 규격 검증)
- session-auto-log (세션 로그)
시간선은 조심해서 읽어야 해요. 원장이 몇 달간 비어 있었다는 기록과, 네 훅에 같은 경로 분기가 들어간 2026-08-19 커밋을 각각 확인했어요. 이 분기 하나로 그 앞선 기간까지 설명할 수는 없어요. 네 훅 각각의 장애 시작 시점도 확인하지 못했어요. 그 커밋의 제목은 "볼트 경로 해석을 새 위치 우선으로"였고, 나중에 만든 검토 도구(lane-review)가 첫 실행에서 바로 이 커밋을 검토 후보로 집어냈어요.
고친 방법 — env 한 줄, 그리고 0행→1행
조치 자체는 한 줄이었어요. settings.json의 env에 OBSIDIAN_VAULT를 정본 경로로 박았어요.
복구 범위는 정확히 이만큼이에요.
- 환경변수 한 줄로 네 훅이 공유하는 경로 설정을 바로잡았어요. 네 훅이 같은 분기를 쓰니까요.
- 출처 기록 훅은 같은 프로브로 원장 0행→1행을 확인했어요. "고쳤을 것이다"가 아니라 행 수가 실제로 1 늘어나는 걸 보고 끝냈어요.
- 문서 사전 검사·사후 검증·세션 로그의 복구 여부는 각 기능의 입력과 결과로 별도 확인해야 해요. 이 세 개는 아직 확인 기록이 없어요.
재발 방지 — 원장 옆에 계기판을 달았어요
직접 원인은 잘못된 볼트 경로 선택이었어요. 이를 늦게 발견한 데에는 관측 수단의 부족이 영향을 줬고요. 그래서 계기판을 만들었어요.
ledger-health.sh를 SessionStart(세션 시작 시점)에 fail-open으로 걸었어요. 원장 행 수, 마지막 기록 시각, 그리고 경로 불일치 경고를 띄워요.- 회귀 테스트도 붙였어요.
OBSIDIAN_VAULT를 일부러 지우고 돌렸을 때 경고가 실제로 나오는지 확인해요. 계기판 자체가 조용히 죽는 걸 막는 장치예요. - 세션 시작 비용은 1.12초에서 0.08초로 줄였어요. env가 정본이면 파일 순회를 통째로 건너뛰거든요. 느린 계기판은 결국 꺼지니까요.
계기판이 주는 보장도 정확히 그어 둘게요. 경로 불일치와 기록 정체는 보여주지만, 정체의 원인이 "훅 미실행"인지 "대상 작업 없음"인지까지는 구분하지 못해요. 거기까지 가려면 실행 여부와 대상 판정 결과를 따로 기록하거나, 통제된 입력으로 실제 기록 성공을 확인하는 프로브가 필요해요.
남은 사각지대 — Bash 편집은 안 잡혀요
정직하게 남겨요. 현재 훅은 Write/Edit 도구에만 반응해요. Bash로 만들거나 고친 파일은 이 훅이 실행되지 않아요.(heredoc — 셸에서 여러 줄 텍스트를 한 번에 파일로 쏟아붓는 문법, sed — 명령줄에서 파일 내용을 찾아 바꾸는 도구. 둘 다 Bash 안에서 도니까 훅이 못 봐요.)
그래서 원장에 기록이 없다는 이유만으로 AI가 수정하지 않았다고 판단할 수 없어요. 감지 범위를 넓히기 전까지는요. 세션 운영 모드가 Bash 편집을 권장하는 환경이라면, 권장을 따를수록 구멍이 커진다는 뜻이기도 해요.
같은 날 나온 동종 사례
세션 로그 파이프라인도 watcher(파일 변화를 감시하다 자동으로 처리를 걸어주는 상주 프로세스)가 제거돼 생성이 멈춰 있었고, 쌓여 있던 로그 12,248건은 평균 1,028바이트짜리 "스켈레톤 노트"(첫 프롬프트 한 줄만 복사된 뼈대 파일)였어요. 그래서 행 수뿐 아니라 실제로 필요한 내용이 기록되는지도 확인해야 해요.
교훈 한 줄
원장을 만들 때는 상태 표시와 함께, 실제로 기록되는지 확인하는 검사도 만들어요.
상태 표시만 있는 기록 장치는 "장치가 죽은 상태"와 "문제가 없던 상태"를 구별하지 못해요. 둘 다 0행이거든요.
실행일·도구 버전
진단·수리 2026-09-13, 계기판 배선 및 회귀 테스트 같은 날. 사용한 셸·런타임 버전은 기록에 남기지 않았어요. 재현할 때는 현재 버전을 기준으로 확인하세요. 위 프로브의 입력 형식도 하네스 버전에 따라 달라질 수 있으니, 자기 훅이 받는 입력 모양을 먼저 확인하고 돌려 보세요.
근거
- 하네스 훅 provenance-record.sh (볼트 경로 분기 원문)
- 하네스 settings.json env 설정 (OBSIDIAN_VAULT 추가분)
- 하네스 CLAUDE.md '관측 먼저, 집행 나중' 절
- 계기판 스크립트 ledger-health.sh (회귀 테스트 포함)
- 발제 자료 facts-ledger §O (2026-09-14 AI 스터디 발제 준비 기록)