Claude Code 훅으로 AI 에이전트의 위험한 명령 막기: 실제로 쓰는 막기 규칙 5가지
Claude Code의 PreToolUse 훅으로 AI가 명령을 실행하기 직전에 검사하는 파이썬 스크립트를 만들고, 잠금 파일 지우기·timeout 감싸기·초기 설정 실행·잠금 없는 Blender 실행·비밀 파일 읽기 다섯 가지를 막는 법을 시험 결과와 함께 따라 합니다.
한국어English
기준: Claude Code 공식 문서 "Hooks reference"와 "Automate actions with hooks", Windows 11의 Python 3.14. 2026년 11월 5일에 확인했습니다. 그림 1·2는 이 글의 예제 스크립트를 실제로 돌린 출력 글자를 터미널 모양으로 옮긴 그림이고, 그림 3은 공식 문서 화면입니다.
Claude Code 같은 AI 에이전트에게 일을 맡기면, 에이전트가 스스로 터미널 명령을 실행합니다. 편하지만 걱정도 생깁니다. "이 파일은 지우지 마", "이 프로그램은 이렇게만 실행해"라고 안내 문서에 적어 두어도, 긴 작업 중에 가끔 잊습니다. 에이전트를 여러 개 함께 돌리면 더 그렇습니다.
**훅(hook)**은 Claude Code가 정해진 순간에 자동으로 실행하는 여러분의 명령입니다. 그중 PreToolUse 훅은 에이전트가 도구(명령 실행, 파일 읽기 등)를 쓰기 바로 직전에 실행됩니다. 건물 출입구의 경비원과 같습니다. 들어가려는 사람(명령)을 하나씩 보고, 출입 금지 목록에 있으면 돌려보냅니다. 에이전트가 규칙을 잊어도 경비원은 잊지 않습니다.
이 글은 Blender 렌더링을 여러 AI 에이전트에게 나눠 맡기는 작업 환경에서 실제로 쓰는 막기 규칙 다섯 가지를, 누구나 따라 만들 수 있게 줄인 예제로 옮깁니다.
이 글을 마치면 아래를 할 수 있습니다.
- PreToolUse 훅이 언제 실행되고 무엇을 받는지 구분합니다.
- 명령을 검사해 막는 파이썬 훅 스크립트를 만듭니다.
- 훅을
.claude/settings.json에 등록합니다. - 막혀야 할 명령과 통과해야 할 명령으로 훅을 먼저 시험합니다.
- 훅으로 막을 수 없는 것을 알아 둡니다.
시작 전에: Claude Code가 설치되어 있어야 합니다. 설치는 Claude Code 입문에 있습니다. 이 글의 예제는 파이썬으로 씁니다. Windows는
py, macOS·Linux는python3으로 실행합니다.
1. PreToolUse 훅이 하는 일
공식 문서는 훅을 "Claude Code가 작업 흐름의 정해진 때에 실행하는, 사용자가 정한 셸 명령"이라고 안내합니다(그림 3). 정해진 때에는 여러 가지가 있고, 이 글에서 쓰는 것은 PreToolUse 하나입니다.
그림 3. Claude Code 공식 문서의 훅 안내 첫 화면 (공식 문서 화면 캡처)
흐름은 이렇습니다.
- 에이전트가 명령을 실행하려고 합니다. 예:
rm -f locks/blender.lock - Claude Code가 그 내용을 JSON으로 만들어 훅 스크립트의 표준 입력(stdin)으로 넘깁니다.
tool_name에는 도구 이름(Bash등),tool_input에는 명령이나 파일 경로가 들어 있습니다. - 훅이 아무것도 출력하지 않고 끝나면, 명령은 평소의 권한 확인 절차로 넘어갑니다.
- 훅이 "거부(deny)"를 출력하면 명령은 실행되지 않고, 훅이 적은 이유가 에이전트에게 전달됩니다.
4번이 중요합니다. 에이전트는 이유를 읽고 다른 방법을 찾습니다. 그래서 이유에는 "하지 마"만이 아니라 **"대신 이렇게 하라"**를 함께 적습니다.
2. 막는 규칙 5가지와 까닭
아래 다섯 가지는 Blender를 여러 에이전트가 함께 쓰는 환경에서 실제로 막고 있는 것입니다. 동시 실행과 애드온 설치 규칙은 실제로 문제가 난 뒤에 생겼고, 나머지는 같은 잠금과 비밀 정보를 지키려고 더한 것입니다.
| # | 막는 것 | 까닭 | 대신 할 일 |
|---|---|---|---|
| 1 | 잠금 파일 지우기 | 잠금은 "지금 Blender를 쓰는 중"이라는 표시입니다. 지우면 다른 에이전트가 같은 그래픽카드에 Blender를 더 띄웁니다 | 잠금 스크립트가 주인 없는 잠금을 스스로 풀게 둠 |
| 2 | Blender를 timeout으로 감싸기 | 시간이 다 되어 강제로 끝나면 잠금이 남습니다 | 오래 걸리는 일은 백그라운드로 실행 |
| 3 | 애드온 설치·환경설정 변경에 --factory-startup 쓰기 | 이 옵션은 사용자의 시작 파일을 읽지 않고 켭니다. 평소 설정과 다른 상태에서 설치·변경을 하게 됩니다 | 바꾸기 전에 환경설정 파일을 백업하고(잘못 바뀌어도 되돌릴 수 있게) 옵션 없이 실행 |
| 4 | 잠금 스크립트 없이 Blender를 화면 없이 실행 | 잠금을 거치지 않으면 동시에 몇 개가 뜨는지 아무도 모릅니다 | run_blender.sh 같은 잠금 스크립트로만 실행 |
| 5 | 비밀 파일(.env, api.txt) 읽기·복사 | API 키가 대화 기록에 남습니다 | 키는 프로그램이 실행 중에 스스로 읽게 함 |
1·2·4번의 바탕에는 실제로 잰 숫자가 있습니다. 한 번은 Blender를 제한 없이 10개까지 함께 띄웠더니, 혼자 돌리면 50초 걸리던 렌더링이 33분 걸렸습니다. 4개로 줄여도 한 건이 20분 안팎이었습니다. 그 뒤로 Blender는 잠금 스크립트를 거쳐 기본 2개까지만 함께 돌게 했고, 위 규칙은 그 잠금이 깨지지 않게 지키는 장치입니다.
**잠금(lock)**은 화장실 문의 "사용 중" 표시와 같습니다. 표시를 억지로 떼면 두 사람이 한꺼번에 들어가게 됩니다. 반대로 나오면서 표시를 안 떼면(강제 종료) 아무도 못 들어갑니다. 1·2번 규칙이 이 두 경우를 막습니다.
표와 예제의 run_blender.sh는 동시에 뜬 Blender 수를 세고 잠금을 거는 실행 스크립트를 가리키는 예시 이름입니다. 여러분에게 그런 스크립트가 없으면 2·4번 규칙은 빼고 쓰세요.
3. 훅 스크립트 만들기
시작 상태: 빈 프로젝트 폴더에서 시작합니다. 폴더 안에 .claude/hooks/ 폴더를 만듭니다.
따라 하기: .claude/hooks/guard.py 파일을 만들고 아래 내용을 넣습니다. 규칙 하나는 "찾을 글자 모양(정규식)"과 "거부 이유" 한 쌍입니다.
import json
import re
import sys
SEP = r"[^\n;|&]*" # 명령 한 토막 안에서만 찾는다
RULES = [
# 1. 잠금 파일 지우기
(re.compile(r"\b(rm|del|Remove-Item)\b" + SEP + r"(locks[\\/]|\.lock\b)", re.I),
"Do not delete lock files. The lock script frees stale locks by itself."),
# 2. Blender를 timeout으로 감싸기
(re.compile(r"(^|[;&|(]\s*)timeout\s" + SEP + r"(blender|run_blender\.sh)", re.I),
"Do not wrap Blender in timeout. Use a background run instead."),
# 3. --factory-startup과 애드온·환경설정 변경을 함께
(re.compile(r"--factory-startup" + SEP + r"(addon|install|userpref|preferences)"
r"|(addon|install|userpref|preferences)" + SEP + r"--factory-startup", re.I),
"Do not use --factory-startup when installing add-ons or changing preferences."),
# 4. 잠금 스크립트 없이 Blender를 화면 없이 실행
(re.compile(r"(^|[;&|(]\s*|\s)[\"']?([\w:.\\/ -]*[\\/])?blender(\.exe)?[\"']?\s+(-b\b|--background\b|-P\b|--python\b)", re.I),
"Run Blender only through run_blender.sh."),
# 5. 셸 명령으로 비밀 파일 읽기·복사
(re.compile(r"\b(cat|type|head|tail|less|more|Get-Content|cp|copy|base64)\b" + SEP
+ r"(api\.txt|\.env(\.(?!example\b)[\w.-]+)?)(?![\w.-])", re.I),
"Do not read or copy secret files (.env, api.txt)."),
]
SECRET_PATH = re.compile(r"(^|[\\/])(api\.txt|\.env(\.(?!example\b)[\w.-]+)?)$", re.I)
def deny(reason):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "[guard] " + reason,
}
}))
sys.exit(0)
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
tool = data.get("tool_name", "")
inp = data.get("tool_input") or {}
if tool in ("Bash", "PowerShell"):
cmd = inp.get("command", "") or ""
for pattern, reason in RULES:
if pattern.search(cmd):
if reason.startswith("Run Blender only") and "run_blender.sh" in cmd:
continue
deny(reason)
elif tool == "Read":
if SECRET_PATH.search(inp.get("file_path", "") or ""):
deny("Do not read secret files (.env, api.txt).")
if __name__ == "__main__":
main()
몇 군데만 짚습니다.
SEP는;,|,&, 줄바꿈을 넘지 않게 합니다.ls locks; rm notes.txt처럼 앞 토막의 "locks"와 뒤 토막의 "rm"을 한 명령으로 잘못 엮지 않으려는 것입니다.- 5번 규칙은
.env.example을 빼 둡니다. 비밀이 아닌 예시 파일이라 읽어도 됩니다. 막을 필요가 없는 것까지 막으면 에이전트가 일을 못 합니다. - 거부할 때는 JSON을 출력하고 종료 코드 0으로 끝냅니다. JSON 대신 종료 코드 2로 끝내고 이유를 표준 오류(stderr)에 쓰는 방법도 있습니다. 둘 중 하나만 쓰세요.
- 입력을 읽지 못하면 아무것도 하지 않고 끝납니다. 이 선택의 약점은 7절에서 다룹니다.
4. settings.json에 등록하기
따라 하기: 프로젝트 폴더의 .claude/settings.json에 아래를 넣습니다. 파일이 이미 있으면 "hooks" 부분만 더합니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell|Read",
"hooks": [
{
"type": "command",
"command": "py \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.py\"",
"timeout": 10
}
]
}
]
}
}
| 칸 | 뜻 |
|---|---|
matcher | 어느 도구에 훅을 걸지. `Bash |
command | 실행할 명령. $CLAUDE_PROJECT_DIR은 세션을 시작한 프로젝트 폴더. macOS·Linux는 py 대신 python3 |
timeout | 몇 초 안에 끝나야 하는지. 정규식 검사는 금방 끝나므로 10초면 넉넉함 |
설정 파일은 세 자리에 둘 수 있습니다. 함께 일하는 사람과 같은 규칙을 쓰려면 프로젝트의 .claude/settings.json에 둡니다.
| 파일 | 걸리는 범위 | 공유 |
|---|---|---|
~/.claude/settings.json | 내 모든 프로젝트 | 안 됨 |
.claude/settings.json | 이 프로젝트 | 저장소에 함께 올릴 수 있음 |
.claude/settings.local.json | 이 프로젝트, 나만 | 안 됨 |
이렇게 설정에 넣은 훅은 서브에이전트(일을 나눠 맡은 보조 에이전트)가 도구를 쓸 때도 똑같이 실행됩니다. 에이전트를 여러 개 돌리는 환경에서 훅이 쓸모 있는 까닭입니다.
5. 먼저 시험하기: 막을 것과 통과시킬 것
등록한 훅을 실제 작업에 맡기기 전에 시험부터 합니다. 잘못 만든 훅은 두 방향으로 틀립니다. 막아야 할 것을 통과시키거나, 멀쩡한 일을 막습니다. 둘 다 시험 목록으로 잡습니다.
따라 하기: guard.py와 같은 폴더(.claude/hooks/)에 test_guard.py를 만들고 아래 내용을 그대로 넣습니다.
"""Self-test for guard.py: py test_guard.py"""
import json
import pathlib
import subprocess
import sys
GUARD = pathlib.Path(__file__).with_name("guard.py")
CASES = [
# (tool, input, should_deny)
("Bash", {"command": "ls renders"}, False),
("Bash", {"command": "bash run_blender.sh -b scene.blend -P build.py"}, False),
("Bash", {"command": "cat .env.example"}, False),
("Bash", {"command": "rm -f locks/blender.lock"}, True),
("Bash", {"command": "timeout 600 bash run_blender.sh -b scene.blend"}, True),
("Bash", {"command": "bash run_blender.sh -b --factory-startup --python install_addon.py"}, True),
("Bash", {"command": "blender -b scene.blend -P build.py"}, True),
("PowerShell", {"command": r'& "C:\Program Files\Blender Foundation\Blender 5.2\blender.exe" --background scene.blend'}, True),
("Bash", {"command": "cat .env.local"}, True),
("Read", {"file_path": r"C:\work\project\api.txt"}, True),
("Read", {"file_path": r"C:\work\project\.env.example"}, False),
]
def run(tool, inp):
payload = json.dumps({"tool_name": tool, "tool_input": inp})
out = subprocess.run([sys.executable, str(GUARD)], input=payload,
capture_output=True, text=True, encoding="utf-8")
return '"deny"' in out.stdout
fails = 0
for tool, inp, want in CASES:
got = run(tool, inp)
mark = "OK " if got == want else "FAIL"
fails += got != want
print(f"{mark} deny={got!s:5} {tool}: {list(inp.values())[0]}")
print("fails:", fails)
sys.exit(1 if fails else 0)
짜임은 세 부분입니다.
CASES: 시험 목록입니다. 한 줄이 (도구 이름, 입력, 막혀야 하는지)입니다. 마지막 칸이True면 막혀야 하고,False면 통과해야 합니다.run(): 한 항목을 Claude Code가 넘기는 것과 같은 꼴의 JSON으로 만들어guard.py의 표준 입력에 넣고 실행합니다. 출력에"deny"가 있으면 막힌 것으로 봅니다.- 마지막 반복문: 항목마다 기대와 결과가 같으면
OK, 다르면FAIL을 찍고, 끝에 틀린 수(fails)를 셉니다.
따라 하기: 터미널에서 .claude/hooks/ 폴더로 옮긴 뒤 py -B test_guard.py를 실행합니다. -B는 파이썬이 캐시 폴더를 만들지 않게 하는 옵션입니다. 그림 2처럼 열한 줄이 모두 OK이고 마지막 줄이 fails: 0이면 됩니다.
그림 2. 예제 훅의 시험 결과. 통과해야 할 4건은 deny=False, 막아야 할 7건은 deny=True, 틀린 것 0건입니다 (실행 출력)
실제 작업 환경의 훅은 이런 시험이 29건이고, 규칙을 고칠 때마다 다시 돌립니다. 새 규칙을 넣을 때는 막혀야 할 예 하나와 막히면 안 되는 비슷한 예 하나를 함께 더합니다. 예를 들어 rm -f locks/blender.lock(막음)을 넣으면, 잠금 스크립트 안에서 timeout이라는 글자를 찾는 grep -rn timeout run_blender.sh(통과) 같은 예도 넣습니다.
6. 막혔을 때 에이전트에게 보이는 것
훅 하나만 따로 실행해 보면 거부 출력을 직접 볼 수 있습니다.
따라 하기: 아래 한 줄을 sample_payload.json으로 저장한 뒤 py -B guard.py < sample_payload.json을 실행합니다. Windows PowerShell은 <를 받지 않으니 cmd /c "py -B guard.py < sample_payload.json"으로 실행합니다.
{"tool_name": "Bash", "tool_input": {"command": "timeout 600 bash run_blender.sh -b scene.blend"}}
그림 1. 2번 규칙에 걸린 명령과 훅의 거부 출력 (Bash 셸에서 실행한 출력. 실제로는 JSON이 한 줄로 나오고, 그림은 보기 좋게 줄을 나눴습니다)
Claude Code 안에서는 이 이유가 에이전트에게 도구 오류처럼 전달됩니다. 이 작업 환경에서는 PreToolUse:Bash hook error: [guard] … 꼴로 보였습니다. 이유 앞에 [guard]를 붙여 두면, 에이전트와 사람 모두 "어떤 장치가 막았는지" 바로 압니다.
이유는 짧게, 대신 할 일까지 씁니다.
| 나쁜 이유 | 좋은 이유 |
|---|---|
| "금지된 명령입니다" | "Blender를 timeout으로 감싸지 마세요. 오래 걸리면 백그라운드로 실행하세요" |
| "보안 위반" | "비밀 파일은 읽지 마세요. 프로그램이 실행 중에 스스로 읽게 두세요" |
7. 훅으로 막을 수 없는 것
훅은 편하지만 만능이 아닙니다. 알아 둘 점이 네 가지 있습니다.
① 글자만 봅니다. 훅은 명령을 실행해 보지 않고 글자 모양만 봅니다. 그래서 엉뚱한 것도 막습니다. 이 글의 예제를 시험하려고 echo로 JSON 한 줄을 넘기는 명령을 실행했더니, 그 JSON 안에 rm -f locks/…라는 글자가 들어 있어서 실제 작업 환경의 훅이 명령 전체를 막았습니다. 시험 입력을 파일로 저장해 넘기는 방법(6절)으로 바꾸어 해결했습니다. 반대로 글자를 바꿔 쓴 명령(변수에 담아 실행하는 등)은 지나갈 수 있습니다.
② 조용히 끝나면 허락이 아닙니다. 훅이 아무 출력 없이 끝나면 평소의 권한 확인으로 넘어갑니다. 훅은 "막는" 쪽에만 쓰고, 허락은 권한 설정이 맡습니다.
③ 훅이 고장 나면 문이 열립니다. PreToolUse 훅이 시간 초과되거나 실패해도 기본으로는 명령을 막지 않습니다. 파이썬이 없거나 경로가 틀리면 모든 명령이 그냥 지나간다는 뜻입니다. 이 글의 예제도 입력을 못 읽으면 조용히 끝납니다. 꼭 막아야 하는 훅이면 "onFailure": "block" 설정을 검토하세요. 공식 문서는 이 설정이 v2.1.295 이후의 Claude Code에서 된다고 적고 있습니다.
④ 확실한 금지는 권한 설정과 함께. 단단한 허용·금지에는 훅보다 권한(permissions) 설정이 알맞습니다. 비밀 파일처럼 절대 열면 안 되는 것은 .claude/settings.json의 "permissions": { "deny": ["Read(**/.env)"] } 같은 금지 목록에도 넣어 두 겹으로 막습니다.
훅은 PreToolUse 말고도 쓸 곳이 있습니다. 예를 들어 사용자가 말을 보낼 때마다 실행되는 UserPromptSubmit 훅은 출력한 글을 에이전트에게 덧붙여 보여 줍니다. 이 작업 환경에서는 대화가 길어져 맥락이 60%, 70%를 넘으면 "새 일을 시작하지 말고 인계 문서를 쓰라"는 알림을 이 훅으로 넣습니다.
연습 문제
- 여러분 프로젝트에서 AI가 절대 하면 안 되는 명령을 세 가지 적어 보세요. 각각 "대신 할 일"도 한 줄씩 적습니다.
- 1번의 첫 명령을 막는 규칙을 3절
RULES에 하나 더해 보세요. test_guard.py에 2번 규칙이 막아야 할 예 하나와, 비슷하지만 통과해야 할 예 하나를 넣고fails: 0이 나오는지 확인합니다.ls locks && rm notes.txt가 막히는지 통과하는지 먼저 짐작해 적어 보고, 시험 목록에 넣어 확인해 보세요.
자주 하는 실수와 해결
| 증상 | 원인 | 해결 |
|---|---|---|
| 훅이 전혀 실행되지 않는다 | matcher의 도구 이름이 틀림, 설정 파일 위치가 다름 | 도구 이름(Bash, Read)과 4절의 파일 위치 확인 |
| 모든 명령이 그냥 지나간다 | 파이썬 명령 이름(py·python3)이나 스크립트 경로가 틀려 훅이 실패함 | 터미널에서 6절처럼 직접 실행해 보기, 필요하면 onFailure |
| 멀쩡한 명령이 막힌다 | 정규식이 너무 넓음, 명령 속 다른 글자에 걸림 | 막히면 안 되는 예를 시험 목록에 넣고 정규식을 좁히기 |
| 에이전트가 같은 명령을 계속 시도한다 | 거부 이유에 대신 할 일이 없음 | 이유에 "대신 …" 한 문장 더하기 |
| 한글 이유가 깨져 보인다 | 출력 인코딩 | 스크립트 처음에 sys.stdout.reconfigure(encoding="utf-8") |
한 줄 정리
규칙마다 "글자 모양 + 대신 할 일" → 시험 목록으로 막을 것·통과시킬 것 확인 → settings.json 등록 → 꼭 막을 것은 권한 설정과 두 겹으로. AI에게 Blender를 맡기기 전에 함께 볼 점검 목록은 아래 첫 글에 있습니다.
함께 읽기
참고 자료
- Claude Code Docs: Hooks reference
- Claude Code Docs: Automate actions with hooks
- Blender 5.2 Manual: Command Line Arguments
- 그림 라이선스: 그림 1·2는 AZTechnology가 이 글의 예제를 실행한 출력입니다. 그림 3은 Claude Code 공식 문서 화면입니다.





