클로드 코드 Hooks: AI 에이전트의 작업 흐름에 끼어들기

클로드 코드 Hooks: AI 에이전트의 작업 흐름에 끼어들기

클로드 코드를 사용하다 보면 “이 파일은 자동으로 수정되지 않았으면 좋겠는데”, “코드 수정 후에 자동으로 Prettier를 돌리고 싶은데”, “Claude가 작업을 마치면 알림을 받고 싶은데”와 같은 생각이 들 때가 있지 않으셨나요? 🤔

이런 요구사항들을 충족시키기 위해 클로드 코드는 Hooks라는 강력한 기능을 제공합니다. Hooks를 사용하면 클로드 코드의 라이프사이클 중 특정 시점에 사용자가 정의한 쉘 명령어나 스크립트를 자동으로 실행할 수 있습니다.

Hooks란 무엇인가요?

Hooks는 클로드 코드가 특정 작업을 수행하기 전이나 후에 자동으로 실행되는 사용자 정의 명령어입니다. LLM이 선택하는 동작이 아니라, 개발자가 미리 정해둔 규칙에 따라 결정론적(deterministic)으로 동작한다는 점이 핵심입니다.

예를 들어, Claude가 TypeScript 파일을 수정할 때마다 자동으로 Prettier를 실행하거나, 특정 디렉토리의 파일을 수정하려 할 때 차단하는 것이 가능합니다. Hooks를 활용하면 코드 포매팅 자동화, 커스텀 알림, 민감한 파일 보호, 명령어 로깅 등 다양한 워크플로우를 구축할 수 있습니다.

Hook 이벤트 종류

클로드 코드는 다양한 시점에 Hook을 실행할 수 있도록 여러 이벤트를 제공합니다.

PreToolUse는 Claude가 도구(Bash, Edit, Write 등)를 호출하기 직전에 실행됩니다. 이 Hook에서 도구 호출을 차단하거나 허용할 수 있어서, 민감한 파일 보호나 명령어 검증에 유용합니다.

PostToolUse는 도구 호출이 성공적으로 완료된 후에 실행됩니다. 파일 수정 후 자동 포매팅을 적용하거나, 변경 사항을 로깅하는 데 활용할 수 있습니다.

Notification은 클로드 코드가 알림을 보낼 때 실행됩니다. 권한 요청(permission_prompt)이나 사용자 입력 대기(idle_prompt) 상황에서 데스크톱 알림이나 Slack 메시지를 보내는 등 커스텀 알림을 구현할 수 있습니다.

Stop은 클로드 코드가 응답을 완료했을 때 실행됩니다. 작업 완료 후 정리 작업이나 알림을 보내는 데 활용할 수 있습니다.

PermissionRequest는 사용자에게 권한 확인 대화상자가 표시될 때 실행됩니다. PreToolUse가 모든 도구 호출 전에 실행되는 것과 달리, 실제로 사용자 승인이 필요한 상황에서만 동작합니다. 외부 서비스로 승인 요청을 라우팅하거나 자동으로 허용/거부 결정을 내리는 데 활용할 수 있습니다.

그 외에도 사용자 프롬프트 제출 시 실행되는 UserPromptSubmit, 서브에이전트 작업 완료 시 실행되는 SubagentStop, 컨텍스트 압축 전에 실행되는 PreCompact, 세션 시작과 종료 시 실행되는 SessionStartSessionEnd 등이 있습니다.

Hooks 설정하기

Hooks는 /hooks 슬래시 커맨드를 통해 대화형으로 설정하거나, 설정 파일을 직접 편집해서 구성할 수 있습니다.

설정 파일은 세 가지 위치에 저장할 수 있습니다. ~/.claude/settings.json은 사용자 전역 설정으로 모든 프로젝트에 적용됩니다. .claude/settings.json은 프로젝트별 설정으로 Git에 커밋하여 팀과 공유할 수 있습니다. .claude/settings.local.json은 로컬 프로젝트 설정으로 커밋하지 않고 개인적으로만 사용합니다. settings.json의 전체 구조와 다른 옵션이 궁금하다면 클로드 코드 설정 가이드에서 이어서 볼 수 있습니다.

환경 변수 이해하기

Hook을 작성할 때 가장 먼저 알아야 할 것은 클로드 코드가 제공하는 환경 변수입니다. 이 환경 변수들 덕분에 복잡한 JSON 파싱 없이도 간단하게 Hook을 작성할 수 있습니다.

$CLAUDE_FILE_PATHS는 현재 도구 호출에 관련된 파일 경로들을 공백으로 구분한 문자열입니다. 예를 들어 Claude가 src/App.tsx 파일을 수정하면, 이 환경 변수에 해당 파일의 절대 경로가 담깁니다.

$CLAUDE_PROJECT_DIR은 클로드 코드가 시작된 프로젝트 루트 디렉토리의 절대 경로입니다. 프로젝트 내 스크립트 파일을 참조할 때 유용합니다.

이제 이 환경 변수들을 활용한 실용적인 예제들을 살펴보겠습니다.

PreToolUse: 실행 전에 막기

PreToolUse는 Claude가 도구를 실행하기 직전에 호출됩니다. 그래서 “이 작업은 아예 시작하면 안 된다”는 규칙을 넣기에 가장 적합합니다. 대표적인 예가 민감한 파일 보호입니다.

프로덕션 환경 설정 파일이나 환경 변수 파일처럼 실수로 수정하면 안 되는 파일들이 있습니다. 이런 파일은 Claude가 편집한 뒤에 되돌리는 것보다, 편집 전에 차단하는 편이 훨씬 안전합니다:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "case \"$CLAUDE_FILE_PATHS\" in *.env*|*package-lock.json*|*.git/*) exit 2;; esac"
          }
        ]
      }
    ]
  }
}

Hook의 종료 코드는 중요한 의미를 가집니다. 종료 코드 0은 성공을 의미하고 작업이 계속 진행됩니다. 종료 코드 2는 작업을 차단하라는 의미입니다. 그 외의 0이 아닌 종료 코드는 오류로 처리됩니다.

PreToolUse는 Bash 명령어를 검사할 때도 유용합니다. 환경 변수만으로 부족하고 도구 입력을 자세히 봐야 한다면, stdin으로 전달되는 JSON을 jq로 파싱하면 됩니다:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-command-log.txt"
          }
        ]
      }
    ]
  }
}

Hooks는 표준 입력(stdin)으로 세션 ID, 도구 이름, 도구 입력 같은 정보를 담은 JSON을 받습니다. 간단한 파일 경로 기반 규칙은 $CLAUDE_FILE_PATHS로 충분하지만, Bash 명령어의 상세 내용이나 권한 요청의 맥락을 봐야 할 때는 stdin JSON을 활용하는 편이 좋습니다. 도구별 허용 규칙이나 권한 모드 자체를 정리하고 싶다면 클로드 코드 권한 설정을 먼저 잡아두면 Hook 규칙도 더 단순해집니다.

PostToolUse: 실행 직후 정리하기

PostToolUse는 도구 호출이 성공적으로 끝난 직후에 실행됩니다. 파일이 실제로 수정된 다음에 동작하므로, 수정된 파일만 대상으로 하는 가벼운 후처리에 잘 맞습니다.

가장 대표적인 예는 코드 자동 포매팅입니다. “저장한 뒤에는 Prettier를 돌려주세요”라고 Claude에게 매번 부탁하는 방식은 LLM이 그 지시를 기억하느냐에 결과가 달려 있는데요, Hook으로 처리하면 사람도 AI도 잊을 수 없는 결정론적인 단계가 됩니다. Claude가 Edit이나 Write 도구로 파일을 수정하면, $CLAUDE_FILE_PATHS에 수정된 파일 경로가 들어옵니다. 이 값을 포매터에 넘기면 전체 프로젝트가 아니라 방금 건드린 파일만 빠르게 정리할 수 있습니다:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bunx prettier --write \"$CLAUDE_FILE_PATHS\""
          }
        ]
      }
    ]
  }
}

Go 프로젝트에서는 gofmt를, Python 프로젝트에서는 black이나 ruff를 사용하도록 비슷하게 설정할 수 있습니다:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "ruff format \"$CLAUDE_FILE_PATHS\""
          }
        ]
      }
    ]
  }
}

더 복잡한 로직이 필요하다면 별도의 스크립트 파일을 작성하고 $CLAUDE_PROJECT_DIR 환경 변수로 참조할 수 있습니다:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
          }
        ]
      }
    ]
  }
}

이렇게 설정하면 프로젝트 루트 경로가 바뀌어도 Hook이 정상 동작합니다. 스크립트 파일은 .claude/hooks/ 디렉토리에 저장하고 실행 권한(chmod +x)을 부여하면 됩니다.

PostToolUse에는 린트나 테스트처럼 시간이 오래 걸리는 작업을 넣지 않는 편이 좋습니다. 파일 하나를 수정할 때마다 전체 검증이 반복되면 Claude의 작업 흐름이 쉽게 끊기기 때문입니다. 그런 작업은 뒤에서 볼 Stop Hook에 두는 편이 자연스럽습니다.

Notification: 기다리는 상황 알려주기

Notification은 클로드 코드가 사용자 입력을 기다리거나 권한 승인을 요청할 때 실행됩니다. Claude가 오래 작업하다가 멈췄는데 터미널을 보고 있지 않아서 놓치는 경우가 있죠. 이럴 때 데스크톱 알림이나 Slack 메시지를 보내면 흐름을 놓치지 않을 수 있습니다.

macOS에서는 osascript로 간단한 데스크톱 알림을 띄울 수 있습니다:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Linux에서는 notify-send를 사용할 수 있고, Slack이나 Discord 웹훅을 호출하는 스크립트를 작성하면 원격에서도 알림을 받을 수 있습니다.

SessionStart: 시작할 때 컨텍스트 넣기

SessionStart는 클로드 코드를 시작할 때마다 원하는 컨텍스트를 자동으로 불러오는 Hook입니다. 예를 들어 최근 Git 이력이나 열려 있는 이슈 목록을 자동으로 제공하면 Claude가 현재 상황을 더 잘 파악합니다:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "echo '## Recent commits' && git log --oneline -10 && echo '## Open issues' && gh issue list --limit 5"
          }
        ]
      }
    ]
  }
}

이 Hook은 세션이 시작될 때 최근 커밋 10개와 열린 이슈 5개를 출력합니다. Hook의 stdout 출력은 Claude의 컨텍스트에 추가되기 때문에 별도로 복사해서 붙여넣을 필요가 없습니다.

SessionStart는 새 세션뿐 아니라 세션 재개, /clear, 컨텍스트 압축 후에도 실행됩니다. stdin JSON의 source 필드(startup, resume, clear, compact)로 어떤 상황인지 구분할 수 있어서, 필요하다면 상황에 따라 다른 컨텍스트를 제공하는 것도 가능합니다.

조금 더 발전시키면 팀원마다 다른 컨텍스트를 동적으로 불러오는 용도로도 쓸 수 있습니다. 모듈마다 알아야 할 사전 지식이 다른 큰 프로젝트라면, 현재 작업 디렉토리나 Git 사용자 정보를 기준으로 적절한 가이드를 자동으로 주입하면 됩니다. 이렇게 해두면 개인마다 설정 파일을 따로 손보지 않아도 각자에게 맞는 환경으로 세션을 시작할 수 있습니다.

PermissionRequest: 권한 요청 라우팅하기

PermissionRequest Hook을 사용하면 권한 승인 대화상자를 외부 서비스로 보낼 수 있습니다. 터미널 앞에 앉아 있지 않아도 WhatsApp이나 Slack에서 승인하거나 거부할 수 있는 거죠:

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/request-approval.sh"
          }
        ]
      }
    ]
  }
}

request-approval.sh 스크립트는 이렇게 작성할 수 있습니다:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // .tool_input.file_path // "N/A"')

# WhatsApp Business API나 Slack 웹훅으로 승인 요청 전송
curl -s -X POST "https://your-webhook-url/approve" \
  -H "Content-Type: application/json" \
  -d "{\"tool\": \"$TOOL\", \"detail\": \"$CMD\"}" > /dev/null

# 응답 대기 (폴링 방식)
for i in $(seq 1 60); do
  DECISION=$(curl -s "https://your-webhook-url/decision/$TOOL")
  if [ "$DECISION" = "allow" ]; then
    jq -n '{hookSpecificOutput: {hookEventName: "PermissionRequest", decision: {behavior: "allow"}}}'
    exit 0
  elif [ "$DECISION" = "deny" ]; then
    jq -n '{hookSpecificOutput: {hookEventName: "PermissionRequest", decision: {behavior: "deny", message: "Denied via remote approval"}}}'
    exit 0
  fi
  sleep 2
done

# 타임아웃 시 기본 동작(사용자에게 직접 물어봄)
exit 0

이 스크립트는 도구 이름과 세부 정보를 외부 서비스로 보내고, 승인 결과를 폴링해서 Claude에 전달합니다. 원격 제어와 함께 사용하면 완전히 원격으로 작업을 관리할 수 있습니다.

Stop: 마지막에 검증하기

Stop은 Claude가 응답을 마치려는 시점에 실행됩니다. 그래서 수정된 파일 하나가 아니라 작업 전체를 대상으로 한 최종 검증에 적합합니다. 예를 들어 린트, 타입 체크, 테스트처럼 시간이 조금 걸리더라도 마지막에 한 번만 확인하면 되는 작업을 넣으면 됩니다.

여기서 중요한 점은 Stop Hook에서는 파일을 고치는 명령보다 검증만 하는 명령을 우선하는 것입니다. 파일 수정은 PostToolUse에서 처리하고, Stop에서는 “마지막 상태가 기준을 만족하는가?”만 확인하는 식으로 책임을 분리하는 거죠.

package.json에 검증용 스크립트가 있다면 다음처럼 최종 검증을 설정할 수 있습니다:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bun run lint && bun run typecheck",
            "timeout": 120,
            "statusMessage": "Running final checks"
          }
        ]
      }
    ]
  }
}

Stop Hook은 단순히 검증만 하는 데서 끝나지 않고, 특정 조건이 충족되지 않았을 때 Claude가 멈추지 않고 계속 작업하게 만들 수도 있습니다. 예를 들어 테스트가 통과하지 않으면 응답을 끝내지 말고 계속 고치라고 지시하는 식입니다:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-before-stop.sh"
          }
        ]
      }
    ]
  }
}
#!/bin/bash
INPUT=$(cat)
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

# 무한 루프 방지: 이미 Stop Hook에 의해 재개된 상태면 그냥 멈추게 함
if [ "$STOP_ACTIVE" = "true" ]; then
  exit 0
fi

# 테스트 실행
if ! bun run test 2>&1 | tail -1 | grep -q "passed"; then
  echo "테스트가 실패했습니다. 실패한 테스트를 확인하고 수정해주세요." >&2
  exit 2
fi

exit 0

stop_hook_active 필드를 확인하는 것이 매우 중요합니다. 이 값이 true이면 이미 Stop Hook에 의해 Claude가 한 번 재개된 상태라는 뜻이에요. 이때 다시 exit 2를 반환하면 무한 루프에 빠지게 됩니다.

검증 외에 또 하나 흥미로운 활용법은 Stop을 설정 개선 장치로 쓰는 것입니다. 세션이 끝나는 시점은 방금 한 작업의 맥락이 가장 또렷한 순간이라서, 이때 작업 내용을 돌아보고 CLAUDE.md에 추가하면 좋을 규칙을 제안하도록 Hook을 짜두면 설정이 시간이 지날수록 점점 다듬어집니다. 같은 시행착오를 다음 세션에서 반복하지 않게 만드는 방법이죠.

여러 Hook 조합하기

실제 프로젝트에서는 Hook을 하나만 쓰기보다 역할별로 조합해서 쓰는 경우가 많습니다. 가벼운 파일 단위 정리는 PostToolUse, 마지막 검증은 Stop, 사람이 필요한 순간의 알림은 Notification에 맡기는 식입니다. 이 예제에서는 포매팅은 PostToolUse가 맡고, Stop은 린트와 타입 체크만 담당하도록 나눠보겠습니다.

예를 들어 package.json 스크립트로 포매팅과 검증을 관리하는 프로젝트라면 다음처럼 구성할 수 있습니다:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bunx prettier --write \"$CLAUDE_FILE_PATHS\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bun run lint && bun run typecheck",
            "timeout": 120,
            "statusMessage": "Running final checks"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

이 조합에서 PostToolUse는 수정 직후 파일을 정리하고, Stop은 마지막에 프로젝트 기준을 확인합니다. Notification은 권한 요청이나 사용자 입력 대기처럼 사람이 개입해야 하는 순간을 알려줍니다. 이렇게 나눠두면 각 Hook이 언제 실행되는지 예측하기 쉽고, 자동화가 과하게 끼어들어 작업 흐름을 방해하는 일도 줄어듭니다.

디버깅 팁

Hooks가 예상대로 동작하지 않을 때는 몇 가지 방법으로 디버깅할 수 있습니다.

우선 /hooks 명령어로 현재 설정된 Hooks를 확인합니다. Hook 명령어에 로깅을 추가해서 입력 데이터와 실행 결과를 파일에 기록할 수도 있습니다.

tee /tmp/hook-input.json | your-hook-command

이렇게 하면 /tmp/hook-input.json 파일에서 Hook이 받은 입력을 확인할 수 있습니다.

마치며

클로드 코드 Hooks는 AI 코딩 에이전트의 동작을 세밀하게 제어할 수 있는 강력한 도구입니다. 코드 포매팅 자동화, 민감한 파일 보호, 커스텀 알림 등 다양한 워크플로우를 구축해서 개발 생산성을 높일 수 있습니다.

특히 팀 프로젝트에서 .claude/settings.json 파일을 Git에 커밋하면 모든 팀원이 동일한 Hook 설정을 공유할 수 있어서 일관된 개발 환경을 유지하는 데 도움이 됩니다.

더 자세한 이벤트 목록과 입출력 형식은 공식 문서에서 확인할 수 있습니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord