AI 에이전트에게 코드를 고치게 하면 변경은 빠르게 쌓이지만, 저장소에 올리기 직전에 무엇을 확인해야 하는지는 흐려지기 쉽습니다. 이 글은 에이전트가 만든 변경을 커밋하기 전에 비밀 값이 섞였는지, 워크플로 파일이 깨졌는지를 로컬에서 먼저 보고 싶은 분에게 맞춘 기록입니다.

제 개인 하네스에는 원래 GitHub Actions의 CI에 actionlint와 gitleaks가 있었습니다. 빈 곳은 커밋 직전과 푸시 직전이었습니다. 이 글은 그 구간을 로컬 검사 스크립트 하나와, 에이전트가 변경을 적용할 때 따를 순서를 적은 문장 하나로 메운 과정을 정리합니다. 스크립트는 추적 중인 워크플로를 actionlint로 보고, gitleaks를 스테이지 변경과 커밋 범위에 각각 돌립니다. 발견은 규칙 ID와 파일 위치만 보고하고 값은 출력하지 않습니다.

여기 나오는 수치와 사건은 2026-10-22 작업 기록에서 가져온 개인 하네스 저장소 두 곳의 기록값입니다. 이 블로그 저장소의 상태가 아닙니다. 스크립트, 시험, 리뷰는 AI 에이전트가 작성하고 다른 에이전트가 검토한 결과입니다. 저는 요구를 정하고 실행 기록과 판정을 읽었습니다. 사람이 코드를 한 줄씩 검토했다는 뜻으로 읽지 마십시오.

출발점: CI에는 있었고 커밋 직전에는 없었다

기록의 계획 단계는 기존 상태를 먼저 정리했습니다. 두 저장소의 CI에는 이미 actionlint와 gitleaks 단계가 있었습니다. 도구 버전은 고정 해시로 받았고, 자체 호스팅 러너에서 돌았습니다. 서버 쪽에는 검사가 있었던 셈입니다.

빈 곳은 로컬이었습니다. 훅과 무관하게 돌릴 수 있는 검사 스크립트가 없었고, gitleaks 설정 파일과 로컬 시험도 없었습니다. 에이전트가 변경을 적용하는 흐름에도 비밀 검사가 들어 있지 않았습니다. CI 결과는 변경이 원격에 올라간 뒤에 나옵니다. 그래서 로컬에서 같은 두 도구를 쓰자는 요청을 제가 했습니다. 에이전트에게 맡길 범위를 어떻게 적을지는 AI 에이전트 작업 계약 글에서 따로 정리했고, 작성과 검증을 나누는 원칙은 교차 검증 글에 적었습니다. 이 글은 그중 비밀 값과 워크플로 오류에 한정합니다.

로컬 도구는 이미 Homebrew로 설치되어 있었습니다. 기록의 버전은 actionlint 1.7.12와 gitleaks 8.30.1이었고, 이는 CI가 고정해 쓰는 버전과 같았습니다. 그래서 설치 명령은 실행하지 않았습니다.

제가 준 방향은 짧았습니다. 하나는 Homebrew에 있는 actionlint와 gitleaks를 검증에 쓰라는 것이었고, 다른 하나는 GitHub 서버에는 아무것도 설치하지 말고 로컬에서만 작업하라는 것이었습니다. 계획 부록은 이를 CI 단계를 바꾸지 않는다는 뜻으로 읽었고, 기존 CI 단계는 그대로 두었습니다.

검사 스크립트가 하는 일

스크립트는 세 가지 검사를 합니다. 첫째, 저장소에서 추적 중인 워크플로 파일을 actionlint로 검사합니다. 둘째, gitleaks를 스테이지된 변경에 대해 돌리는 검사입니다. 셋째, 지정한 커밋 범위에 대해 돌리는 검사입니다. 두 gitleaks 검사는 흐름 문장에서 각각 커밋 전과 푸시 전에 실행하도록 정해져 있습니다.

actionlint의 저장소 README는 2026-10-29에 읽은 기준으로 저장소 안에서 actionlint 명령을 실행하면 워크플로를 자동으로 찾아 오류를 검사한다고 설명합니다. 같은 README에서 종료 코드 규칙은 찾지 못했습니다. 그래서 이 글의 종료 코드 표는 스크립트가 정한 계약이며, actionlint의 공식 규칙으로 인용하지 않습니다.

gitleaks 설정은 useDefault만 씁니다. gitleaks 저장소 README(2026-10-29 조회)의 설정 예시는 useDefault가 바이너리에 내장된 기본 설정을 확장한다고 설명합니다. 기준선도 이 설정으로 충분했습니다. 두 저장소의 develop 브랜치와 전체 이력을 검사했을 때 발견이 0건이었으므로, 허용 목록 항목은 하나도 넣지 않았습니다.

같은 README는 커밋 범위를 --log-opts로 지정하는 방법을 설명합니다. 예시는 --all commitA..commitB 같은 git log 범위 형태입니다. 기록에는 범위 검사를 직접 실행한 결과가 있고 발견이 없었습니다. 반면 제가 읽은 시점의 master README에서는 --staged 설명을 찾지 못했습니다. 기록에서 스테이지 검사는 가짜 토큰을 잡아 종료 1을 냈으므로, 기록한 버전에서는 동작했습니다. 다만 README 문서로 그 플래그를 확인하지는 못했습니다. 이 차이는 그대로 밝혀 둡니다.

발견 보고는 규칙 ID와 파일 위치(file:line)만 씁니다. 값은 출력하지 않습니다. 종료 코드는 0, 1, 2 세 가지입니다. 기록에서 0은 깨끗한 트리, 1은 스테이지된 가짜 토큰과 의도적인 워크플로 오류에서 확인했습니다. 2가 무엇을 뜻하는지는 기록에서 확인하지 못했고, 아래 표에서도 미확인으로 남깁니다.

시험을 먼저 쓰고 빈 스텁에서 실패를 확인하다

구현보다 시험을 먼저 만들었습니다. 시험은 로컬 전용 파일 하나였고, T1부터 T8까지 여덟 가지를 확인합니다. Homebrew 도구가 있어야 돌아가며, CI의 검사 대상 글롭 밖에 두었습니다. 그래서 CI는 이 시험을 실행하지 않았고, 기대 실패 목록에도 항목을 더하지 않았습니다.

스크립트 대신 종료 코드 0만 내는 빈 스텁을 먼저 두었습니다. 기록의 의도된 RED는 T1, T3, T4, T6, T7 실패와 T2, T5, T8 통과였습니다. 여덟 개 중 세 개가 통과하고 다섯 개가 실패한 결과입니다. 빈 스크립트가 모든 시험을 통과한다면 시험은 아무것도 막지 못합니다. 그래서 실패가 예상한 자리에서 나는지를 구현 전에 확인했습니다. 구현을 넣은 뒤에는 여덟 개가 모두 통과했습니다(8 passed, 0 failed).

두 번째 저장소 사본에도 같은 시험과 같은 스텁을 두었고, 의도된 RED도 같았습니다. 이 사본은 나중에 따로 정정된 부분이 있는데, 아래 리뷰 항목에서 다시 씁니다.

리뷰에서 걸린 결함과 정정

리뷰는 시험 쪽과 로직 쪽을 따로 열었습니다. 기준에 못 미치면 작성자가 정정하고, 정정 뒤 재리뷰는 하지 않는 방식이었습니다. 기록에 남은 결함은 네 가지였고, 모두 정정으로 처리됐습니다. 재리뷰가 없었다는 점은 뒤의 한계에서 다시 씁니다.

--staged 뒤에 붙은 저장소 경로

로직 리뷰가 막은 항목은 B1이었습니다. 스테이지 검사가 --staged 뒤에 저장소 경로를 위치 인자로 붙이고 있었습니다. 리뷰는 이 호출 형태가 gitleaks의 명세와 맞지 않아, 스테이지 변경을 제대로 보지 못할 수 있다고 지적했습니다.

정정은 위치 인자를 없애는 것이었습니다. gitleaks는 저장소 안에서 실행하게 바꿔, 검사 대상이 인자 대신 작업 디렉터리로 정해지도록 했습니다. gitleaks README에는 "If there is no target specified as a positional argument, then gitleaks will attempt to scan the current working directory as a git repo."라는 문장이 있어, 정정된 형태와 맞습니다. 정정 뒤 시험은 모두 통과했고, 가짜 토큰 픽스처는 종료 1과 "github-pat leak.txt:1"이라는 규칙 ID와 위치만 보고했습니다.

호출자의 작업 트리에 기대던 시험

두 번째 결함은 T8이었습니다. T8은 스크립트를 호출자의 저장소에서 돌려 보는 시험이었습니다. 그래서 결과가 호출자 작업 트리에 남은 변경 상태에 따라 달라졌습니다. 같은 커밋인데도 시험 결과가 흔들리는 것이었고, 리뷰는 이를 차단 항목으로 올렸습니다.

정정은 검사 대상을 바꾸는 것이었습니다. 시험은 HEAD의 깨끗한 클론을 임시 디렉터리에 만들어 그것을 검사합니다. 클론이 실패하면 "T8 setup failed"로 보고합니다. 결과가 작업 트리 상태에 기대지 않게 된 것입니다. 이 정정도 재리뷰 없이 반영됐습니다.

값이 출력에 있어도 통과하던 시험

세 번째 결함은 두 번째 저장소 사본의 T4였습니다. 리뷰는 T4가 커밋된 토큰 보고에 값이 노출되어도 통과할 수 있다고 지적했습니다. 시험이 토큰 값의 부재를 확인하지 않은 틈이었습니다.

정정은 T3과 같은 방식으로, 생성된 토큰 값이 출력에 없다는 것을 단언하게 했습니다. 두 사본의 시험 파일이 잠시 달랐던 이유는 이 정정이 한쪽에만 들어갔기 때문입니다. 나중의 적용 흐름 변경에서 첫 사본의 T4도 같은 단언으로 맞췄습니다. 그 변경의 설명에는 스크립트가 이미 값을 가리므로 이 단언에는 관찰 가능한 RED가 없다고 적혀 있습니다. 즉 그 맞춤은 시험을 강화한 것이지, 새 실패를 보여 준 것이 아닙니다.

적용 흐름 문장의 순서 단언

적용 흐름 문장은 에이전트가 작성 파일을 적용한 뒤의 순서를 정합니다. 적용한 경로만 스테이지하고, 커밋 전에 스테이지 검사를 돌리고, 푸시 전에 범위 검사를 돌립니다. 검사가 실패하면 커밋이나 푸시를 막고, 규칙 ID와 file:line으로만 보고합니다. 값으로는 보고하지 않으며, 이력을 다시 쓰거나 강제 푸시로 넘기지 않습니다.

처음 초안에는 강제 푸시 금지가 없었습니다. 최종 문장에는 그 조항이 들어갔습니다. 시험 쪽 결함은 더 구체적이었습니다. 리뷰는 시험이 문장에 특정 문구가 있는지만 보고 있어서, 발견이 커밋과 푸시를 막는다는 조항, 강제 푸시 금지, 그리고 스테이지 검사와 커밋이 범위 검사와 푸시보다 먼저라는 순서가 빠져도 통과할 수 있다고 지적했습니다. 정정 뒤 시험의 단언은 열한 개입니다. 여기에는 --staged 명시, --range develop..HEAD 명시, 커밋 전과 푸시 전 문구, 규칙 ID와 file:line, 값 금지, 이력 재작성 금지, 적용 경로만 스테이지, 차단 조항, 순서, 강제 푸시 금지가 들어 있습니다.

적용 흐름에 검사를 끼우기

흐름 문장은 문서에 들어가기 전에 활성화 절차를 거쳤습니다. 활성화 전에 원래 파일을 백업하고, 변경 전후 해시를 기록했습니다. 변경은 흐름 문서에 두 줄을 더한 것, 새 시험 파일을 더한 것, 그리고 T4 한 줄을 맞춘 것이었습니다. 활성화 뒤 라이브에서 새 적용 흐름 시험은 11개 중 11개, 로컬 시험은 8개 중 8개가 통과했습니다. 되돌림은 라이브 해시가 변경 후 값과 같은지 확인한 뒤 백업을 되돌리는 절차로 정해 두었습니다.

그림은 그 순서를 보여 줍니다. 커밋 전 스테이지 검사와 푸시 전 범위 검사가 각각 어느 자리에 있는지, 그리고 발견이 나온 단계에서 흐름이 멈추는지를 봐 주십시오.

에이전트 변경을 적용하고 적용 경로만 스테이지한 뒤, 스테이지 검사에서 발견이 있으면 커밋을 차단하고 없으면 커밋하며, 범위 검사에서 발견이 있으면 푸시를 차단하고 없으면 푸시하는 순서를 나타낸 흐름도
발견이 나온 단계에서 커밋이나 푸시가 멈춘다는 점이 이 순서의 핵심입니다. 개념도이며 실제 실행 화면이 아닙니다.

라이브 확인과 종료 코드

활성화 뒤 두 저장소의 병합된 develop에서 임시 클론을 만들어 세 가지를 확인했습니다. 첫째, 일부러 만든 워크플로 오류는 종료 1로 끝났고, 정의되지 않은 속성을 가리키는 오류가 파일의 7행 24열 위치와 함께 보고됐습니다. 둘째, 실행 중에 만든 가짜 토큰을 스테이지하자 종료 1이 나왔고, 발견은 "github-pat leak.txt:1"처럼 규칙 ID와 위치로만 나왔습니다. 두 저장소의 출력을 합쳐 토큰 값을 grep했지만 적중이 없었습니다. 셋째, 깨끗한 트리에서는 종료 0이었습니다.

합쳐진 출력에는 도구 버전 줄과 워크플로 오류 진단도 함께 들어 있었습니다. 이 확인은 값이 빠졌는지를 보는 것이어서, 출력 전체가 깨끗하다는 뜻은 아닙니다. 임시 클론은 확인 뒤 지웠습니다. 종료 코드의 의미는 아래 표로 정리했습니다.

로컬 검사 스크립트의 종료 코드 0, 1, 2별 기록에서 확인한 상황과 출력 규칙, 적용 흐름의 동작을 나열한 표
종료 코드 2는 기록에서 상황을 확인하지 못해 미확인으로 표시했습니다. 0과 1의 값은 실행 기록에서 가져왔습니다.

계획 검증 단계에서 막힌 설정 사고

검사 스크립트와 별개로, 같은 날 계획 검증 단계가 한 번 막혔습니다. 계획 검증은 작성 모델과 다른 인스턴스가 계획 초안을 한 번 검토하는 단계입니다. 첫 시도는 모델 턴이 한 번도 돌지 않고 실패했습니다.

원인은 검증용 외부 CLI의 설정 파일이었습니다. MCP 서버 항목 하나가 enabled=false만 담은 채로 있었는데, 해당 CLI 버전이 그 블록을 invalid transport로 거부했습니다. 그 블록은 다른 실행의 활성화 변경이 더한 것이었고, 검증 요청을 만든 뒤 실행 전에 들어왔습니다. 같은 문제는 검증 프로필 말고도 네 개의 검토 프로필에 있었습니다. 기록은 이를 요청의 결함도 쿼터 소진도 아닌 설정 또는 전송 실패로 분류했고, 쿼터는 쓰지 않았다고 적었습니다.

복구는 그 활성화 변경을 되돌리는 것이었습니다. 검증을 돌리던 쪽은 라이브 하네스 파일을 직접 고치지 않았습니다. 두 번째 시도는 첫 요청의 바이트를 그대로 재사용했습니다. 이번에는 모델 턴이 돌았고, 점수 게이트가 실패를 냈습니다. 두 항목이 기준인 7점에 못 미쳤고, 차단 지적이 두 개 있었습니다. 하나는 실제 스크립트가 병합되기 전에 증거를 붙인 직접 검사를 돌리라는 것이었고, 다른 하나는 파일별 종료 코드를 따로 잡고 로컬 RED와 GREEN을 따로 판정하라는 것이었습니다. 앞선 실패를 루프가 가릴 수 있다는 이유였습니다.

정정은 한 번만 했고 재검증은 하지 않았습니다. 규칙이 그렇게 정해져 있었습니다. 제가 보기에 이 사고에서 얻은 것은, 검증 설정 파일의 변경도 검증 대상이라는 점입니다. 검사 스크립트를 아무리 잘 시험해도 검증 단계의 설정이 깨지면 계획 검토는 돌지 않습니다.

기록에서 확인하지 못한 것과 한계

다음 항목은 기록에서 확인하지 못했거나 이 글의 판단 범위를 넘습니다.

  • 기록 기간: 2026-10-22 하루의 기록입니다. 이후 실제 커밋과 푸시에서 검사가 몇 번 막았는지, 오탐이 있었는지는 기록에 없습니다.
  • 잘린 출처: 계획 문서의 5241자 구간은 출처 묶음에서 잘려 있었습니다. 푸시 로그, 작성자 응답, 스테이지 diff도 읽지 못했습니다.
  • 원인 미확인 실패: 일부 점검 시도의 실패, 즉 린트 도구 한 번과 타입 검사 한 번의 종료 1, 두 번째 저장소 시험 한 번에서 추가된 줄 15개의 원인은 기록에 없습니다.
  • 생략한 리뷰: 두 번째 저장소 사본의 로직 리뷰는 생략했습니다. 근거는 스크립트와 설정 파일이 리뷰를 마친 첫 사본과 바이트 단위로 같고, 결정적 점검이 수용 기준을 덮으며 되돌릴 수 있다는 것이었습니다. 다만 보안 관련 파일은 리뷰를 유지한다는 규칙이 있어, 기록도 이 긴장을 인정했습니다. 이 판단이 맞았는지는 기록만으로 결론 낼 수 없습니다.
  • 재리뷰 없는 정정: 이 실행의 정정은 모두 한 번이었고 재리뷰하지 않았습니다. 정정이 새 결함을 만들지 않았다는 근거는 결정적 시험과 점검뿐입니다.
  • 작동 범위: 이 검사는 실행된 곳에서만 돕니다. 기록에 있는 것은 에이전트가 따를 적용 흐름 문장입니다. 사람이 직접 하는 커밋이나 다른 기기의 커밋이 이 문장의 적용을 받는지는 기록에 없으며, 적용 대상이 아닐 가능성이 있습니다(추정). 서버 쪽 CI는 기존 그대로입니다.
  • 검사 범위: 기준선 0건은 검사가 아무것도 놓치지 않았다는 증거가 아닙니다. 기록에서 확인한 입력은 임시로 만든 가짜 토큰과 임시 워크플로 파일뿐입니다.
  • 범위 밖 사건: 같은 실행 기록에는 이 주제와 무관한 작업 조정 사고가 있어 본문에서 뺐습니다.

자주 묻는 질문

CI에 이미 있는 검사를 로컬에 또 둘 이유가 있나?

기록 기준으로 두 저장소의 CI에는 이미 두 도구가 있었지만, 커밋 직전과 푸시 직전에 돌릴 수 있는 로컬 검사는 없었습니다. CI 단계는 그대로 두고, 로컬 검사를 에이전트의 적용 흐름에 끼웠습니다.

검사에서 찾은 비밀 값은 터미널이나 로그에 어떻게 남나?

규칙 ID와 파일 위치만 보고하고 값은 출력하지 않습니다. 가짜 토큰을 스테이지한 실행에서 출력 어디에도 토큰 값이 없음을 grep으로 확인했습니다.

발견이 나오면 이력을 고치거나 강제 푸시로 넘겨도 되나?

적용 흐름 문장은 둘 다 해결 수단으로 허용하지 않습니다. 발견은 커밋이나 푸시를 막고, 규칙 ID와 위치로 보고됩니다.

허용 목록은 왜 비워 두었나?

두 저장소의 develop과 전체 이력 검사에서 발견이 0건이었기 때문입니다. 기록에는 나중에 허용 목록을 추가하는 기준이 없습니다.