AX 팁 4 — 검토 기록 없는 커밋을 훅이 멈춘다

작성

  • AX 팁
  • Claude Code
  • 개발 환경 자동화
  • 커밋

AI 에이전트가 만든 변경을 커밋하기 전에 검토를 거치게 하려면, 「검토를 받아라」는 규칙을 적는 것보다 커밋 명령 자체를 훅으로 가로채서 「마지막 커밋 뒤에 검토 에이전트 호출 기록이 있는가」를 세션 기록에서 확인하는 쪽이 확실합니다. Claude Code 의 PreToolUse 훅은 Bash 도구가 실행되기 직전에 명령과 세션 기록 경로를 받으므로, git commit 이고 대상이 규칙·훅 파일이면 기록을 읽어 검토 호출이 없을 때 deny 를 돌려주면 됩니다. 오탈자 같은 작은 수정은 커밋 메시지에 [verifier-skip: 이유] 를 적어 통과시키되, 그 글자가 커밋 이력에 그대로 남게 합니다.

이 글은 혼자 쓰는 개발 환경을 AI 에이전트 중심으로 바꾸면서(AX, AI 전환) 만든 장치를 하루에 하나씩 정리하는 연재의 네 번째 편입니다. 지난 편은 서브에이전트 보고를 300자로 줄이는 훅이었습니다.

「검토는 꼭 거친다」는 규칙이 왜 지켜지지 않았을까?

제 지침 저장소에는 에이전트가 일하는 방식을 정한 규칙 문서와, 그 규칙을 기계로 지키게 하는 훅 스크립트가 함께 들어 있습니다. 이 파일들은 한 줄만 틀려도 다음 세션 전부에 영향을 주기 때문에, 「규칙·훅을 바꾸는 커밋은 검토 에이전트(verifier)를 먼저 거친다」는 규칙을 일찍부터 두었습니다.

문제는 규칙이 문장으로만 있었다는 점입니다. 에이전트는 자기 변경을 「작은 수정」이라고 판단하면 검토를 건너뛰고 바로 커밋했습니다. 사람이 보기에 작지 않은 변경도 많았습니다. 규칙을 더 강한 말로 고쳐 써도 같은 일이 반복됐고, 2026-10-18 에 「문장으로는 안 된다, 기계로 확인하자」로 방향을 바꿨습니다.

git 의 pre-commit 훅으로는 왜 안 될까?

처음 떠오른 자리는 git 의 pre-commit 훅이었습니다. 커밋 직전에 돌고, 0 이 아닌 코드로 끝나면 커밋이 막히니 자연스러운 선택입니다. 그런데 git 훅은 「검토 에이전트가 돌았는가」를 알 길이 없습니다. 그 사실은 git 이 아니라 에이전트 세션 안에 있습니다.

Claude Code 는 세션의 모든 도구 호출을 JSON Lines 파일(.jsonl)로 남기고, PreToolUse 훅에 그 파일 경로(transcript_path)를 함께 넘겨줍니다. 그러니 커밋 명령을 git 이 아니라 에이전트 쪽에서 가로채면, 같은 세션에서 지금까지 어떤 도구를 불렀는지 전부 읽을 수 있습니다. 검토가 있었는지 아는 쪽에서 막는 것이 맞았습니다.

훅은 무엇을 어떻게 확인할까?

확인 순서는 세 단계입니다. 첫째, 명령에 든 git 호출을 나눠 commit 이 있고 대상 저장소가 지침 저장소인지 봅니다(git -C 경로 commit 처럼 경로를 준 꼴도 같이 잡습니다). 둘째, 이번 커밋에 들어갈 파일을 모읍니다. 이미 스테이징된 것에, 같은 명령의 git add 인자를 더합니다. -A 나 . 이면 바뀐 파일 전부로 칩니다. 그중 규칙 문서·출력 형식·훅 폴더·설정 동기화 스크립트 같은 「검토 대상 경로」가 하나라도 있으면 셋째로 넘어갑니다. 셋째, 세션 기록에서 마지막으로 성공한 git commit 과 마지막 검토 에이전트 호출의 순서를 비교합니다.

핵심인 셋째 단계만 줄이면 이렇습니다.

def reviewed_since_last_commit(transcript, current_id):
    uses, failed = [], set()
    for line in open(transcript, encoding="utf-8"):
        for c in (json.loads(line).get("message") or {}).get("content") or []:
            if c.get("type") == "tool_use" and c.get("id") != current_id:
                uses.append(c)
            elif c.get("type") == "tool_result" and c.get("is_error"):
                failed.add(c.get("tool_use_id"))
    last_review = last_commit = -1
    for i, c in enumerate(uses):
        if c["name"] == "Agent" and REVIEW_AGENT_RE.search(c["input"].get("subagent_type", "")):
            last_review = i
        elif c["name"] == "Bash" and c["id"] not in failed and re.search(r"\bgit\b[^;&|]*\bcommit\b", c["input"].get("command", "")):
            last_commit = i
    return last_review > last_commit

실패한 도구 호출(is_error)을 빼는 줄이 중요합니다. 검토 뒤에 커밋을 한 번 시도했다가 다른 이유로 실패했다면, 그 실패한 커밋 때문에 「검토가 커밋보다 앞에 있다」고 잘못 판정하면 안 되기 때문입니다. 지금 가로채고 있는 호출 자체(current_id)도 뺍니다.

검토로 인정하는 에이전트 이름은 정규식으로 세 가지만 둡니다. 이름에 「verify」나 「review」가 들어간다고 다 인정하면, 비슷한 이름의 skill 호출로 통과하는 길이 생깁니다.

REVIEW_AGENT_RE = re.compile(r"^(oh-my-claudecode:)?(verifier|code-reviewer|security-reviewer)$")
GATED_RE = re.compile(r"^(AGENTS\.md|claude-global\.md|output-styles/|scripts/hooks/|scripts/git-hooks/|scripts/sync-settings\.py)")

훅 등록은 보통의 PreToolUse 와 같습니다. 거부할 때는 permissionDecision 을 deny 로 돌려주고, 이유를 문장으로 함께 보냅니다. 에이전트는 그 문장을 읽고 검토 에이전트를 부른 뒤 다시 커밋합니다.

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "python3 \"$HOME/.claude/hooks/cc-guardrail.py\"", "timeout": 10 }] }
    ]
  }
}
지침 저장소 커밋 전 확인git commit 명령 → 규칙·훅 파일 포함?; 규칙·훅 파일 포함? → 커밋 진행 (아니오); 규칙·훅 파일 포함? → [verifier-skip] 표지? (예); [verifier-skip] 표지? → 커밋 진행 (있음); [verifier-skip] 표지? → 세션 기록 읽기 (없음); 세션 기록 읽기 → 마지막 커밋 뒤 검토 호출?; 마지막 커밋 뒤 검토 호출? → 커밋 진행 (예); 마지막 커밋 뒤 검토 호출? → 거부 — 검토 먼저 (아니오)아니오예있음없음예아니오git commit 명령규칙·훅 파일 포함?[verifier-skip]표지?세션 기록 읽기마지막 커밋 뒤 검토호출?거부 — 검토 먼저커밋 진행
지침 저장소 커밋 전 확인 — 검토 기록이 없으면 커밋 명령 자체가 거부된다

처음 만든 날 검토 에이전트가 무엇을 잡았을까?

이 훅 자체도 규칙·훅 파일이라, 만든 날 검토 에이전트에게 먼저 보였습니다. 2026-10-18 커밋 본문에 남아 있는 지적은 세 가지였습니다. 무인 자동화가 돌리는 커밋은 세션에 검토 호출이 없어 전부 거부된다는 것, 훅 입력에 지금 호출의 ID 가 없는 경우 지금 커밋이 「마지막 커밋」으로 잡혀 판정이 틀린다는 것, 그리고 skill 방식의 검증 호출을 어떻게 셀지였습니다. 세 가지를 반영한 뒤에야 커밋했습니다. 검토를 강제하는 장치가 첫 검토에서 구멍 세 개를 드러낸 셈이라, 이 방식이 필요하다는 확신이 오히려 굳어졌습니다.

그 뒤로도 두 번 조였습니다. 2026-10-21 에는 Claude 검토 에이전트 기록을 먼저 요구하고, 다른 모델에게 묻는 교차 검증은 그 뒤에만 인정하도록 바꿨습니다. 2026-10-22 에는 커져 가던 가드 스크립트를 검사 종류별 모듈로 나눠, 이 검사는 161줄짜리 파일 하나가 됐습니다(2026-10-22 기준, 가드 폴더의 파일은 22개).

숫자는 어떻게 나왔을까?

훅을 넣은 2026-10-18 부터 2026-10-22 까지 지침 저장소 커밋을 git log 로 세어 봤습니다. 전체 289건 가운데 검토 대상 경로를 건드린 커밋이 150건이고, 그중 38건이 [verifier-skip: 이유] 표지로 통과했습니다. 정합성 자동 복구 표지([auto-fix])는 2건입니다. 나머지 110건은 검토 에이전트 기록이 있어야만 커밋될 수 있었던 건입니다.

검토 대상 경로를 건드린 커밋 150건의 통과 경로검토 기록으로 통과: 110건; [verifier-skip] 표지: 38건; [auto-fix] 표지: 2건020406080100검토 기록으로 통과110건[verifier-skip]표지38건[auto-fix] 표지2건
검토 대상 경로를 건드린 커밋 150건의 통과 경로 — 2026-10-18 ~ 2026-10-22, git log 로 센 값

38건이 적지 않아 보일 수 있는데, 표지에는 이유를 반드시 적게 해 두었습니다. 그래서 「왜 건너뛰었나」를 나중에 git log 한 번으로 다시 볼 수 있습니다. 건너뛰기 자체를 막는 것보다, 건너뛴 이유가 이력에 남는 쪽을 택했습니다.

지금 하루는 어떻게 달라졌나?

규칙·훅 파일을 바꾸는 커밋 앞에는 이제 늘 검토 에이전트 호출이 한 번 끼어듭니다. 에이전트가 「작은 수정」이라고 판단해도 훅은 그 판단을 묻지 않고 기록만 봅니다. 오늘 이 글을 쓰기 직전에도 가드 모듈을 한 줄 고쳤는데, 검토 에이전트를 띄우는 데 두 번 실패했습니다. 그 상태에서는 커밋이 거부되므로, 검토가 실제로 돈 뒤에야 커밋할 수 있습니다. 불편하지만 그것이 이 훅을 둔 이유입니다.

한 가지 대가는 있습니다. 세션 기록을 못 읽으면 훅은 막지 않고 통과시킵니다(fail-open). 기록 파일이 깨졌다는 이유로 모든 커밋이 멈추는 것보다는 낫다고 판단했지만, 그 틈이 있다는 것은 알고 씁니다.

자주 묻는 질문

git 의 pre-commit 훅과 같이 쓸 수 있나요?

같이 쓸 수 있고 역할이 다릅니다. git 훅은 린트·시험처럼 파일만 보고 판정할 수 있는 검사에 맞고, 이 훅은 「세션에서 검토 에이전트를 불렀는가」처럼 git 이 모르는 사실을 봅니다. 다만 --no-verify 로 git 훅을 건너뛰는 것까지 막고 싶다면, 그것도 같은 PreToolUse 훅에서 명령 문자열로 거부하면 됩니다.

에이전트가 다른 꼴의 명령으로 우회하지 않나요?

git commit 글자만 찾으면 git -C 경로 commit 이나 cd 경로 && git commit 을 놓칩니다. 그래서 명령을 &&·;·| 로 나눈 뒤 조각마다 git 호출과 하위 명령을 따로 해석합니다. 그래도 새 꼴이 나오면 시험 사례로 추가합니다. 이 검사에는 「검토 없이 커밋」「검토 뒤 커밋」「검토 뒤 커밋 뒤 다시 커밋」「표지로 통과」 네 가지 시험이 붙어 있습니다.

검토 에이전트가 통과시켰는지도 확인하나요?

아니요. 훅이 보는 것은 「호출 기록이 있는가」뿐이고, 검토 결과를 읽어 통과 여부를 판정하지는 않습니다. 결과를 반영해 고치는 것은 에이전트와 사람의 몫으로 두었습니다. 훅이 검토 결과까지 해석하기 시작하면 판정이 틀릴 때 고치기가 훨씬 어려워집니다.

사이트를 만들어볼까요?

무료 상담 받기