
Claude Code hook을 사용하여 AI 코딩 어시스턴트의 규율을 보강하기 — 개인 운용 설계 패턴 참고
요약
Claude Code의 hook 메커니즘을 활용하여 LLM 코딩 어시스턴트의 개발 워크플로우 규율을 강화하는 설계 패턴을 소개합니다. 메모리 파일의 한계를 극복하기 위해 PreToolUse, PostToolUse 등의 훅을 사용하여 위험한 명령어를 차단하고 에러를 명시적으로 관리하는 방법을 다룹니다.
핵심 포인트
- Claude Code의 hook을 통한 강제적 규율 보강
- 메모리 파일의 주의 사항 한계 극복 방법
- PreToolUse를 활용한 위험한 Bash 명령어 예방
- PostToolUseFailure를 통한 에러 재시도 방지
- 명령어 타임아웃 및 연결 명령어(&&) 사용 제한
LLM 코딩 어시스턴트 (Claude Code 등)에게 dev workflow (개발 워크플로우)를 맡기다 보면, memory file (메모리 파일)의 「주의 사항」만으로는 똑같은 실수를 반복한다는 느낌을 받을 때가 있습니다.
Claude Code에는 Stop / PreToolUse / PostToolUse / PostToolUseFailure hook (훅) 메커니즘이 있으며, 이를 사용하면 「LLM이 잊어버리더라도 hook이 exit 2로 block (차단) + stderr message (표준 에러 메시지)를 흘려보내는」 방식으로 규율을 보강할 수 있습니다.
본고는 제가 개인적으로 운용하고 있는 hook의 설계 패턴 + 일반적으로 유용해 보이는 예시를 정리한 것입니다. 환경이나 취향에 따라 맞고 틀림이 있으므로, 어디까지나 참고 수준으로 읽어주시기 바랍니다.
LLM assistant (LLM 어시스턴트)에게 build (빌드) / deploy (배포) / git push (깃 푸시) 등을 맡기면, 다음과 같은 anti-pattern (안티 패턴)이 발생했습니다:
git push를 한 직후에 end-turn (턴 종료)을 하여 deploy fail (배포 실패)을 알아차리지 못함 - Tool (도구)이 internal error (내부 에러)로 실패해도 silent retry (조용한 재시도)를 해버림
- Bash command (Bash 명령어)를
&&로 연결하면 permission prompt (권한 프롬프트) 폭발로 인해 silent-stall (조용한 정지)이 발생하는데, 무심코 연결하게 됨 - secret token (비밀 토큰)을 포함한 값을 cmd (명령어)에 써버림
memory file (메모리 파일)에 「주의 사항」을 넣어두어도, 다른 turn (턴)이나 다른 session (세션)에서는 평범하게 잊어버리는 느낌이 있었습니다. LLM은 context window (컨텍스트 윈도우)와 주의력의 편차로 인해 규율을 유지하기 어렵기 때문에, 강제할 수 있는 것은 hook (훅)으로 보강하는 편이 저에게는 편하다고 느꼈습니다.
| event (이벤트) | timing (타이밍) | 용도 |
|---|---|---|
| PreToolUse | tool 실행 전 | 위험한 cmd (명령어) 예방 |
| PostToolUse | tool 실행 후 (성공) | 출력의 후처리・notification (알림) |
| PostToolUseFailure | tool 실행 후 (실패) | tool error (도구 에러)의 silent retry (조용한 재시도) 방지 |
| Stop | turn 종료 전 | end-turn (턴 종료) 규율의 최종 check (체크) |
제가 편의상 나누어 둔 역할:
- PreToolUse = 불이 나기 전에 막는다 (예방)
- PostToolUseFailure = 화재 연기를 사용자로부터 숨기지 않는다 (transparency, 투명성)
- Stop = 화재 진압을 확인하고 돌아간다 (closure, 종결)
1. Bash 연결 cmd (명령어) 경고 — && / ; / || 연결을 block (차단). 연결하면 permission prompt (권한 프롬프트) 폭발로 인해 silent-stall (조용한 정지)이 일어나기 쉬우므로 저는 분리하도록 했습니다.
#!/usr/bin/env bash
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
...
2. 장시간 cmd (명령어)의 timeout (타임아웃) 명시 — npm run build / git clone 등으로 timeout (타임아웃) 지정이 없는 경우 block (차단). silent-stall (조용한 정지) 예방.
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
timeout_ms=$(printf '%s' "$input" | jq -r '.tool_input.timeout // 0')
if echo "$cmd" | grep -qE 'npm (run|install|ci|build)|git clone|pip install'; then
...
3. secret token (비밀 토큰) literal (리터럴)의 cmd (명령어) 쓰기 — sk-, gho_, AKIA, JWT 등의 패턴을 cmd (명령어) 내에서 검출 → block (차단). env variable (환경 변수) 경유를 권장.
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -qE '\b(sk-[a-zA-Z0-9]{20,}|gho_[a-zA-Z0-9]{20,}|AKIA[A-Z0-9]{16}|eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\b'; then
echo "[hook:no-token-literal] secret literal을 cmd에 직접 작성하는 것은 피하십시오. env variable (환경 변수) 사용을 권장합니다." >&2
...
4. 거대 파일 (huge file)의 offset 없는 읽기 — 1MB를 초과하는 파일을 offset 또는 limit 없이 Read tool에 전달하면 컨텍스트 (context)가 비대해지기 (bloat) 때문에 차단 (block) 합니다.
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')
offset=$(printf '%s' "$input" | jq -r '.tool_input.offset // 0')
if [ -f "$file_path" ]; then
...
5. Tool failure (도구 실패) 노출 — 도구 (tool)가 런타임 에러 (runtime error)로 실패한 직후, 조용한 재시도 (silent retry)를 차단합니다.
echo "[hook:tool-failure] Tool failed at runtime." >&2
echo "Report the failure and propose a retry path before continuing." >&2
exit 2
6. Unhandled tool error (처리되지 않은 도구 에러) 시 end-turn 차단 — 트랜스크립트 (transcript)의 끝부분 (tail)에서 가장 최근의 tool error envelope를 검출하여, 후속 어시스턴트 텍스트 (assistant text)에 retry/error/실패 관련 어휘 (vocabulary)가 없다면 차단합니다.
#!/usr/bin/env bash
input=$(cat)
transcript=$(printf '%s' "$input" | jq -r '.transcript_path // ""')
...
7. push 후의 deploy polling (배포 폴링) — git push origin main을 검출한 후, 후속 어시스턴트 텍스트에 deploy polling 관련 어휘가 없다면 차단합니다. 이것은 제가 이번에 만들어서 가장 효과를 본 hook입니다.
tail_json=$(tail -200 "$transcript")
push_idx=$(printf '%s\n' "$tail_json" | awk '
/"type":"assistant"/ { next }
...
Claude Code hook은 stdin에 JSON을 전달합니다. 주요 필드 (field):
.tool_input.command(Bash tool의 경우).tool_input.file_path(Read/Write/Edit의 경우).transcript_path(Stop hook의 경우, 해당 세션의 JSONL 트랜스크립트 경로)
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
Stop hook에서는 transcript_path의 JSONL을 tail scan 하여 가장 최근의 컨텍스트 (context)를 확인할 수 있습니다:
tail_json=$(tail -200 "$transcript")
어시스턴트 (assistant) 자신의 설명문을 스캔 (scan)에서 제외하지 않으면 오검출이 발생합니다 (어시스턴트가 "git push 했습니다"라고 작성한 텍스트를 "push가 수행되었다"라고 읽고 차단해 버리는 경우):
awk '
/"type":"assistant"/ { next }
/\[hook:/ { next }
...
exit 0
= passthrough (규율 위반 없음) -
exit 2
= block + stderr가 LLM의 다음 turn input에 reminder (상기 사항)로서 삽입됨
stderr에 쓰는 메시지는 LLM이 다음 turn에서 읽으므로, 「무엇이 위반인지 + 대처 방법」을 2~3줄로 작성하는 것이 효과적이라는 인상을 받았습니다.
{
"hooks": {
"PreToolUse": [
...
처음부터 전부 갖출 필요는 없다고 느끼고 있으며, 막힐 때마다 하나씩 추가하고 있습니다. 판단 기준:
memory rule로 해결 가능: context-dependent (문맥 의존적) 한 판단 (revert 해야 하는지, commit을 더 세분화해야 하는지 등)
hook화 하고 싶음: 강제 가능한 pattern (연결된 cmd, token literal, push 후 polling 등)
「강제 가능한 규율은 강제한다, memory rule로 얼버무리지 않는다」를 저는 자신의 지침으로 삼고 있지만, 환경에 따라 판단은 달라질 수 있다고 생각합니다.
memory rule (주의 사항)만으로는 저는 규율을 유지하기 어려워, 강제 가능한 규율은 Claude Code hook으로 보강하고 있습니다:
PreToolUse: 위험한 cmd를 실행하기 전에 중단
PostToolUseFailure: tool error를 사용자로부터 숨기지 않음
Stop: end-turn 전의 규율 violation (위반)을 최종 check
hook 개발은 점진적이어도 괜찮다고 느낍니다. memory rule + hook의 2단계 설계가 저에게는 잘 맞았습니다.
어디까지나 개인적인 운용 사례이므로, 참고가 될 만한 부분만 가져가시길 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기