가이드

Claude Code hooks: 에이전트의 규칙을 보장으로 바꾸는 방법

업데이트: · Skillaria.Top

Claude Code 훅은 Claude Code 자체가 특정 이벤트에서 실행하는 셸 명령입니다. 도구 호출 전후, 프롬프트 제출 시, 세션 시작 시, 에이전트가 멈추기 직전에 실행되며 settings.json에서 설정합니다. 종료 코드 2는 동작을 차단하고 에이전트에게 오류 메시지를 전달하므로, CLAUDE.md의 규칙을 모델이 잊을 수 없는 보장으로 바꿔 줍니다.

Claude Code 훅이란 무엇인가

훅은 모델이 아니라 Claude Code 프로그램이 작업의 정해진 시점에 실행하는 명령입니다. 에이전트가 rm -rf를 실행하려고 하면 훅이 작동해 명령을 통과시킬지 결정합니다. 에이전트가 파일을 수정하면 훅이 포매터를 돌립니다. 에이전트가 작업을 마치려고 하면 훅이 테스트를 실행하고, 테스트가 실패하는 동안에는 멈추지 못하게 합니다.

CLAUDE.md의 지침과의 핵심 차이는 이것입니다. 지침은 텍스트이고, 모델은 그것을 어느 정도 확률로만 따릅니다. 반면 훅은 결정론적인 코드입니다. 컨텍스트가 얼마나 길든, 에이전트가 무엇을 판단했든, 세션 백 분째에 규칙을 잊었든 상관없이 항상 실행됩니다. 그래서 claude code hooks는 바람을 보장으로 바꾸는 기본 수단입니다.

CLAUDE.md의 규칙만으로는 부족한 이유

흔한 장면을 생각해 보세요. 에이전트가 기능을 추가했고, 단위 테스트는 초록이며, 채팅창에는 자신 있게 "완료"라고 적혀 있습니다. 하지만 아무도 end-to-end 시나리오를 돌리지 않았고, 린터도 실행되지 않았으며, 마이그레이션은 중간에 실패했습니다. CLAUDE.md에는 "종료 전에 테스트를 실행하라"고 적혀 있었는데도 말입니다. 모델이 일부러 속인 것은 아닙니다. 모델이 스스로를 평가하고, 그 자기 평가가 믿을 수 없을 뿐입니다.

여기서 하네스의 원칙이 나옵니다. 기계적으로 확인할 수 있는 것은 모델이 아니라 기계가 확인합니다. "린터를 돌려라"라는 규칙은 린터를 돌리는 훅이 됩니다. "운영 설정 파일을 건드리지 마라"라는 규칙은 그 파일의 쓰기를 막는 훅이 됩니다. 텍스트 지침은 남겨 두되, 에이전트가 왜 멈췄는지 설명하는 역할만 합니다.

이벤트: 훅이 언제 실행되나

모든 훅은 세션 수명 주기의 이벤트에 연결됩니다. 주요 이벤트는 다음과 같습니다.

이벤트실행 시점전형적인 용도
PreToolUse도구 호출 직전(Bash, Edit, Write 등)위험한 명령 차단, 파일 보호
PostToolUse도구 호출이 성공한 직후포매터, 린터, 수정 후 빠른 테스트
UserPromptSubmit사용자가 프롬프트를 보낸 직후, 처리 전컨텍스트 추가, 비밀 정보가 든 프롬프트 차단
Stop에이전트가 응답을 끝내려 할 때검사를 통과하기 전까지 멈추지 못하게 하기
SubagentStop서브에이전트가 작업을 마칠 때서브에이전트 결과 검사
SessionStart세션을 시작하거나 재개할 때상태 불러오기: git status, 계획, 진행 기록
SessionEnd세션이 끝날 때정리, 로그 기록
NotificationClaude Code가 알림을 보낼 때(예: 권한 승인 대기)소리나 휴대폰 알림
PreCompact컨텍스트를 압축하기 직전압축 전에 중요한 내용 보존

신뢰성 측면에서 가장 유용한 이벤트는 PreToolUse, PostToolUse, Stop입니다. 이 셋이 에이전트의 세 가지 약점을 막습니다. 위험한 행동, 검증되지 않은 수정, 너무 이른 "완료" 보고입니다.

훅 설정 위치: settings.json

훅은 Claude Code의 설정 파일에 적습니다.

  • ~/.claude/settings.json — 개인 설정이며 모든 프로젝트에 적용됩니다.
  • 저장소의 .claude/settings.json — 팀 전체가 공유하며 git에 커밋합니다.
  • .claude/settings.local.json — 프로젝트 로컬 설정이며 git에 들어가지 않습니다.

구조는 이벤트 → matcher가 있는 그룹 목록 → 명령 목록입니다.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

matcher는 도구 이름이나 그에 대한 정규식입니다. 예를 들면 Bash, Edit|Write, .*처럼 씁니다. 도구와 관련된 이벤트에서만 필요하며, Stop이나 UserPromptSubmit에서는 생략할 수 있습니다. 변수 CLAUDE_PROJECT_DIR은 프로젝트 루트를 가리키므로, 스크립트는 .claude/hooks/에 두고 이 변수로 호출하는 것이 편합니다. 현재 설정된 훅은 Claude Code 안에서 /hooks 명령으로 확인할 수 있습니다.

훅은 데이터를 어떻게 받고 어떻게 응답하나

입력: stdin의 JSON

Claude Code는 이벤트 정보를 담은 JSON을 훅에 전달합니다. 세션 ID, 작업 디렉터리, 이벤트 이름이 들어 있고, 도구 이벤트라면 tool_name과 tool_input도 들어 있습니다. Bash의 경우 명령은 tool_input.command에, Edit과 Write의 경우 경로는 tool_input.file_path에 있습니다. 이 값은 jq로 읽으면 편합니다.

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf build/" },
  "cwd": "/home/dev/project"
}

출력: 종료 코드

코드결과
0성공, 작업 계속. UserPromptSubmit과 SessionStart에서는 stdout이 에이전트의 컨텍스트에 추가됩니다.
2차단 오류. stderr이 에이전트에게 전달되고 동작이 멈춥니다. PreToolUse에서는 도구 호출이 취소되고, Stop에서는 에이전트가 계속 작업합니다.
그 외비차단 오류. stderr은 사용자에게 보이고 작업은 계속됩니다.

종료 코드 2가 핵심 장치입니다. 에이전트는 stderr의 텍스트를 피드백으로 받아 스스로 고칠 수 있습니다. 명령을 다시 쓰거나, 테스트를 고치거나, 파일을 건드리지 않습니다. 그래서 stderr에는 "오류"라고만 쓰지 말고, 이유를 분명히 쓰고 대신 무엇을 해야 하는지 알려 주세요.

종료 코드 외에도 훅은 stdout에 "decision": "block"과 "reason" 같은 JSON 결정을 출력할 수 있습니다. 세밀한 조정에 쓸 수 있지만, 대부분의 작업에는 코드 2와 stderr 메시지면 충분합니다.

예시 1: 수정할 때마다 린터와 테스트 실행

Edit|Write에 매칭된 PostToolUse 훅은 파일이 바뀐 뒤에 실행됩니다. 도구는 이미 실행되었으므로 수정을 되돌릴 수는 없습니다. 하지만 코드 2를 반환하면 린터 출력이 에이전트에게 전달되고, 에이전트는 30분 뒤가 아니라 즉시 오류를 고칩니다.

#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.py)
    if ! out=$(ruff check "$file" 2>&1); then
      echo "린터가 $file에서 오류를 찾았습니다:" >&2
      echo "$out" >&2
      exit 2
    fi ;;
  *.php)
    if ! out=$(php -l "$file" 2>&1); then
      echo "$out" >&2
      exit 2
    fi ;;
esac
exit 0

이 자리에 10분짜리 전체 테스트를 넣지 마세요. 훅은 수정할 때마다 실행됩니다. 수정 직후에는 빠른 검사(문법, 린터, 해당 모듈의 테스트)만 하고, 무거운 검사는 Stop 훅에 두세요.

예시 2: 위험한 명령 차단

Bash에 매칭된 PreToolUse 훅은 명령이 실행되기 전에 그 내용을 봅니다. 코드 2는 호출을 취소하고, 에이전트는 그 이유를 전달받습니다.

#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
  echo "프로젝트 정책에 따라 명령이 차단되었습니다: $cmd. 위험한 작업은 사람이 수행합니다." >&2
  exit 2
fi
exit 0

파일도 같은 방식으로 보호합니다. Edit|Write에 거는 훅이 file_path를 검사해서 .env, 운영 설정, 생성된 코드, 이미 적용된 마이그레이션 파일에 쓰는 것을 막습니다. 다만 정규식 검사는 우회될 수 있습니다. 에이전트가 같은 명령을 다른 모양으로 쓸 수도 있습니다. 훅은 강력한 안전장치이지만 접근 권한이나 샌드박스를 대신하지는 못합니다.

예시 3: 테스트가 빨간색이면 에이전트가 "완료"라고 말할 수 없게 하기

Stop 훅은 에이전트가 작업을 끝내려고 할 때 실행됩니다. 테스트가 실패하면 코드 2가 에이전트를 멈추지 못하게 하고, 테스트 출력은 다음 작업으로 에이전트에게 전달됩니다. 이것은 완료 정의(Definition of Done)를 기계로 옮긴 버전입니다.

#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# 무한 루프 방지: 훅이 이미 한 번 작업을 연장했다면 통과
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! out=$(npm test --silent 2>&1); then
  echo "테스트가 실패했으므로 작업이 끝나지 않았습니다:" >&2
  echo "$out" | tail -n 40 >&2
  exit 2
fi
exit 0

필드 stop_hook_active는 에이전트가 stop 훅 때문에 작업을 이어 가는 중일 때 true가 됩니다. 이 검사가 없으면 훅이 에이전트를 끝없이 돌릴 수 있습니다. 바로 "밤새 엉뚱한 방향으로 일하는" 상황입니다. 자율 루프에서는 여기에 시도 횟수 한도와, 사람을 부르는 조건을 추가하세요.

예시 4: 세션 시작 시 상태 불러오기

SessionStart에서 stdout은 컨텍스트에 들어갑니다. 현재 브랜치, 커밋되지 않은 변경, 계획의 다음 작업을 출력해 두면, 에이전트가 빈 상태에서 시작하지 않습니다.

#!/usr/bin/env bash
echo "브랜치: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0

훅이 규칙을 보장으로 바꾸는 방식

harness의 규칙은 세 단계로 나눠 볼 수 있습니다. 첫 번째는 CLAUDE.md의 텍스트로, 어떻게 일해야 하는지 설명하지만 확률적으로만 지켜집니다. 두 번째는 실행 가능한 검사입니다. 테스트, E2E, 위반 시 실패하는 아키텍처 규칙 스크립트가 여기에 해당합니다. 세 번째는 훅입니다. 적절한 순간에 이 검사를 모델의 개입 없이 자동으로 실행합니다.

실용적인 규칙은 이렇습니다. 에이전트가 같은 규칙을 두 번 어겼다면, CLAUDE.md에 대문자 경고를 하나 더 추가하지 마세요. 대신 그 위반을 스크립트로 확인할 수 있는지 물어보세요. 가능하다면 스크립트를 만들고 훅에 연결합니다. 그러면 텍스트 지침은 짧아지고, 동작은 더 안정됩니다.

텍스트 규칙훅으로 얻는 보장
"코드를 표준 스타일로 포매팅하라"PostToolUse가 수정된 파일에 포매터를 실행
"force push는 하지 마라"Bash에 대한 PreToolUse가 명령을 차단, exit 2
"테스트가 빨간색이면 완료라고 말하지 마라"Stop이 테스트를 실행하고 종료를 막음
".env를 수정하지 마라"Edit|Write에 대한 PreToolUse가 경로를 검사
"어디서 멈췄는지 기억하라"SessionStart가 계획과 git status를 출력

흔한 실수

  • 코드 2 대신 코드 1 사용. 스크립트가 코드 1로 종료하면 에이전트는 아무것도 알아채지 못하고 동작은 그대로 통과합니다. 차단은 코드 2만 합니다.
  • stdout에 메시지 쓰기. 코드 2일 때 에이전트에게 전달되는 것은 stderr입니다. >&2를 사용하세요.
  • 자주 실행되는 이벤트에 느린 훅. PostToolUse에서 전체 테스트를 돌리면 수정 한 번에 1분이 걸립니다. 무거운 검사는 Stop에 두세요.
  • 끝없는 Stop. stop_hook_active 검사나 한도가 없으면 에이전트는 끝없이 돕니다.
  • 상대 경로 사용. 훅은 세션의 작업 디렉터리에서 실행됩니다. $CLAUDE_PROJECT_DIR을 사용하세요.
  • 검토하지 않은 남의 훅. 훅은 확인 없이 여러분의 권한으로 실행됩니다. 다른 저장소의 스크립트는 신뢰하기 전에 읽어 보세요.

요약

Claude Code hooks는 가장 중요한 규칙을 텍스트에서 코드로 옮깁니다. PreToolUse는 위험한 행동을 막고, PostToolUse는 모든 수정을 확인하며, Stop은 너무 이른 완료 보고를 막고, SessionStart는 에이전트에게 프로젝트에 대한 기억을 되돌려 줍니다. 에이전트가 이미 어긴 규칙 두세 개부터 훅으로 만들어 보세요. stderr 메시지는 이해하기 쉽게 쓰세요. 그러면 에이전트가 스스로 오류를 고치고, 여러분은 과정이 아니라 결과를 확인하게 됩니다. 검증, 완료 확인, 자율 루프는 실습 중심의 과정에서 함께 다룹니다.

자주 묻는 질문

훅을 바꾼 뒤 Claude Code를 다시 시작해야 하나요?

settings.json의 변경은 새 세션에서 가장 확실하게 적용됩니다. 현재 설정된 훅은 /hooks 명령으로 확인할 수 있습니다.

훅을 bash가 아닌 언어로 작성할 수 있나요?

네. 훅은 어떤 명령이든 될 수 있습니다. Python, Node.js 스크립트나 실행 파일도 됩니다. 중요한 것은 stdin의 JSON을 읽고 올바른 종료 코드를 반환하는 것뿐입니다.

에이전트는 성공한 훅의 출력을 볼 수 있나요?

보통은 볼 수 없습니다. 코드 0이면 출력이 컨텍스트에 들어가지 않습니다. 예외는 UserPromptSubmit과 SessionStart로, 여기서는 stdout이 에이전트의 컨텍스트에 추가됩니다.

서브에이전트에도 훅이 적용되나요?

네. 서브에이전트의 도구 호출도 같은 PreToolUse와 PostToolUse 훅을 거칩니다. 서브에이전트가 끝날 때는 별도의 SubagentStop 이벤트가 있습니다.

훅을 저장소에 커밋해도 안전한가요?

네. .claude/settings.json과 .claude/hooks/ 안의 스크립트는 팀이 함께 쓰는 일반적인 방식입니다. 개인용 훅은 settings.local.json이나 ~/.claude/settings.json에 두세요.