JIN.PROC // v3.1
~/ / 프로젝트 / 2026-04-29-lazyclaude-deep-8-hook-detective

[LazyClaude 심층 8] Hook Detective — 90MB jsonl을 마이닝해 "왜 막혔지"를 찾기

block error 메시지 → hook id 칩 → 자동 점프. dispatcher 디코더로 node -e "…"를 사람이 읽는 체인으로

DATE 2026.04.29 UPDATED 2026.04.29 READ ~ 4 MIN WORDS 616

LazyClaude 심층 8편. Claude Code의 hook 시스템은 강력한데, 뭔가 막혔을 때 그 원인을 찾는 게 가장 어렵다. Hook Detective는 이 부분만 푸는 도구다.

문제

Claude Code에서 어떤 도구 호출이 block 됐을 때, 사용자에게 보이는 건 보통 다음 중 하나다:

  • “PreToolUse hook blocking error from command: …” (긴 노이즈)
  • 한 줄짜리 막연한 reject 메시지
  • 또는 아무 메시지도 없이 차단

설치된 plugin hook이 100개를 넘기면, 어떤 hook이 막았는지 를 사람이 짚는 게 매우 어렵다. 대시보드에 찾는 기능 이 없으면, 사용자는 그냥 plugin을 끄고 만다.

Hook Detective의 입력

세 종류의 입력으로 단서를 모은다:

  1. Block error 메시지 — 사용자가 붙여넣는 에러
  2. 세션 jsonl 트랜스크립트~/.claude/projects/*/sessions/*.jsonl
  3. 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.jsCLAUDE_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 ]

이 버튼은:

  1. user settings.json을 백업 (.bak)
  2. 모든 plugin의 hooks.json을 백업
  3. 위 risky 정의를 모두 주석화 (삭제 X — 복원 가능)

사용자가 발 빠르게 작업으로 돌아갈 수 있게. 영구 변경 아님 — 백업 파일 한 번 클릭이면 원복.

검색과 필터

100개를 빠르게 좁히는 두 축:

  • 검색창: hook id substring 매칭. pre:bash 입력 → bash 관련만
  • 칩 필터: scope (user / plugin), event (Pre/Post/Session…), risky-only

/키로 글로벌 spotlight 띄우기 — Cmd-K 패턴 그대로.

일반화 — 찾는 도구 의 5가지 책임

이 디자인은 LazyClaude 외에서도 그대로 쓸 수 있다 (예: 큰 분량의 로그·플러그인·룰 시스템):

  1. 단서 추출 — 에러 메시지에서 id 자동 추출
  2. 점프 — id 클릭 시 정확한 카드로 스크롤 + 펄스
  3. 마이닝 — 로그를 통계화해 가장 자주 가 보이게
  4. 디코딩 — 래퍼 체인을 사람의 언어로
  5. 응급 처치 — 일괄 비활성화 + 자동 백업

한 줄 요약

“왜 막혔지?”는 시스템에 찾는 도구 가 없으면 죽는 질문이다.


다음 편: LazyClaude 심층 9 — Computer Use · Memory · Advisor Lab 이전 편: LazyClaude 심층 7 — Multi-provider 워크플로우