나의 터미널 AI 어시스턴트를 위한 자가 업데이트 지식 베이스 (Claude Code hooks)
요약
Claude Code의 훅(hooks) 기능을 활용하여 AI 코딩 어시스턴트가 스스로 지식 베이스(KB)를 업데이트하고 검색할 수 있는 시스템 구축 방법을 소개합니다. 세션 시작, 프롬프트 제출, 종료 시점에 실행되는 훅을 통해 문맥을 자동으로 주입하고 학습 내용을 저장하는 워크플로우를 제안합니다.
핵심 포인트
- Claude Code의 lifecycle hooks를 이용한 자동화된 지식 관리
- UserPromptSubmit 훅을 통한 효율적인 컨텍스트 주입 전략
- grep 기반의 가볍고 빠른 검색 시스템 설계
- Stop 훅 사용 시 발생할 수 있는 성능 저하 문제와 주의사항
저는 하루의 대부분을 AI 코딩 어시스턴트와 함께 터미널에서 보냅니다. 매 세션마다 까다로운 수정 사항, 설정상의 주의점(config gotcha), 작은 실행 지침서(runbook)와 같이 기억할 만한 가치가 있는 문제들을 해결하곤 합니다. 그러고 나면 그것들을 잃어버리곤 했습니다. 그것들은 탭을 닫으면 사라지는 스크롤백 버퍼(scrollback buffer) 속에 머물러 있었기 때문입니다. 한 달 뒤에 저는 똑같은 문제를 다시 해결해야 했습니다.
아이디어
어시스턴트가 스스로 노트를 유지하도록 만드는 것입니다. 세 가지 구성 요소가 필요합니다:
- 모든 프롬프트(prompt) 시점에 작은 마크다운 지식 베이스 (Knowledge Base, KB)를 검색하고 관련 항목을 주입(inject)하여, 어시스턴트가 문제를 다시 도출하는 대신 이전의 문맥(context)을 바탕으로 답변하게 합니다.
- 세션이 종료될 때, 보관할 가치가 있는 모든 내용을 다시 KB로 캡처합니다.
- 세션이 시작될 때, 인덱스(index)를 로드하여 어시스턴트가 무엇이 존재하는지 알게 합니다.
Claude Code에는 **훅(hooks)**이 있습니다. 이는 라이프사이클 이벤트(lifecycle events) 발생 시 실행되는 셸(shell) 명령입니다. 이 중 세 가지가 전체 루프를 커버합니다.
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "/path/kb-load.sh" }] }],
...
아래 내용은 이들을 연결하면서 배운 유용한 정보들입니다.
검색(Retrieval): 모든 메시지에서 실행되는 UserPromptSubmit
UserPromptSubmit은 사용자가 보내는 각 프롬프트마다 실행되며, 표준 입력(stdin)으로 프롬프트 텍스트를 전달받고, 모델이 실행되기 전에 문맥(context)을 주입할 수 있습니다. 이것이 모델이 스스로 찾아볼지 결정하게 두는 대신, KB가 자동으로 확인되도록 만드는 훅입니다.
세 가지 설계 규칙: 비용이 저렴해야 하고 (grep 사용, LLM 미사용), 가벼워야 하며 (상위 일치 항목만 주입하고 전체 파일은 절대 덤프하지 않음), 프롬프트를 절대 차단(block)해서는 안 됩니다.
#!/usr/bin/env bash
# 참고: `set -e`를 사용하지 마세요. 0이 아닌 값으로 종료되는 UserPromptSubmit 훅은 프롬프트를 차단(BLOCK)합니다.
# 모든 실패 경로는 출력 없이 `exit 0`으로 이어져야 합니다.
...
핵심은 grep이 영리하다는 것이 아닙니다. 훅(hook)이 검색을 한 번 수행하고, 결과의 순위를 매긴 뒤, 모델에게 읽어야 할 특정 파일들을 전달한다는 점입니다. 모델은 어떤 파일이나 도구를 확인해야 할지 추측하는 것을 멈추게 됩니다. 관련 항목이 이미 모델 앞에 놓여 있기 때문입니다. 프롬프트당 수십 개의 작은 마크다운 (Markdown) 파일을 스캔하는 데는 100ms 미만이 소요되며, 전체 지식 베이스 (KB)가 아닌 상위 5개 항목만 주입하는 것이 핵심적인 이점입니다.
Capture: 실수, 그리고 해결책
저의 첫 번째 버전은 어시스턴트가 답변을 마칠 때마다 실행되는 Stop 훅에서 캡처(capture)를 수행했습니다. 이 캡처는 헤드리스 (headless) AI 호출(claude -p ...)을 생성하여 트랜스크립트 (transcript)를 읽고 지식 베이스 (KB) 항목을 작성합니다. 처음에는 잘 작동했지만, 곧 어시스턴트가 고통스러울 정도로 느려졌습니다. 매 턴이 끝날 때마다 몇 분 동안 "running stop hook"이라는 메시지가 떴습니다.
이유는 다음과 같습니다. Stop 훅은 매 턴마다 동기적 (synchronously)으로 실행됩니다. 30턴의 세션은 헤드리스 캡처를 약 30번 실행했으며, 각 실행마다 점점 더 커지는 트랜스크립트를 다시 읽어야 했습니다. 저는 동일한 대화를 반복해서 다시 캡처하고 있었던 것입니다.
제가 실제로 원했던 단위는 단 한 번 캡처되는 세션 (start부터 /clear까지)이었습니다. 그것은 /clear 명령 시와 종료 시에 실행되는 SessionEnd입니다. 그래서 저는 캡처 로직을 그곳으로 옮겼고, 진짜 교훈을 얻었습니다:
SessionEnd는 길거나 백그라운드에서 실행되는 작업을 수행할 수 없습니다. 이 훅은 논블로킹 (non-blocking, 부수 효과만 발생) 방식으로 문서화되어 있으며, 여기서 백그라운드로 실행한 프로세스가 계속 생존한다는 보장이 없습니다. 세션이 종료되고 있으므로, 그 자식 프로세스들은 세션과 함께 종료될 수 있습니다.
따라서 SessionEnd에서 실행된 몇 분짜리 헤드리스 캡처는 중간에 끊기게 됩니다.
해결책은 이를 두 개의 훅으로 나누는 것입니다.
SessionEnd는 사소하고 즉각적인 작업만 수행합니다. 종료된 트랜스크립트의 경로를 큐 (queue)에 넣는 것입니다.
#!/usr/bin/env bash
set -euo pipefail
[ -n "${KB_CAPTURE:-}" ] && exit 0 # 재귀 방지 (아래 참조)
...
(다음 세션의) SessionStart가 큐를 비우고 백그라운드에서 캡처를 실행합니다. 이 방식은 현재 세션이 살아있는 상태에서 실행되므로 안전합니다.
QUEUE="$KB/tools/queue.txt"; LOCK="$KB/tools/.lock"
if [ -s "$QUEUE" ] && mkdir "$LOCK" 2>/dev/null; then # mkdir은 원자적 단일 배출 잠금(atomic single-drain lock) 역할을 합니다
mv "$QUEUE" "$QUEUE.wip" # 새로운 인큐(enqueue)가 유실되지 않도록 원자적으로 점유합니다
...
최종 효과: 세션당 한 번의 캡처가 수행되며, 크리티컬 패스(critical path)에서 벗어나 실행됩니다. 유일한 트레이드오프(trade-off)는 /clear를 입력하는 즉시 기록되는 대신, 긴 작업이 반드시 생존할 수 있는 유일한 지점인 다음 세션의 시작 시점(몇 초 후)에 쓰기가 이루어진다는 점입니다.
두 가지 작은 참고 사항이 있습니다. macOS에는 setsid가 없으므로, setsid 대신 nohup ... &와 disown(bash 내장 명령어)을 사용하여 분리(detach)하십시오. 그리고 백그라운드로 보내기 전에 표준 입력(stdin)을 읽으십시오. 분리된 복사본에는 표준 입력이 없기 때문입니다.
비용을 낮추고 안전하게 만든 세 가지 요소
1. 캡처를 위한 더 저렴한 모델. 이는 요약 및 쓰기(summarize-and-write) 작업이므로 가장 비싼 모델을 사용할 필요가 없습니다. 모델을 고정(Pin)하십시오.
KB_CAPTURE=1 claude -p "$(cat capture-prompt.md)" --model <cheap-fast-model> \
--allowedTools "Read,Edit,Write,Grep,Glob"
대략 5배 더 저렴하며 작업이 빠르게 완료되므로, 대기 중인(queued) 캡처들이 신속하게 처리됩니다.
2. 토큰을 소비하기 전의 사전 필터링. 대부분의 세션은 캡처할 가치가 없습니다. 빠른 grep을 통해 AI 호출 여부를 결정(gate)합니다.
grep -qiE 'root cause|next step|blocked on|draft|fix|<ticket-pattern>' "$TRANSCRIPT" || exit 0
3. 재귀 방지 장치, 그리고 이를 가장 먼저 설정할 것. 실행되는 헤드리스(headless) claude는 훅(hook) 등록을 포함한 사용자의 환경을 상속받습니다. 따라서 자체적인 훅을 실행하고, 이는 다시 또 다른 헤드리스 claude를 실행하는 식으로 끝없이 반복됩니다. 하나의 환경 변수가 이 루프를 끊어줍니다. 모든 훅의 최상단에 [ -n "${KB_CAPTURE:-}" ] && exit 0를 넣고, 헤드리스 호출을 실행할 때 KB_CAPTURE=1을 설정하십시오. 이것이 없다면, 단 하나의 실제 세션이 무한한 AI 호출 트리로 확산됩니다.
경제성
프롬프트당: 100ms 미만의 grep 실행과 약 5개의 항목에 대한 작은 컨텍스트 주입(context injection)이 발생하며, 이는 매칭되는 결과가 있을 때만 수행됩니다. 세션당: 저렴한 캡처(capture)가 한 번 발생하며, 세션에서 실제로 무언가를 학습했을 때만 수행됩니다(쓰기 전에 사전 필터링 및 "변경 사항이 있었는가"를 확인하는 절차 포함).
이것이 트레이드오프(trade-off)입니다. 지식을 캐싱(caching)하기 위해 아주 작고 제한된 비용을 지불함으로써, 향후 세션에서는 비용이 많이 드는 작업들, 즉 대규모 코드베이스를 다시 읽거나, 도구(tools)를 다시 쿼리하거나, 이미 해결한 문제를 다시 조사하는 과정을 건너뛸 수 있습니다. 단 한 번의 재유도(re-derivation)를 피하는 것만으로도 수많은 캡처 비용을 충당할 수 있습니다.
요약된 교훈들
UserPromptSubmit은 검색(retrieval)을 주입하는 곳입니다. 이는 모든 프롬프트에서 실행되며 모델 앞에 컨텍스트를 추가할 수 있는 유일한 훅(hook)입니다.Stop에서 실행되는 모든 것은 매 턴(turn)의 크리티컬 패스(critical path)에서 실행됩니다. 즉각적으로 처리되도록 유지하십시오.SessionEnd에서는 오래 걸리는 작업이나 백그라운드 작업을 실행할 수 없습니다. 거기서는 작업 큐(enqueue)에 넣기만 하고, 프로세스가 유지되는 다음SessionStart에서 무거운 작업(heavy lifting)을 수행하십시오.- 어시스턴트를 생성하는 훅을 실행하기 전에 재귀(recursion)가 발생하지 않도록 방지하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기