본문으로 건너뛰기

Hook이 전부다

· 약 3분
JS Koo
Developer
시리즈 안내

이 글은 Claude Code Notifier 개발기 시리즈의 2편입니다. (총 4편)

알림을 보내는 건 쉽다. 어려운 건 언제 보낼지 아는 거다.

Claude Code에는 Hook이라는 게 있다. 특정 시점에 내가 원하는 스크립트를 실행시켜준다. 작업 끝났을 때, 권한 요청할 때, 세션 종료할 때. 그 순간을 잡아서 뭔가를 할 수 있다.

이게 전부다.

네 개의 Hook

Hook언제
UserPromptSubmit내가 뭔가 입력하면
StopClaude가 일 끝내면
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를 쓴다. 없으면 grepsed로 때운다. 완벽하지 않다. 중첩 JSON은 못 파싱한다. 근데 이 플러그인에선 중첩 JSON이 필요 없다.

의존성을 줄이는 게 더 중요하다. 설치할 게 많아지면 사람들이 안 쓴다.

정리

prompt 입력 → 시간 저장

작업 완료 → 20초 넘었나? → 알림

권한 요청 → 바로 알림

세션 종료 → 파일 삭제

이게 전부다. 복잡하게 만들 이유가 없다.

Hook 네 개. 조건문 하나. 그리고 시간 체크.

다음 글에서는 이걸 macOS, Linux, Windows에서 다 돌아가게 만든 얘기를 할 거다. "그거 macOS 전용이에요?"라는 말을 듣고 시작된 삽질이다.


시리즈 목차:


문서: Claude Code Notifier Docs

GitHub: https://github.com/js-koo/claude-code-notifier