AI 사용 감사 로그: 커밋 트레일러로 어떤 도구·모델이 관여했는지 남기고 CI에서 모으기
무엇을 남기고 무엇은 남기지 않을지, git 트레일러와 수집 스크립트로 구현하는 추적성

핵심 요약
- AI 관여 여부·도구·모델·이슈는 남기고, 프롬프트와 응답 원문, 자격 증명은 남기지 않는다.
- Git 트레일러는 커밋 메시지 끝의 키: 값 블록이며, git log의 %(trailers:key=...)로 값만 꺼낼 수 있다.
- 이 사이트 저장소는 27개 커밋 중 18개에 AI 공동 작성자 트레일러가 있었지만, 나머지가 AI 미사용인지 기록 누락인지는 구분할 수 없었다.
- AI-Assisted: no까지 명시하게 하고, PR 범위에서 누락을 수집해 처음에는 경고로, 이후 실패로 운영한다.
보안 사고 조사나 내부 감사에서 "이 변경은 누가, 어떤 도구로 만들었고 누가 승인했는가"라는 질문을 받으면, 코드 리뷰 기록만으로는 답하기 어렵다. AI 코딩 도구의 관여 여부가 어디에도 남아 있지 않기 때문이다. 이 글은 별도 시스템 없이 Git 커밋 트레일러로 최소한의 추적 정보를 남기고, CI에서 모아 누락을 찾는 방법을 정리한다. 기록은 많이 남기는 것이 아니라, 나중에 질문에 답할 수 있는 최소 항목을 빠짐없이 남기는 것이 목표다.
남길 것과 남기지 않을 것
| 구분 | 항목 | 이유 |
|---|---|---|
| 남긴다 | AI 관여 여부, 도구 이름, 모델 식별자 | 어떤 변경에 AI가 관여했는지 범위를 좁힐 수 있다 |
| 남긴다 | 작성자, 검토자, 관련 이슈 번호, 시점 | 책임과 맥락을 재구성할 수 있다 |
| 남기지 않는다 | 프롬프트 원문, 모델 응답 원문 | 연결 문자열, 토큰, 고객 데이터가 섞이기 쉽고 저장 자체가 사고가 된다 |
| 남기지 않는다 | 자격 증명, 내부 URL | 저장소와 로그는 생각보다 넓게 복제된다 |
요구 수준은 상황에 따라 다르다. 대부분의 팀은 "어떤 커밋에 AI가 관여했는가"만으로 충분하고, 규제 산업이나 외주 코드 인수인계처럼 근거가 필요한 경우에만 모델 식별자와 검토자까지 필수로 둔다.
Git 트레일러로 남기기
Git 문서에 따르면 트레일러는 커밋 메시지 끝의 키: 값 줄이고, 본문과 빈 줄로 구분된 블록이어야 한다. 키는 영문자·숫자·하이픈으로 구성한다. Signed-off-by가 대표적인 예다. 이미 많은 AI 도구가 Co-Authored-By 트레일러를 남기고 있다.
이 사이트 저장소의 실제 기록
이 사이트는 콘텐츠와 코드 작업을 Claude Code로 하며, 커밋에 Co-Authored-By 트레일러가 남는다. 2026-10-11 기준으로 집계한 결과다. pretty-formats 문서의 %(trailers:key=...,valueonly)는 지정한 키의 값만 꺼낸다.
$ git log --format='%(trailers:key=Co-Authored-By,valueonly)' | grep . | sed 's/<.*>//' | sort | uniq -c
12 Claude Opus 5.5
6 Claude Sonnet 5$ git rev-list --count HEAD
2727개 커밋 중 18개에 AI 공동 작성자 트레일러가 있고, 모델별로 나뉜다. 나머지 9개는 트레일러가 없는데, 이것이 "AI를 쓰지 않은 커밋"인지 "기록이 빠진 커밋"인지는 지금 구조로는 구분할 수 없다. 그래서 감사 목적이라면 AI를 쓰지 않은 경우에도 명시적으로 남기게 하는 편이 낫다.
팀 트레일러 규칙 정하기
- 트레일러 키를 정한다. 예:
AI-Assisted: yes|no,AI-Tool,AI-Model,Issue. git commit --trailer나 커밋 템플릿으로 남긴다. AI 도구의 규칙 파일(AGENTS.md 등)에도 같은 키를 적어 에이전트가 커밋할 때 남기게 한다.- PR 단계에서 범위 안 커밋의 트레일러를 수집해 누락을 확인한다.
- 검토자는 커밋 작성자가 스스로 적는 값보다 PR 승인 기록에서 가져오는 것이 정확하다. 트레일러의
Reviewed-by는 보조 정보로만 쓴다. - 수집한 레코드를 변경할 수 없는 저장소에 추가 전용으로 보관한다.
git commit -m "쿠폰 검증 로직 추가" \
--trailer "AI-Assisted: yes" \
--trailer "AI-Tool: claude-code" \
--trailer "AI-Model: claude-sonnet-5" \
--trailer "Reviewed-by: @backend-lead" \
--trailer "Issue: #482"PR 범위의 트레일러 수집 스크립트
아래 스크립트는 git log로 범위 안 커밋의 트레일러를 읽어 JSON 레코드로 만들고, AI-Assisted가 없거나, yes인데 도구·검토자가 빠진 커밋을 찾는다. 커밋 메타데이터만 읽고 코드나 프롬프트 내용은 다루지 않는다.
// 사용법: node collect-ai-trailers.mjs <범위> 예) origin/main..HEAD
// 범위 안 커밋의 AI 관련 트레일러를 JSON 레코드로 뽑고, AI-Assisted가 yes인데 필수 항목이 빠진 커밋을 찾는다.
// 프롬프트·응답 원문은 수집하지 않는다. 커밋 메타데이터만 읽는다.
import { execFileSync } from 'node:child_process';
const range = process.argv[2] || 'HEAD';
const SEP = '\x1e';
const FMT = ['%H', '%an', '%aI', '%s', '%(trailers:key=AI-Assisted,valueonly)', '%(trailers:key=AI-Tool,valueonly)',
'%(trailers:key=AI-Model,valueonly)', '%(trailers:key=Reviewed-by,valueonly)', '%(trailers:key=Issue,valueonly)'].join('%x1f');
const out = execFileSync('git', ['log', `--format=${FMT}${SEP}`, range], { encoding: 'utf8' });
const records = out.split(SEP).map((s) => s.trim()).filter(Boolean).map((line) => {
const [sha, author, date, subject, assisted, tool, model, reviewer, issue] = line.split('\x1f').map((v) => v.trim());
return { sha: sha.slice(0, 7), author, date, subject, aiAssisted: assisted || 'unknown', tool, model, reviewer, issue };
});
const missing = records.filter((r) => r.aiAssisted === 'unknown' || (r.aiAssisted === 'yes' && (!r.tool || !r.reviewer)));
console.log(JSON.stringify(records, null, 1));
missing.forEach((r) => console.log(`MISSING ${r.sha} "${r.subject}" — ${r.aiAssisted === 'unknown' ? 'AI-Assisted 트레일러 없음' : 'AI-Tool 또는 Reviewed-by 없음'}`));
console.log(`커밋 ${records.length}건 · 누락 ${missing.length}건`);
process.exitCode = missing.length ? 1 : 0;예제 저장소에 커밋 네 개를 만들었다. 모든 항목을 남긴 커밋, 도구만 남긴 커밋, AI를 쓰지 않았다고 남긴 커밋, 아무것도 남기지 않은 커밋이다. 2026-10-11 실행 결과의 요약이다(JSON 레코드 일부 생략).
$ node collect-ai-trailers.mjs main..HEAD
[
{
"sha": "6e2ca7c",
"subject": "쿠폰 검증 로직 추가",
"aiAssisted": "yes",
"tool": "claude-code",
"model": "claude-sonnet-5",
"reviewer": "@backend-lead",
"issue": "#482"
},
...
]
MISSING 87bf0e6 "로그 레벨 조정" — AI-Assisted 트레일러 없음
MISSING 6e612d0 "쿠폰 만료 테스트 추가" — AI-Tool 또는 Reviewed-by 없음
커밋 4건 · 누락 2건
exit=1종료 코드가 1이므로 CI에 넣으면 누락이 있는 PR은 병합 전에 막힌다. 처음부터 막으면 반발이 크므로 몇 주는 경고로만 운영하고, 누락 비율이 낮아진 뒤 실패로 바꾸는 것이 현실적이다.
보관과 조회
- 레코드는 커밋 해시, 파일 경로, 이슈 번호로 조회할 수 있게 저장한다. 감사에서 자주 받는 질문은 "이 파일은 언제 왜 바뀌었나", "이 릴리스에 어떤 모델이 관여했나"다.
- 변경 불가능한 저장소(추가 전용 버킷, 별도 저장소)에 보관하고 접근 권한을 제한한다.
- 빌드·배포 이벤트에 같은 커밋 해시를 붙여 두면 배포 기록과 이어서 볼 수 있다. 호출 로그 스키마는 LLMOps 모니터링 구현에서 다뤘다.
PR 템플릿에서 AI 사용 범위를 표시하게 하는 방법은 AI 생성 코드 리뷰 규칙에서 다뤘다. 트레일러는 커밋 단위, PR 템플릿은 변경 묶음 단위의 기록이라 함께 쓰면 서로를 보완한다.
초보자가 자주 실수하는 포인트
- 재현을 위해 프롬프트와 응답 원문을 저장소나 로그에 그대로 남긴다.
- AI를 쓴 커밋만 표시하게 해 "미사용"과 "누락"을 구분하지 못한다.
- 검토자를 커밋 작성자가 직접 적는 트레일러에만 의존한다.
- 처음부터 누락을 CI 실패로 처리해 팀이 규칙을 우회한다.
체크리스트
- 트레일러 키와 필수 항목을 팀 문서에 정했다.
- AI 도구의 규칙 파일에도 같은 트레일러 규칙을 적었다.
- PR 범위의 트레일러 수집이 CI에서 실행된다.
- 검토자 정보는 PR 승인 기록에서 확인한다.
- 수집 레코드를 추가 전용 저장소에 보관한다.
자주 묻는 질문
Co-Authored-By 트레일러만으로 충분하지 않나요?
어떤 모델이 관여했는지는 알 수 있지만, 트레일러가 없는 커밋이 AI 미사용인지 기록 누락인지는 구분할 수 없습니다. 감사 근거가 필요하면 AI-Assisted: no처럼 미사용도 명시하게 하는 편이 좋습니다.
스쿼시 병합하면 트레일러가 사라지지 않나요?
스쿼시 커밋 메시지를 어떻게 만드느냐에 따라 달라집니다. 개별 커밋의 트레일러가 합쳐지지 않는 설정이라면 PR 단계에서 수집한 레코드를 따로 보관하거나, 스쿼시 메시지에 트레일러를 옮기는 규칙을 둡니다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.