초대하고 적립

초대 보상 안내

초대 링크를 공유하세요. 친구가 링크로 가입하고 충전하면 이후 충전마다 표시된 보상을 받을 수 있습니다.

Claude Code Stop Hook: “완료” 전에 검사하기

테스트 실패나 파일 누락을 감지해 종료를 막는 짧은 로컬 검사를 설정합니다.

목차

Claude Code Stop Hook: “완료” 전에 검사하기

Claude Code가 “완료”라고 답했다고 해서 테스트를 실행했거나 build 산출물이 최신이라는 뜻은 아닙니다. 이는 에이전트가 현재 턴을 끝내려 한다는 뜻일 뿐입니다. Stop Hook은 종료 직전에 짧은 로컬 검사를 실행하고, 명확한 조건이 실패했을 때 대화를 계속하게 할 수 있습니다. CI, 전체 테스트 스위트, 사람의 승인 절차를 대체하지는 않습니다.

Stop Hook, CLAUDE.md, CI의 역할은 서로 다릅니다

  • Stop Hook은 응답이 끝날 때 짧은 검사를 실행합니다. git diff --check, 범위를 좁힌 테스트, 예상 파일의 존재 확인에 적합합니다.
  • CLAUDE.md는 에이전트가 지켜야 할 규칙과 실행할 명령을 알려 주지만, 파일 자체가 명령을 실행하지는 않습니다.
  • CI는 push 또는 pull request 이후 별도 환경에서 실행됩니다. 팀의 필수 게이트는 여전히 CI입니다.

이 hook에 deploy, 게시, 외부 서비스 쓰기 작업을 넣지 마세요. 턴이 끝날 때마다 실행되는 부수 효과는 실패했을 때 재현하거나 되돌리기 어렵습니다.

검증 가능한 완료 조건을 정합니다

hook을 설정하기 전에 다음 네 가지를 적어 둡니다.

  1. 주장: 턴이 끝난 뒤 에이전트가 말해도 되는 내용입니다. 예를 들면 “build를 생성했다”입니다.
  2. 증거: 그 주장을 확인하는 명령 또는 파일입니다. 예를 들어 npm test -- --runInBand와 test -s dist/app.js입니다.
  3. 성공: 두 검사가 모두 종료 코드 0으로 끝나는 것입니다.
  4. Stop 차단: 실패 시 decision: "block"과 짧은 reason을 담은 JSON을 반환합니다.

먼저 hook 없이 이 명령을 실행해 보세요. 몇 분이 걸리거나 네트워크가 필요하다면 더 작은 로컬 검사로 줄이고, 전체 실행은 CI에 맡깁니다.

Claude Code를 API provider로 연결했다면 먼저 최신 BetterToken 설정 가이드를 열고 도구에 자신의 API Key를 설정한 뒤 짧은 테스트 요청을 보내세요. 그런 다음 Dashboard에서 예상한 모델, 상태, 토큰 사용량을 확인합니다. 로컬 hook에는 키가 필요하지 않으며 로그에도 기록하지 마세요.

최소 Stop Hook을 설정합니다

.claude/settings.json에 짧은 timeout을 둔 프로젝트 hook을 추가합니다.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/check-before-stop.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

검사 스크립트는 scripts/check-before-stop.sh로 저장합니다. 아래 명령은 Node 프로젝트용 예시이므로 실제 리포지토리에 있는 명령과 경로로 바꾸세요.

#!/usr/bin/env sh
set -eu

if [ -t 0 ]; then
  input='{"stop_hook_active":false}'
else
  input=$(cat)
fi

stop_hook_active=$(printf '%s' "$input" | node -e '
let raw = "";
process.stdin.on("data", chunk => raw += chunk);
process.stdin.on("end", () => {
  try { process.stdout.write(String(Boolean(JSON.parse(raw).stop_hook_active))); }
  catch { process.stdout.write("false"); }
});')

block_stop() {
  if [ "$stop_hook_active" = "true" ]; then
    printf '%s\n' "Stop 검사가 계속 실패합니다: $block_reason. 수동으로 실행하세요. CI는 계속 필수입니다." >&2
    exit 0
  fi

  BLOCK_REASON="$block_reason" node -e 'process.stdout.write(JSON.stringify({decision:"block",reason:`Stop 검사 실패: ${process.env.BLOCK_REASON}`}) + "\n")'
  exit 0
}

if ! npm test -- --runInBand >/dev/null 2>&1; then
  block_reason='npm test를 실행하고 실패한 테스트를 수정하세요'
  block_stop
fi

if ! test -s dist/app.js; then
  block_reason='dist/app.js를 다시 생성하세요'
  block_stop
fi

printf '%s\n' 'Stop 검사 통과: tests 및 dist/app.js'
exit 0

파일에 실행 권한을 추가합니다.

chmod +x scripts/check-before-stop.sh

현재 Claude Code 문서에 따르면 Stop Hook은 코드 0과 구조화된 JSON을 반환할 수 있습니다. decision: "block"은 턴 종료를 막고 reason은 원인을 전달합니다. 0이 아닌 코드와 timeout에는 별도의 hook 오류 동작이 있으므로 그것만 차단 계약으로 삼지 마세요. 검사는 설정 시간 안에 끝나야 합니다.

다음 종료 시도에서 Stop Hook 때문에 Claude Code가 이미 계속 실행 중이면 stop_hook_active가 true입니다. 예시의 fail-open 분기는 같은 실패를 다시 차단하지 않고 stderr에 짧은 경고를 쓴 뒤 0을 반환합니다. 루프를 막더라도 CI 게이트는 계속 필요합니다. 반복 차단이 필요하면 문서화되지 않은 고정 횟수를 가정하지 말고 자체 카운터와 명시적 한계를 정하세요.

전체 흐름을 수동으로 검증합니다

스크립트가 터미널에서 시작되는지만 확인하지 말고 모든 분기를 시험하세요.

  1. 스크립트의 dist/app.js를 임시로 dist/missing.js로 바꾸고 printf '%s\n' '{"stop_hook_active":false}' | ./scripts/check-before-stop.sh; echo $?를 실행합니다. "decision":"block", 짧은 reason, 코드0이 담긴 JSON이 예상 결과입니다.
  2. 올바른 경로로 되돌린 뒤 build를 만들고 같은 명령을 다시 실행합니다. 예상 코드는 0입니다.
  3. 다시 존재하지 않는 파일을 지정하고 Claude Code에 작고 되돌릴 수 있는 변경을 요청합니다. decision: "block"이 대화를 계속 유지하면서 검사 이유를 돌려주는지 확인합니다.
  4. 경로를 고치지 않은 상태에서 printf '%s\n' '{"stop_hook_active":true}' | ./scripts/check-before-stop.sh; echo $?를 실행합니다. 스크립트는 stderr에 짧은 경고를 쓰고 코드0을 반환해야 합니다. 이것으로 루프 방지 분기를 검증할 수 있습니다.
  5. 경로를 복구하거나 최신 산출물을 만듭니다. 다음 종료에서는 hook이 0을 반환하고 턴 종료를 허용해야 합니다.

이 절차는 실제로 작동하는 Stop Hook과, 터미널에서는 실패하지만 Claude Code의 종료는 막지 못하는 스크립트를 구분해 줍니다.

차단된 Stop에서 복구합니다

두 경우를 구분하세요. hook이 decision: "block" JSON을 반환하면 reason을 읽습니다. 검사 조건이 정상적으로 실패한 경우입니다. 해당 검사를 수동 실행하고 테스트나 코드를 고친 뒤 파일이 현재 명령에서 생성됐는지 확인하고 Claude Code 작업을 다시 시도합니다.

hook 명령 자체가 0이 아닌 코드나 timeout으로 끝나면 reason으로 확인된 차단이 아니라 hook 실행 오류입니다. 오류와 stderr를 읽고 스크립트를 수동 실행한 뒤 경로, 권한, 의존성 또는 시간 한도를 고쳐 재검증하세요.

기록할 내용은 검사 이름과 결과로 제한하세요. API Key, .env 내용, 전체 prompt, 전체 테스트 로그는 출력하지 않습니다. git diff --check가 통과했다고 해서 비즈니스 로직이 맞다는 뜻은 아니며, 파일이 존재한다고 해서 build가 최신인 것도 아닙니다. hook은 명시적으로 코드에 넣은 주장만 확인할 수 있습니다.

API workflow를 분리합니다

API provider를 사용할 때는 자신의 계정에서 Key를 만들고 관리합니다. Stop Hook은 로컬에 남아 있으므로 Key, 전체 prompt, Dashboard 로그에 접근할 필요가 없습니다. API 문제가 생기면 Base URL과 설정에 관한 최신 문서를 확인하고, 그 진단을 로컬 종료 검사와 섞지 마세요.

출처

LLM 워크플로를 최적화할 준비가 되셨나요?

하나의 API로 모델을 연결하고 키와 AI 비용을 관리하세요.

무료로 시작하기