Hook이 전부다
이 글은 Claude Code Notifier 개발기 시리즈의 2편입니다. (총 4편)
알림을 보내는 건 쉽다. 어려운 건 언제 보낼지 아는 거다.
Claude Code에는 Hook이라는 게 있다. 특정 시점에 내가 원하는 스크립트를 실행시켜준다. 작업 끝났을 때, 권한 요청할 때, 세션 종료할 때. 그 순간을 잡아서 뭔가를 할 수 있다.
이게 전부다.
네 개의 Hook
| Hook | 언제 |
|---|---|
| UserPromptSubmit | 내가 뭔가 입력하면 |
| Stop | Claude가 일 끝내면 |
| Notification | 권한 요청하거나 대기 중이면 |
| SessionEnd | 세션 끝나면 |
네 개 다 쓴다. 빠지는 건 없다.
흐름
내가 입력함
↓
[UserPromptSubmit] → 시간 기록
↓
Claude 작업
↓
[Stop / Notification] → 알림 보냄
↓
[SessionEnd] → 정리
단순하다. 단순한 게 좋다.
시간을 기록하는 이유
1편에서 말한 "20초 이상만 알림"을 구현하려면 시간을 재야 한다.
# 입력할 때
date +%s > timestamp-${SESSION_ID}.txt
# 끝났을 때
start=$(cat timestamp-${SESSION_ID}.txt)
now=$(date +%s)
elapsed=$((now - start))
if [ $elapsed -lt $MIN_DURATION ]; then
exit 0
fi
UserPromptSubmit에서 저장하고, Stop에서 비교한다.
권한 요청은 다르다
권한 요청에는 시간 체크가 없다. 즉시 알려준다.
왜? 기다리는 건 Claude지 내가 아니니까. Claude가 파일 만들려고 하는데 내 허락이 필요하다. 그걸 1시간 동안 모르면 1시간을 버리는 거다.
case "$NOTIFICATION_TYPE" in
"permission_prompt")
# 즉시 알림
notify "$MSG_PERMISSION"
;;
"idle_prompt")
# 즉시 알림
notify "$MSG_IDLE"
;;
*)
# 작업 완료 - 시간 체크 필요
if should_notify; then
notify "$MSG_COMPLETED"
fi
;;
esac
세 줄짜리 조건문이다. 이게 알림 로직의 전부다.
hooks.json
설정 파일은 이렇게 생겼다.
{
"hooks": {
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/save-prompt.sh"
}]
}],
"Stop": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/notify.sh"
}]
}],
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/notify.sh"
}]
},
{
"matcher": "idle_prompt",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/notify.sh"
}]
}
],
"SessionEnd": [{
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/cleanup-session.sh"
}]
}]
}
}
matcher가 중요하다. permission_prompt라고 쓰면 권한 요청만 잡는다. idle_prompt라고 쓰면 대기 상태만 잡는다.
세션 독립성
터미널 세 개를 띄우면 세션 ID가 세 개 생긴다.
터미널 1: session-abc123
터미널 2: session-def456
터미널 3: session-ghi789
파일도 세 개다.
timestamp-abc123.txt
timestamp-def456.txt
timestamp-ghi789.txt
섞이지 않는다. 터미널 1이 끝나면 터미널 1만 알림이 온다.
이건 필수다. 멀티 세션을 못 쓰면 의미가 없다.
jq가 없어도 된다
JSON 파싱에 보통 jq를 쓴다. 근데 다들 jq가 있는 건 아니다.
json_get() {
if command -v jq &> /dev/null; then
# jq 있으면 jq 씀
echo "$1" | jq -r ".$2"
else
# 없으면 grep으로 때움
echo "$1" | grep -oE "\"$2\":[[:space:]]*\"[^\"]*\"" | \
sed "s/\"$2\":[[:space:]]*\"\(.*\)\"/\1/"
fi
}
jq가 있으면 jq를 쓴다. 없으면 grep과 sed로 때운다. 완벽하지 않다. 중첩 JSON은 못 파싱한다. 근데 이 플러그인에선 중첩 JSON이 필요 없다.
의존성을 줄이는 게 더 중요하다. 설치할 게 많아지면 사람들이 안 쓴다.
정리
prompt 입력 → 시간 저장
↓
작업 완료 → 20초 넘었나? → 알림
↓
권한 요청 → 바로 알림
↓
세션 종료 → 파일 삭제
이게 전부다. 복잡하게 만들 이유가 없다.
Hook 네 개. 조건문 하나. 그리고 시간 체크.
다음 글에서는 이걸 macOS, Linux, Windows에서 다 돌아가게 만든 얘기를 할 거다. "그거 macOS 전용이에요?"라는 말을 듣고 시작된 삽질이다.
시리즈 목차:
- 1편: Claude Code, 알림이 필요해
- 2편: Hook이 전부다 (현재 글)
- 3편: macOS 전용이에요?
- 4편: Create pull request
