개인정보처리방침© 2026 DEV BAK - 기술블로그. All rights reserved.
DEV BAK - 기술블로그
Claude

Claude Code Hooks — PreToolUse·PostToolUse로 에이전트의 도구 실행을 코드로 제어하기

공식 문서 기반 | Claude Code hooks · PreToolUse · PostToolUse

Claude Code를 도입하고 나서 얼마 지나지 않아, 저도 비슷한 불안감을 느꼈습니다. "Claude가 알아서 rm -rf를 치면 어떡하지?" 팀에 처음 AI 에이전트를 붙여보던 시절, 신뢰와 긴장 사이에서 꽤 오랫동안 줄타기를 했거든요. 그러다 공식 문서에서 Hooks를 발견하고 나서 상황이 달라졌습니다.

이 글을 다 읽고 나면, Claude가 어떤 도구를 실행하기 직전과 직후에 여러분만의 로직을 끼워 넣는 방법을 알게 됩니다. 위험 명령어 차단, 자동 포맷팅, 감사 로그, CI 연동까지 — 이 글에서는 PreToolUse로 도구 실행을 사전에 차단하고, PostToolUse로 반응형 자동화를 구성하는 패턴을 Claude Code 공식 문서를 기반으로 살펴봅니다.

Hooks는 겉으로 보면 단순한 이벤트 리스너처럼 생겼습니다. 그런데 핵심은 타이밍입니다. 실행 전에 끼어들 수 있느냐, 후에만 반응할 수 있느냐 — 이 차이가 "막을 수 있는 것"과 "막을 수 없는 것"을 나눕니다. LLM에게 "이런 건 하지 말아줘"라고 프롬프트로 부탁하는 것과, 훅으로 실행 자체를 차단하는 것은 신뢰도 면에서 완전히 다른 이야기입니다.


핵심 개념

훅이 끼어드는 정확한 타이밍

Claude Code의 에이전트 루프는 이런 흐름으로 흘러갑니다.

SessionStart → PreToolUse → (도구 실행) → PostToolUse → ... → Stop

PreToolUse는 Claude가 도구를 실행하기 직전에 발동합니다. 아직 아무 일도 일어나지 않은 상태입니다. 이 시점에 훅이 deny를 반환하면 해당 도구 호출 자체가 취소됩니다. 심지어 도구에 넘길 입력값을 수정하는 것도 가능합니다.

PostToolUse는 도구 실행이 완료된 직후에 발동합니다. 솔직히 처음엔 이걸로도 뭔가를 막을 수 있을 거라 기대했는데, 이미 파일이 쓰인 뒤입니다. 롤백은 불가능합니다. 대신 자동 포맷팅, 테스트 실행, 감사 로그 기록처럼 "이미 일어난 일에 반응하는 자동화"에 딱 맞습니다.

에이전트 루프(Agent Loop): Claude Code가 사용자 요청을 처리하기 위해 반복적으로 도구를 선택하고 실행하는 내부 사이클. 훅은 이 루프의 특정 시점에 사용자 정의 로직을 주입합니다.

훅 등록과 기본 설정

설정 파일은 두 곳에 둘 수 있습니다. 전역 설정은 ~/.claude/settings.json, 프로젝트 한정 설정은 .claude/settings.json입니다. 구조는 이렇게 생겼습니다.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash /path/to/validator.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          { "type": "command", "command": "prettier --write $TOOL_INPUT_FILE_PATH" }
        ]
      }
    ]
  }
}

matcher에는 도구 이름을 정규식으로 지정합니다. 기본적으로 대소문자를 구분하며, 도구 이름은 정확히 맞춰야 합니다(bash가 아니라 Bash). |로 여러 도구를 동시에 잡을 수 있고(Write|Edit), .*로 모든 도구를 대상으로 할 수도 있습니다.

핸들러 타입은 다섯 가지입니다.

타입 안정성 설명
command GA Shell 명령어 또는 스크립트 실행
http GA 외부 HTTP 엔드포인트 호출
mcp_tool GA 연결된 MCP 서버의 도구 호출
prompt GA Claude 모델을 단일 턴 판정자로 활용
agent 실험적 Claude Code 서브에이전트 실행

훅이 stdin으로 받는 데이터 구조

훅 스크립트는 stdin으로 도구 호출 정보를 JSON 형태로 받습니다. Bash 도구를 예로 들면 이렇게 생겼습니다.

json
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/build",
    "description": "빌드 디렉토리 정리"
  }
}

jq -r '.tool_input.command'로 실제 명령어를 꺼내는 이유가 여기 있습니다. 도구 타입마다 tool_input의 키 구조가 다르기 때문에, 새로운 도구를 대상으로 훅을 작성할 때는 이 스키마를 먼저 확인하는 습관이 중요합니다.

결정은 stdout에 이런 형식으로 반환합니다.

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "위험한 명령어입니다"
  }
}

permissionDecision에는 allow, deny, ask(사용자에게 확인 요청) 중 하나를 넣을 수 있습니다.

exit code — 처음엔 헷갈리는 부분

저도 이 부분에서 한참 삽질했는데, 한 번 정리해두면 나중에 편합니다.

exit code 의미
0 성공. stdout에 JSON이 있으면 파싱해서 사용
2 블로킹 오류. PreToolUse에서 작업이 중단되고 stderr 내용이 Claude에게 피드백됨
그 외 비제로 비블로킹 오류. 실행은 계속되고 stderr는 verbose 모드에서만 표시

블로킹 오류(exit 2): 스크립트가 단순히 실패한 게 아니라 "이 작업을 진행하면 안 된다"는 신호입니다. Claude가 stderr 내용을 읽고 판단을 재조정합니다.

deny 결정 자체는 성공적으로 처리된 것이므로 exit 0으로 끝내는 것이 맞습니다. exit 0이 "도구를 허용한다"는 뜻이 아닙니다 — stdout에 담긴 JSON이 실제 결정을 담고 있습니다.


실전 적용

예시는 간단한 것부터 복잡한 순서로 배치했습니다. 훅을 처음 도입할 때는 예시 1부터 시작해보시면 진입 장벽이 낮습니다.

예시 1: 파일 저장 후 자동 포맷팅 (PostToolUse)

PostToolUse의 자동 포맷팅이 가장 시작하기 좋습니다. Claude가 파일을 쓸 때마다 Prettier가 자동으로 따라붙는 구조인데, PR에서 "왜 포맷팅이 안 되어 있어요?" 소리를 들을 일이 없어집니다.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": "prettier --write \"$TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
        }]
      }
    ]
  }
}

$TOOL_INPUT_FILE_PATH는 Claude Code가 PostToolUse 훅 실행 시 자동으로 주입해주는 환경변수입니다. 별도로 export할 필요 없이 바로 참조할 수 있습니다. || true는 Prettier가 지원하지 않는 파일 형식(예: .sh, .md)에서 훅 자체가 오류로 처리되지 않도록 하는 방어 코드입니다.

예시 2: 위험 명령어 자동 차단 (PreToolUse)

rm -rf나 강제 push 같은 명령어를 Claude가 실행하려 할 때 사전에 막는 패턴입니다.

이 예시에서는 jq를 사용합니다. 설치되어 있지 않다면 macOS는 brew install jq, Ubuntu/Debian은 apt install jq로 설치할 수 있습니다. 팀 개발 환경에 미리 포함되어 있는지 확인해두는 것이 좋습니다.

bash
#!/bin/bash
# pre-bash-guard.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
 
BLOCKED_PATTERNS=("rm -rf /" "git push --force" ":(){:|:&};:" "chmod 777")
 
for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qF -- "$pattern"; then
    echo '{
      "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "위험 명령어가 감지되어 실행을 차단했습니다"
      }
    }'
    exit 0
  fi
done
json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "bash pre-bash-guard.sh" }]
      }
    ]
  }
}
코드 포인트 설명
INPUT=$(cat) stdin으로 들어오는 도구 입력 JSON 전체를 읽음
jq -r '.tool_input.command' Bash 도구의 실제 명령어 문자열 추출
grep -qF -- "$pattern" 고정 문자열로 검색(-F). 패턴에 공백이나 특수문자가 있어도 단어 분리 없이 안전하게 동작
permissionDecision: "deny" Claude에게 "이 도구 호출을 취소해라"라고 전달
exit 0 deny 결정 자체는 성공적으로 처리됐음을 의미 — 허용이 아님

예시 3: 비동기 감사 로그 (PostToolUse, async)

모든 도구 호출을 로깅하고 싶은데 로깅 때문에 에이전트 루프가 느려지는 건 피하고 싶다면 async: true를 활용할 수 있습니다. 저희 팀에서는 에이전트가 실제로 어떤 도구를 얼마나 호출하는지 파악하는 데 이 패턴이 큰 도움이 됐습니다. 정책을 다듬을 때 눈에 보이는 데이터가 있으니까요.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [{
          "type": "command",
          "command": "bash audit-logger.sh",
          "async": true
        }]
      }
    ]
  }
}

async: true를 붙이면 Claude의 루프를 블로킹하지 않고 백그라운드에서 실행됩니다. asyncRewake: true 옵션을 추가하면 로깅이 완료된 후 Claude를 다시 깨워 후속 처리를 이어가게 할 수도 있습니다. 단, async: true는 type: "command"에서만 지원됩니다. prompt나 agent 타입 훅에는 적용되지 않습니다.

예시 4: HTTP 훅으로 CI 트리거

외부 CI 시스템과 연동이 필요한 경우 type: "http" 핸들러를 활용할 수 있습니다. 처음 이 기능을 봤을 때 "webhook을 이렇게 간단하게 붙일 수 있구나"라는 느낌이었는데, 실제로도 설정 자체는 단순합니다.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "http",
          "url": "https://ci.example.com/webhook",
          "headers": { "Authorization": "Bearer ${CI_TOKEN}" },
          "allowedEnvVars": ["CI_TOKEN"]
        }]
      }
    ]
  }
}

headers 안의 ${CI_TOKEN}은 shell 보간이 아닙니다. allowedEnvVars에 명시된 환경변수 이름을 JSON 설정 안에서 ${VARNAME} 형식으로 참조하는 Claude Code 자체 문법입니다. allowedEnvVars에 이름이 없으면 보안상의 이유로 값이 주입되지 않습니다.

예시 5: 프로덕션 경로 보호 (PreToolUse + Prompt 훅)

type: "prompt" 핸들러는 Claude 모델 자체를 단일 턴 판정자로 쓰는 방식입니다. 복잡한 조건을 shell 스크립트로 코딩하지 않고 자연어로 정의할 수 있어서 유연합니다.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [{
          "type": "prompt",
          "prompt": "이 파일 경로가 /prod/ 또는 /release/ 디렉토리를 포함하면 'deny', 그렇지 않으면 'allow'로만 응답하세요."
        }]
      }
    ]
  }
}

다만 이 방식에는 실용적인 한계가 있습니다. LLM이 deny 또는 allow 한 단어만 반환해야 하는데, 간혹 "deny입니다" 또는 "allow가 맞습니다"처럼 자연어로 응답하면 파싱이 실패할 수 있습니다. 명확한 경로 패턴은 shell 스크립트로 처리하고, prompt 훅은 규칙으로 표현하기 어려운 의미론적 판단에 보조 수단으로 쓰는 것이 더 안정적입니다. LLM 호출이 발생하므로 레이턴시와 비용도 감안하는 것이 좋습니다.


장단점 분석

장점

항목 내용
결정론적 제어 LLM 판단에 의존하지 않고 규칙 기반으로 도구 실행을 보장할 수 있음
보안 게이트 PreToolUse로 위험 작업을 에이전트 루프 외부에서 원천 차단 가능
자동화 일관성 포맷팅, 테스트, 알림 등을 모든 세션에 균일하게 적용
핸들러 조합 command · http · mcp_tool · prompt · agent를 혼합해서 활용 가능
범위 분리 전역/프로젝트 레벨 설정을 독립적으로 관리 가능

단점 및 주의사항

항목 내용 대응 방안
PostToolUse 롤백 불가 이미 파일이 쓰인 후 트리거되므로 변경사항을 되돌릴 수 없음 위험한 파일 쓰기는 반드시 PreToolUse로 제어
stdin 파싱 의존 훅 스크립트가 JSON stdin을 직접 파싱해야 해서 jq 등 도구 설치 필요 팀 개발 환경에 jq 포함 여부를 사전 확인
동기 훅 성능 영향 느린 동기 훅이 모든 도구 호출을 블로킹해 UX가 나빠질 수 있음 오래 걸리는 작업은 async: true 적용
async 타입 제약 비동기 실행은 type: "command"만 지원, prompt·agent 훅은 불가 prompt 훅은 경량 판정 로직에만 사용
prompt 훅 파싱 불안정 LLM이 deny/allow 외 형식으로 응답할 경우 파싱 실패 가능 명확한 조건은 shell 스크립트로 처리, prompt 훅은 보조 수단으로 활용
디버깅 복잡성 여러 훅이 중첩될 때 실행 순서와 exit code 추적이 어려움 verbose 모드(--verbose)로 stderr 출력을 확인

실무에서 가장 흔한 실수

  1. PostToolUse로 파일 삭제나 DB 변경을 막으려는 시도 — 이미 실행된 뒤입니다. 막고 싶다면 반드시 PreToolUse를 써야 합니다.

  2. exit code 0과 2를 혼동하는 경우 — "스크립트가 정상 종료됐으니 도구도 허용되겠지"라고 생각하기 쉬운데, deny 결정은 JSON stdout으로 전달하는 거지 exit code로 결정되지 않습니다. exit 2는 "뭔가 잘못됐으니 멈춰라"는 별도의 신호입니다.

  3. matcher에 도구 이름을 소문자로 쓰는 경우 — bash라고 쓰면 매칭이 안 됩니다. 도구 이름은 대소문자를 정확히 맞춰 Bash, Write, Edit으로 작성하는 것을 권장합니다.


마치며

Claude Code Hooks는 AI 에이전트에 "우리 팀의 규칙"을 코드로 심는 가장 확실한 방법입니다. 그리고 이 셋을 조합하기 시작하면 단순한 차단 규칙을 넘어서는 그림이 그려집니다. 포맷팅 훅으로 코드 품질을 자동 관리하고, 보안 훅으로 위험 명령어를 막고, 감사 로그로 에이전트의 행동 패턴을 이해하다 보면 — Claude가 점점 팀의 작업 방식에 맞게 길들여지는 느낌이 납니다.

지금 시작해볼 수 있는 3단계:

  1. ~/.claude/settings.json에 Prettier 자동 포맷팅 설정 한 줄만 추가해볼 수 있습니다. Claude가 파일을 쓸 때마다 포맷팅이 따라붙는 걸 바로 확인할 수 있어서, 훅이 실제로 어떻게 동작하는지 감을 잡기에 가장 좋은 시작점입니다.

  2. PreToolUse로 위험 명령어 차단 스크립트를 팀 저장소의 .claude/settings.json에 추가해볼 수 있습니다. 처음엔 deny 대신 ask로 설정해두면, Claude가 사용자에게 한 번 더 확인을 요청하는 방식으로 부드럽게 운영할 수 있습니다.

  3. async: true로 감사 로그를 붙여볼 수 있습니다. matcher: ".*"로 모든 도구를 잡아 로컬 파일에 기록해두면, 에이전트가 실제로 어떤 도구를 얼마나 호출하는지 눈으로 확인하면서 정책을 다듬어 나갈 수 있습니다.

훅을 하나씩 붙여가다 보면, Claude를 더 믿고 쓸 수 있게 됩니다. 신뢰는 막연한 낙관이 아니라, 이런 구조에서 나오는 거니까요.


참고 자료

공식 문서

  • Hooks Reference — Claude Code 공식 문서
  • Automate Workflows with Hooks — Claude Code 공식 가이드

커뮤니티 가이드

  • Claude Code Hooks: Complete Guide to All 12 Lifecycle Events — claudefa.st (커뮤니티)
  • Claude Code Hooks: 6 Production Patterns (2026) — Pixelmojo (커뮤니티)
  • Claude Code Hooks: A Practical Guide to Workflow Automation — DataCamp
  • Claude Code Hooks Complete Guide (March 2026 Edition) — SmartScope (커뮤니티)
  • Implementing PostToolUse Hooks — CodeSignal
  • Claude Code Hooks: Security Gates for Agent Workflows — DEV Community
  • Claude Code Hook Control Flow — Steve Kinney
  • Claude Code Hooks: Complete Reference (32+ Events) — The Prompt Shelf (커뮤니티)
  • GitHub - claude-code-hooks-mastery
  • Understanding Claude Code Hooks Documentation — PromptLayer Blog
#ClaudeCode#Hooks#PreToolUse#PostToolUse#AI에이전트#보안게이트#자동화#Bash#JSON#CI-CD
공유하기

목차

핵심 개념훅이 끼어드는 정확한 타이밍훅 등록과 기본 설정훅이 stdin으로 받는 데이터 구조exit code — 처음엔 헷갈리는 부분실전 적용예시 1: 파일 저장 후 자동 포맷팅 (PostToolUse)예시 2: 위험 명령어 자동 차단 (PreToolUse)예시 3: 비동기 감사 로그 (PostToolUse, async)예시 4: HTTP 훅으로 CI 트리거예시 5: 프로덕션 경로 보호 (PreToolUse + Prompt 훅)장단점 분석장점단점 및 주의사항실무에서 가장 흔한 실수마치며참고 자료

추천 포스트

Claude Opus 4.8 Dynamic Workflows와 Effort Control — 병렬 에이전트로 코드베이스 마이그레이션을 자동화하는 구조
Claude

Claude Opus 4.8 Dynamic Workflows와 Effort Control — 병렬 에이전트로 코드베이스 마이그레이션을 자동화하는 구조

Anthropic이 2026년 5월 28일 공개한 Claude Opus 4.8을 처음 봤을 때, 솔직히 "또 숫자만 올라간 업데이트겠지"라고 생각했습니다. Opus 4.7이 나온 지 41일밖에 안 됐으니까요. 그런데 릴리스 노트를 읽다가 "단일 세션에서 최대 1,000개의 병렬 서브에이...

2026년 05월 30일읽는 데 20분
Claude Code /goal·세션 관리: 하루 이상 걸리는 작업을 AI와 끊김 없이 이어가는 법
Claude

Claude Code /goal·세션 관리: 하루 이상 걸리는 작업을 AI와 끊김 없이 이어가는 법

Claude Code를 쓰다 보면 어느 순간 이런 답답함이 찾아옵니다. "지금 Claude가 뭘 하고 있는 건지 도대체 알 수가 없네." 파일을 읽고, 코드를 수정하고, 터미널 명령어를 실행하는데 전체 흐름이 한눈에 안 들어오죠. 저도 처음엔 매번 채팅창 스크롤을 올려 가며 "아까 뭘 ...

2026년 05월 12일읽는 데 17분
Claude Code MCP와 `.claude/rules/`로 팀별 AI 도구 접근 권한을 선언적으로 분리하는 방법
Claude

Claude Code MCP와 `.claude/rules/`로 팀별 AI 도구 접근 권한을 선언적으로 분리하는 방법

AI 코딩 도구를 팀 전체에 도입하려다 보면 생각보다 빨리 이런 고민에 부딪히게 됩니다. "백엔드 개발자한테는 DB 접근이 필요한데, 프론트엔드 개발자가 실수로 을 날리면 어떡하지?" 저도 처음엔 그냥 모든 팀원에게 동일한 MCP 서버 설정을 뿌려놨습니다. 그러다 어느 날 스테이징 DB...

2026년 05월 06일읽는 데 21분
`Claude Code .claude/rules/`로 팀별 AI 규칙을 모듈화하는 법 — 프론트엔드, 백엔드, 보안 팀 분리 전략
Claude

`Claude Code .claude/rules/`로 팀별 AI 규칙을 모듈화하는 법 — 프론트엔드, 백엔드, 보안 팀 분리 전략

팀이 커지면서 가 점점 뚱뚱해지는 걸 경험해본 적 있는가? 저도 처음엔 하나의 파일에 모든 규칙을 때려 넣었다가, 어느 순간 500줄이 넘어가면서 "이걸 대체 누가 관리하지?" 싶었다. 프론트엔드 팀이 React 규칙을 수정하면 백엔드 팀과 머지 충돌이 나고, 보안 팀이 새 정책을 추가...

2026년 05월 06일읽는 데 15분
Claude Code Status Line 커스터마이징 — 세션 정보를 터미널에 항상 띄우는 법
Claude

Claude Code Status Line 커스터마이징 — 세션 정보를 터미널에 항상 띄우는 법

솔직히 Claude Code를 처음 쓸 때 가장 불안했던 건 "지금 컨텍스트를 얼마나 쓰고 있지?", "비용이 얼마나 나가고 있지?" 같은 것들이었습니다. 명령어를 수시로 치는 건 작업 흐름을 끊기도 하고, 뭔가 대시보드처럼 항상 보이면 좋겠다는 생각이 들었죠. 그러다 발견한 게 St...

2026년 04월 20일읽는 데 18분
Claude Code MCP 서버 연동과 Hooks로 PostgreSQL·REST API 호출까지 자동화하기
Claude

Claude Code MCP 서버 연동과 Hooks로 PostgreSQL·REST API 호출까지 자동화하기

저도 처음엔 "Claude Code가 코드 짜는 걸 도와주는 거 아닌가?"라고만 생각했습니다. 그러다 MCP 서버와 Hooks를 조합하는 순간 그 생각이 완전히 바뀌었습니다. Claude Code가 직접 PostgreSQL 스키마를 탐색하고, SQL을 생성하고, 결과를 감사 로그 API...

2026년 04월 19일읽는 데 21분