콘텐츠로 이동

6. 규칙을 강제로

터미널 창
git log -1 --oneline && grep -n "push" CLAUDE.md

첫 줄에 5단계 커밋(style: 접근성 지적 반영)이, 둘째 줄에 3단계에서 넣은 “push는 사람이 한다. 커밋까지만 한다” 줄이 나와야 합니다. 커밋이 다르면 5. 디자인 다듬기로, 줄이 없으면 3. 게이트 먼저로 돌아가 그 줄부터 넣습니다.

문장만 있을 때 무슨 일이 일어나는지 먼저 관찰합니다. 그다음 같은 규칙을 훅 스크립트로 옮기고, 등록 전에 파이프로 입력을 넣어 세 분기를 확인합니다. 등록한 뒤 같은 요청을 다시 관찰합니다. CLAUDE.md가 권고이고 훅이 강제인 이유는 CLAUDE.md 실전 가이드와 Hooks가 다룹니다.

  1. 관찰합니다. 원격이 없으니 실제로 push되지는 않습니다. 시켜 봅니다.
git push origin main 을 실행해 주세요.

거부할 수도 있고 시도할 수도 있습니다. 문장으로만 적은 규칙은 어느 쪽 결과도 보장하지 않습니다. 어느 쪽이 나왔는지만 적어 두고, 어느 쪽이 나올지 미리 예측하지 않습니다.

  1. 훅 스크립트를 만듭니다. hooks/block-push.sh:
#!/bin/sh
# PreToolUse(Bash): git push를 차단한다. push는 사람이 한다.
input=$(cat)
cmd=$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).tool_input?.command??"")}catch{process.stdout.write("")}})')
case "$cmd" in
*"git push"*)
echo "push는 사람이 직접 한다. 커밋까지만 하고 멈춘다." >&2
exit 2
;;
esac
exit 0

훅이 하는 일은 훅 입력 JSON에서 tool_input.command 하나를 꺼내는 것뿐이므로 Node의 -e 한 줄로 끝납니다. Node는 준비물에 이미 있으므로 인터프리터를 따로 깔지 않습니다.

  1. 등록하기 전에 파이프로 입력을 넣어 먼저 확인합니다. 왜 등록 전에 확인하는지는 Hooks 레시피가 다룹니다.
터미널 창
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | sh hooks/block-push.sh; echo "exit=$?"
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | sh hooks/block-push.sh; echo "exit=$?"
echo 'not json' | sh hooks/block-push.sh; echo "exit=$?"

종료 코드는 순서대로 exit=2, exit=0, exit=0이어야 합니다. 세 번째 줄이 0이어야 JSON이 아닌 입력에서 훅이 통과합니다. 이 훅과 무관한 다른 도구 호출을 막지 않는다는 뜻입니다.

  1. .claude/settings.json에 등록합니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "f=\"$CLAUDE_PROJECT_DIR/hooks/block-push.sh\"; if [ -f \"$f\" ]; then sh \"$f\"; fi",
"timeout": 5
}
]
}
]
}
}

가드 형태가 Hooks 레시피의 UserPromptSubmit 예와 다릅니다. 그 예의 가드는 || true로 끝납니다. 스크립트가 실패해도 exit 0으로 덮어써서 프롬프트를 막지 않기 위한 것입니다. 여기서는 차단이 목적이므로 || true를 붙이면 안 됩니다. 붙이는 순간 exit 2가 exit 0으로 덮여 훅이 아무것도 막지 못합니다. 3번의 첫 줄 끝에 || true를 붙여 다시 실행하면 메시지는 그대로 출력되고 종료 코드만 exit=0으로 바뀝니다.

  1. /hooks를 열어 PreToolUse 아래에 이 훅이 보이는지 확인합니다. /hooks는 읽기 전용 뷰어이고, 설정 파일 편집은 파일 워처가 자동으로 반영합니다. 보이지 않으면 세션을 재시작합니다.

  2. 1번을 다시 시킵니다.

git push origin main 을 실행해 주세요.

이번에는 훅 메시지가 표시되고 실행이 막힙니다.

  1. 끝 비용과 컨텍스트 사용률을 읽어 docs/tutorial-log.md의 “6. 규칙 강제” 행에 적습니다. 메모 열에는 1번에서 거부했는지 시도했는지를 적습니다.

  2. 훅과 기록표를 함께 커밋합니다.

터미널 창
git add hooks .claude/settings.json docs/tutorial-log.md
git commit -m "chore: push 차단 훅"
터미널 창
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | sh hooks/block-push.sh; echo "exit=$?"
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | sh hooks/block-push.sh; echo "exit=$?"
echo 'not json' | sh hooks/block-push.sh; echo "exit=$?"

세 줄의 종료 코드가 순서대로 exit=2, exit=0, exit=0이어야 합니다. 등록 후 6번에서 push 시도가 훅 메시지와 함께 막혀야 합니다.

값은 7번에서 이미 적었습니다. 시작 상태를 확인한 직후 시작 비용을, 커밋 직전에 끝 비용과 컨텍스트 사용률을 각각 적고 차액으로 읽습니다. 세션 수는 이 단계에서 연 세션 개수입니다. 5번에서 세션을 재시작했다면 그 개수를 그대로 적습니다.

  • 등록했는데 6번에서 안 걸리면 /hooks에 훅이 보이는지 확인하고, 보이지 않으면 세션을 재시작합니다. 설정 파일 위치가 틀렸을 수 있습니다. 설정한 대로 동작하지 않을 때를 확인합니다.
  • 훅을 걸었더니 push와 무관한 다른 명령까지 막히면 3번 세 번째 분기(JSON이 아닌 입력)가 exit=0을 내는지 다시 확인합니다. 파싱 실패에서 exit 2를 내고 있다면 스크립트를 고칩니다.
  • 3번 세 분기가 모두 exit=0이면 명령 추출이 빈 문자열이라 case가 걸리지 않는 것입니다. node --version이 나오는지 먼저 확인합니다. 첫 분기가 exit=2를 낼 때까지는 훅이 아무것도 막지 못하는 상태입니다.
  • 훅이 느리게 느껴지면 프로세스 기동 시간까지 포함해서 잽니다. 기준은 Rules에 있습니다.