블로그

Claude Code의 Hooks 2026: 단계별 설정과 바로 쓸 수 있는 예제 7가지

Markdown

.claude/settings.json에 바로 복사해 붙여 넣을 수 있는 hook 7개: .env 보호, prettier, main 브랜치 강제 푸시, em dash, 개발 서버. 핸들러 유형 4가지와 이벤트 31개.

전체 글 읽기

Claude Code의 hook은 Claude 작업 전후에 자동으로 실행되는 셸 명령, HTTP 호출 또는 짧은 LLM 검사입니다. CLAUDE.md로는 처리할 수 없는 일을 맡습니다. 특정 도구 이벤트 검사, 수정 후 포맷, 세션 시작 맥락 주입에 사용합니다. 일치 PreToolUse는 해당 호출을 거부할 수 있지만 정규식 예시는 모든 파일·Git 경로의 보안 경계가 아닙니다. 다음 내용에서는 핸들러 유형 4가지, 라이프사이클 이벤트 31개, “어떤 규칙을 hook으로 만들어야 하는가”를 판단하는 테스트, .claude/settings.json에 바로 복사할 수 있는 hook 7개, 그리고 제가 직접 겪은 함정 3가지를 살펴봅니다.

Ailoot 문서: API, 키, 설정.

Claude Code에서 hook이란?

핵심 내용

hook은 특정 라이프사이클 이벤트에 응답해 Claude Code가 실행하는 셸 명령, HTTP 호출, 짧은 LLM 프롬프트 또는 하위 에이전트입니다. 모델이 무시하거나 잊을 수 있는 CLAUDE.md의 지시와 달리 hook은 매번 똑같이 실행됩니다. 제안이 아니라 코드입니다.

반년 동안 “.env를 직접 수정하지 말 것”이라는 규칙을 CLAUDE.md에 넣어 두었습니다. Claude는 한 달에 한 번씩 이 규칙을 어겼습니다. 일부러 그런 것은 아니었습니다. 긴 세션이 끝날 무렵이면 그냥 잊어버렸습니다. 한 번은 .env에 토큰이 하나 더 들어갔고, 또 한 번은 중복 이름의 변수가 사용 중이던 변수를 덮어썼습니다.

일치한 직접 편집 도구가 .env 경로를 주면 hook이 그 호출을 거부합니다. Bash 리디렉션, 스크립트, MCP와 다른 경로는 별도 제어가 필요하며 파일을 불변으로 만드는 예시는 아닙니다.

Hook은 사용자가 정의하는 셸 명령, HTTP 엔드포인트 또는 LLM 프롬프트이며, Claude Code 라이프사이클의 특정 시점에 자동으로 실행됩니다.

— Anthropic, https://code.claude.com/docs/en/hooks

다시 말해 hook은 특정 이벤트에서 Claude Code가 실행하는 사용자 명령입니다. 파일 수정, Bash 실행, 세션 시작 또는 하위 에이전트 완료는 모두 hook을 연결할 수 있는 이벤트입니다.

바이브 코딩을 하는 사람에게 중요한 이유:

  • 실수로 편집하지 않도록 비밀 정보가 담긴 파일을 보호합니다.
  • 코드를 생성할 때마다 prettier/eslint를 자동으로 실행합니다.
  • 명시적인 허가 없이는 main 브랜치에 git push --force를 실행하지 못하게 합니다.
  • 세션이 시작될 때마다 최신 컨텍스트를 주입합니다.
  • 감사를 위해 Claude의 작업을 기록합니다.

CLAUDE.md는 컨텍스트를 처리합니다. Hook은 작업을 처리합니다. 둘은 서로 다른 것이며, 이를 혼동하는 것이 초보자가 가장 흔히 하는 실수입니다.

Hook은 CLAUDE.md 및 skill과 어떻게 다른가요?

핵심 내용

CLAUDE.md는 모델이 세션 시작 시 읽고 따르려고 하는 텍스트입니다. Hook은 모델과 관계없이 Claude Code 프로세스가 이벤트에 응답해 실행하는 셸 명령입니다. Skill은 Claude가 Skill 도구로 호출할 수 있는 이름 있는 지침 패키지로, 함수와 비슷합니다. 세 가지는 서로 다른 종류의 작업을 처리합니다.

Anthropic 문서는 CLAUDE.md와 hook의 차이를 명시적으로 설명합니다.

CLAUDE.md의 내용은 시스템 프롬프트의 일부가 아니라 시스템 프롬프트 다음에 사용자 메시지로 전달됩니다. Claude는 이를 읽고 따르려고 하지만, 엄격하게 준수한다는 보장은 없습니다.

— Anthropic, 메모리 문서, https://code.claude.com/docs/en/memory

달리 말하면 CLAUDE.md의 내용은 시스템 지시가 아니라 사용자의 첫 메시지로 Claude에 전달됩니다. 모델은 이를 읽고 따르려고 합니다. 하지만 긴 세션의 끝 무렵처럼 컨텍스트가 커지고 오래된 지시가 밀려날 때는 특히 보장할 수 없습니다.

Hook은 다르게 작동합니다. settings.json에 저장되고 모델에 의존하지 않으며, PreToolUse, PostToolUse, SessionStart 등의 특정 이벤트에서 Claude Code 프로세스가 실행합니다. 명령은 Claude가 결과를 받기 전에 실행되며, 작업을 물리적으로 차단하거나(exit 2) 편집 후 파일을 수정할 수 있습니다(exit 0).

비교:

기능CLAUDE.mdHookSkill
위치프로젝트 루트의 CLAUDE.md.claude/settings.json.claude/skills/ 폴더
실행 주체모델이 사용자 메시지의 일부로 읽음이벤트 발생 시 Claude Code 프로세스Skill 도구를 통한 모델
실행 보장없음(best-effort)일치하도록 설정한 명령 hook이 코드를 실행하며 실패·종료 동작은 이벤트에 따라 다름모델 호출을 통해 실행(grant가 있어도 셸 수준은 아님)
용도프로젝트 컨텍스트, 어조, 스타일, 라우팅제한, 포맷팅, 특정 파일 검사매개변수가 있는 복잡한 다단계 절차
실행 시점세션 내 모델 메시지마다라이프사이클 이벤트 발생 시모델이 호출할 때
작업을 차단할 수 있나요?아니요예(exit 2)아니요

전체 CLAUDE.md 템플릿과 파일을 계속 관리하기 위한 규칙 6가지는 2026년 CLAUDE.md 설정 방법: 바로 쓸 수 있는 템플릿과 규칙 6가지에서 확인할 수 있습니다. 이름 있는 기능으로서의 skill은 Claude Code Skills: 나만의 라이브러리 만들기에서 알아보세요.

Hook은 가장 낮은 수준입니다. 모델도, 해석도 없이 셸만 작동합니다.

Hook은 Plan Mode, /compact와 함께 context engineering 도구 중 하나입니다.

Hook 유형 4가지: Command, HTTP, Prompt, Agent 중 무엇을 선택할까요?

핵심 내용

Claude Code에는 핸들러 유형이 4가지 있습니다. Command는 가장 흔한 셸 스크립트입니다. HTTP는 팀과 통합을 위해 사용자의 URL로 POST 요청을 보냅니다. Prompt는 의미를 확인하기 위해 짧은 Haiku 호출을 합니다. Agent는 심층 검증을 위해 도구 접근 권한이 있는 하위 에이전트를 만듭니다. 초보자는 Command와 Prompt면 충분합니다. 작업의 90%를 처리할 수 있습니다.

유형기능선택할 때복잡도
Command셸 명령을 실행하고 stdin으로 JSON을 받은 뒤 종료 코드와 stdout으로 응답합니다90%의 경우: 경로 확인, 포맷팅, 콘텐츠에 grep 실행, lsof, git 작업낮음
HTTPJSON 본문을 담은 POST 요청을 사용자 URL로 보내고 JSON 응답을 받습니다팀/CI에 규칙을 두어야 할 때: 중앙 집중식 검증, 공유 상태 대시보드, 원격 감사중간(서버 필요)
Prompt(기본적으로) Haiku에 짧은 프롬프트를 보내 의미 평가에 대한 예/아니요 응답을 받습니다grep만으로는 부족할 때: “이 글이 기업적인 어조인지 평가해 줘”낮음
Agent여러 단계의 검사를 위해 Read/Grep/Glob 접근 권한이 있는 하위 에이전트를 만듭니다강도 높은 검증: “변경된 파일을 모두 찾고 각각에 대응하는 테스트가 있는지 확인해 줘”높음

가장 흔한 실수는 Prompt가 필요한 작업을 Command로 처리하려는 것입니다. 예를 들어 “.md 파일에서 em dash를 쓰지 마”는 grep으로 검사할 수 있으므로 Command가 이상적입니다. 하지만 “기업적인 어조로 쓰지 마”는 의미에 관한 문제라 Prompt가 필요합니다.

처음 시작한다면 Command를 사용하세요. 아래의 바로 쓸 수 있는 hook 7개는 모두 Command입니다. 준비가 되면 HTTP/Prompt/Agent를 사용하면 됩니다.

settings.json은 어디에 있으며 설정 수준은 4가지로 어떻게 나뉘나요?

핵심 내용

Claude Code는 네 위치에서 설정을 찾습니다. enterprise(MDM으로 배포), 사용자(~/.claude/settings.json), 프로젝트(저장소 루트의 .claude/settings.json), 로컬(.claude/settings.local.json, git에 포함되지 않음)입니다. 네 파일은 모두 세션 시작 시 병합되며, 더 낮은 수준의 설정이 우선합니다.

수준경로적용 범위git에 포함되나요?
Enterprise/Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux)MDM을 통해 팀 전체에 적용아니요
사용자~/.claude/settings.json사용자 한 명의 모든 프로젝트아니요
프로젝트저장소 루트의 .claude/settings.json이 프로젝트, 팀 전체예
로컬.claude/settings.local.json이 프로젝트, 이 사용자만아니요 (.gitignore에 있음)

아래에서 설명하는 hook 7개는 프로젝트의 .claude/settings.json에 보관합니다. 따라서 저장소를 복제하면 자동으로 포함되고, Claude는 새 창에서도 같은 규칙을 따릅니다. 개인 실험용 hook은 .claude/settings.local.json에 넣어 팀 작업에 방해가 되지 않도록 합니다.

네 파일은 모두 세션이 시작될 때 병합됩니다. 규칙이 충돌하면 낮은 수준이 우선합니다(로컬이 프로젝트보다 우선하고, 프로젝트가 사용자보다 우선합니다).

첫 hook을 만드는 가장 빠른 방법은 Claude Code 안에서 대화형 /hooks 메뉴를 사용하는 것입니다. 목록에서 이벤트를 고르고 matcher(예: Bash 또는 Edit)를 선택한 다음 명령을 붙여 넣으세요. Claude Code가 올바른 JSON을 settings.json에 작성해 줍니다.

라이프사이클 이벤트 31개: 초보자에게 필요한 것은 무엇인가요?

핵심 내용

현재 Claude Code에는 라이프사이클 이벤트가 31개 있으며 실행 빈도에 따라 세 가지로 나뉩니다. 세션당 한 번(SessionStart/SessionEnd), 턴당 한 번(UserPromptSubmit/Stop), 도구를 호출할 때마다(PreToolUse/PostToolUse) 실행됩니다. 초보자에게는 31개 중 4개가 필요합니다. 제한을 위한 PreToolUse, 자동 수정을 위한 PostToolUse, 컨텍스트를 위한 SessionStart, 마지막 검사를 위한 Stop입니다.

이벤트는 세 가지 빈도로 발생합니다. 세션당 한 번(SessionStart, SessionEnd), 턴당 한 번(UserPromptSubmit, Stop, StopFailure), 그리고 에이전트 루프 내에서 도구를 호출할 때마다(PreToolUse, PostToolUse)입니다.

— Anthropic, hooks 참조 문서, https://code.claude.com/docs/en/hooks

혼동하지 않도록 세 가지 실행 빈도를 이해하는 것이 중요합니다.

  1. 세션당 한 번 - SessionStart, SessionEnd, Setup. Claude Code가 시작되고 종료될 때 한 번 실행됩니다. 컨텍스트 초기화와 임시 파일 정리에 사용하세요.
  2. 턴당 한 번 - UserPromptSubmit, UserPromptExpansion, Stop, StopFailure. 사용자의 각 메시지와 모델이 멈출 때마다 발생합니다. 대화를 기록하고 최종 결과를 검사할 때 사용하세요.
  3. 도구를 호출할 때마다 - PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, PostToolBatch, SubagentStart, SubagentStop. 파일 편집, Bash 실행, 읽기 또는 검색 때마다 발생합니다. 제한, 포맷팅, 검증이 실행되는 가장 빈번한 계층입니다.

전체 이벤트 31개에는 비동기/반응형 이벤트도 포함됩니다. TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification입니다. 고급 시나리오용이므로 지금은 건너뛰세요.

규칙을 hook에 넣을지 CLAUDE.md에 둘지 판단하는 다섯 가지 질문

핵심 내용

규칙이 컨텍스트가 아닌 작업에 관한 것이고, 안전/돈/데이터와 관련 있으며, grep/regex로 검사할 수 있고, 어겼을 때 되돌리기 어려우며, Claude가 이미 한 번 이상 어긴 적이 있다면 hook으로 만들면 됩니다. 다섯 질문 중 “예”가 세 개 미만이면 CLAUDE.md에 둡니다. 제 80줄에 이 테스트를 적용해 5개 규칙을 hook으로 옮겼습니다.

저는 이 기준을 제안이 아니라 법처럼 적용합니다. CLAUDE.md와 hook 중 무엇을 선택할지 간단한 다섯 질문 테스트로 정리했습니다. “예”가 세 개 이상이면 규칙을 hook에 넣습니다. 세 개 미만이면 텍스트에 둡니다.

“어떤 규칙을 hook으로 만들어야 하는가” 테스트:

  1. 이 규칙은 컨텍스트가 아니라 작업에 관한 것인가요? (“rm -rf를 실행하지 마”는 작업이고, “TypeScript 프로젝트가 있어”는 컨텍스트입니다.)
  2. 이 규칙은 안전, 돈 또는 데이터에 관한 것인가요? (“.env를 건드리지 마”, “키를 공개하지 마”.)
  3. grep, regex 또는 셸 명령의 종료 코드로 확인할 수 있나요? (“.md를 수정한 다음 em dash가 없는지 확인해”.)
  4. 규칙을 어기면 결과를 되돌리는 데 큰 수고가 드나요? (“main에 강제 푸시”는 되돌리기 어렵고, “문장 끝에 마침표를 빠뜨림”은 쉽습니다.)
  5. Claude가 이 규칙을 한 번 이상 잊는 것을 이미 본 적이 있나요?

예: “em dash를 쓰지 말고 하이픈만 사용해.” 작업인가요? 예. 안전과 관련 있나요? 아니요. grep으로 확인할 수 있나요? 예. 되돌리기 어렵나요? 아니요. 위반을 본 적 있나요? 예. 5개 중 3개: hook에 넣으세요.

반례: “저는 vibe coder이지 개발자가 아닙니다. 속어 없이 써 주세요.” 작업인가요? 컨텍스트에 더 가깝습니다. 안전과 관련 있나요? 아니요. regex로 확인할 수 있나요? 너무 어렵습니다. 되돌리기 어렵나요? 아니요. 위반을 본 적 있나요? 예. 5개 중 1개: CLAUDE.md에 두세요.

제 규칙 중 다섯 개가 테스트를 통과했습니다. 이 규칙들을 hook으로 옮겼고 나머지 75%는 텍스트 파일에 남겼습니다.

바로 복사할 수 있는 hook 7개

핵심 내용

아래는 .claude/settings.json에 바로 넣어 쓸 수 있는 hook 7개입니다. .env 수정 차단, prettier 자동 실행, main 브랜치 강제 푸시 방지, em dash 검사, npm run dev 전에 포트 비우기, 테스트 성공 후 자동 커밋, 세션 시작 시 최신 컨텍스트 주입입니다. 각각을 파일에 복사하고 Claude Code를 다시 시작하면 작동합니다.

hook 7개는 모두 프로젝트 루트의 .claude/settings.json 파일 하나에 저장됩니다. 각 항목에는 이벤트 유형, 도구 필터(matcher), 셸 명령이 있습니다. PreToolUse의 종료 코드 2는 일치 호출을 실행 전에 거부합니다. PostToolUse에서는 이미 동작이 일어났고 피드백만 제공해 되돌리지 못합니다.

대부분의 hook 이벤트에서 작업을 차단하는 종료 코드는 2뿐입니다. Claude Code는 종료 코드 1을 차단하지 않는 오류로 처리하고 작업을 계속합니다. Unix에서 관례적으로 1이 실패를 뜻하더라도 마찬가지입니다.

— Anthropic, hooks 참조 문서, https://code.claude.com/docs/en/hooks

exit 0은 성공을 뜻합니다. exit 1은 오류를 뜻하지만 Claude는 계속 진행합니다(아래 첫 번째 안티패턴입니다). exit 2는 이벤트별로 다릅니다. PreToolUse는 차단할 수 있지만 PostToolUse는 수행된 동작의 피드백만 보고합니다. 무엇이 잘못되었는지 Claude에 알려 주세요.

Hook 1. 직접적인 .env 편집 검사 (PreToolUse, exit 2)

.env, .env.local, .env.production 등과 일치하는 모든 파일을 직접 수정하려는 시도를 차단합니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo '`.env`를 직접 수정하지 마세요. 추가할 변수를 알려주시면 제가 파일을 열겠습니다.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

기능: Edit/Write/MultiEdit가 실행되기 전에 파일 경로를 가져와 grep으로 .env 패턴과 일치하는지 확인합니다. 일치하면 exit 2와 함께 메시지를 반환합니다. Claude는 stderr를 보고 이렇게 말합니다. “.env를 수정하려 했지만 hook이 차단했습니다. 추가할 변수를 알려 주세요.”

Hook 2. 코드를 수정할 때마다 prettier 자동 실행 (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
          }
        ]
      }
    ]
  }
}

기능: Edit/Write가 끝나면 경로를 가져옵니다. 확장자가 .ts/.tsx/.js/.jsx/.json이면 &를 사용해 prettier를 백그라운드에서 실행합니다. 제한이 아니라 포맷팅이므로 항상 0으로 종료합니다. prettier가 실패해도 괜찮습니다. 파일은 기존 상태로 남습니다.

hook 1과의 차이는 근본적입니다. PreToolUse + exit 2는 작업 전에 적용되는 강제 차단입니다. PostToolUse + exit 0은 작업 후에 적용되는 가벼운 수정입니다. 서로 다른 작업을 위한 두 가지 모드입니다.

Hook 3. main 브랜치에 git push --force 실행 차단 (PreToolUse, exit 2)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'main 브랜치 강제 푸시는 금지되어 있습니다. 새 브랜치를 만들거나 제가 수동으로 하도록 요청하세요.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

기능: Bash가 시작될 때마다 명령에 push가 포함되어 있는지, --force/--force-with-lease/-f가 포함되어 있는지, main을 언급하는지 확인합니다. 세 조건이 모두 일치하면 명확한 메시지와 함께 차단합니다.

예전에는 이 규칙을 CLAUDE.md에 대문자로 적어 두었습니다. “잘못된 커밋을 되돌리려고 강제 푸시해 줘”라고 요청하면 Claude는 그래도 약 2주에 한 번 규칙을 어겼습니다. 표시한 명령은 잡지만 별칭, 스크립트, 생략 목적 브랜치, 다른 Git 클라이언트는 우회할 수 있습니다. 원격 main 보호와 에이전트 자격 증명 제한을 강제하세요.

Hook 4. 텍스트 파일의 em dash 차단 (PostToolUse, exit 2)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"파일 $FILE에 em dash(U+2014)가 있습니다. 일반 하이픈(U+002D)으로 바꾸세요.\" >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

기능: PostToolUse의 exit 2는 이미 편집한 뒤 실패를 Claude에 알려주며 쓰기를 막거나 되돌리지 않습니다. Claude가 파일을 편집한 뒤 em dash를 남기면 hook이 실패하고 Claude는 피드백을 받아 다시 시도합니다. 2–3회 세션을 거치면 Claude는 이 문자를 더 이상 삽입하지 않습니다.

grep은 $'\xe2\x80\x94'를 사용해 em dash 문자(U+2014)를 정확히 검색합니다. 이는 UTF-8 바이트를 표기하는 Bash 표기법입니다. 그냥 grep -q '-'라고 쓰면 일반 하이픈까지 잡아 모든 파일에서 실행됩니다. 눈에 보이지 않는 문자 하나가 전체 로직을 망가뜨릴 수 있습니다.

Hook 5. npm run dev 전에 포트 3000 비우기 (PreToolUse, exit 0)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo '시작 전에 포트 3000을 비웠습니다' >&2; }; exit 0"
          }
        ]
      }
    ]
  }
}

기능: npm run dev(또는 pnpm/yarn)를 실행하기 전에 포트 3000을 사용 중인 프로세스를 종료합니다. 제한이 아니라 준비 작업이므로 항상 0으로 종료합니다. Claude가 “포트 사용 중”이라는 메시지를 보고 코드 문제라고 생각해 이것저것 수정하기 시작하는 일을 막아 줍니다. 실제로는 이전 서버가 계속 실행 중이었을 뿐인 경우가 있습니다.

Hook 6. 테스트 성공 후 자동 커밋 (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
          }
        ]
      }
    ]
  }
}

기능: stdin JSON을 한 번만 읽고 표시한 정확한 테스트 명령만 허용합니다. tool_response.exit_code가 명시적 0일 때만 진행하고 설치 버전에 필드가 없으면 성공을 추정하지 말고 커밋을 생략합니다. src/orders.ts와 tests/orders.test.ts는 작업 소유의 실제 파일로 바꾸세요. 이미 staged 변경이 있으면 멈춰 무관한 작업을 넣지 않습니다. add 실패는 중지, 빈 diff는 커밋 없음이며 일반 커밋 훅은 켜 둡니다. 활성화 전에 성공·실패·결과 누락 입력을 오프라인 시험하세요.

안티패턴: 매 Edit에 git commit --no-verify 추가. 검사를 유지하세요. 검사가 이 자동화를 재귀 호출하면 트리거를 좁히거나 검사를 재설계하고 기본적으로 우회하지 마세요. 자동 커밋 전에 staged diff와 소유 범위를 검토하세요.

Hook 7. 세션 시작 시 최신 컨텍스트 주입 (additionalContext가 있는 SessionStart)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
          }
        ]
      }
    ]
  }
}

기능: 세션이 시작될 때 현재 Git 브랜치와 커밋되지 않은 변경 사항 목록을 수집하고 additionalContext 필드가 있는 JSON을 반환합니다. Claude Code는 이 텍스트를 첫 시스템 메시지에 주입하므로 Claude는 시작부터 현재 위치와 이전 세션에서 남은 작업을 알 수 있습니다.

additionalContext는 Claude가 환경의 현재 상태나 막 완료한 작업에 대해 알아야 하는 정보에 사용하세요. 환경 상태(현재 브랜치, 배포 대상, 활성화된 feature flag), 조건부 프로젝트 규칙(방금 수정한 파일에 적용할 테스트 명령), 외부 데이터(담당자에게 배정된 미해결 이슈, 최근 CI 결과) 등이 있습니다.

— Anthropic, hooks 참조 문서, https://code.claude.com/docs/en/hooks

이 hook을 확장하려면 최신 커밋과 설명, GitHub Actions 상태, Linear/Jira의 활성 스프린트, 현재 저장소의 미해결 이슈를 가져올 수 있습니다.

누구나 겪는 hook의 함정 3가지

핵심 내용

첫 번째 함정: exit 1로 끝나는 hook은 아무 알림 없이 통과하고 Claude는 오류를 무시합니다. 두 번째: hook에서 무거운 작업을 하면 60초 후 중단됩니다. 세 번째: hook은 프로젝트의 .env가 아니라 ~/.zshrc의 환경 변수만 볼 수 있습니다. 각각 한 줄로 해결할 수 있지만, 이런 함정을 모르면 “hook이 작동하지 않는다”고 생각하기 쉽습니다.

함정 1. exit 1로 끝나는 hook은 아무 알림 없이 통과

가장 위험한 문제입니다. 일주일 동안 .env 보호 hook이 작동한다고 생각했지만, hook에 null 검사를 하지 않고 jq -r '.tool_input.file_path'를 넣었던 것으로 드러났습니다. tool_input이 비어 있으면 jq는 코드 1로 종료했습니다. Claude는 exit 1을 “hook 오류, 계속 진행”으로 처리했으므로 작업이 그대로 진행됐습니다. 왜 계속 .env를 건드리는지 이해할 수 없었습니다. 입력이 빈 상태로 터미널에서 hook을 수동 실행하고 나서야 원인을 확인했습니다.

해결 방법은 한 가지 규칙입니다. 모든 hook은 null을 검사하는 것으로 시작하고 || exit 0으로 끝내세요. 그러면 예상치 못한 종료가 조용히 통과하는 대신 “안전한” 종료가 됩니다.

# 안전한 시작 및 종료 템플릿:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... 로직 ...
exit 0   # 또는 한 줄에 || exit 0

함정 2. 느린 hook은 시간 초과로 중단됨

Edit 후 파일 전체에서 eslint --fix를 실행하는 hook이 있었습니다. 큰 파일(1000+줄)은 처리하는 데 8–10초가 걸렸습니다. Claude Code의 기본 hook 시간 제한은 60초이므로 충분히 여유 있다고 생각했습니다. 하지만 1분 안에 Edit가 여러 번 발생하면 hook이 쌓여 대기열에 들어가고, 일부는 아예 건너뛰어집니다. 로그에 “hook timed out”이라고 표시된 것을 보고서야 알아차렸습니다.

해결 방법: hook에서 무거운 작업을 해야 한다면 백그라운드에서 실행하세요. 결과를 기다리지 않고 eslint --fix "$FILE" &를 사용합니다. 그러면 실제 작업은 백그라운드에서 진행되고 hook은 몇 밀리초 만에 exit 0을 반환합니다.

# 백그라운드에서 실행하는 무거운 작업:
npx eslint --fix "$FILE" &
exit 0

함정 3. hook은 프로젝트 환경 변수를 볼 수 없음

hook에서도 Node.js 앱처럼 process.env를 사용할 수 있다고 생각했습니다. 그렇지 않습니다. hook은 Claude Code 부모 프로세스가 생성한 셸에서 실행되며 ~/.zshrc 또는 ~/.bashrc에 있는 값과 Claude Code가 명시적으로 전달한 값만 볼 수 있습니다. hook이 .env에 있는 OPENAI_API_KEY에 의존한다면 .env를 직접 읽어야 합니다.

해결 방법: hook 명령에서 .env를 명시적으로 읽으세요.

# 프로젝트 환경을 hook에 전달:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# 또는 dotenv 사용:
dotenv -e .env -- node my-validator.js

패키지마다 별도의 .env 파일을 사용하는 모노레포에서는 특히 불편합니다. PWD를 전달하고 명령 시작 부분에서 cd "$PWD"를 명시적으로 실행하면 해결할 수 있습니다.

커밋 전에 hook을 테스트하려면 어떻게 해야 하나요?

핵심 내용

hook은 셸 명령입니다. 가짜 stdin을 사용해 터미널에서 수동으로 실행하고, 종료 코드와 stderr를 확인할 수 있습니다. Claude Code가 실행하기 전에 이 모든 작업을 할 수 있습니다. 이렇게 하면 디버깅에 드는 시간을 아끼고 자체 작업을 차단하는 hook을 방지할 수 있습니다.

기본 테스트 시나리오. JSON에서 hook 명령을 가져와 터미널을 열고 Claude Code가 보낼 JSON을 전달합니다.

# hook 1 테스트 (.env 보호):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
  echo "BLOCKED" >&2
  exit 2
} || exit 0
'
echo "Exit code: $?"

stderr에 BLOCKED가 표시되고 Exit code: 2가 나와야 합니다. exit 0이 나온다면 정규식이 작동하지 않는 것이니 grep을 수정하세요.

확인할 시나리오:

  1. 정상 경로 - 파일이 차단되어야 합니다. hook 1에서는 .env, hook 3에서는 git push --force main을 사용하세요.
  2. 차단되지 않는 경로 - 파일이 차단되지 않아야 합니다. hook 1에서는 README.md, hook 3에서는 git push origin feature-branch를 사용하세요.
  3. 예외 상황 - 빈 입력, null 값, 예상하지 못한 문자입니다. hook은 exit 0을 반환하거나 명확한 오류와 함께 실패해야 합니다. 조용히 통과하면 안 됩니다.

실험용 hook을 둘 곳. .claude/settings.json이 아니라 .claude/settings.local.json(git에 포함되지 않음)에 두세요. 실제 작업 대신 echo를 사용해 새 hook을 PostToolUse 아래에 넣고, 작동하는지 확인한 다음 운영용 settings.json으로 옮기세요.

// settings.local.json에서 실험 - /tmp/hook-debug.log에 기록:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}

Claude Code에서 5–10회 편집한 다음 /tmp/hook-debug.log를 열어 어떤 파일에서 hook이 실행됐는지 확인하세요. 파일이 비어 있다면 matcher가 맞지 않는 것이니 정규식을 수정하세요.

다음 단계는?

핵심 내용

exit 0으로 끝나는 PostToolUse hook부터 시작하세요. 가장 안전하고 아무것도 차단하지 않습니다. 일주일 후에는 .env를 보호하거나 강제 푸시를 방지하도록 exit 2를 사용하는 첫 PreToolUse hook을 추가하세요. 2주가 지나면 CLAUDE.md는 200줄짜리 계약서에서 간결한 50–80줄의 컨텍스트 설명으로 줄어들고, 선택한 이벤트 검사는 hook으로 옮기고 강제 접근 제한은 권한, sandbox, 원격 보호에서 따로 시행합니다.

다음 주에 할 일 5가지:

  1. CLAUDE.md에서 grep으로 찾아보세요. “never”, “forbidden”, “always”, “must” 같은 단어를 검색하세요. 이런 규칙은 각각 hook 후보입니다. 규칙이 명령형이라면 텍스트 파일은 이를 강제하지 못합니다.
  2. PostToolUse + exit 0으로 시작하세요. 가장 안전한 방식입니다. 변경은 적용하지만 아무것도 차단하지 않습니다. prettier(hook 2), 개발 서버 종료(hook 5), 테스트 후 자동 커밋(hook 6)이 있습니다.
  3. 확신이 생긴 뒤에 첫 PreToolUse + exit 2를 추가하세요. 예를 들어 .env를 보호하거나(hook 1) 강제 푸시를 방지할 수 있습니다(hook 3). 강제 차단이므로 grep 명령을 잘못 쓰면 사용자 자신도 차단될 수 있습니다.
  4. 커밋 전에 hook을 수동으로 테스트하세요(위 섹션 참조). 디버깅 시간을 크게 줄여 줍니다.
  5. 실험용으로 settings.local.json을 사용하세요. 실제 작업 대신 echo를 사용하는 새 hook을 여기에 넣으세요.

Hook을 설정했다면 이 클러스터의 다음 단계는 Claude Code에서 변경 사항을 되돌리는 방법입니다. hook이 위험한 작업을 통과시켰을 경우에 대비할 수 있습니다. Claude가 세션 사이에 프로젝트 구조를 기억하게 하고 싶다면 Claude Code에서 Second Brain 만들기(business/ 폴더 + 연결된 지식 베이스)를 구성하세요. 프로젝트의 skill에 재사용할 이름을 붙이는 방법은 Claude Code Skills를 참조하세요.

Ailoot을 통해 신경망 서비스를 이용해야 한다면 Ailoot을 살펴보세요.

자세히 보기

Claude Code의 Hooks 2026: 단계별 설정과 바로 쓸 수 있는 예제 7가지: 자주 묻는 질문

hook JSON 입력은 왜 한 번만 읽어야 하나요?

stdin은 스트림이라 첫 jq가 소비하면 뒤에서는 EOF가 됩니다. 입력을 한 번 저장하고 명령·결과 필드를 추출하세요. 결과가 없거나 불명확하면 미검증으로 다루고 자동 커밋을 생략하세요.

정규식 hook이 보안 경계를 만드나요?

아닙니다. 특정 이벤트와 인식 가능한 문자열만 검사합니다. 리디렉션, 별칭, 스크립트, MCP, 다른 쓰기가 우회할 수 있으므로 제한 키·파일 권한·sandbox·보호 브랜치로 강제 경계를 두세요.

모든 글