클로드 코드에서 Hook을 써야 하는 이유 — UserPromptSubmit 실전 예시

클로드 코드(Claude Code)를 한동안 쓰다 보면 비슷한 답답함을 마주합니다. “커밋 전에 항상 린트 돌려줘”라고 CLAUDE.md에 적어놨는데도 가끔은 건너뛰고, “답할 때 현재 시각도 같이 알려줘”라고 시켜놔도 모델은 자기 학습 시점을 기준으로 어림짐작합니다. 프롬프트와 메모리는 모델이 “참고”하는 자료일 뿐, 반드시 실행을 강제하는 장치가 아니기 때문입니다.

이 한계를 메우는 공식 메커니즘이 바로 HOOK(훅)입니다. 클로드 코드의 lifecycle event에 셸 명령이나 HTTP 호출을 결정적으로 결합해, 모델의 의지와 무관하게 “반드시” 실행되도록 만드는 기능입니다. 이번 글에서는 훅을 써야 하는 네 가지 이유와, 제가 실제로 운영 중인 UserPromptSubmit 훅 — 매 프롬프트마다 현재 한국시각을 자동 주입하는 스크립트를 처음부터 끝까지 함께 정리해 드립니다.

HOOK이란 무엇인가

클로드 코드는 세션 시작·종료, 사용자 프롬프트 제출, 도구 호출 전후, 작업 정지 등 정해진 시점마다 lifecycle event를 발생시킵니다. HOOK은 이 이벤트가 발생할 때 자동으로 실행되는 사용자 정의 작업입니다. 공식 문서에 따르면 다음 다섯 가지 형태를 지원합니다.

  • Command 훅: 셸 스크립트·바이너리를 실행. 이벤트 데이터는 stdin JSON으로, 결과는 exit code와 stdout으로 전달
  • HTTP 훅: 이벤트 JSON을 외부 URL로 POST
  • MCP tool 훅: 연결된 MCP 서버의 툴 호출
  • Prompt 훅: 단일 턴으로 별도 클로드 모델에 평가 요청
  • Agent 훅: 서브 에이전트를 띄워 조건 검증

설정은 settings.json(사용자·프로젝트·로컬 계층)에 작성하고, /hooks 슬래시 커맨드로 현재 등록된 훅을 읽기 전용으로 확인할 수 있습니다. 자세한 내용은 Anthropic 공식 Claude Code 문서에 정리돼 있습니다.

HOOK을 써야 하는 네 가지 이유

1. 결정적 실행이 보장됩니다

가장 큰 이유입니다. CLAUDE.md에 “커밋 전 테스트 실행”이라고 적어두면 모델은 80~90%의 확률로 따르지만, 컨텍스트가 길거나 사용자가 급하게 “일단 커밋해”라고 하면 건너뛸 수 있습니다. 반면 PreToolUse 훅으로 git commit 호출 직전에 테스트 스크립트를 강제 실행하도록 묶어두면, 테스트가 깨질 때 훅이 exit code 1을 반환해 커밋 자체를 차단할 수 있습니다.

2. 매번 같은 지시를 반복할 필요가 없습니다

“오늘 날짜 알려줘”, “KST 기준으로 답해줘”, “/Users/me/Projects 안에서만 작업해줘” 같은 환경 정보를 매 세션·매 프롬프트마다 손으로 적는 건 비효율적입니다. 훅으로 한 번만 등록해두면 모든 세션·모든 프롬프트에 자동 적용됩니다.

3. 외부 시스템·검증 도구와 한 줄로 연결됩니다

PostToolUse 훅에서 eslint·black·mypy를 자동 실행하거나, Stop 훅에서 Slack에 “세션 종료” 메시지를 보내거나, Notification 훅에서 macOS osascript로 알림을 띄우는 것이 모두 한 줄짜리 셸 명령으로 가능합니다. 모델이 “해줄까요?”라고 묻기를 기다리지 않아도 됩니다.

4. 컨텍스트를 자동으로 주입할 수 있습니다

UserPromptSubmit 훅은 stdout으로 특정 형식의 JSON을 반환하면, 그 내용이 사용자 프롬프트와 함께 모델에게 system 컨텍스트로 전달됩니다. 사용자에게는 보이지 않지만 모델 입장에서는 매 프롬프트마다 “현재 시각”, “현재 git 브랜치”, “남은 토큰 추정치” 같은 환경 정보를 자동으로 받게 됩니다.

주요 lifecycle event 한눈에 보기

이벤트발생 시점대표 활용
SessionStart세션 시작·재개 시인사 메시지, 환경 변수 점검, 메모리 로드
UserPromptSubmit매 사용자 프롬프트 제출 직전현재 시각·git 상태 등 컨텍스트 자동 주입
PreToolUse도구 호출 직전위험 명령 차단, 사전 검증, 권한 분기
PostToolUse도구 호출 성공 직후코드 자동 포맷, 린트, 테스트 트리거
Notification알림 발생 시OS 알림, Slack/메일 푸시
Stop모델이 작업을 끝낼 때완료 알림, 세션 요약 저장, 메트릭 기록
SessionEnd세션 종료 시로그 정리, 메모리 압축, 백업

이 중 UserPromptSubmitStop은 매처(matcher)를 지원하지 않아 항상 실행되고, PreToolUse·PostToolUse는 도구명을 매처로 지정해 특정 도구에만 걸 수 있습니다.

UserPromptSubmit 실전 예시 — 현재 시각 자동 주입

제가 실제로 운영 중인 훅을 그대로 보여드립니다. 목적은 단순합니다. 매 프롬프트가 모델에 들어갈 때마다 “현재 시각: YYYY-MM-DD HH:MM:SS KST (요일)” 한 줄을 system 컨텍스트로 함께 전달하는 것입니다. 모델은 자기 학습 시점이 아니라 현재 한국시각을 기준으로 답할 수 있게 됩니다.

1단계: 셸 스크립트 작성

홈 디렉터리 아래 훅 디렉터리를 만들고 다음 스크립트를 저장합니다. 경로 예시: ~/.claude/hooks/inject_current_time.sh

#!/bin/bash
# UserPromptSubmit hook: inject current KST time as additional context
# stdin: {"session_id": "...", "prompt": "..."} (we don't read it)
# stdout: JSON with hookSpecificOutput.additionalContext

ts=$(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M:%S')
wd=$(TZ=Asia/Seoul date '+%u')
case "$wd" in
1) day=월요일 ;;
2) day=화요일 ;;
3) day=수요일 ;;
4) day=목요일 ;;
5) day=금요일 ;;
6) day=토요일 ;;
7) day=일요일 ;;
esac

ctx="현재 시각: ${ts} KST (${day})"

printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\n' "$ctx"

저장 후 실행 권한을 부여합니다.

chmod +x ~/.claude/hooks/inject_current_time.sh

2단계: settings.json에 등록

전역 적용을 원한다면 ~/.claude/settings.json, 프로젝트에만 적용하려면 .claude/settings.json에 다음 블록을 추가합니다.

{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "/Users/사용자명/.claude/hooks/inject_current_time.sh",
"timeout": 5
}
]
}
]
}
}

timeout은 초 단위입니다. 훅이 응답을 5초 안에 못 주면 무시됩니다. 사용자 입력 흐름을 막지 않도록 짧게 잡는 것이 좋습니다.

3단계: 작동 원리 이해

UserPromptSubmit 훅이 발화되면 클로드 코드는 stdin으로 세션 메타데이터 JSON을 보내고, 훅의 stdout을 회수합니다. stdout이 다음 형식의 JSON이면 additionalContext 필드 값이 모델에게 system 컨텍스트로 자동 추가됩니다.

JSON 키역할
hookSpecificOutput.hookEventName훅 이벤트명. UserPromptSubmit으로 고정
hookSpecificOutput.additionalContext모델에 전달할 system 컨텍스트 문자열

사용자에게는 추가 컨텍스트가 보이지 않지만, 모델은 프롬프트와 함께 “현재 시각: 2026-04-25 16:12:57 KST (토요일)” 같은 한 줄을 매번 받게 됩니다. 학습 시점 추론으로 “어제 일요일이었으니 오늘은 월요일이군요” 같은 오답을 내지 않게 되는 핵심 장치입니다.

4단계: 등록 확인

클로드 코드에서 /hooks를 입력하면 현재 활성화된 훅 목록을 읽기 전용으로 확인할 수 있습니다. UserPromptSubmit 항목 아래 방금 등록한 스크립트 경로가 보이면 정상입니다.

또 다른 활용 아이디어

  • Stop 훅 + osascript: 모델이 답변을 끝낼 때마다 macOS 알림으로 “작업 완료” 띄우기
  • PreToolUse 훅 + 위험 명령 차단: rm -rf / 같은 패턴이 포함된 Bash 호출은 셸 스크립트가 exit 1로 막기
  • PostToolUse(Edit·Write) + 자동 포맷: 파일이 수정되면 prettier·black을 자동 실행
  • SessionStart 훅: 메모리 디렉터리에서 최근 7일 작업 로그를 모아 컨텍스트로 주입
  • Notification 훅 + Slack Webhook: 권한 확인 다이얼로그가 뜨면 폰으로 푸시 알림

주의할 점

  • 훅은 모델이 손댈 수 없는 자리에서 실행되므로 잘못 짜면 사용자 흐름을 통째로 막을 수 있습니다. 반드시 짧은 timeout과 안전한 종료 코드를 함께 설계하세요.
  • UserPromptSubmit 훅은 매 프롬프트마다 실행되므로, 무거운 외부 API 호출이나 LLM 호출을 직접 넣으면 응답 지연이 누적됩니다. 캐싱과 비동기 처리를 고려해야 합니다.
  • 사용자 계층(~/.claude/settings.json)에 등록한 UserPromptSubmit 훅이 일부 서브디렉터리에서 실행되지 않는 이슈가 보고된 적이 있습니다. 안 먹히면 프로젝트 단위 .claude/settings.json에 동일하게 등록해 보세요.
  • 훅 출력 JSON에 따옴표·줄바꿈이 들어갈 때는 셸의 printf 인용 처리가 깨지기 쉽습니다. 운영용 훅은 가능하면 Python·Node 스크립트로 작성해 json.dumps로 직렬화하는 편이 안전합니다.

자주 묻는 질문

훅과 슬래시 커맨드(skill)는 어떻게 다른가요?

슬래시 커맨드와 스킬은 모델이 능동적으로 호출하는 도구고, 훅은 이벤트 발생 시 자동으로 실행되는 시스템 장치입니다. 모델 의도와 무관하게 “반드시” 실행돼야 하는 동작은 훅으로, 모델이 판단해 호출해도 되는 작업은 스킬·슬래시 커맨드로 분리하는 것이 좋습니다.

HOOK이 작동하는지 디버깅은 어떻게 하나요?

훅 스크립트 안에서 echo "디버그 메시지" >> /tmp/hook.log 같은 식으로 파일에 로그를 남기면 됩니다. stdout은 클로드 코드가 가로채 JSON으로 해석하므로 디버그 출력은 stderr나 별도 파일에 분리해 두는 게 안전합니다.

다른 운영체제에서도 같은 스크립트가 작동하나요?

Linux와 macOS는 위 셸 스크립트가 그대로 동작합니다. Windows는 bash 대신 PowerShell·WSL을 사용하거나, 크로스 플랫폼 호환을 위해 Python 스크립트로 작성한 뒤 python3 명령으로 호출하는 방식이 권장됩니다.

UserPromptSubmit 훅이 너무 자주 실행돼서 부담되지는 않나요?

본문 예시처럼 date 명령 한 줄짜리는 1ms 안에 끝나므로 체감 지연이 없습니다. 외부 API 호출처럼 응답이 길어지는 작업은 별도 백그라운드 데몬으로 빼두고, 훅에서는 캐시된 결과만 읽어 반환하는 패턴이 안전합니다.