Hooks로 자동 코드 검사: 작성 즉시 품질 보장

PostToolUse Hook으로 Claude가 파일을 쓴 후 lint를 자동 실행. 종료 코드 2로 오류를 Claude에 피드백해 쓰기→검사→수정 루프를 자동화.


Claude Code는 코드를 빠르게 작성하지만, lint 실행을 잊는 경우가 있습니다. "쓰고 나서 검사도 해줘"라고 부탁하면 할 때도 있고 빠뜨릴 때도 있습니다. Hooks가 이 문제를 해결합니다——코드 검사를 "Claude의 책임"에서 "시스템의 책임"으로 옮기는 것입니다.

이 글은 하나에 집중합니다: PostToolUse Hook을 사용해 Claude가 코드를 작성한 후 자동으로 검사를 트리거하여, 모든 파일 변경이 코딩 규칙을 충족하도록 만드는 방법입니다.


접근 방식: PostToolUse + 파일 타입 필터링

코드 검사 Hook의 로직은 간단합니다:

  1. Claude가 Write 또는 Edit으로 파일을 작성
  2. Hook이 이 이벤트를 가로채서 파일 경로를 획득
  3. 파일 확장자에 따라 실행할 검사 도구를 결정
  4. 검사 통과: 종료 코드 0, Claude 계속 진행
  5. 검사 실패: 종료 코드 2, 오류 출력을 Claude에 피드백하여 수정하게 함

종료 코드 2가 핵심입니다——Claude가 오류를 받아 자동 수정하고, "작성→검사→수정"의 루프가 형성됩니다.


기본 설정: 단일 언어 프로젝트

Ruby 프로젝트 (RuboCop)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.rb ]] && rubocop -A \"$file\" --no-color -q || true'"
          }
        ]
      }
    ]
  }
}

-A 옵션으로 RuboCop이 수정 가능한 문제를 자동으로 고칩니다. 수정 불가능한 문제는 stderr로 출력되고, 비정상 종료 코드와 함께 Claude가 처리합니다.

JavaScript/TypeScript 프로젝트 (ESLint)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" =~ \\.(js|ts|jsx|tsx)$ ]] && npx eslint --fix \"$file\" || true'"
          }
        ]
      }
    ]
  }
}

Python 프로젝트 (ruff)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.py ]] && ruff check --fix \"$file\" && ruff format \"$file\" || true'"
          }
        ]
      }
    ]
  }
}

고급 설정: 다중 언어 프로젝트

실제 프로젝트는 여러 언어가 혼재하는 경우가 많습니다. 검사 로직을 독립 스크립트로 분리하면 JSON에 명령어를 쌓는 것보다 유지보수가 훨씬 편합니다.

.claude/hooks/lint.sh 생성:

#!/bin/bash
set -e

input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

[[ -z "$file" ]] && exit 0
[[ ! -f "$file" ]] && exit 0

ext="${file##*.}"

case "$ext" in
  rb)
    rubocop -A "$file" --no-color -q
    ;;
  js|ts|jsx|tsx)
    npx eslint --fix "$file" --quiet
    ;;
  py)
    ruff check --fix "$file"
    ruff format "$file"
    ;;
  go)
    gofmt -w "$file"
    golangci-lint run "$file" 2>&1
    ;;
  *)
    exit 0
    ;;
esac

settings.json에서 스크립트 참조:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/lint.sh"
          }
        ]
      }
    ]
  }
}

스크립트는 주석 추가, 단독 테스트, git 커밋을 통한 팀 공유가 가능합니다.


Claude 자동 수정 유도

종료 코드 2 + stderr 출력이 Claude 자동 수정의 핵심입니다:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

[[ -z "$file" || ! -f "$file" ]] && exit 0

ext="${file##*.}"

run_lint() {
  local output exit_code
  output=$(rubocop "$file" --no-color 2>&1)
  exit_code=$?

  if [[ $exit_code -ne 0 ]]; then
    echo "$output" >&2
    exit 2  # 원래 종료 코드 대신 2 사용
  fi

  rubocop -A "$file" --no-color -q
}

[[ "$ext" == "rb" ]] && run_lint

타입 검사: 별도 Hook

타입 검사는 보통 lint보다 느리므로 별도로 설정하는 것이 좋습니다:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.ts ]] && npx tsc --noEmit --skipLibCheck 2>&1 | head -20 || true'"
          }
        ]
      }
    ]
  }
}

검사가 필요 없는 파일 제외

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')

[[ -z "$file" || ! -f "$file" ]] && exit 0

skip_patterns=(
  "vendor/"
  "node_modules/"
  "db/schema.rb"
  "db/queue_schema.rb"
  ".min.js"
  "_test.go"
)

for pattern in "${skip_patterns[@]}"; do
  [[ "$file" == *"$pattern"* ]] && exit 0
done

ext="${file##*.}"
[[ "$ext" == "rb" ]] && rubocop -A "$file" --no-color -q

실제 동작 흐름

설정 후 워크플로우:

  1. Claude에게 Ruby 파일 작성 요청
  2. Claude가 Write 도구로 작성
  3. Hook이 자동으로 rubocop -A 실행——3건의 문제 발견
  4. RuboCop이 2건 자동 수정, 3번째는 판단 필요
  5. 3번째 오류가 stderr로 Claude에 피드백
  6. Claude가 코드 수정, Hook 재트리거
  7. 두 번째 검사 통과——완료

수동으로 검사 명령어를 실행할 필요가 전혀 없습니다.


전역 vs 프로젝트 수준

전역(~/.claude/settings.json): 모든 프로젝트에서 동일한 규칙을 사용할 때 적합합니다.

프로젝트 수준(.claude/settings.json을 git에 커밋): 팀 프로젝트에 권장. 저장소를 열면 자동으로 올바른 설정이 적용됩니다.

두 가지는 공존 가능하며, 프로젝트 수준 hooks는 전역과 병합되어 모두 실행됩니다.


정리

Hooks로 자동 코드 검사를 구현하는 핵심은 세 단계입니다:

  1. PostToolUse + "Write|Edit" matcher로 파일 쓰기 가로채기
  2. 파일 경로를 획득하여 확장자별로 해당 lint 도구에 분배
  3. 실패 시 종료 코드 2로 오류를 Claude에 피드백하여 자동 수정

한 언어부터 시작해서 작동하면 다른 언어를 추가하세요. 스크립트를 .claude/hooks/에 커밋하여 팀이 공유하면, 한 번 설정으로 장기간 효과를 누릴 수 있습니다.