[LazyClaude 심층 8] Hook Detective — 90MB jsonl을 마이닝해 "왜 막혔지"를 찾기
block error 메시지 → hook id 칩 → 자동 점프. dispatcher 디코더로 node -e "…"를 사람이 읽는 체인으로
LazyClaude 심층 8편. Claude Code의 hook 시스템은 강력한데, 뭔가 막혔을 때 그 원인을 찾는 게 가장 어렵다. Hook Detective는 이 부분만 푸는 도구다.
문제
Claude Code에서 어떤 도구 호출이 block 됐을 때, 사용자에게 보이는 건 보통 다음 중 하나다:
- “PreToolUse hook blocking error from command: …” (긴 노이즈)
- 한 줄짜리 막연한 reject 메시지
- 또는 아무 메시지도 없이 차단
설치된 plugin hook이 100개를 넘기면, 어떤 hook이 막았는지 를 사람이 짚는 게 매우 어렵다. 대시보드에 찾는 기능 이 없으면, 사용자는 그냥 plugin을 끄고 만다.
Hook Detective의 입력
세 종류의 입력으로 단서를 모은다:
- Block error 메시지 — 사용자가 붙여넣는 에러
- 세션 jsonl 트랜스크립트 —
~/.claude/projects/*/sessions/*.jsonl - hook 정의 파일들 —
~/.claude/settings.json+ 각 plugin의hooks.json
각 입력은 단독으로는 부족하다. 셋을 연결 해야 답이 보인다.
Hook id 칩 — 입력에서 단서 뽑기
block 에러 텍스트를 정규식으로 훑어 hook id 후보 를 추출한다.
import re
HOOK_ID_RE = re.compile(r"\b([a-z]+:[a-z\-_]+:[a-z\-_]+)\b")
def hook_ids_in(error_text: str) -> list[str]:
return list(dict.fromkeys(HOOK_ID_RE.findall(error_text)))
UI는 이 id들을 클릭 가능한 칩으로 렌더링한다:
[pre:bash:dispatcher] [pre:edit-write:gateguard]
칩을 클릭하면 — 해당 hook 카드로 자동 스크롤 + 펄스 효과. 100개 카드 사이에서도 즉시 찾는다.
90MB jsonl 마이닝 — 가장 자주 막은 hook
가장 최근 60개의 transcript jsonl만 스캔해서 hook block 라인을 카운트한다.
import os, glob, re
from collections import Counter
BLOCK_PAT = re.compile(r"hook\s+blocking\s+error", re.I)
def recent_block_top_n(n=10):
counter = Counter()
files = sorted(glob.glob(
os.path.expanduser("~/.claude/projects/*/sessions/*.jsonl")),
key=os.path.getmtime, reverse=True)[:60]
for path in files:
with open(path, "r") as f:
for line in f:
if not BLOCK_PAT.search(line): continue
for hid in HOOK_ID_RE.findall(line):
counter[hid] += 1
return counter.most_common(n)
대시보드 상단에 🚨 Recently blocked hooks 패널로 노출. 가장 자주 막은 hook 이 이름순으로 보인다.
(이 스캔이 1편의 TTL+mtime 캐시 패턴과 결합해서 0.97s → 0.026s로 떨어진다.)
Dispatcher 디코더 — node -e "…" 풀어내기
Claude Code의 hook은 종종 한 줄로 node -e "..." 같은 래퍼를 쓴다. 그 안에서 또 plugin bootstrap을 거쳐, 결국 진짜 hook 핸들러 가 호출된다. 사용자가 그 체인을 읽는 건 거의 불가능.
Hook Detective는 이 체인을 사람이 읽는 형태로 디코드한다:
node -e "…"
↳ scripts/hooks/plugin-hook-bootstrap.js
↳ scripts/hooks/run-with-flags.js
↳ scripts/hooks/gateguard-fact-force.js
flags: standard, strict
각 단계의 의미:
node -e "…"— 인라인 부트스트랩plugin-hook-bootstrap.js—CLAUDE_PLUGIN_ROOT결정run-with-flags.js— hook id를 인자에서 분리- 마지막 — 실제 핸들러 + flag 목록
이걸 카드의 🔬 Detail 모달에서 한눈에. 왜 차단됐는지 의 추적 경로가 시각적으로 드러난다.
Risky chip — 진짜 위험한 hook만
PreToolUse + Edit/Write/Bash 매처를 가진 hook은 창의적 작업을 가장 자주 막는다. 이 조합에는 자동으로 🚨 칩이 붙는다.
def is_risky(hook_def: dict) -> bool:
if hook_def.get("event") != "PreToolUse": return False
matchers = set(hook_def.get("matchers", []))
return bool(matchers & {"Edit", "Write", "Bash", "MultiEdit"})
“위험”이라는 단어가 부정적 으로만 쓰이는 게 아니다 — 사용자가 일부러 박은 가드레일 일 수도 있다. 단지 언제 막을 수 있는지 보이게 하라.
일괄 비활성화 — 응급 처치
100개 plugin hook 사이에서 어느 것이 막는지 모를 때, 마지막 수단:
[ Bulk-disable risky hooks ]
이 버튼은:
- user
settings.json을 백업 (.bak) - 모든 plugin의
hooks.json을 백업 - 위 risky 정의를 모두 주석화 (삭제 X — 복원 가능)
사용자가 발 빠르게 작업으로 돌아갈 수 있게. 영구 변경 아님 — 백업 파일 한 번 클릭이면 원복.
검색과 필터
100개를 빠르게 좁히는 두 축:
- 검색창: hook id substring 매칭.
pre:bash입력 → bash 관련만 - 칩 필터: scope (user / plugin), event (Pre/Post/Session…), risky-only
/키로 글로벌 spotlight 띄우기 — Cmd-K 패턴 그대로.
일반화 — 찾는 도구 의 5가지 책임
이 디자인은 LazyClaude 외에서도 그대로 쓸 수 있다 (예: 큰 분량의 로그·플러그인·룰 시스템):
- 단서 추출 — 에러 메시지에서 id 자동 추출
- 점프 — id 클릭 시 정확한 카드로 스크롤 + 펄스
- 마이닝 — 로그를 통계화해 가장 자주 가 보이게
- 디코딩 — 래퍼 체인을 사람의 언어로
- 응급 처치 — 일괄 비활성화 + 자동 백업
한 줄 요약
“왜 막혔지?”는 시스템에 찾는 도구 가 없으면 죽는 질문이다.
다음 편: LazyClaude 심층 9 — Computer Use · Memory · Advisor Lab 이전 편: LazyClaude 심층 7 — Multi-provider 워크플로우