본문 바로가기

AI Development

Claude Code가 스스로 에러 잡게 만드는 법 — Hooks 실전 설정

반응형

Claude Code로 코드 짜다 보면 이런 상황이 생겨요.

Claude: 구현 완료했습니다!
나: (확인해보니) lint 에러 10개, 테스트 2개 실패, .env 파일 커밋됨

Hooks는 Claude가 특정 동작을 하기 전/후에 자동으로 스크립트를 실행해요. 이 문제를 구조적으로 막아요.


Hooks가 뭔가

Claude 동작 흐름:

파일 수정 전  → PreToolUse Hook 실행
              → (실패하면 Claude 동작 중단)
파일 수정
파일 수정 후  → PostToolUse Hook 실행
세션 종료 전  → Stop Hook 실행

즉, Claude가 코드를 짜는 중간중간에 자동 검증이 들어가요. 문제 있으면 Claude가 스스로 고쳐요.


설정 방법

.claude/hooks/ 폴더에 JSON 파일로 설정해요.

.claude/
└── hooks/
    └── default.json

기본 구조:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "실행할 명령어"
          }
        ]
      }
    ]
  }
}

matcher는 어떤 Claude 툴 사용 시 실행할지 지정해요.

주요 matcher:
Write       → 파일 새로 생성할 때
Edit        → 파일 수정할 때
MultiEdit   → 여러 파일 동시 수정할 때
Bash        → bash 명령 실행할 때
Read        → 파일 읽을 때

실전 설정 1 — TypeScript 프로젝트

.claude/hooks/default.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $PROJECT_ROOT && pnpm typecheck 2>&1 | head -20"
          },
          {
            "type": "command",
            "command": "cd $PROJECT_ROOT && pnpm lint --fix 2>&1 | tail -10"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo '실행 명령: '$TOOL_INPUT | grep -E 'rm -rf|curl.*|.env' && echo 'BLOCKED' && exit 1 || exit 0"
          }
        ]
      }
    ]
  }
}

실제 동작:

Claude: payment.ts 파일 수정 완료
→ Hook 자동 실행: pnpm typecheck
  → 에러 발견: "Property 'amount' does not exist on type 'Payment'"
→ Hook 자동 실행: pnpm lint --fix
  → 5개 자동 수정됨
→ Claude가 결과 보고 스스로 타입 에러 수정
→ 다시 typecheck 통과 확인
→ 완료 보고

Claude한테 말 안 해도 타입 에러랑 lint 에러는 스스로 잡아요.


실전 설정 2 — 보안 자동 스캔

.claude/hooks/security.sh:

#!/bin/bash
# 위험한 패턴 감지

MODIFIED_FILE="$1"

# .env 파일 실수로 커밋하려는지 체크
if echo "$MODIFIED_FILE" | grep -qE "\.env$|\.env\.local"; then
  echo "🚨 경고: .env 파일은 커밋하면 안 됩니다!"
  exit 1
fi

# 하드코딩된 시크릿 감지
if grep -qE "(api_key|password|secret|token)\s*=\s*['\"][^'\"]{8,}" "$MODIFIED_FILE" 2>/dev/null; then
  echo "🚨 경고: 하드코딩된 시크릿 감지됨! 환경변수로 이동하세요."
  grep -nE "(api_key|password|secret|token)\s*=\s*['\"][^'\"]{8,}" "$MODIFIED_FILE"
  exit 1
fi

echo "✅ 보안 체크 통과"
exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/security.sh $TOOL_OUTPUT_PATH"
          }
        ]
      }
    ]
  }
}

실제 동작:

Claude: config.ts 파일 생성
→ 보안 Hook 실행
  → "api_key = 'sk-1234abcd...'" 감지
  → 🚨 경고 출력, exit 1
→ Claude가 경고 보고 자동으로 수정:
  → process.env.API_KEY 로 변경
→ 다시 보안 체크 통과
→ 완료

시크릿 하드코딩을 Claude가 스스로 잡아서 고쳐요.


실전 설정 3 — 테스트 자동 실행

파일 수정 후 관련 테스트만 자동으로 돌려요.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/run-related-tests.sh $TOOL_OUTPUT_PATH"
          }
        ]
      }
    ]
  }
}

.claude/hooks/run-related-tests.sh:

#!/bin/bash
MODIFIED_FILE="$1"

# 테스트 파일 자체면 그냥 실행
if echo "$MODIFIED_FILE" | grep -q "\.test\."; then
  pnpm test "$MODIFIED_FILE"
  exit $?
fi

# 소스 파일이면 대응하는 테스트 파일 찾아서 실행
TEST_FILE="${MODIFIED_FILE/src\//tests/}"
TEST_FILE="${TEST_FILE/.ts/.test.ts}"

if [ -f "$TEST_FILE" ]; then
  echo "🧪 관련 테스트 실행: $TEST_FILE"
  pnpm test "$TEST_FILE"
  exit $?
else
  echo "⚠️  테스트 파일 없음: $TEST_FILE"
  echo "테스트를 작성해주세요."
  exit 0
fi

실제 동작:

Claude: src/services/payment.service.ts 수정
→ Hook: tests/services/payment.service.test.ts 자동 실행
  → 테스트 결과:
    ✓ 정상 결제 처리 (23ms)
    ✗ 잔액 부족 시 에러 반환 → FAILED
      Expected: AppError(400)
      Received: undefined
→ Claude가 실패 본 후 자동으로 수정
→ 테스트 재실행 → 전체 통과
→ 완료 보고

실전 설정 4 — PR 전 최종 검사

세션 종료 전 자동으로 전체 체크해요.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/pre-commit-check.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/pre-commit-check.sh:

#!/bin/bash
echo "🔍 세션 종료 전 최종 체크..."

FAILED=0

# 1. 타입 체크
echo "1/4 타입 체크..."
if ! pnpm typecheck; then
  echo "❌ 타입 에러 있음"
  FAILED=1
fi

# 2. lint
echo "2/4 Lint 체크..."
if ! pnpm lint; then
  echo "❌ Lint 에러 있음"
  FAILED=1
fi

# 3. 테스트
echo "3/4 테스트 실행..."
if ! pnpm test; then
  echo "❌ 테스트 실패"
  FAILED=1
fi

# 4. .env 파일 staged 여부
echo "4/4 민감 파일 체크..."
if git diff --cached --name-only | grep -qE "\.env"; then
  echo "❌ .env 파일이 staged됨! git reset HEAD .env 실행 필요"
  FAILED=1
fi

if [ $FAILED -eq 0 ]; then
  echo "✅ 모든 체크 통과! PR 올려도 됩니다."
else
  echo "⚠️  수정 필요한 항목이 있습니다."
fi

실제 동작:

나: /exit (세션 종료)
→ Stop Hook 자동 실행:
  1/4 타입 체크... ✅ 통과
  2/4 Lint 체크... ✅ 통과
  3/4 테스트 실행... ❌ 2개 실패
  4/4 민감 파일 체크... ✅ 통과

  ⚠️  수정 필요한 항목이 있습니다.

→ Claude: 테스트 실패 자동 수정 후 재실행
→ 전체 통과 후 세션 종료

전체 hooks 파일 합본

.claude/hooks/default.json 실제 프로덕션 설정:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $PROJECT_ROOT && pnpm typecheck 2>&1 | head -20"
          },
          {
            "type": "command",
            "command": "bash .claude/hooks/security.sh $TOOL_OUTPUT_PATH"
          },
          {
            "type": "command",
            "command": "bash .claude/hooks/run-related-tests.sh $TOOL_OUTPUT_PATH"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/check-dangerous-command.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/pre-commit-check.sh"
          }
        ]
      }
    ]
  }
}

Hooks 전후 비교

Hooks 없이:

Claude: 기능 구현 완료!
나: PR 올리려고 보니까...
  - 타입 에러 5개
  - lint 에러 8개
  - 테스트 3개 실패
  - API 키 하드코딩 1개
나: (다시 Claude한테 설명하고 수정 요청 → 30분 낭비)

Hooks 있을 때:

Claude: 파일 수정
→ 타입 에러 감지 → 자동 수정
→ 보안 이슈 감지 → 자동 수정
→ 테스트 실패 감지 → 자동 수정
Claude: 기능 구현 완료! (모든 체크 통과)
나: PR 바로 올림

 

 

📌 관련 글

CLAUDE.md 잘 쓰는 법

 

CLAUDE.md 잘 쓰는 법 — 세션마다 시니어 개발자를 고용하는 효과

Claude Code를 처음 쓰면 이런 일이 반복돼요.세션 1: "우리 프로젝트는 TypeScript 씁니다"세션 2: 또 "TypeScript 써요"세션 3: 또또 "TypeScript요..."Claude Code는 매 세션마다 기억을 초기화하고 시작해요.아무

cell-devlog.tistory.com

Claude Code 4레이어 컨텍스트 시스템

 

매번 설명 반복하다 지쳤다면 — Claude Code 4레이어 컨텍스트 시스템

Claude Code를 처음 쓰면 이런 상황이 반복돼요.세션 1: "우리 프로젝트는 TypeScript 쓰고, Fastify야, 테스트는 Vitest야"세션 2: 또 설명세션 3: 또 설명매번 새 Claude한테 온보딩을 반복하는 거예요. 이게

cell-devlog.tistory.com

 

반응형