
Claude Code의 PreToolUse 훅 활용 가이드——AI의 조작을 「실행되기 전에」 검사하기
요약
Claude Code의 PreToolUse 훅을 활용하여 AI의 도구 사용 직전에 조작을 검사하고 제어하는 방법을 설명합니다. 위험한 명령어 실행을 차단하거나 특정 파일을 보호하는 가드레일을 설정하는 실무적인 가이드를 제공합니다.
핵심 포인트
- PreToolUse 훅을 통해 AI 조작 실행 전 스크립트 개입 가능
- 위험한 명령어(rm -rf 등) 및 중요 파일 편집 차단 가능
- 거부 사유를 Claude에게 전달하여 AI가 스스로 대안을 찾도록 유도
- settings.json 설정을 통한 맞춤형 가드레일 구축 방법 안내
Claude Code는 정말 일을 잘해줍니다. 파일을 수정하고, 명령어를 입력하고, 테스트까지 돌려줍니다. 든든한 파트너입니다.
하지만 함께 일하다 보면 이런 생각이 드는 순간이 있습니다.
"그 조작, 실행하기 전에 한 번 보여줬으면 좋았을 텐데"
인간이라면 순간적으로 손이 멈출 조작——rm -rf,
git push --force,
중요한 기록 파일에 대한 직접 편집——을, AI는 선의로 생각하며 망설임 없이 실행합니다. "이 파일은 건드리지 마세요"라고 부탁해도, 대화가 길어지면 잊어버립니다. 악의는 없습니다. 다만, 말로 하는 약속은 AI 상대로는 오래가지 않습니다.
여기서 사용할 수 있는 것이 Claude Code의 **PreToolUse 훅 (Hook)**입니다. 훅이란 정해진 타이밍에 직접 만든 처리를 끼워 넣을 수 있는 메커니즘을 말합니다. 이름은 **Pre(〜전) + Tool Use(도구 사용)**로, Claude Code의 AI는 모든 조작을 "도구 (Tool)"를 통해 실행하기 때문에, "도구를 사용하기 전" = 조작이 실행되기 직전을 가리킵니다. 여기에 직접 만든 스크립트를 개입시켜 "통과·중단·인간에게 확인 요청"을 기계적으로 판정할 수 있는 것입니다.
이 기사에서는 제가 실제로 운용하고 있는 훅을 바탕으로,
- 위험한 명령어를 실행 전에 중단하기
- "직접 편집 금지" 파일을 보호하기
- 상황에 따라 가드 (Guard)의 ON/OFF를 전환하기
라는 3가지 실례와, 실제 운용하며 알게 된 설계의 팁을 정리합니다. 내일, 당신의 Claude Code에 첫 번째 가드를 설치하는 것이 목표입니다.
Claude Code는 파일 편집도 명령어 실행도 모두 "도구 (Tool)"라고 불리는 부품을 통해 수행합니다 (Bash・Write・Edit・Read 등).
PreToolUse 훅은 그 도구가 실행되기 직전에 발화합니다.
Claude 「이 명령어를 실행하려고 합니다」
↓
PreToolUse 훅 기동 (Claude Code가 무엇을 하려고 하는지 전달됨)
...
포인트는 ★의 행입니다. 거부 이유는 Claude 본인에게 전달됩니다. Claude는 그것을 읽고 다른 방법을 다시 생각합니다. 즉, 단순한 통행 금지가 아니라, "이 앞은 공사 중입니다. 우회로는 이쪽입니다"라는 표지판으로서 기능합니다.
설정은 settings.json (프로젝트라면 .claude/settings.json, 모든 프로젝트 공통이라면 ~/.claude/settings.json)에 작성합니다.
{
"hooks": {
"PreToolUse": [
...
]
}
}
matcher:어떤 도구에서 발화시킬 것인가. 도구 이름의 완전 일치로, "Edit|Write"와 같이 여러 개를 지정할 수도 있습니다 -
command:기동할 스크립트 (당신이 작성하는 판정 요원입니다) -
timeout:초 단위 지정. ⚠생략하면 기본값은 600초입니다 (후술합니다)
훅이 발화하면, Claude Code는 "지금부터 실행하려는 조작의 내용"을 JSON으로 정리하여 당신의 스크립트 표준 입력 (stdin)으로 보냅니다. 발신자가 Claude Code, 수신자가 당신의 스크립트라는 방향입니다. 전달되는 JSON은 다음과 같은 형태입니다 (발췌).
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
...
}
tool_input의 내용은 도구마다 다릅니다 (Bash라면 command, Write라면 file_path와 content와 같은 식입니다).
JSON을 받은 스크립트의 업무는 내용을 검사하여 "실행해도 되는지"의 판정을 Claude Code에 반환하는 것입니다. "실행시키지 않겠다"라고 반환하면, Claude Code는 해당 도구 실행 (명령어 실행이나 파일 편집)을 중단합니다. 이것이 이 기사에서 말하는 "중단"입니다.
그 판정의 반환 방식에는 2가지가 있습니다.
| 방식 | 방법 | 적합한 상황 |
|---|---|---|
| 간편함 | exit 2로 종료하고, 이유를 stderr로 전달 | 어쨌든 중단하고 싶을 때 |
| 정중함 | exit 0으로 종료하고, 판정을 JSON으로 stdout으로 전달 | 이유나 대체 수단까지 전달하고 싶을 때 |
차례대로 실례를 통해 살펴보겠습니다.
먼저 가장 심플한 형태부터. rm -rf와 git push --force를 포함하는 명령어를 거부합니다.
.claude/hooks/bash_guard.sh
:
#!/bin/bash
payload=$(cat)
cmd=$(echo "$payload" | jq -r '.tool_input.command // """)
...
exit 2로 종료하면 해당 도구 실행은 차단되며, stderr에 작성한 문장이 그대로 Claude에게 피드백으로 전달됩니다. Claude 입장에서는 "실행하려고 했으나 거부되었고, 이유가 돌아왔다"는 상태가 됩니다. 그러면 "그럼 rm -rf를 그만두고, 대상을 확인하면서 하나씩 삭제하겠습니다"와 같이 다시 시도하게 됩니다.
단 수십 줄에 불과하지만, "사고가 나지 않기를" 바라는 기도가 "사고가 나지 않는 메커니즘"으로 바뀌는 순간입니다.
제 환경에는 "AI가 직접 편집해서는 안 되는 파일"이 있습니다. 작업 기록 파일인데, 정해진 스크립트를 통해서만 내용을 추가하기를 원합니다. 말로 "직접 쓰지 마"라고 부탁하던 시절에는, 잊을 만하면 사고를 치곤 했습니다.
이처럼 "제지하면서도 올바른 길을 알려주고 싶은" 상황에서는 JSON 방식이 적합합니다. 사례 1의 bash_guard.sh와는 **별개의 독립된 훅 (Hook)**으로 만듭니다. settings.json의 PreToolUse 배열에 엔트리를 하나 더 추가하세요 (훅은 여러 개를 나열할 수 있습니다).
{
"matcher": "Write|Edit",
"hooks": [
...
판정 스크립트 본체인 .claude/hooks/write_guard.py
:
#!/usr/bin/env python3
import json
import sys
...
permissionDecision에 지정할 수 있는 주요 값은 3가지입니다.
| 값 | 의미 |
|---|---|
"deny" | 실행시키지 않음. 이유가 Claude에게 전달됨 |
"allow" | 허가 확인 프롬프트를 생략하고 통과시킴 |
"ask" | 통상적인 확인 프롬프트를 띄워 인간이 판단하게 함 |
여기서 실운용상 가장 효과적인 것이 additionalContext입니다. 이 칸에 작성한 문장은 Claude의 문맥 (Context)에 확실히 포함됩니다. "차단"만 하면 Claude는 어찌할 바를 모르거나 다른 우회로를 찾기 시작하지만, "대신 이 스크립트를 사용해"라고 적어두면 순순히 그쪽으로 넘어갑니다. 통행금지 표지판에는 우회로 지도를 첨부하는 것과 같습니다. 이것만으로도 거부된 이후 AI의 움직임이 눈에 띄게 달라집니다.
세 번째는 약간의 응용입니다. 저는 외부에서 iPhone으로 Claude Code를 조작할 때가 있는데, 작은 화면에서는 오조작이 두렵습니다. 외부 모드일 때만 특정 조작을 차단하거나 확인 절차를 거치게 하고 싶습니다.
훅 자체는 상설로 두되, 플래그 파일의 유무로 동작을 전환합니다. 이것도 사례 1, 2와는 별개의 독립된 훅입니다. settings.json에는 보호하고 싶은 도구를 matcher에 나열한 엔트리(예: `
대책은 전단(前段)을 쉘 스크립트(shell script)로 작성하여, 외부 명령어를 단 하나도 호출하지 않고 대상 여부를 필터링하는 것입니다.
payload=$(cat)
case "$payload" in
*decision_log.md*) ;; # 대상일 때만 아래로 진행
...
case는 쉘의 내장 기능(built-in)이므로 프로세스가 하나도 생성되지 않습니다. 이 방식으로 구성한 후에는 대상이 아닌 편집의 경우 1자리 밀리초(ms) 단위로 통과하게 되었습니다.
참고로 최근 버전에는 if라는 필드가 있어, "if": "Edit(*.md)"와 같이 인자(argument) 수준의 필터링을 설정 측에 작성할 수도 있습니다. 최신 환경이라면 이 방법도 검토해 보시기 바랍니다.
생략 시 기본값은 600초입니다. 판정 스크립트가 멈추면 파일 편집 자체가 10분 동안 중단됩니다. 저는 일괄적으로 10초로 설정해 두고 있습니다.
판정 스크립트 자체에 버그가 있어 '대상인지 아닌지 알 수 없는' 상태가 되었을 때, 거부(deny) 쪽으로 처리하면 아무것도 편집할 수 없게 됩니다. 판정할 수 없을 때는 통과시키되 로그에는 남기는 방식이 안전합니다. 규칙을 지키게 하려는 메커니즘이 본업을 중단시키는 것이 가장 슬픈 사고입니다.
JSON 출력에는 유사한 필드가 여러 개 있으며, 전달되는 목적지가 다릅니다.
| 필드 | 전달 대상 |
|---|---|
additionalContext | Claude의 문맥 (행동을 변화시키고 싶은 문구는 여기에 작성) |
permissionDecisionReason | 거부 이유로서 화면이나 기록에 표시됨 |
systemMessage | 인간의 화면에 표시되는 경고 |
저는 처음에 이 부분을 혼동하여, 정성스럽게 이유를 작성했음에도 AI가 똑같은 실수를 반복하는 상황을 만들었습니다. AI의 행동을 변화시키고 싶은 문구는 additionalContext에 넣는다고 기억해 두면 헤매지 않습니다.
덧붙여, 훅(hook)은 Claude Code가 백그라운드에서 실행하는 서브 에이전트(sub-agent, 병렬로 작동하는 또 다른 작은 AI)의 조작에도 동일하게 적용됩니다. "본체는 멈췄는데 부하 직원은 그냥 지나간다"와 같은 우회로는 없었습니다.
- PreToolUse 훅을 사용하면 AI의 조작을 실행되기 전에 검사할 수 있다. 간편하게 중단하려면
exit 2+stderr, 정중하게 유도하려면 JSON +additionalContext를 사용한다. - 전단은 쉘 내장 기능으로 즉시 탈출,
timeout은 명시, 망설여지면 통과시키는 것이 실무 운영의 삼종신기(三種の神器)다.
AI에게 계속해서 "조심해"라고 말하는 생활에서, "조심하지 않아도 사고가 나지 않는" 환경 구축으로. 첫 번째 가드는 이 기사의 사례 1을 복사하면 10분 만에 작동합니다. 꼭 여러분의 Claude Code에 적용해 보세요.
- 공식 문서 (Hooks 가이드): https://code.claude.com/docs/en/hooks-guide
- 공식 문서 (Hooks 레퍼런스): https://code.claude.com/docs/en/hooks
- 애초에 왜 "말로 해서 지키게 하는 것"을 그만두었는지에 대한 전말은 Zenn에 작성했습니다: https://zenn.dev/triponte/articles/claude-code-hook-enforce-rules
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기