어느 날 밤, Claude에게 '오래된 빌드 캐시만 지워줘'라고 요청한 후, auto-accept 상태로 위험한 삭제 명령이 실행되려 했습니다. 승인을 모두 수동으로 되돌리자, 이번에는 다음 날 아침까지 승인 대기 상태에서 멈추게 됩니다. 단순히 permissions의 allow/deny만으로는 '이 패턴만 기계적으로 막고, 나머지는 전부 자동화'라는 경계를 설정할 수 없었습니다. hooks를 이용해 이 경계를 재설계한 결과, 위험한 작업만 차단하면서도 야간 배치(batch)를 망가뜨리지 않고 자동화를 할 수 있게 되었습니다.
이 글에서는 hooks에 맡길지/맡기지 않을지에 대한 선정 기준을 표로 정리하고, PreToolUse 가드의 복사-붙여넣기 완벽 형태(settings.json과 Python 1 파일 및 표준 라이브러리만 사용)와 설계가 잘못되었을 때 망가지는 3가지 실패 패턴까지 다룹니다.
| 판단 축 | hooks에 적합 (자동화할 부분) | hooks 외부 (permissions 또는 인간의 판단) |
|---|
| 판단의 성질 | 문자열 패턴으로 기계적으로 결정됨 | 문맥을 읽고 결정함 |
| ... | | |
왼쪽 열만 hooks에 배치합니다. 오른쪽 열을 hooks에 넣으면 지연, 무한 루프, 승인지옥 중 하나가 반드시 발생합니다 (후술할 실패 1~3과 관련됩니다).
- permissions: 상시적인 허용/거부입니다. 판단은 사람이 한 번만 수행합니다 (allow/deny 목록).
- hooks: 이벤트가 발생하는 순간 외부 명령이 실행됩니다. 판단은 스크립트가 매번 수행합니다.
- 경계:
'절대 통과시키지 않을 목록'은 permissions.deny에 배치해야 합니다. 옵션이나 경로의 조합에 따라 달라지는 '조건부 차단'은 hooks에 배치해야 합니다.
이 구분을 하지 않고 hooks만으로 모든 것을 처리하려고 하면, 거부 목록이 정규 표현식 안에 파묻혀 나중에 읽을 수 없는 설정이 됩니다.
- 설정 파일:
.claude/settings.json
(리포지토리에서 공유)
그리고 .claude/settings.local.json
(개인용).
- 구조: 이벤트 이름 → matcher (정규 표현식으로 툴 이름을 제한) → hooks 배열 (type: command).
- hook 명령어는
stdin에 JSON을 받습니다 (tool_name, tool_input, cwd 등).
- 종료 코드:
0 = 계속 진행 / 2 = 차단. 2일 경우 stderr가 그대로 모델에게 반환되어 Claude는 멈춘 이유와 대체안을 스스로 도출합니다.
- timeout(초)를 지정할 수 있습니다. 미지정된 긴 처리는 야간 배치에서 문제가 될 수 있으므로 반드시 명시해야 합니다.
- 현재 디렉토리는 실행 방식에 따라 달라지기 때문에, 파일 참조는
$CLAUDE_PROJECT_DIR
로 해결합니다.
| # | 대상 | 이벤트와 matcher | 이유 |
|---|
| 1 | 위험한 Bash 명령어 차단 | PreToolUse × Bash | 패턴 일치로 기계적으로 결정됨. 삭제 실행 후에 감지해도 늦음 |
| ... | | | |
| # | 중단한 것 | 발생할 수 있는 상황 | |
| --- | --- | --- | --- |
| 1 | UserPromptSubmit에서 무거운 전처리 | 프롬프트 제출 때마다 기다려져 체감이 항상 느려짐 | |
| ... | | | |
3번이 까다롭습니다. 가드는 '멈추는' 역할까지만 하고, '움직이는' 코드를 hooks에 작성하지 않습니다. 모델의 움직이는 방향을 바꾸고 싶다면, 프롬프트와 템플릿 쪽에서 설계해야 합니다.
먼저 .claude/settings.json
(새로 만드는 경우 이대로 사용합니다. 기존 파일인 경우 hooks 키를 통합하세요):
{
"permissions": {
"deny": [
...
다음으로 .claude/hooks/guard.py
(표준 라이브러리만 사용한 1파일 완전 형태):
#!/usr/bin/env python3
# PreToolUse 가드: 위험한 패턴은 exit 2, 그 외는 exit 0
import json, re, sys
...
.claude/hooks/format_after_write.sh
(PostToolUse 측. 정리는 가벼우므로 허용합니다):
#!/bin/sh
# 편집 후 정리. 정리에 실패해도 멈추지 않음 (exit 0)
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
...
chmod +x .claude/hooks/guard.py .claude/hooks/format_after_write.sh
- Claude Code 세션에서
/hooks를 열고, PreToolUse와 PostToolUse가 목록에 표시되는지 확인한다. Bash로 git reset --hard를 실행해 본다. "BLOCK: dangerous Bash pattern"이 모델에게 반환되고 대체 방침을 제시하는 것을 확인한다. 또한, rm /tmp/ok_test.txt와 같은 제외 패턴도 통과시켜서 너무 많이 거부하고 있지는 않은지 확인한다.
| # | 잘못된 설계 | 발생한 일 | 수정 방법 |
|---|
| 1 | 가드를 PostToolUse에 배치함 | 삭제가 실행된 후에 감지된다. 대상이 사라져 복구할 수 없었다 | 삭제/강제 덮어쓰기는 반드시 PreToolUse에서 막아야 한다 |
| ... | | | |
1번이 가장 복구하기 어렵습니다. 감지 시점이 실행 후인 대상(삭제, 강제 덮어쓰기, push)은 반드시 PreToolUse에서 막아주세요.
도입 전에 이 6가지를 확인합니다. 모두 "예"라면 hooks에 적합합니다:
| # | 확인 항목 |
|---|
| 1 | 판단을 문자열 패턴으로 작성할 수 있는가 (모델에게 설명이 필요 없는가) |
| ... | |
- 본 기사의 완전한 형태를 그대로 복사하여
guard.py 파일만 넣는다 (settings.json 통합, chmod, /hooks 확인, git reset --hard에서의 동작 확인까지 10분 소요).
- 일주일 동안 운영하며 오탐(誤検知)이 없다면, PostToolUse의 정형화(整形)를 추가한다. 품질 검사는 hooks에 작성하지 않고 CI로 내보낸다.
- 분리표(棲み分け表)로 돌아가 확인한다: 절대 거부는
permissions.deny에 남겨두고, 조건부 차단만 hooks의 정규식에 추가하고 끝낸다.
순서가 중요합니다. deny 목록 → PreToolUse 가드 → PostToolUse 정형화 순으로 추가해야 합니다. 반대로 하면 "막히지 않은 삭제"와 "정형화의 무한 수정"이 동시에 발생하여 원인을 알 수 없게 됩니다.
본 기사와 같은 구현 패턴을 가로질러 정리한 치트시트와, Obsidian・Notion 템플릿 모음을 무료 배포하고 있습니다. 8권 분량의 내용에는 "무엇을 어떤 도구로 할지" 판단 차트도 포함되어 있습니다.
📗 MCP 실전 입문 — AI 에이전트를 확장하는 Model Context Protocol (Kindle・Kindle Unlimited 무제한 읽기 대상)