훅이 늘어나면 체감 속도가 떨어집니다. 편집마다 포맷터 2초, 테스트 30초가 앞을 막으면 Claude도 사람도 기다립니다. "async": true는 그 기다림을 없애는 옵션입니다. 이 글은 동기와 비동기의 차이, 설정 한 줄, 확인 방법, 그리고 비동기로 돌리면 안 되는 훅을 정리합니다. 다음 글은 SubagentStop입니다.
핵심 요약
- 기본 훅은 동기입니다. 끝날 때까지 Claude가 멈춥니다.
"async": true면 프로세스를 띄우고 바로 진행합니다. type: "command"에서만 쓸 수 있습니다.prompt·agent·http훅은 비동기가 안 됩니다.- 비동기 훅은 차단·결정을 못 합니다.
decision,permissionDecision,continue는 무시됩니다. 막아야 하는 훅은 동기로 둡니다. - 끝난 뒤 stdout의 JSON에
additionalContext가 있으면 다음 턴에 Claude에게 전달되고,systemMessage는 사용자에게 보입니다. - 완료 알림은 기본 숨김입니다.
Ctrl+O자세히 보기나--verbose로 확인합니다.
동기 훅과 비동기 훅은 무엇이 다른가
동기 훅은 이벤트 → 훅 실행 → 완료 → 다음 단계 순서로 진행됩니다. 그래서 PreToolUse에서 차단이 가능하고, Stop에서 "아직 끝나지 않았다"고 되돌릴 수 있습니다. 대신 훅이 걸리는 시간만큼 전체가 멈춥니다.
비동기 훅은 이벤트 → 훅 프로세스 시작 → 곧바로 다음 단계입니다. 훅은 뒤에서 혼자 돌다가 끝나면 결과를 다음 대화 턴에 흘려보냅니다. 입력은 동기 훅과 똑같은 JSON을 stdin으로 받습니다. 차이는 "결과를 기다리느냐"뿐이라 스크립트를 고칠 필요가 없습니다.
| 항목 | 동기(기본) | 비동기(async: true) |
|---|---|---|
| Claude 진행 | 훅 완료까지 대기 | 즉시 진행 |
| 차단·결정 | 가능(이벤트에 따라) | 불가 |
| 출력 전달 | 즉시 | 프로세스 종료 후 다음 턴에 additionalContext, 사용자에게 systemMessage |
| 타임아웃 기본 | 600초 | 동일 600초, timeout으로 조정 |
| 중복 제거 | 같은 명령은 한 번 | 발생마다 별도 프로세스, 중복 제거 없음 |
| 지원 타입 | 전부 | command만 |
한 가지 변형이 asyncRewake: true입니다. async를 포함하며, 백그라운드 훅이 종료 코드 2로 끝나면 세션이 유휴 상태여도 Claude를 즉시 깨워 stderr(비어 있으면 stdout)를 시스템 알림으로 보여 줍니다. 오래 걸리는 배포나 테스트가 실패했을 때 "지금 봐야 한다"고 알리는 용도입니다. 일반 async에서는 종료 코드 2가 로그에만 남고 Claude를 깨우지 않습니다.
설정 방법: 한 줄만 추가하면 된다
핸들러 객체에 "async": true를 넣습니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
"async": true,
"timeout": 300
}
]
}
]
}
}
timeout은 백그라운드 프로세스의 최대 시간(초)입니다. 결과를 Claude에게 돌려주려면 스크립트 마지막에 JSON을 찍습니다.
#!/bin/bash
# .claude/hooks/run-tests-async.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
case "$FILE" in *.ts|*.tsx) ;; *) exit 0 ;; esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
if OUT=$(pnpm test 2>&1); then
MSG="테스트 통과 ($FILE 편집 후)"
else
MSG="테스트 실패 ($FILE 편집 후): $(echo "$OUT" | tail -20)"
fi
jq -nc --arg m "$MSG" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$m}}'
JSON은 동기 훅과 같은 스키마로 검증되며 타입이 틀린 필드는 버려집니다(--debug에 경고). v2.1.202 이전에는 비동기 훅의 잘못된 JSON이 세션을 충돌시킬 수 있었으니 구버전이면 업데이트부터 합니다.
동작은 어떻게 확인하나
비동기 훅은 완료 알림이 기본적으로 억제됩니다. 세 가지 방법이 있습니다.
Ctrl+O로 자세히 보기를 켜면 백그라운드 훅 완료가 트랜스크립트에 표시됩니다.claude --verbose로 시작해도 같습니다.- 스크립트가 파일에 흔적을 남기게 합니다. 위 예제라면
pnpm test결과를~/.claude/test-async.log에도>>로 남겨 두면, Claude와 무관하게tail로 확인할 수 있습니다. additionalContext가 전달됐는지는 다음 턴에서 Claude에게 "방금 테스트 결과 받았어?"라고 물어보면 됩니다. 세션이 유휴 상태면 결과는 다음 사용자 입력 때까지 대기합니다.
비동기는 실패해도 조용하므로, 도입 초기에는 파일 로그를 붙여 며칠 관찰한 뒤 뗍니다.
비동기 훅은 어디에 쓰면 좋은가
기준은 하나입니다. 결과가 다음 행동을 막아야 하는가. 막아야 하면 동기, 아니면 비동기입니다.
코드 자동 정형
Prettier·ESLint --fix처럼 파일만 고치고 판단이 필요 없는 후처리는 비동기의 대표 후보입니다. 다만 Claude가 같은 파일을 연달아 편집하는 중에 포맷터가 뒤에서 파일을 바꾸면 Edit의 old_string이 어긋나 편집이 실패할 수 있습니다. 포맷이 빠르다면(1초 안팎) 동기로 두는 편이 오히려 안정적이고, 프로젝트 전체 린트처럼 긴 것만 비동기로 뺍니다.
로그 수집·알림
Stop 훅의 알림음, 데스크톱 알림, 외부 채널로 보내는 메시지, 편집 로그 적재는 결과를 기다릴 이유가 없습니다. 네트워크가 느린 순간에 세션이 같이 멈추는 일도 사라집니다. 알림 스크립트가 exit 0으로 끝나야 한다는 규칙은 비동기에서도 같습니다.
SessionStart에서의 환경 구축
의존성 설치, 컨테이너 기동, 대용량 데이터 동기화처럼 수십 초 걸리는 준비는 비동기로 넘기면 세션이 바로 열립니다. 대신 두 가지를 감안합니다. 첫 명령이 실행되기 전에 준비가 끝난다는 보장이 없으므로, 스크립트가 끝나면 additionalContext로 "설치 완료"를 알려 Claude가 그 뒤에 빌드를 돌리게 합니다. 그리고 CLAUDE_ENV_FILE에 쓰는 환경 변수처럼 첫 명령부터 필요한 것은 짧은 동기 훅에 따로 둡니다.
정리
"async": true한 줄로 훅이 백그라운드로 가고 Claude는 즉시 진행합니다.command타입 전용입니다.- 차단·결정은 불가. 정책을 강제하는 훅은 반드시 동기로 둡니다.
- 결과는 다음 턴에
additionalContext(Claude)와systemMessage(사용자)로 전달됩니다. - 실패가 조용하므로 도입 초기에는 파일 로그와
Ctrl+O자세히 보기로 관찰합니다. - 긴 테스트·린트·알림·환경 구축은 비동기, 빠른 포맷과 차단 검사는 동기가 기본 배치입니다.
자주 묻는 질문
async: true를 붙이면 무엇이 달라지나요?
훅 프로세스를 띄운 뒤 완료를 기다리지 않고 바로 다음 단계로 넘어갑니다. 입력 JSON은 동기 훅과 같고, 끝난 뒤 stdout의 JSON에 additionalContext가 있으면 다음 대화 턴에 Claude에게 전달됩니다. 차단이나 결정은 할 수 없습니다.
어떤 훅에 async: true를 쓰면 좋은가요?
결과가 다음 행동을 막을 필요가 없는 것들입니다. 긴 테스트 실행, 프로젝트 전체 린트, 알림 전송, 로그 적재, 세션 시작 시 의존성 설치나 컨테이너 기동이 대표적입니다. type: "command" 훅에서만 가능합니다.
동기 훅과 비동기 훅을 같은 이벤트에 섞어도 되나요?
됩니다. 같은 matcher 그룹의 hooks 배열에 동기 핸들러와 "async": true 핸들러를 함께 두면, 동기 쪽은 완료까지 기다리고 비동기 쪽은 뒤에서 돕니다. 차단 검사는 동기, 후처리는 비동기로 나누는 것이 전형적인 구성입니다.
비동기로 돌리면 안 되는 경우도 있나요?
있습니다. PreToolUse의 위험 명령 차단, UserPromptSubmit의 기밀 정보 차단, Stop의 "아직 끝나지 않았다" 판단처럼 결정을 돌려줘야 하는 훅은 비동기에서 무시되므로 반드시 동기로 둡니다. 또 Claude가 연속 편집 중인 파일을 뒤에서 바꾸는 포맷터는 편집 실패를 일으킬 수 있어 짧은 것은 동기가 낫습니다.