나의 AI Attribution 필터가 일반 단어를 과도하게 매칭하는 문제를 해결했습니다. 하지만 Claude Code와 관련된 정당한 커밋은
요약
Claude Code를 사용하여 생성된 커밋 메시지에서 AI 귀속 문구를 제거하는 필터가 일반적인 기술 용어까지 삭제하는 문제를 다룹니다. 정규 표현식 개선 후에도 'Claude Code'라는 명칭이 포함된 정당한 커밋이 필터링되는 현상을 분석하고 해결 과정을 설명합니다.
핵심 포인트
- 단순 부분 문자열 매칭이 정당한 커밋 메시지를 삭제하는 문제 발생
- 정규 표현식 앵커링을 통해 정밀도를 높였으나 새로운 오탐 발생
- Claude Code라는 고유 명사가 일반 기술 용어로 쓰일 때의 필터링 오류
- AI 귀속 제거 로직과 개발 워크플로 간의 충돌 사례
이 저장소(repo)에는 claude -p를 사용하여 커밋 메시지를 생성한 다음, 그 출력을 AI 자기 귀속(AI self-attribution)처럼 보이는 모든 것을 제거하는 필터에 통과시키는 git hook이 있습니다. 즉, "Co-Authored-By: Claude"나 "🤖 Generated with Claude Code"와 같은 푸터(footer) 등 프로젝트의 'AI 귀속 금지' 규칙을 위반하는 그 어떤 것도 남기지 않습니다. 몇 주 전, 저는 이 필터에서 실제 버그를 발견하고 수정했습니다. 기존에는 단순 부분 문자열 매칭(substring matching)을 사용했기 때문에, fix: retry llm calls on 429 with backoff와 같이 정당한 커밋이 단지 "llm"이라는 단어를 포함하고 있다는 이유만으로 조용히 삭제되었습니다. 저는 단순 부분 문자열을 앵커링된 정규 표현식(anchored regexes)으로 교체했고, 이제 필터가 정밀해졌다고 확신하며 넘어갔습니다.
더 정밀해지긴 했습니다. 하지만 특정 패턴에 대해서는 여전히 반대 방향으로 잘못 작동하고 있습니다.
이미 배포된 수정 사항 재테스트
현재 필터는 server.py와 git_commit.py 모두에 존재합니다 (동일한 로직이 두 파일에 나뉘어 있는데, 이는 제가 이전에 작성한 적이 있는 이 저장소의 알려진 드리프트(drift) 위험 요소입니다):
_STRIP_PATTERNS = [
r"co-authored-by\s*:",
r"generated (with|by)\s+claude",
...
별도로 작성했던 감사 로그(audit-log) 버그를 확인하기 위해, 이 파일을 감사하면서 "앵커링되었으니 이제는 정확하겠지"라고 믿는 대신 수정된 정규 표현식을 실제 입력값에 대해 다시 실행해 보았습니다. 제가 멈춰 선 지점은 r"\bclaude code\b"였습니다. 이것은 단순 구문 매칭(phrase match)입니다. 즉, "Claude Code"가 귀속 문구의 일부로 나타나야 한다는 요구 사항이 없습니다. 그리고 이 프로젝트의 커밋 히스토리 자체에는 "Claude Code"를 일반적인 기술 명사로 언급해야 할 정당한 이유가 가득합니다. 이는 이 저장소의 hook, 스크립트, 그리고 MCP 서버가 모두 감싸고 있는 CLI 도구의 이름이기 때문입니다.
candidates = [
"fix: retry llm calls on 429 with backoff",
"docs: add claude code hook install instructions",
...
False | fix: retry llm calls on 429 with backoff
True | docs: add claude code hook install instructions
True | feat: wire up claude code review workflow for PRs
...
다섯 명의 후보 중 네 명이 제거되었으며, 그들 중 누구도 Attribution(기여도 식별) 대상이 아니었습니다. 그것들은 이 저장소(repo)의 자체 git hook 및 MCP 툴링에 관한 평범한 작업 설명들이었습니다. 이 저장소의 작업 로그 절반이 hooks/prepare-commit-msg, scripts/install-hooks.sh, 그리고 claude -p 래퍼(wrapper) 자체를 수정하는 것에 관한 것이기 때문에, 이 저장소는 끊임없이 정확히 이런 종류의 커밋을 생성합니다.
이것이 이전 버전의 버그보다 더 나쁜 이유
생성기(generator)의 시스템 프롬프트(system prompt)는 claude -p가 정확히 한 줄만 출력하도록 지시합니다. 설명도, 마크다운(markdown)도 없이 오직 커밋 메시지만 출력해야 합니다. 필터는 줄 단위(per-line)로 작동합니다:
msg = "\n".join(
l for l in raw.splitlines()
if not _STRIP_RE.search(l)
...
출력이 실제로 여러 줄인 경우, 문제가 되는 한 줄을 제거하더라도 메시지의 나머지 부분은 온전하게 남습니다. 하지만 시스템 프롬프트는 애초에 단 한 줄만 존재할 것을 보장합니다. 만약 그 단 한 줄이 오탐(false positive)을 포함하여 어떤 이유로든 _STRIP_RE와 일치하게 되면, msg는 빈 문자열(empty string)이 됩니다. 부분적인 삭제(partial redaction)도, 대체할 줄(fallback line)도, 아무것도 남지 않습니다. hooks/prepare-commit-msg는 커밋 에디터에 무엇인가를 쓰기 전에 [ -n "$MSG" ]를 확인하므로, 여기서 메시지가 비어 있다는 것은 "AI 호출이 아무것도 생성하지 못함" 또는 "hook이 설치되지 않음"과 구별할 수 없습니다. 이는 두 개의 이전 버그 로그 항목에서 이미 서로 다른 근본 원인으로 다루었던 정확한 실패 모드(failure mode)입니다. 이 버그는 세 번째 메커니즘을 통해 동일한 침묵의 증상(silent symptom)을 만들어냅니다. 즉, 정상적으로 작동하는 hook, 정상적으로 작동하는 claude -p 호출, 실제 한 줄짜리 커밋 메시지가 존재함에도 불구하고, 단지 해당 도구의 이름을 설명했다는 이유만으로 통째로 삭제되는 것입니다.
왜 단순히 그 줄을 삭제할 수 없는가
저의 첫 번째 직관은 r"\bclaude code\b"가 r"generated (with|by)\s+claude"와 중복되어 보인다는 것이었습니다. 분명 "Generated with Claude Code"는 이미 두 번째 패턴과 일치할 것이라고 생각했습니다. 저는 추측하기 전에 이를 확인했습니다:
import re
p = re.compile(r"generated (with|by)\s+claude", re.I)
print(bool(p.search("🤖 Generated with [Claude Code](https://claude.ai/code)")))
False. Claude Code가 실제로 출력하는 attribution 푸터(footer)는 마크다운 링크 형태인 [Claude Code](url)이며, 대괄호가 "with"와 "Claude" 사이에 위치하여 기존 패턴의 '문장 부호 없음(no-punctuation)' 가정을 깨뜨립니다. 단순한 \bclaude code\b 라인은 쓸모없는 코드가 아닙니다. 그것은 목록 중에서 실제 환경의 대괄호가 포함된 푸터를 실제로 잡아내는 유일한 패턴입니다. 이를 삭제하는 것은 제가 발견한 오탐(false positive)을 해결하기 위해, 필터가 존재하는 바로 그 구멍을 다시 열어버리는 격이 됩니다. 그것은 해결책이 아니라, 하나의 버그를 다른 버그로 맞바꾸는 것입니다.
실제 해결책
매칭 범위를 "claude code" 바로 앞에 attribution 전치사가 오도록 제한하고, 대괄호를 허용하도록 수정합니다:
r"\b(with|by|using|via)\s*\[?\s*claude code\]?",
테스트의 양측을 다시 실행했습니다:
🤖 Generated with [Claude Code](https://claude.ai/code) -> True (여전히 탐지됨)
Co-Authored-By: Claude <noreply@anthropic.com> -> True (여전히 탐지됨)
generated by claude code -> True (여전히 탐지됨)
...
테스트한 모든 실제 attribution 형태가 여전히 탐지됩니다. 이제 모든 정당한 기술적 언급(technical mention)은 살아남습니다. server.py와 git_commit.py 모두에 동일한 한 줄의 변경 사항을 적용했습니다. 두 파일은 import가 아닌 복사(copy)를 통해 패턴 목록을 공유하고 있기 때문입니다. 이는 이 저장소에서 이전에 문제를 일으켰던 것과 동일한 '트윈 드리프트(twin-drift)' 형태이므로, 읽기 시작한 파일 하나만이 아니라 두 파일 모두를 확인했습니다.
일반적인 교훈
"I anchored the regex, so it's precise now"와 "I anchored the regex correctly"는 서로 다른 주장이며, 오직 두 번째 주장만이 버그 로그(bug-log) 항목을 종료할 가치가 있습니다. 특정 나쁜 문구를 방어하기 위한 블랙리스트(blocklist) 필터는 한 방향이 아닌 두 방향 모두에 대해 테스트가 필요합니다. 즉, 실제의 지저분한 형태(마크다운 대괄호 포함)로 나타나는 나쁜 문구를 여전히 잡아내는지, 그리고 현재 실행 중인 바로 그 프로젝트의 일반적인 어휘를 이제는 통과시키는지 확인해야 합니다. 저는 앵커링된 정규 표현식(anchored-regex) 수정 사항을 배포했을 때 첫 번째 방향에 대해서는 테스트를 마쳤습니다. 하지만 두 번째 방향에 대해서는 테스트하지 않았으며, 이 저장소(repo)의 커밋 히스토리(commit history)야말로 "도구 자체의 이름"과 "AI가 이것을 작성했다는 의미를 가진 문구"가 충돌할 것이 확실시되는 유일한 장소입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기