
Claude Code hooks로 AI 에이전트의 폭주를 "구조"로 막는 5가지 레시피 — 위험 명령 차단·자동 포맷팅·보호 파일·테스트
요약
Claude Code의 hooks 기능을 활용하여 AI 에이전트의 위험한 명령 실행을 구조적으로 차단하는 방법을 다룹니다. LLM의 확률적 특성으로 인한 지시 불이행 문제를 셸 스크립트의 결정적 실행을 통해 해결하는 5가지 레시피를 제공합니다.
핵심 포인트
- LLM의 지시는 확률적이지만, 셸 스크립트의 후크는 결정적임
- PreToolUse 후크를 통해 위험한 명령 실행 전 차단 가능
- 인간은 정책을 설계하고, 셸은 이를 강제하는 구조가 핵심
- 세션, 턴, 툴 단위의 다양한 이벤트 타이밍 활용 가능
AI에게 "이것은 하지 말아줘"라고 부탁해도, 다음 세션에서는 아무렇지 않게 어겨지곤 합니다. CLAUDE.md에 "운영 환경의 마이그레이션은 건드리지 마라", "push는 마음대로 하지 마라"라고 적어두었는데, 정신을 차려보니 에이전트가 rm -rf를 입력하려고 하고 있었다…. 이런 경험 없으신가요.
솔직히 말씀드리겠습니다. 부탁은, 깨집니다. 하지만 구조는 깨지지 않습니다.
이 기사는 Claude Code의 **hooks (후크)**를 사용하여, AI 코딩 에이전트의 "실행하는 순간"에 반드시 작동하는 감시병을 배치하여, 위험한 조작을 멈추거나 편집할 때마다 자동으로 포맷팅하는 방법을, 아무것도 모르는 초보자도 이해할 수 있도록, 그대로 팀에 도입할 수 있는 수준까지 작성해 나갑니다. 복사해서 바로 작동하는 설정과 스크립트 5가지 레시피, 그리고 마지막에 약간 무서운 "exit code의 함정"까지 다룹니다.
기술적인 이야기로 들어가기 전에, 이것만 먼저 말씀드리겠습니다. 이 부분이 흔들리면, 후크는 "AI에게 전부 맡기는 도구"가 되어 버려 오히려 사고를 유발합니다.
| 누가 | 무엇을 담당하는가 |
|---|---|
| 인간 | 무엇을 차단할지에 대한 정책 설계 (어떤 명령·어떤 파일을 보호할 것인가) |
| 인간 | 불가역적 조작 (삭제·push·배포·과금)의 최종 승인 |
| AI | 후크 스크립트의 구현안·수정안 제시 (초안 작성자) |
| 결정적인 셸 (Deterministic Shell) | 후크의 실행 그 자체 (매번·동일하게·반드시 실행) |
포인트는, "무엇을 보호할 것인가"를 결정하는 것은 인간이고, "매번 그것을 실행하는 것"은 결정적인 셸이라는 점입니다. AI는 구현을 돕는 역할일 뿐, 감시자 그 자체로 만들어서는 안 됩니다. 이 경계 설정이 오늘 내용의 뼈대입니다.
애초에, 왜 부탁은 깨지는 걸까요?
이유는 간단합니다. LLM (대규모 언어 모델)의 출력은 확률적이기 때문입니다. CLAUDE.md나 AGENTS.md에 적은 지시는 모델에게 있어 "참고할 문맥 중 하나"일 뿐입니다. 긴 작업 도중에 문맥이 옅어지거나, 다른 지시와 충돌하면 조용히 무시될 때가 있습니다. 악의가 있는 게 아니라, 그런 구조인 것입니다.
반면에, 셸 스크립트는 **결정적 (Deterministic)**입니다. rm -rf라는 문자열을 발견하면, 100번 중 100번 차단합니다. 기분에 따라 눈감아주는 일은 없습니다.
CLAUDE.md의 지시 = 부탁 (확률적·깨질 수 있음)
hooks의 셸 실행 = 검문소 (결정적·매번 반드시 통과)
그래서 발상을 전환합니다. "AI가 잘 지키게 만들자"라고 노력하는 것이 아니라, "AI는 말을 듣지 않는다는 전제"하에, 실행하는 순간을 물리적으로 에워싸는 것. 이것이 후크의 사상입니다.
후크를 한마디로 말하면, **"특정 타이밍에 자동으로 발화하는, 당신이 정한 셸 명령(shell command)"**입니다.
이미지는 건물의 입구에 서 있는 감시병입니다. 누군가 들어오려는 "순간"에 반드시 말을 겁니다. 통과시켜도 되는 사람인지 확인하고, 안 된다면 멈춥니다. Claude Code의 후크도 마찬가지로, 에이전트가 무언가 동작하려고 하는 순간에 개입하여 당신의 스크립트를 실행해 줍니다.
Claude Code의 이벤트는 크게 3가지 타이밍(Cadence)으로 나뉩니다.
- 세션 단위…
SessionStart/SessionEnd(대화의 시작·종료 시 1회) - 턴 단위…
UserPromptSubmit/Stop등 (당신이 한 번 말을 걸고, AI가 답변을 마칠 때까지) - 툴 단위…
PreToolUse/PostToolUse등 (AI가 파일 편집이나 명령 실행이라는 "도구"를 사용할 때마다)
오늘의 주인공은 이 툴 단위의 두 가지입니다.
- PreToolUse (툴을 사용하기 "전")… 아직 실행되지 않았으므로, 멈출 수 있다. 위험 명령의 차단은 여기서 이루어집니다.
- PostToolUse (툴을 사용한 "후")… 이미 실행되었으므로 멈출 수는 없지만, 결과를 보고 AI에게 다시 시키는 피드백을 할 수 있습니다. 편집 후의 자동 포맷팅이나 lint는 여기서 이루어집니다.
설정은 settings.json에 다음과 같은 3계층 형태로 작성합니다.
{
"hooks": {
"PreToolUse": [
...
]
}
}
읽는 법은 이렇습니다. "PreToolUse (툴 실행 전)에, matcher가 Bash (즉, Bash 툴을 사용하려고 할 때)일 때만, block-dangerous.sh를 실행해 줘". matcher는 tool_name...
에 대해 매칭되며, Bash와 같은 완전 일치나, Edit|Write와 같은 | 구분자, 정규 표현식도 사용할 수 있습니다.
설정 파일의 위치는 3가지가 있으며, 우선순도와 용도가 다릅니다.
~/.claude/settings.json… 개인용 (모든 프로젝트 공통).claude/settings.json… 프로젝트 공유용 (Git에 넣어 팀원과 배포).claude/settings.local.json… 프로젝트 개인 설정 (gitignore 대상)
팀에서 지키고 싶은 규칙은 .claude/settings.json에 넣고 커밋하는 것이 핵심입니다. 이렇게 하면 "새로 합류한 사람의 머신에서도, 동일한 파수꾼이 처음부터 서 있는" 상태를 만들 수 있습니다.
훅(Hook)은 매우 강력하지만, 훅만으로 모든 것을 해결하려고 하면 무거워집니다. 추천하는 방식은 보호 타이밍을 3개 계층으로 나누는 것입니다.
| 계층 | 보호 시점 | 도구 | 역할 |
|---|---|---|---|
| 1 | 파일 편집 · 명령 실행 순간 | Claude Code hooks | AI의 실수를 저지르기 전에 멈추거나 수정 |
| 2 | 커밋 순간 | git pre-commit 훅 | 인간 · AI를 불문하고 커밋 전에 검사 |
| 3 | PR 순간 | CI (GitHub Actions 등) | 머지(Merge) 전 최종 게이트 |
이 3개 계층으로 보면, 훅은 "가장 안쪽 · 가장 앞단"의 방어입니다. 여기서 막아낼 수 있다면, 애초에 오염된 코드가 커밋이나 PR로 진행되지 않습니다. 반대로 훅을 통과하더라도 뒤에 pre-commit과 CI가 대기하고 있습니다. 완벽한 벽 하나를 만들려고 하지 말고, 방향이 다른 울타리를 겹쳐라. 이것이 안전 설계의 기본입니다.
그럼, 안쪽의 제1계층인 훅(Hook) 레시피 4개를 구체적으로 만들어 보겠습니다.
먼저 "5분 만에 적용하는 첫 번째 레시피"입니다. 이것만으로도 도입할 가치가 있습니다.
에이전트가 Bash 명령을 실행하려고 하는 순간, PreToolUse 훅이 tool_input.command (실행하려는 명령 문자열)를 받습니다. 훅에는 이 정보가 표준 입력 (stdin)에 JSON 형태로 흘러 들어옵니다. 그것을 jq (JSON을 추출하는 커맨드라인 도구)로 추출하여, 위험한 패턴이라면 멈추는 흐름입니다.
.claude/hooks/block-dangerous.sh:
#!/usr/bin/env bash
# 위험한 명령을 PreToolUse에서 차단함
set -euo pipefail
...
하고 있는 일을 한국어로 설명하자면, "AI가 입력하려는 명령에 rm -rf나 git push --force 또는 curl ... | sh (인터넷을 통해 스크립트를 그대로 실행)가 포함되어 있다면, exit 2로 중단하고 그 이유를 표준 에러 (stderr)에 작성한다"입니다.
이 **exit 2가 Claude Code의 훅에서는 "차단의 신호"**가 됩니다.
PreToolUse에서 exit 2를 하면, 툴 호출 자체가 실행되지 않고 stderr에 작성한 메시지가 Claude에게 돌아갑니다. 즉, AI는 "아, 이건 차단되었구나. 이유는 이것이군"이라고 이해하고 다른 방법을 다시 생각할 수 있습니다. 단순히 멈추는 것에 그치지 않고, 이유를 전달하여 궤도를 수정하게 만드는 것이 깔끔한 방식입니다.
다음은 AI가 파일을 작성한 "이후"의 이야기입니다.
PostToolUse는 실행 후의 이벤트이므로 툴을 차단할 수는 없습니다. 하지만 결과를 보고 동작할 수는 있습니다. 가장 전형적인 사용법은 파일이 편집될 때마다 포맷터 (Formatter, 정렬)와 linter (문법 · 스타일 검사)를 자동으로 적용하는 것입니다. 이를 통해 AI의 출력은 항상 프로젝트의 규칙에 맞춘 상태가 됩니다.
.claude/hooks/format-after-edit.sh:
#!/usr/bin/env bash
# 파일 편집 시마다 정렬 + lint 적용
set -euo pipefail
...
설정 측면에서는 PostToolUse를 사용하며, 파일을 수정하는 툴 (Edit과 Write)에만 적용합니다.
{
"hooks": {
"PostToolUse": [
...
]
}
}
여기서 기억해 두어야 할 점은, PostToolUse는 차단할 수는 없는 대신 stderr(표준 에러)에 작성한 내용이 Claude에게 제시된다는 점입니다. 따라서 lint 경고를 stderr로 흘려보내면, AI가 이를 읽고 "그럼 고쳐야겠네"라며 스스로 수정하게 됩니다. 사람이 매번 "정렬해줘", "lint 통과시켜줘"라고 말할 필요가 없어집니다. 사소해 보이지만, 이것이 매우 효과적입니다.
.env (환경 변수 = API 키 등의 비밀 정보가 포함되기 쉬운 파일), secrets/ 하위 디렉토리, migrations/ (DB 구조 변경)와 같이 **"AI가 멋대로 건드리면 무서운 파일"**들이 있죠. 이런 곳은 편집 도구의 PreToolUse로 보호합니다.
레시피 1에서는 exit 2로 중단시켰지만, Claude Code에는 또 다른, JSON으로 이유를 포함한 판단을 반환하는 세련된 방법이 있습니다. exit 0 상태를 유지하면서, 표준 출력(stdout)에 hookSpecificOutput이라는 객체를 내보내는 방식입니다.
.claude/hooks/protect-files.sh:
#!/usr/bin/env bash
set -euo pipefail
input="$(cat)"
...
permissionDecision에는 deny (거부), allow (허용), ask (사람에게 확인)를 지정할 수 있습니다. 여기서는 deny로 설정하고 이유도 덧붙였습니다. AI에게는 "이 파일은 보호되어 있으며, 사람의 확인이 필요하구나"라는 문맥이 제대로 전달됩니다.
주의할 점이 하나 있습니다. exit 2로 중단하는 방식과 이 JSON을 반환하는 방식은 섞어서 사용할 수 없습니다. 하나로 결정해야 합니다.
exit 2를 했을 경우, stdout의 JSON은 무시됩니다. JSON으로 세밀하게 제어하고 싶다면 "exit 0 + JSON 출력", 무조건 차단하고 싶을 뿐이라면 "exit 2"를 선택하세요. 이 차이점만 머릿속에 넣어두시기 바랍니다. 마지막은 AI가 "네, 완료했습니다"라며 대화를 마치려는 순간인 Stop 이벤트입니다. 여기서 테스트나 타입 체크(Type Check)를 실행하여, 통과하지 못했다면 끝내지 못하도록 만듭니다.
.claude/hooks/verify-on-stop.sh:
#!/usr/bin/env bash
# 대화를 마치기 전에 테스트와 타입 체크를 강제함
set -uo pipefail
...
Stop 이벤트에서 exit 2를 하면, 공식 동작으로서 "Claude를 정지시키지 않고 대화를 계속하게" 만듭니다. 즉, 테스트가 실패(Red) 상태라면 "아직 끝낼 수 없어, 이것 좀 고쳐줘"라며 AI 스스로에게 다시 돌려보냅니다. 그냥 묵인하고 끝내는 것이 아니라, 통과(Green)할 때까지 붙잡아 두는 것입니다. 이것이 가장 기분 좋은 자기 수정 루프입니다.
다만, 이는 너무 강력하기 때문에 주의가 필요합니다. 테스트 자체가 망가져 있으면 무한히 끝나지 않을 수 있습니다. 도입 초기에는 횟수나 타임아웃 상한을 별도로 두거나, 우선 가벼운 체크(타입 체크만 하는 등)부터 시작하는 것이 안전합니다.
이 부분은 솔직히 정말 중요하므로 독립해서 다루겠습니다.
일반적인 Unix 감각으로는 "에러라면 exit 1이겠지"라고 생각하실 겁니다. 하지만 Claude Code의 훅에서는 차단의 신호는 exit 2뿐입니다.
exit 0 … 판단 없음. 일반적인 흐름으로 진행
exit 2 … 블로킹 에러(Blocking Error). stderr가 Claude에게 돌아가며, 대상 이벤트를 차단함
그 외 (exit 1 포함) … 비블로킹 에러(Non-blocking Error). 경고는 발생하지만, 처리는 그대로 진행됨
즉, 당신이 "위험하니까 멈추고 싶다"라고 생각해서 exit 1을 작성하면, 경고는 뜨지만 AI의 조작은 그대로 통과되어 버립니다. "효과가 있는 줄 알았는데 전혀 작동하지 않는 훅"이 완성되는 것입니다. 이것이 가장 조용하게 사고를 일으키는 지점입니다. 정책을 강제하고 싶을 때는 반드시 exit 2를 사용하세요.
(세부적인 예외로, 워크트리(Worktree) 생성인 WorktreeCreate는 "0이 아니면 무엇이든 중지"와 같이 이벤트마다 차이가 있습니다. 헷갈린다면 공식 문서의 "exit code 2 behavior per event" 표를 보는 것이 가장 확실합니다.)
훅은 강력하기 때문에 설계를 잘못하면 새로운 리스크를 낳습니다. 이 부분은 신중하게 정리하겠습니다.
-
비밀이나 로그를 훅(Hook)을 통해 외부로 유출하지 마세요. AI에게 원인을 분석하게 하고 싶어서 로그나 스택 트레이스 (Stack Trace)를 외부 서비스로 보내는 훅을 작성하고 싶어질 때가 있습니다. 하지만 그 안에 API 키나 개인정보가 섞여 있다면 그것은 유출입니다. 훅에서 외부로 무언가를 보낸다면 반드시 마스킹 (Masking)을 거쳐야 합니다. 원칙적으로 비밀은 '외부로 나간다 = 유출된다'라고 생각하세요.
-
비가역적 작업은 훅을 통해 인간 승인 게이트(Human Approval Gate)로 만드세요. 삭제,
push, 배포, 과금처럼 '되돌릴 수 없는 작업'은 AI의 판단만으로 통과시키지 마세요.PreToolUse에서 감지하여permissionDecision: "ask"로 설정하거나, 혹은deny하여 인간의 수작업으로 넘기세요. 되돌릴 수 있는 작업은 자동으로, 되돌릴 수 없는 작업은 인간이 담당합니다. 여기서 반드시 선을 그어야 합니다. -
훅 스크립트 자체를 리뷰 대상으로 삼으세요. 훅은 매번 자동으로 실행되므로 영향력이 매우 큽니다. 따라서 스크립트의 추가 및 변경은 일반적인 코드와 마찬가지로 PR (Pull Request)을 통해 리뷰해야 합니다. '편리하니까'라는 이유로 검증 없이 추가해서는 안 됩니다.
이 세 가지를 지킨다면 훅은 '안심하고 AI에게 맡기기 위한 울타리'가 될 것이며, '새로운 구멍'이 되지 않을 것입니다.
훅 설계는 AI와 대화하며 아이디어를 짜는 것(Wall-hitting)이 빠릅니다. 단, 최종 판단은 인간이 해야 합니다.
1. 정책 점검 (Policy Inventory) 프롬프트
당신은 CI/보안 설계자입니다. 다음 리포지토리에서 AI 코딩 에이전트가
"절대로 해서는 안 되는 작업"과 "접근해서는 안 되는 파일"을 분류해 주세요.
- 출력은 표 형식으로. 열은 "대상 (명령어 또는 파일 패턴) / 리스크 / 권장 액션 (deny / ask / 자동 수정)".
...
2. 훅 스크립트 생성 리뷰 프롬프트
다음 요구사항을 만족하는 PreToolUse 훅을 bash로 작성해 주세요. 작성한 후에 스스로 리뷰하여
"위험한 간과 사항"을 3가지 제시해 주세요.
요구사항: Bash 도구 실행 전에, 명령어에 rm -rf / git push --force / 외부 스크립트의
...
3. exit code 설계 리뷰 프롬프트
다음 훅 스크립트를 리뷰해 주세요. 관점은 "차단해야 하는데 그대로 통과되는지",
"exit 0과 exit 2의 사용 구분이 올바른지", "JSON 출력 방식과 exit 2 방식을 혼용하고 있지 않은지"의 3가지입니다.
문제가 있다면 수정 버전과, 그 수정으로 무엇이 바뀌는지 한 줄씩 설명해 주세요.
...
어떤 프롬프트든 마지막에 "이것으로 결정한다"라고 정하는 것은 인간입니다. AI는 조사 담당자이자 초안 작성자일 뿐입니다. 파수꾼의 규칙을 승인하는 것은 바로 당신 자신입니다.
처음부터 4개를 넣을 필요는 없습니다. 하나부터 시작하세요.
.claude/hooks/폴더를 만들고, 레시피 1(위험 명령어 차단)만 배치합니다.chmod +x를 잊지 마세요..claude/settings.json에PreToolUse×Bash매처(Matcher)로 등록하고,jq가 설치되어 있는지 확인합니다.- 일부러 "
rm -rf ./tmp를 실행해줘"라고 AI에게 요청하여, 제대로 차단되는지 동작을 확인합니다. - 효과가 있다면
.claude/settings.json을 커밋하여 팀에 배포합니다. 그 후 포맷팅 → 보호 파일 → 테스트 순으로 하나씩 추가해 나갑니다.
훅이란 파고들수록 "미래의 자신과, 팀과, 다음 AI 세션에 남기는 메시지"와 같습니다.
오늘 당신이 훅 하나를 작성해 두면, 내일의 당신은 한밤중에 AI가 rm -rf를 입력하려 해도 훅이 묵묵히 이를 막아주는 환경에서 일할 수 있습니다. 포맷팅도 알아서 적용되고, 보호 파일도 지켜지며, 테스트가 실패한 상태에서 "완료했습니다"라는 말을 듣지 않게 됩니다. 그것은 바로 내일의 자신이 "고마워요"라고 말하게 만드는 시스템이라고 생각합니다.
게다가 이 파수꾼들은 한 번 작성해 두면 프로젝트가 지속되는 한 매일 계속해서 일해 줍니다. 일회성 지시가 아니라, 쌓여가는 자산 (Code as Capital) 입니다. 무엇을 차단할지에 대한 의도 (What / Why)는 인간이 결정하고, 그것을 매번 실행하는 수고 (How)는 셸과 AI에게 맡깁니다. 이 분담을 통해 안심하고 AI에게 고삐를 넘겨줄 수 있게 됩니다.
부탁은 깨질 수 있습니다. 하지만 시스템은 당신이 작성한 그대로 계속해서 지켜줄 것입니다.
생성형 AI 활용 엔지니어 & 세 아이의 아빠. AI × 개발의 실전 지식을 매일 발신하고 있습니다 → X: @akira_papa_AI
- Claude Code Hooks 레퍼런스 (이벤트 목록 · 설정 스키마 · exit code · JSON 출력): https://code.claude.com/docs/en/hooks
- Claude Code Hooks 가이드 (퀵 스타트 · 예시): https://code.claude.com/docs/en/hooks-guide
- Claude Code 설정 (settings.json의 위치와 범위): https://code.claude.com/docs/en/settings
- Git 공식 githooks (pre-commit 등의 제2계층): https://git-scm.com/docs/githooks
- pre-commit 프레임워크: https://pre-commit.com/
- jq 매뉴얼: https://jqlang.github.io/jq/manual/
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기