Advanced (업데이트: 2026. 6. 1.)

Claude Code Hooks 입문: 작업 전후 자동 검사로 사고 줄이기

초보자를 위한 Claude Code Hooks 실무 가이드. 위험 명령 차단, 로그 저장, 자동 포맷과 테스트까지 다룹니다.

Claude Code Hooks 입문: 작업 전후 자동 검사로 사고 줄이기

Claude Code Hooks는 Claude가 작업하기 전후에 자동으로 실행되는 검사입니다. 매번 “위험한 명령은 실행하지 마”, “수정 후 format 해줘”, “테스트도 돌려줘”라고 말하는 대신, 이런 규칙을 Claude Code의 작업 흐름 안에 넣을 수 있습니다.

처음 배우는 사람은 이벤트 이름을 모두 외울 필요가 없습니다. PreToolUse 는 브레이크, PostToolUse 는 정비, UserPromptSubmit 은 접수 기록, Stop 은 나가기 전 최종 확인이라고 생각하면 됩니다. Hooks는 리뷰, 권한 설정, CI를 대체하지 않습니다. 대신 반복되는 확인을 잊기 어렵게 만드는 실무용 안전장치입니다.

이 글은 Claude Code Hooks 공식 문서Claude Code settings 공식 문서 를 기준으로 작성했습니다. 프로젝트 컨텍스트는 CLAUDE.md 베스트 프랙티스, 권한 설계는 Claude Code 권한 가이드 와 함께 보면 좋습니다.

먼저 배울 4가지 이벤트

Claude Code에는 많은 Hook 이벤트가 있지만, 팀에서 바로 효과를 보려면 아래 네 가지부터 시작하면 됩니다.

이벤트적합한 용도예시
UserPromptSubmit사용자 프롬프트가 Claude에 전달되기 전요청 로그 저장, 비밀정보 의심 문자열 확인
PreToolUse도구 실행 직전위험한 Bash 명령, 프로덕션 삭제 명령 차단
PostToolUse도구 실행 직후format, lint, 관련 테스트 실행
StopClaude가 답변을 끝내려 할 때Git 충돌 확인, 요약 저장, 검증 누락 알림

실행되면 안 되는 작업은 PreToolUse 에 둡니다. 이미 수정이 끝난 뒤 정리하거나 검증할 작업은 PostToolUse 가 맞습니다. 요청 품질을 분석하려면 UserPromptSubmit, 세션이 애매하게 끝나는 것을 막으려면 Stop 을 사용합니다.

공유 규칙은 보통 .claude/settings.json 에 둡니다. 개인 실험은 .claude/settings.local.json 이 안전합니다. 팀에서 쓰는 경우에는 어떤 Hook이 차단하고 어떤 Hook이 로그만 남기는지 README에 적어두는 것이 좋습니다.

복사해서 시작하는 최소 설정

아래 설정은 프롬프트 로그, 위험한 Bash 차단, 편집 후 품질 검사, 종료 시 요약 저장을 포함합니다. 처음부터 모든 것을 자동화하기보다 작게 시작하는 편이 운영에 강합니다.

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/log-prompt.mjs"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/block-dangerous-command.mjs"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/run-quality-checks.mjs",
            "timeout": 120
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/stop-summary.mjs"
          }
        ]
      }
    ]
  }
}

핵심은 중첩된 hooks 배열입니다. 현재 설정 구조에서는 이벤트, matcher 그룹, 그리고 type: "command" 같은 handler를 순서대로 씁니다. 오래된 예제처럼 matcher 옆에 command 를 바로 두는 방식은 피하는 편이 좋습니다.

Use case 1: PreToolUse로 위험 명령 차단

첫 번째 use case는 명령 실행 전 안전장치입니다. Claude Code가 빠르게 작업할수록 위험한 순간은 명령이 끝난 뒤가 아니라 실행되기 직전입니다. PreToolUse 는 Bash 입력을 읽고 명백히 위험한 경우 실행을 거부할 수 있습니다.

.claude/hooks/block-dangerous-command.mjs 로 저장합니다.

import fs from "node:fs";

const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
const command = String(input.tool_input?.command || "");

const denyPatterns = [
  /rm\s+-rf\s+(\/|\*|\.|\$HOME)/i,
  /cat\s+\.env(\.|$|\s)/i,
  /printenv/i,
  /aws\s+.*\s+delete-/i,
  /gcloud\s+.*\s+delete/i,
  /kubectl\s+delete\s+(namespace|deployment|secret)/i,
  /DROP\s+DATABASE/i
];

const matched = denyPatterns.find((pattern) => pattern.test(command));

if (matched) {
  console.log(JSON.stringify({
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: `Blocked by project hook: ${matched}`
    }
  }));
  process.exit(0);
}

이 스크립트는 완벽한 보안 도구가 아닙니다. 정규식은 우회될 수 있습니다. 그래도 .env 출력, 큰 디렉터리 삭제, 클라우드 리소스 삭제, Kubernetes 리소스 삭제 같은 흔한 실수를 막는 데는 충분히 도움이 됩니다. 더 강한 통제가 필요하다면 Claude Code 권한 설정, 보호 브랜치, CI, 사람의 승인과 함께 사용해야 합니다.

Use case 2: UserPromptSubmit로 요청 로그 남기기

두 번째 use case는 프롬프트 관찰입니다. Claude Code 결과가 흔들릴 때 원인은 모델보다 요청이 모호한 경우가 많습니다. “고쳐줘”, “정리해줘”, “좋게 만들어줘” 같은 요청만 남기면 성공 조건이 흐려집니다.

.claude/hooks/log-prompt.mjs 로 저장합니다.

import fs from "node:fs";
import path from "node:path";

const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
const dir = path.join(process.cwd(), ".claude", "hook-logs");
fs.mkdirSync(dir, { recursive: true });

const record = {
  time: new Date().toISOString(),
  event: input.hook_event_name,
  cwd: input.cwd,
  prompt: input.prompt || input.user_prompt || ""
};

fs.appendFileSync(
  path.join(dir, "prompts.jsonl"),
  JSON.stringify(record) + "\n",
  "utf8"
);

이 로그는 먼저 로컬에만 보관하세요. 프롬프트에는 고객명, 내부 URL, 실수로 붙여넣은 토큰, 업무 정보가 들어갈 수 있습니다. .claude/hook-logs/.gitignore 에 넣고, 저장 기간과 접근 권한을 정해야 합니다.

Use case 3: PostToolUse로 format과 test 자동 실행

세 번째 use case는 매일 쓰는 품질 workflow입니다. Claude가 파일을 수정한 직후 formatter와 작은 테스트를 실행하면, 매번 프롬프트에 “format하고 테스트해줘”라고 쓰지 않아도 됩니다.

.claude/hooks/run-quality-checks.mjs 로 저장합니다.

import fs from "node:fs";
import { execFileSync } from "node:child_process";

const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
const filePath = String(input.tool_input?.file_path || "");

const isSourceFile = /\.(js|jsx|ts|tsx|css|md|mdx|json)$/.test(filePath);
if (!isSourceFile) process.exit(0);

const run = (cmd, args) => {
  try {
    return execFileSync(cmd, args, {
      cwd: process.cwd(),
      encoding: "utf8",
      stdio: ["ignore", "pipe", "pipe"]
    });
  } catch (error) {
    return String(error.stdout || "") + String(error.stderr || "");
  }
};

const messages = [];
messages.push(run("npx", ["prettier", "--write", filePath]));

if (/\.(js|jsx|ts|tsx)$/.test(filePath)) {
  messages.push(run("npm", ["test", "--", "--runInBand"]));
}

console.log(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: "PostToolUse",
    additionalContext: messages.join("\n").slice(-4000)
  }
}));

큰 저장소에서는 매번 전체 테스트를 돌리지 마세요. Hooks가 느려지면 사람들은 금방 꺼버립니다. 먼저 format만 적용하고, 그다음 관련 테스트를 추가하고, 오래 걸리는 검사는 async Hook이나 CI로 보내는 것이 좋습니다.

Use case 4: Stop에서 끝나도 되는지 확인

Stop 은 Claude가 답변을 마치려는 순간 실행됩니다. 여기서는 무거운 검사를 매번 돌리기보다, Git 충돌처럼 명확한 미완료 상태만 막거나 요약 로그를 남기는 방식이 적절합니다.

import fs from "node:fs";
import { execFileSync } from "node:child_process";

const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");

if (input.stop_hook_active) {
  process.exit(0);
}

let status = "";
try {
  status = execFileSync("git", ["status", "--short"], {
    cwd: process.cwd(),
    encoding: "utf8"
  });
} catch {
  process.exit(0);
}

if (status.includes("UU ")) {
  console.error("Git conflict remains. Resolve conflicts before finishing.");
  process.exit(2);
}

fs.mkdirSync(".claude/hook-logs", { recursive: true });
fs.appendFileSync(
  ".claude/hook-logs/stop.jsonl",
  JSON.stringify({
    time: new Date().toISOString(),
    lastAssistantMessage: input.last_assistant_message || "",
    gitStatus: status.slice(0, 2000)
  }) + "\n"
);

Stop hook은 너무 엄격하게 만들면 Claude가 끝나지 못하는 루프처럼 느껴질 수 있습니다. stop_hook_active 를 확인하고, 차단 조건은 좁게 유지하세요. 익숙해지기 전에는 차단보다 로그 저장을 우선하는 것이 안전합니다.

pitfall: 자주 생기는 실패와 회피법

첫 번째 pitfall은 Hooks를 권한 시스템처럼 믿는 것입니다. Command hook은 사용자의 시스템 권한으로 실행됩니다. 잘못된 hook은 파일을 삭제하거나 민감한 정보를 읽을 수 있습니다. 입력 검증, 경로 검증, 비밀 파일 제외는 반드시 필요합니다.

두 번째 pitfall은 동기 Hook에 너무 많은 일을 넣는 것입니다. 편집 후 format은 괜찮지만, 매번 build, 전체 test, 브라우저 test, deploy까지 실행하면 workflow가 무거워집니다. 빠른 검사는 PostToolUse, 느린 검사는 async Hook 또는 CI로 나눠야 합니다.

세 번째 pitfall은 로그 정책 없이 기록만 쌓는 것입니다. 로그는 문제 해결에 좋지만 비밀정보가 남을 수 있습니다. 저장 위치, 저장 기간, 접근 권한, Git 제외 규칙을 함께 정하세요.

네 번째 pitfall은 matcher를 비즈니스 로직으로 착각하는 것입니다. matcher: "Bash" 는 Bash 도구 이벤트를 handler로 보내는 필터일 뿐입니다. 실제 허용 또는 차단 판단은 스크립트 내부에서 해야 합니다.

Masa식 검증 메모

실제로 써보면 가장 효과가 큰 부분은 format과 test 결과를 additionalContext 로 Claude에게 되돌려주는 것입니다. 사람이 로그를 다시 붙여넣지 않아도 Claude가 다음 수정을 이어갈 수 있습니다. 위험 명령 차단은 모든 문제를 막지는 못하지만, 무서운 실수를 실행 직전에 멈춘다는 점에서 가치가 큽니다.

팀에 처음 도입한다면 3단계가 좋습니다. 첫 주는 프롬프트 로그만 남기고, 다음에 편집 후 format을 켜고, 마지막으로 소수의 Bash 차단 규칙을 추가합니다. 이렇게 하면 자동화가 부담이 아니라 신뢰를 쌓는 장치가 됩니다.

권한, CLAUDE.md, Hooks, review workflow를 함께 정리하고 싶다면 Claude Code training 페이지 를 참고하세요. 개인 개발자라면 이 내용을 Gumroad 체크리스트로 만들어 새 프로젝트 시작 템플릿으로 써도 좋습니다.

정리

Claude Code Hooks는 AI 개발을 안정적으로 만드는 실무 가드레일입니다. UserPromptSubmit 으로 요청을 이해하고, PreToolUse 로 위험 동작을 막고, PostToolUse 로 수정 결과를 검증하고, Stop 으로 명확한 미완료를 남기지 않습니다.

작게 시작하고 로그를 보면서 규칙을 늘리세요. 복잡하지만 금방 꺼지는 자동화보다, 단순하고 계속 켜둘 수 있는 Hook이 훨씬 강합니다.

#Claude Code #Hooks #automation #security #testing
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.