리더보드 1위와 2위는 태스크 0.36개 차이였다. 내 하네스엔 두 달 반 죽어 있던 게이트가 있었다

8월 첫 주에 AI 툴 뉴스가 두 갈래로 왔다. 한쪽은 가격이었다. OpenAI가 7월 30일에 Luna 입력 단가를 80% 내렸고, Anthropic은 Opus 5를 직전 세대 절반 값에 내놨다. 다른 한쪽은 순위였다. Terminal-Bench 2.1 리더보드에서 1위와 2위가 0.4%p 차이로 붙어 있었다. 두 소식을 나란히 놓고 보니 드는 생각이 있었다. 지금이 툴을 갈아탈 타이밍인가.

갈아타기 전에 하나만 확인하고 싶었다. 0.4%p라는 게 대체 뭘 재는 숫자인가. 리더보드는 퍼센트로 나오는데 그 퍼센트의 분모가 몇인지는 표에 안 적혀 있다. 그래서 분모를 찾아 곱해봤다.

89개였다. 0.4%p는 태스크 0.36개다.

여기까진 예상한 결론이었다. "벤치마크 몇 %p 차이로 툴 고르지 마라"는 말은 나도 여러 번 했고 여러 번 들었다. 확인 사살에 가까운 산수였다.

그런데 산수를 끝내고 나니 질문이 하나 남았다. 벤치 점수가 내 결과를 안 가른다면, 가르는 건 뭔가. 하네스라고 생각했으니 내 하네스를 세어보러 갔다. 그리고 세다가, 두 달 반 동안 아무것도 검증하지 않은 채로 돌고 있던 게이트를 다시 마주쳤다. 모델을 0.4%p 위로 올릴지 말지 고민하는 동안, 내 쪽 검증 레이어는 0%로 돌고 있었다.

8월에 뉴스가 두 갈래로 왔다

먼저 가격 쪽부터 본다. 7월 30일에 OpenAI가 단가를 내렸는데, 내림폭이 모델마다 달랐다.

모델 이전 (입력/출력, 100만 토큰) 7월 30일 이후
Luna $1 / $6 $0.20 / $1.20
Terra $2.50 / $15 $2 / $12
Sol $5 / $30 동결

Luna 입력이 80% 빠졌다. Terra는 20%쯤 내렸고, 최상위인 Sol은 안 움직였다. Anthropic 쪽은 Opus 5가 $5/$25로, Fable 5의 절반 값이다. Sonnet 5는 도입가로 $2/$10을 붙여뒀고 8월 31일 무렵까지만 그 값이다. 이후엔 $3/$15로 올라간다.

이 표에서 내 눈에 먼저 들어온 건 내림폭이 아니라 내림의 모양이었다. 싼 모델은 급격히 싸지고 비싼 모델은 값이 그대로다. 값을 안 내린 자리가 성능 경쟁이 붙어 있는 자리라는 뜻이다. 뒤집으면, 값이 급격히 싸진 자리는 성능이 이미 충분해서 더 이상 값으로 방어할 필요가 없어진 구간이다.

이 단가 얘기는 그 자체로 한 편 쓸 만한 주제고, 실제로 며칠 전에 내 커밋의 92%가 AI를 거쳤고 대안 경로는 7배 느렸다는 걸 세어본 글을 썼다. 그때 확인한 게 있다. 학습 설비 쪽에서 벌어진 계산 착오의 청구서가 조정되는 자리는 늘 추론 단가와 지원 정책이다. 그러니까 8월의 가격 인하는 나한테 "싸졌다"는 뉴스라기보다 "값이 움직이는 축이 아직 살아 있다"는 신호에 가까웠다.

다른 한쪽 뉴스가 리더보드다.

순위 모델 Terminal-Bench 2.1
1 GPT-5.6 Sol (xhigh effort) 89.5%
2 Claude Opus 5 (max effort) 89.1%
3 GPT-5.6 Terra (max effort) 88.0%

Terminal-Bench는 샌드박스 터미널 안에서 실제 작업을 시키는 벤치다. 모델 훈련부터 시스템 관리까지 89개 태스크가 들어 있고, 에이전트가 셸을 잡고 끝까지 해내면 통과다. 코드 스니펫 하나 맞히는 방식이 아니라 도구를 쓰는 능력을 재는 쪽이라, 나처럼 터미널에 에이전트를 붙여 쓰는 사람에겐 다른 벤치보다 관련이 있어 보였다.

그래서 더 궁금해졌다. 89.5와 89.1의 거리는 실제로 얼마나 되나.

0.4%p를 태스크 개수로 바꿔봤다

퍼센트는 거리감을 지운다. 89.5와 89.1을 나란히 적어두면 눈이 알아서 "거의 같네"라고 읽어주긴 하는데, 그 "거의"가 얼마인지는 안 나온다. 그래서 분모를 곱했다.

89 × 0.004 = 0.356

1위와 2위를 가른 건 태스크 0.36개다. 태스크는 0.36개 단위로 존재하지 않으니 이건 사실상 "한 개도 차이 안 났다"는 뜻이다. 89개짜리 시험에서 두 응시자가 정확히 같은 개수를 맞혔는데, 부분 점수나 재시도 평균 같은 걸 반영하다 보니 소수점 아래에서 갈린 것에 가깝다.

2위와 3위로 내려가도 사정이 크게 다르지 않다.

89 × 0.011 = 0.979

한 개다. 태스크 하나가 통과에서 실패로 뒤집히면 2위와 3위가 자리를 바꾼다. 1위와 3위 사이도 1.5%p이니 태스크로는 1.34개다. 상위 세 개가 전부 태스크 한두 개 안에 들어 있다.

에이전트 태스크에서 태스크 하나가 뒤집히는 데는 별게 필요하지 않다. 셸 명령의 출력 순서가 조금 다르게 오거나, 타임아웃이 아슬아슬하게 걸리거나, 파일 시스템 상태가 직전 스텝에서 살짝 달라져 있으면 된다. 같은 모델을 같은 조건에서 두 번 돌려도 이 정도 폭은 나온다. 그러니까 0.4%p는 실력 차가 아니라 그날의 순위다.

여기에 하나가 더 있다. 표를 다시 보면 1위 기록은 xhigh effort고 2위 기록은 max effort다. 추론에 얼마나 힘을 쓸지 설정한 값이 서로 다르다. 두 기록은 같은 조건에서 나온 게 아니다. 각 진영이 자기 모델에서 가장 좋은 숫자가 나오는 설정을 골라 제출했을 것이고, 그건 부정행위가 아니라 리더보드라는 형식이 원래 그렇게 굴러가는 방식이다. 다만 그 결과로 만들어진 표를 "A가 B보다 낫다"로 읽으면 표가 하지 않은 말을 읽는 게 된다.

effort 설정이 뭘 바꾸는지는 이름만 봐도 짐작이 간다. 답을 내기 전에 모델이 스스로 굴리는 추론 분량이다. 높일수록 같은 질문에 더 많은 토큰을 태운다. 그러니까 xhigh와 max가 가리키는 건 서로 다른 모델이 아니다. 같은 모델을 얼마나 오래, 얼마나 비싸게 굴렸느냐다.

이 사실이 표를 읽는 방식을 바꾼다. 리더보드 한 줄은 "이 모델이 이만큼 한다"보다 "이 설정으로 이만큼 태웠더니 이 점수가 나왔다"에 가깝다. 그런데 태운 양은 표에 안 적혀 있다. 89.5% 옆에 xhigh라는 라벨만 붙어 있지, 그 89개 태스크를 도는 데 토큰이 몇 개 들었는지는 없다. 배수가 얼마인지 나는 모른다. 공개된 자리에서 본 적이 없다. 다만 추론 분량을 올리는 설정이 비용을 그대로 둔 채 점수만 올려주지는 않는다는 건 확실하다.

성능이 모델의 고정 속성이 아니게 됐다는 뜻이기도 하다. 조절 가능한 값이 됐다. 같은 모델을 얼마나 비싸게 돌릴지 내가 정하고, 그 선택에 따라 점수가 움직인다. 그러면 "어느 모델이 낫냐"는 질문은 "얼마를 태울 거냐"는 질문과 분리되지 않는다. 두 질문을 분리한 채로 답을 주는 형식이 리더보드다.

앞의 가격표를 다시 꺼내면 이 얘기가 선명해진다. 1위를 낸 Sol은 7월 30일 인하에서 유일하게 값이 안 움직인 모델이다. $5/$30 그대로다. 그 자리에서 xhigh를 걸고 89.5%가 나왔다. 같은 날 Luna는 입력 단가가 $0.20까지 떨어졌다. 두 사건은 같은 표에 놓이지 않는다. 하나는 최상단 성능을 가장 비싼 설정으로 밀어붙인 기록이고, 다른 하나는 하단 단가가 무너진 사건이다. 축이 서로 다르다.

따라가다 보면 순위가 뒤집히는 조건도 달라 보인다. 표에서 2위가 1위가 되는 데 새 모델이 필요하지 않다. 같은 모델을 한 단계 높은 설정으로 다시 제출하기만 해도 태스크 하나쯤은 움직일 수 있다. 앞에서 본 대로 태스크 하나면 순위가 바뀐다. 그러니 다음 달에 순위가 뒤바뀌었다는 소식이 오면, 그게 모델이 좋아진 결과인지 제출 조건이 올라간 결과인지 표만 보고는 구분이 안 된다.

내가 매달 결제하는 쪽은 후자 축이다. 그리고 나는 max effort로 일하지 않는다. 하루에 도는 작업의 대부분은 파일 몇 개 읽고 한 줄 고치는 일이고, 거기에 최대 추론을 걸 이유가 없다. 리더보드 상단의 두 기록은 각 진영이 자기 모델에서 뽑아낼 수 있는 최대치를 뽑은 조건이다. 내가 실제로 쓰는 조건은 그 표에 안 나온다. 그러니까 0.4%p가 내 환경으로 옮겨오는지조차 확인할 방법이 없고, 옮겨온다 해도 태스크 0.36개다. 벤치 순위표와 월말 청구서는 서로 다른 두 장의 종이인데, 툴을 갈아탈지 고민하는 동안 나는 앞장만 보고 있었다.

이쯤에서 원래 질문으로 돌아왔다. 나는 이 표를 보고 툴을 갈아탈지 고민하고 있었다. 그런데 이 표가 실제로 알려주는 건 "상위 모델들이 이 벤치에서는 서로 구분되지 않는다"까지다. 그럼 내가 매일 체감하는 결과 차이는 어디서 오는 건가. 표 바깥에서 온다는 얘기가 된다.

그럼 내 결과를 가른 건 뭐였나

표 바깥이 어디인지는 이미 알고 있다고 생각했다. 하네스다.

이 감각을 내가 짠 건 하네스가 아니라 아웃터 루프였다에서 한 번 정리한 적이 있다. 그때 도달한 지점은 이랬다. 지각하고 추론하고 행동하고 관찰하는 이너 루프는 내가 짜는 게 아니다. 하네스가 준다. 내가 짜는 건 그 루프를 언제 켜고, 무슨 일감을 물리고, 나온 결과를 무엇으로 검증하고, 어느 조건에서 멈출지다.

그렇다면 내가 실제로 짜놓은 게 얼마나 되는지 세어볼 만했다. 파일을 열어 셌다.

항목 개수 위치
에이전트 6 .claude/agents/
스킬 20 .claude/skills/
3 (총 234줄) .claude/hooks/

훅 234줄의 내역은 pre-commit-check.sh 166줄, session-start.sh 56줄, stop-reminder.sh 12줄이다. 세 개 중 하나가 전체의 70%를 차지한다.

이 비율이 말이 되나 싶어 한 번 더 들여다봤다. 그리고 말이 된다는 걸 확인했다. session-start.sh는 세션 시작할 때 파일 세 개를 컨텍스트 앞에 붙이는 일만 한다. stop-reminder.sh는 다음 세션용 메모가 비어 있으면 한 줄 안내를 띄운다. 둘 다 편의 장치다. 반면 pre-commit-check.sh는 커밋을 실제로 막는 유일한 코드다. 무언가를 막는 코드는 항상 길어진다. 막을지 말지 판단하는 데 필요한 예외 처리가 붙기 때문이다.

스킬 20개 쪽은 다르게 읽힌다. 스킬은 실행되는 코드가 아니라 상황에 맞게 불려 나오는 문서다. 리서치할 때 뭘 확인하는지, 본문 쓸 때 어떤 표현을 쓰지 않는지, 발행 전에 뭘 세는지가 각각 파일로 적혀 있다. 20은 내가 그동안 명시적으로 적어둔 규칙 묶음의 개수다. 기능이 스무 개 붙어 있다는 뜻이 아니다. 모델을 갈아끼워도 이건 그대로 남는다. 내 결과를 가르는 게 이쪽이라고 생각한 근거가 여기 있었다.

훅이 세 개뿐이라는 게 적어 보일 수도 있는데, 며칠 전 keyv 침해가 .claude/settings.json에 SessionStart 훅을 심는 경로였다는 걸 확인하고 내 레포 여섯 곳의 훅 29개를 전부 세어봤을 때 생각이 좀 바뀌었다. 훅은 자동으로 실행되는 코드다. 개수가 많다는 건 자동 실행 지점이 많다는 뜻이고, 그 각각이 검토 대상이다. 적은 게 미덕일 수 있다.

여기까진 그냥 인벤토리였다. 문제는 세다가 걸린 자리에서 시작됐다.

세다가 걸린 것, 두 달 반 동안 아무것도 안 한 게이트

AI_AUTOMATION.md의 변경 이력을 훑는데 2026-07-29 행에 이렇게 적혀 있었다. "발행 게이트 제거." 그 게이트가 도입된 건 2026-05-12다. 두 달 반 동안 돌았고, 돌아가는 내내 아무것도 검증하지 않았다.

구조는 이랬다. 발행 스크립트인 deploy_to_blogger.py가 업로드 직전에 _workspace/briefs/{slug}-ai-check.md 파일을 열어서 판정: 합격이라는 문자열을 grep했다. 없으면 업로드를 막는다. 표면적으로는 그럴듯하다. AI 문체 검사를 통과한 글만 발행된다는 얘기니까.

그 문자열을 누가 쓰는지가 문제였다.

검사받는 에이전트가 직접 썼다. 문체 검사를 수행한 에이전트가 검사 결과를 파일에 적고, 발행 스크립트가 그 파일에서 자기 판정을 읽는다. 검문소와 통행증 발급처가 같은 사람이었다. 이 구조에서 게이트가 실제로 검사한 건 글의 품질이 아니라 서류의 존재다.

실패 방식이 하나 더 있었다. 같이 붙어 있던 품질 게이트는 파일 전체에서 \bFAIL\b를 찾아서, 걸리면 막았다. 그런데 체크리스트 문서에 "FAIL 조건 없음"이라고 적으면 그 줄에도 FAIL이 들어 있다. 아무 문제 없는 글이 "문제 없음"이라고 적혀 있다는 이유로 발행이 막혔다.

두 개를 합치면 실패 방향이 정확히 반대로 서 있었다는 게 보인다.

  • 나쁜 글은 통과한다. 파일에 판정: 합격이라고 쓰기만 하면 된다.
  • 좋은 글이 차단된다. 브리프 파일을 안 만들었거나, 만들었는데 그 안에 FAIL이라는 단어가 스쳤으면 업로드가 안 된다.

게이트가 정상 작업을 막기 시작하면 사람은 게이트를 고치지 않는다. 끈다. 실제로 그 게이트에는 도입 시점부터 --skip-gate 플래그가 같이 붙어 있었다. 우회로가 처음부터 문에 달려 있었다. 2026-07-29에 게이트를 제거하면서 이 플래그는 하위 호환으로 받되 무시하도록 바꿨다.

그런데 두 달 반이다. 그동안 나는 왜 한 번도 이상하다고 느끼지 못했나. 이 질문을 좀 오래 붙들고 있었는데, 답이 게이트 바깥에 있었다.

통과할 때 게이트는 아무 말도 안 한다. 이 레포는 그걸 운영 규칙으로도 적어뒀다. 성공하면 조용히 넘어가고 로그를 남기지 않는다. 검사가 시끄러워지는 건 실패했을 때뿐이다. 막힌 파일 이름을 찍고 이유를 찍고 종료 코드를 1로 내려놓는다.

그리고 이 게이트는 실패한 적이 없었다. 에이전트가 매번 판정: 합격을 썼으니까. 스크립트는 매번 그 문자열을 찾았고, 찾았으니 조용히 다음 줄로 넘어갔다. 발행은 계속 됐다. 터미널에는 업로드 성공만 떴다.

여기가 진짜 문제인데, 조용히 통과하는 게이트와 아예 존재하지 않는 게이트는 로그가 똑같다. 둘 다 아무것도 출력하지 않는다. 내가 deploy_to_blogger.py를 직접 열어보기 전까지, 그 코드가 돌고 있다는 것과 그 코드가 지워져 있다는 것을 구분해줄 관측 지점이 나한테 없었다. 매번 통과한다는 사실은 게이트가 일하고 있다는 증거처럼 읽혔다. 같은 데이터가 게이트가 아무것도 안 한다는 증거이기도 하다는 건 나중에 알았다.

\bFAIL\b 오탐 쪽은 사정이 달랐다. 그건 소리를 냈다. 정상 글이 막히면 나는 즉시 알았고, 그래서 즉시 대응했다. 파일 문구를 바꾸거나 --skip-gate를 붙이거나. 두 실패가 같은 게이트 안에 나란히 들어 있었는데 하나만 내 눈에 들어왔던 이유가 이거다. 나를 성가시게 한 쪽만 내가 인지했다. 나를 방해하지 않는 고장은 고장으로 접수되지 않는다. 이번에 이게 다시 눈에 들어온 것도 게이트를 점검하러 갔기 때문이 아니다. 리더보드 숫자가 이상해서 내 하네스에 뭐가 몇 개 있는지 세어보다가 변경 이력 한 줄에 걸렸다. 목적이 다른 작업을 하다 우연히 지나간 자리였다. 우연에 기대지 않고 이걸 발견할 경로가 내 레포에 있었는지는 아직 모르겠다.

이 구조가 왜 낯익은가 했더니, 며칠 전에 다른 각도에서 한 번 만났던 형태였다. 계획서를 먼저 쓰고 그대로 구현시키는 방식이 왜 나한테 안 맞았는지 쓴 글에서 확인한 게 이거였다. 승인받은 설계 문서가 있어도 그 문서가 틀렸으면 버그는 그대로 나간다. 문서의 존재가 검증을 대신할 수 없다. 그때는 그 얘기를 개발 절차 쪽에서 했는데, 발행 게이트는 같은 병을 코드로 박아둔 버전이었다. 서류가 있으면 통과. 서류가 뭐라고 적혀 있는지는 서류를 쓴 쪽이 정함.

살아남은 게이트는 뭐가 달랐나

게이트를 다 걷어낸 건 아니다. pre-commit-check.sh의 시크릿 스캔은 그대로 뒀다. 왜 이건 남겼나를 설명하려면 그 코드가 실제로 뭘 보는지 봐야 한다.

이 훅은 스테이지된 파일을 하나씩 열어서 정규식을 돌린다. AKIA로 시작하는 16자리 대문자 영숫자, ghp_로 시작하는 GitHub 토큰, xoxb- 같은 Slack 토큰, 사설키 헤더와 base64 본문의 동시 존재 여부. 걸리면 커밋을 막고, 탐지된 값 자체는 절대 출력하지 않는다. 출력하는 순간 그게 2차 유출이니까.

여기서 결정적인 차이가 나온다. 파일에 AKIA 패턴이 있느냐 없느냐는 모델이 뭐라고 주장해도 값이 안 바뀐다. 에이전트가 "이 파일엔 시크릿이 없습니다"라고 아무리 확신에 차서 써도, grep은 그 문장을 안 읽는다. 파일을 읽는다.

이 훅에서 내가 나중에야 의미를 알아본 부분이 하나 더 있다. 오탐을 줄이려고 붙여둔 두 장치다. 하나는 자리표시자 허용목록이다. 값에 EXAMPLE이나 FAKE, DUMMY, CHANGE_ME 같은 게 박힌 줄은 문서용 예시로 보고 넘긴다. 실제 발급된 시크릿에 EXAMPLE이 들어갈 일은 없으니까. 다른 하나는 규칙별 적용 범위다. AKIA 처럼 구조가 뚜렷한 패턴은 모든 파일에 돌리지만, password: 뒤에 뭔가 길게 붙어 있으면 잡는 추측성 규칙은 코드와 설정 파일에만 돌린다. 마크다운 문서에 예시 비밀번호가 적혀 있는 건 정상이기 때문이다.

이 두 장치가 붙어 있는 이유가 코드 주석에 적혀 있는데, 읽고 나서 좀 뜨끔했다. 앞에서 죽은 게이트를 두고 내가 한 말이 여기엔 처음부터 적혀 있었다. --no-verify 한 번이면 된다는 것. 탐지율만 올리고 오탐을 방치한 게이트는 꺼진 게이트가 된다. 이건 앞서 본 \bFAIL\b 오탐과 정확히 같은 문제인데, 한쪽은 예외 처리를 붙여서 살아남았고 다른 한쪽은 붙이지 못해서 죽었다.

그래서 판별 기준이 하나 나왔다. 이 글에서 내가 쓰게 된 유일한 도구이기도 하다.

게이트가 검사하는 값을, 검사받는 쪽이 쓸 수 있는가?

쓸 수 있으면 그건 게이트가 아니라 서류함이다. 이 기준으로 내 레포의 검사 항목들을 다시 분류해봤다.

검사 대상 검사받는 쪽이 그 값을 쓸 수 있나 게이트인가
시크릿 패턴 매칭 못 씀 (파일에 있거나 없거나) 게이트
글자 수, 이미지 개수, 내부링크 개수, 자리표시 주석 못 씀 (파일 열어 세면 나옴) 게이트
판정: 합격 문자열 씀 (에이전트가 직접 적음) 서류함
0~100 품질 점수 씀 (같은 모델이 매김) 서류함

아래 두 줄은 제거했고 위 두 줄은 남겼다. 발행 전에 세는 네 숫자가 길이, 히어로 이미지, 내부링크, 자리표시 주석인 것도 같은 이유다. 넷 다 파일을 열어 세면 나오는 값이고, 글을 쓴 쪽이 값을 바꿀 방법이 없다.

물론 이건 필요조건이지 충분조건이 아니다. 검사 대상을 제대로 골랐어도 검사가 실제로 도달하는지는 별개 문제다. 이 시크릿 스캔도 이미 두 번 뚫렸다. 가짜 시크릿 여섯 개를 넣고 돌려봤더니 탐지가 0건이었던 적이 있었고, 그걸 고친 다음엔 훅이 커밋되는 파일을 안 보고 워킹 트리를 보고 있었던 적도 있었다. 검사 대상은 옳았는데 검사 범위가 틀렸던 경우다.

그래도 이 두 실패는 고칠 수 있는 종류였다. 패턴을 추가하고 읽는 대상을 스테이지된 블롭으로 바꾸면 된다. 판정: 합격 게이트는 그렇게 못 고친다. 문자열을 누가 쓰느냐를 바꾸지 않는 한 어떤 패턴을 추가해도 같은 자리로 돌아온다.

같은 실수를 한 번 더 했다

여기까지 쓰고 나서 좀 찜찜했다. 같은 형태가 하나 더 있었기 때문이다.

이 레포에는 "하루 최대 1편"이라는 규칙이 있었다. L1 규칙, 그러니까 위반 금지로 박혀 있었다. 근거는 관측이었다. 2026년 4월과 5월에 나흘 동안 35편을 몰아 올렸고, 그 뒤에 AdSense 거절 통보가 왔다. 사유는 "가치가 별로 없는 콘텐츠"였다.

그런데 구글 AdSense 프로그램 정책에는 발행 빈도 조항이 없다. 이 규칙은 정책에서 온 게 아니라 레포가 스스로 만든 것이다. 두 사건이 같이 나타났다는 관측에서 앞의 것을 원인으로 굳혔다.

그 35편을 다시 세어보면 다른 숫자가 나온다. 길이 중앙값 3,600자, 이미지 0장, 내부링크 0개. 벌크 타이밍은 이 글들이 한꺼번에 노출된 경로였지 그 자체가 변수는 아니었을 가능성이 높다.

여기서 한 걸음 물러서야 한다. 나는 지금 "얇으면 거절된다"는 새 인과를 세우고 싶은 게 아니다. 그건 방금 비판한 오류를 방향만 바꿔서 반복하는 것이다. 실제로 얇은 뉴스 요약 30편으로 승인된 사이트도 있다. 내가 확인할 수 있는 건 여기까지다. 내 레포가 검증되지 않은 근거를 L1 규칙으로 박아뒀고, 그 결과 조건을 다 갖춘 글이 "오늘 두 번째"라는 이유로 발행되지 못하고 있었다. 거절 사유의 진짜 인과가 뭐였는지는 여전히 모른다.

2026-08-04에 이 규칙을 폐기하고 편당 조건 확인으로 바꿨다. 발행 시각을 세는 대신 네 숫자를 센다. 관측 수치는 하나도 안 버렸다. 35편도, 3,600자도, 0장도 그대로 문서에 남아 있다. 바뀐 건 그 관측에서 규칙으로 넘어가는 다리다.

두 사례를 겹쳐놓고 보니 공통점이 하나 보였다. 둘 다 내가 뭔가를 검증하고 있다고 느끼게 해주는 장치였다는 것. 판정: 합격이 파일에 적혀 있으면 문체를 검사한 기분이 들고, 오늘 한 편만 올렸으면 품질을 지킨 기분이 든다. 그 기분이 실제 검증을 대체하는 동안 두 달 반이 지나갔다.

터미널로 들어온 스캐너에도 같은 질문을 한다

이 기준을 손에 쥐고 있으니 7월 뉴스 하나가 다르게 보였다.

7월 22일에 Claude Security 플러그인이 베타로 나왔다. Claude Code 세션 안에서 멀티에이전트로 취약점을 스캔하고, 사용자가 고르면 그 항목을 패치 파일로 만들어준다. 방향 자체는 담론의 흐름과 맞는다. AI 코딩 툴의 평가축이 생산성에서 보안과 컴플라이언스, 감사 추적, 거버넌스 쪽으로 옮겨가고 있고, 이건 그 축 위에 놓인 도구다.

내가 걸리는 지점은 "AI가 쓴 코드를 AI가 스캔한다"는 구조 자체가 아니다. 그 구조는 틀리지 않았다. 스캐너가 검사하는 게 "이 파일에 문자열 결합으로 만든 SQL이 있냐" 같은, 모델 출력과 무관하게 파일에서 확인되는 사실이라면 그건 유효한 검사다. 정규식이 하던 일을 더 잘하는 것에 가깝다.

틀리는 건 다른 자리다. 스캐너가 만들어낸 자체 판정 라벨을 통과 조건으로 삼는 순간이다. "스캔 완료, 심각도 높음 0건"이라는 출력을 CI가 grep해서 배포를 허용하기 시작하면, 그건 내가 두 달 반 동안 돌렸던 그 게이트와 같은 물건이 된다. 라벨을 쓰는 쪽과 검사받는 쪽이 같으니까.

그래서 이 플러그인에 물어야 할 질문은 성능이 아니다. 이 도구가 내놓는 결과물 중에 어디까지가 파일에서 확인되는 사실이고, 어디부터가 도구의 판단인가. 그리고 그 경계를 쓰는 사람이 구분할 수 있게 출력이 설계돼 있는가.

경계가 어디쯤 그어질지는 돌려보지 않아도 짐작해볼 수 있다. "이 줄에서 문자열 결합으로 SQL을 만들고 있다"는 파일에서 확인된다. 줄 번호가 붙고, 그 줄을 열면 있거나 없다. 반면 "이건 실제로 악용 가능하다", "심각도 높음"은 판단이다. 그 값이 어디서 들어오는지, 앞단에 검증 레이어가 있는지, 그 경로가 외부에 열려 있는지에 달려 있다. 같은 코드가 프로젝트마다 다르게 판정되고, 같은 프로젝트 안에서도 앞단이 바뀌면 판정이 따라 바뀐다. 판단은 파일 하나에 담겨 있지 않고 주변 맥락에 흩어져 있다.

CI가 grep할 수 있는 건 앞쪽뿐이다. 패턴과 줄 번호는 값이 고정돼 있으니 조건으로 쓸 수 있다. 그런데 사람이 자동화하고 싶어지는 건 늘 뒤쪽이다. 앞쪽만 세면 경고가 수백 개 나오고 그중 대부분은 손댈 필요가 없다. 뒤쪽 라벨은 그 수백 개를 세 개로 줄여준다. 배포를 막을 조건으로 삼기에 딱 좋은 모양이다. 쓸모 있는 쪽과 검증 가능한 쪽이 정확히 반대편에 서 있고, 나는 두 달 반 동안 쓸모 있어 보이는 쪽을 조건으로 걸어뒀던 사람이다.

나는 아직 이걸 안 돌려봤다. 그러니까 이 도구가 어느 쪽인지는 모른다. 그래서 돌려본다면 정확도부터 재지는 않을 것 같다. 먼저 볼 건 출력의 모양이다. 패턴이 걸린 위치와 그 위치에 대한 평가가 한 줄에 섞여 나오는지, 아니면 따로 떨어져 나오는지. 섞여 나오면 자동화하는 쪽에서 둘을 분리할 방법이 없고, 분리할 수 없으면 통과 조건으로 승격시킬 수 있는 부분도 없다. 반대로 위치와 패턴이 별도 필드로 떨어져 나오면 거기까지는 조건으로 쓸 수 있다. 판정 라벨은 사람이 읽는 자리에 두고.

이건 도구의 성능에 대한 요구가 아니라 출력 형식에 대한 요구다. 정확도는 올라갈 여지가 늘 있지만, 사실과 판단이 한 문자열에 뭉쳐 나오는 형식은 정확도가 아무리 올라가도 같은 자리로 돌아온다. 판정: 합격이 그랬다. 그 게이트의 문제는 문체 검사가 부정확했다는 게 아니었다. 검사 결과가 그 문자열 하나로 압축돼서 나왔고, 그 문자열을 검사받는 쪽이 썼다는 것이었다.

다만 돌려볼 때 뭘 볼지는 정해졌다. 스캔 결과를 그대로 믿을지가 아니라, 그 결과의 어느 부분을 자동화된 차단 조건으로 승격시킬 수 있는지를 볼 것이다. 승격시킬 수 있는 부분은 생각보다 적을 것 같다는 예감이 있는데, 이건 예감이지 결론이 아니다.

모델을 0.4%p 올리기 전에

원래 질문으로 돌아오면 이렇다. Terminal-Bench에서 1위와 2위가 태스크 0.36개 차이라면, 모델을 갈아타서 얻을 수 있는 건 소수점 이하다. 반면 내 하네스에는 두 달 반 동안 0을 검증한 게이트가 있었다. 어느 쪽을 손대는 게 이득인지는 계산할 필요도 없었다.

그래서 벤치마크 순위를 들여다보기 전에 자기 레포에서 세볼 만한 게 몇 가지 생겼다.

하네스에 뭐가 몇 개 있는지 센다. 에이전트, 스킬, 훅, 그리고 훅의 줄 수. 나는 6과 20과 3과 234가 나왔다. 개수 자체가 좋고 나쁜 건 아닌데, 세보기 전엔 이게 많은지 적은지도 몰랐다는 게 문제였다.

게이트마다 앞의 질문을 한 번씩 던져본다. 이 값을 검사받는 쪽이 쓸 수 있나. 답이 예면 선택지는 둘이다. 지우거나, 검사 대상을 파일에서 확인되는 사실로 바꾸거나.

그 게이트가 마지막으로 뭔가를 실제로 막은 게 언제인지 본다. 기억이 안 나면 안 막고 있었을 확률이 높다. 내 경우엔 두 달 반이었고, 그동안 한 번도 이상하다고 느끼지 못했다.

우회 플래그도 찾아본다. --skip-gate 가 문에 달려 있다면 그 게이트는 이미 신뢰받지 못하고 있다.

그리고 마지막으로, 규칙마다 근거가 어디서 왔는지 적어둔다. 정책 문서에서 온 것과 내가 관측에서 유추한 것을 같은 무게로 취급하지 않는다. 내 레포의 "하루 1편"은 후자였는데 전자인 척 L1 자리에 앉아 있었다.

리더보드는 계속 갱신될 것이고, 1위와 2위는 계속 붙어 있을 것이다. 그 표를 몇 번 더 들여다보는 동안 내 게이트가 또 조용히 0을 검증하고 있을 수도 있다. 다음에 순위가 바뀌었다는 소식을 보면, 표를 보기 전에 .claude/hooks 부터 열어볼 생각이다.

댓글

이 블로그의 인기 게시물

Google I/O 끝나고, 1편을 다시 펼쳤다

M4 맥북에서 로컬 LLM으로 5일을 살아본 후

AI랑 4개월 일하면서 네 번 같은 자리에서 막혔다