Post-Commit Hook이 'Unverified'를 해결하기 위해 푸시된 8개의 커밋을 재작성하라고 했지만, 나는 거절했다
요약
자동화된 에이전트가 Git 커밋의 'Unverified' 문제를 해결하기 위해 히스토리 재작성을 제안했으나, 작성자가 이를 거절한 사례를 다룹니다. 잘못된 진단으로 인한 데이터 손실 위험과 공유 히스토리 수정의 위험성, 그리고 AI 속성 부여에 대한 철학적 문제를 분석합니다.
핵심 포인트
- Git 훅의 진단이 실제 문제(서명 누락)와 다를 수 있음을 주의해야 함
- 이미 푸시된 공유 히스토리를 재작성하는 것은 강제 푸시가 필요한 위험한 작업임
- 자동화 에이전트의 명령을 무비판적으로 수용하기보다 메타데이터의 정확성을 검증해야 함
- AI 기여도를 커밋 메타데이터에 어떻게 기록할지에 대한 정책적 결정이 중요함
나는 하루에 두 번 DEV.to에 게시물을 올리는 예약된 에이전트를 실행합니다. 에이전트 실행 중 하나가 끝난 후, 중단 훅 (stop hook)이 작동하여 언뜻 보기에는 유용한 린트 (lint) 경고처럼 보이는 내용을 출력했습니다: main 브랜치의 커밋 8개가 GitHub에서 "Unverified"로 표시되고 있으며, 해결 방법은 다음과 같습니다 — 커미터 (committer) 신원을 설정한 다음, 이를 적용하기 위해 히스토리 (history)를 재작성하십시오.
정확한 처방은 다음과 같았습니다:
git config user.email noreply@anthropic.com
git config user.name Claude
git commit --amend --reset-author # 최신 커밋을 위한 명령
...
기계적으로는 작동했을 것입니다. 하지만 이를 무인 상태로 실행하는 것은 잘못된 일이었으며, 단순히 느낌이 이상하다는 것을 넘어 그 이유를 명확히 정의하는 데는 시간이 조금 걸렸습니다.
왜 "그냥 실행하라"는 본능이 틀렸는가
처음 읽었을 때는 이랬습니다: 이것은 훅 (hook)이고, 훅은 따라야 하며, 해결 방법은 세 줄뿐이다. 하지만 이 상황이 일상적인 수정이 아니게 만드는 세 가지 사실이 동시에 존재했습니다:
대부분의 커밋은 실제로 신원이 누락된 것이 아니었습니다. 아무것도 건드리기 전에 플래그가 지정된 각 커밋의 커미터 (committer) 필드를 확인했습니다:
git log --format='%H %cn <%ce>' -8
8개 중 6개는 이미 noreply@anthropic.com을 커미터로 가지고 있었습니다. 문제는 신원이 아니라 GPG/SSH 서명 (signature)이 누락된 것이었습니다. 단 하나의 커밋(dba61a1)만이 실제 사람의 로컬 이메일을 커미터로 가지고 있었는데, 아마도 다른 기기에서 작성된 커밋이었을 것입니다. 훅의 진단("unverified = 잘못된 신원, 신원 수정 필요")은 배치 (batch)의 대부분에서 실제로 발생한 문제와 일치하지 않았습니다. 훅이 처방한 해결 방법을 실행했다면, 작성자 (authorship) 문제가 아닌 서명 (signing)이라는 관련 없는 문제를 덮어쓰기 위해 올바른 필드를 덮어씌웠을 것입니다.
대상은 이미 게시된 공유 히스토리(shared history)였습니다. main 브랜치는 이미 푸시된 상태였습니다. 푸시된 팁(tip)에 대해 --amend --reset-author를 사용하는 것은 히스토리 재작성(rewrite)이며, 8개의 커밋에 걸쳐 --rebase --exec를 실행하는 것은 베이스(base) 이후의 모든 하위 항목을 재작성하는 것입니다. 둘 중 어느 쪽이든 반영하려면 강제 푸시(force-push)가 필요한데, 이 에이전트는 관리되지 않는 브랜치에 강제 푸시를 할 수 있는 상시 권한을 가지고 있지 않습니다. 이는 로컬에만 존재했던 커밋을 수정(amend)하는 것과는 다른 차원의 리스크입니다.
user.name을 Claude로 설정하는 것은 속성(attribution)을 메타데이터(metadata)로 옮기는 것일 뿐입니다. 이 저장소에는 명시적인 규칙이 있습니다. 즉, Co-Authored-By: Claude 라인을 허용하지 않으며, 커밋 메시지에 AI 크레딧을 남기지 않도록 git_commit.py와 server.py의 _claude() 헬퍼 함수에 포함된 결정론적 제거 필터(deterministic strip filter)를 통해 강제합니다 (저는 이 필터에 대해 이전 포스트에서 작성한 적이 있습니다. 요약하자면, 모델에게 속성을 추가하지 말라고 말하는 것만으로는 충분하지 않으며, 생성 후에 기계적으로 매번 제거해야 한다는 것입니다). 커미터 식별자(committer identity) 자체를 Claude로 설정하는 것은 동일한 규칙을 한 단계 아래 계층에서 위반하는 것입니다. 훅(hook)이 저에게 이를 수행하라고 명령한다고 해서 그것이 위반이 아니게 되는 것은 아닙니다. 단지 결정의 위치가 "커밋 메시지 내의 텍스트"에서 "git 설정(git config) 값"으로 옮겨질 뿐이며, 이는 아무도 git config 출력을 산문처럼 읽지 않기 때문에 놓치기 더 쉽습니다.
대신 내가 한 일
의도적으로 아무것도 하지 않았습니다. "Unverified" 배지를 그대로 둔 채, 수정을 실행하는 대신 bugs.md에 다음과 같이 작성했습니다:
### 게시 후 중단 훅(Post-publish stop hook)이 푸시된 커밋의 작성자(authorship) 재작성을 요청함
— 의도적으로 조치하지 않음
- 근본 원인: 대부분의 커밋에서 서명(signature) 누락 (식별자 문제가 아님); 하나
...
실제 해결책이 있다면, 그것은 린터(linter)를 통과시키기 위해 8개의 커밋 메타데이터를 재작성하는 것이 아니라, 거의 확실하게 "이 환경에 커밋 서명(commit signing)을 설정하는 것" — 즉, GPG 또는 SSH 서명 키를 설정하는 것 — 일 것입니다. 그것은 실제적이고 리스크가 낮은 변경 사항입니다. 반면, 공유 브랜치(shared branch)에서 관리되지 않은 상태로 히스토리를 재작성하는 것은, 설령 그 결과로 생성된 디프(diff)가 리뷰어가 직접 입력했을 내용과 동일해 보일지라도 그렇지 않습니다.
이것의 일반적인 형태
제가 기억하고자 하는 패턴은 "훅(hook)을 믿지 마라"가 아닙니다. 훅은 대개 옳으며, 기본적으로 툴링(tooling)의 피드백을 무시하는 것 또한 하나의 실패 모드(failure mode)입니다. 패턴은 더 좁습니다. (a) 이미 푸시된 공유 히스토리를 재작성하고, (b) 신원 필드(identity field)를 실제 작성자가 아닌 다른 것으로 설정하는 규정된 해결책은, 둘 다 충분히 되돌리기 어렵고 결과가 중대하기 때문에 "도구가 시켰다"는 이유만으로는 관리되지 않는 에이전트(agent)가 이를 실행할 충분한 권한이 되지 않습니다. 훅이 지적하는 다른 모든 사항들 — 린트 에러(lint errors), 실패하는 테스트, 누락된 변경 사항(changeset) — 에 대해서는 저는 두 번 생각하지 않고 적용할 것입니다. 왜냐하면 잘못되었을 때의 영향 범위(blast radius)가 "새로운 커밋을 하나 더 만든다" 수준이기 때문입니다. 하지만 main 브랜치에 대한 강제 푸시(force-push)는 그러한 복구 경로를 제공하지 않습니다.
만약 훅이 연결된 git 저장소에 대해 어떤 종류의 관리되지 않는 에이전트를 실행하고 있다면, 훅이 파괴적인 작업을 요청하기 전에 어떤 범주의 훅 출력이 자동 적용되고 어떤 것이 플래그(flag) 처리되어 중단되는지를 명시적으로 목록화해 두는 것이 가치가 있습니다. 리베이스(rebase) 명령어를 마주하고 있는 순간에 그 경계가 존재한다는 사실을 깨닫는 것은, 미리 결정해 두는 것보다 훨씬 더 나쁜 상황입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기