DHH는 날짜를 짚었고 저는 AI에 손코딩을 넘긴 날을 모릅니다

이미지
DHH가 올해 Rails World에서 한 발표를 요약한 영상을 몇 편 봤습니다. 원본 발표를 처음부터 끝까지 본 건 아니고, 한국어로 정리해 준 유튜브 영상 네 편을 이어서 봤습니다. 발표를 남이 정리해 준 걸로 보고 대충 알았다고 넘어가는 것도, 생각해 보면 요즘 제가 코드를 대하는 방식이랑 비슷합니다. 원본을 다 읽지 않고 정리된 결과를 보고, 이상한 데가 있으면 그때 찾아봅니다. 내용이 많았는데 제일 오래 남은 건 날짜 하나였습니다. 2025년 11월 24일. 영상에 따르면 DHH는 이날을 "우리 시대의 코닥 브라우니"라고 불렀다고 합니다. Claude Opus 4.5가 나온 날입니다. 1900년에 1달러짜리 카메라가 나오면서 초상화를 그리던 화가들이 일감을 잃고 방향을 틀었던 것처럼, 그날부터 프로그래머의 일이 바뀌었다는 얘기였습니다. 그리고 기술은 비탈길처럼 매끄럽게 오르는 게 아니라 한참 평평하다가 어느 날 한 칸 뛰는 계단처럼 온다고 했습니다. 영상을 보면서 저도 모르게 제 날짜를 찾고 있었습니다. 저는 언제였지. 생각이 안 났습니다. 마지막으로 직접 친 코드 어림잡을 수 있는 건 있습니다. 2025년 9월쯤부터는 제가 코드를 직접 쓴 기억이 없습니다. DHH가 짚은 날보다 두 달쯤 앞입니다. 이걸 날짜라고 하기는 좀 어렵습니다. 9월 몇 일에 무슨 일이 있어서 그날부터 안 쓴 게 아니라, 거꾸로 짚어 올라가다 보니 그쯤부터는 기억이 비어 있다는 정도입니다. 어느 날 키보드에서 손을 뗀 게 아니라 손으로 친 마지막 줄이 언제였는지 생각해 보니 안 떠오르는 겁니다. 두 달 앞이라고 해서 제가 남들보다 빨랐다는 뜻도 아닙니다. 그때 무슨 모델을 쓰고 있었는지도 정확히 기억이 안 납니다. DHH는 특정 모델이 나온 날을 짚었는데 저는 어떤 모델 때문에 넘어왔는지를 말할 수가 없습니다. 모델이 바뀐 날은 발표가 있으니 찾아보면 나오겠지만, 제가 바뀐 날은 아무 데도 적혀 있지 않습니다. 커밋 기록을 뒤져 보면 뭔가 나올...

Terminal-Bench 1·2위 격차 0.4%p 를 태스크 수로 환산하면

밝은 실내 육상 트랙 결승선 부근에서 두 레인의 간격이 거의 붙어 있는 클로즈업

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으로 돌고 있었습니다.

가격표와 순위표

가격 쪽은 모델마다 내린 폭이 달랐습니다. Luna는 크게 내렸고 Terra는 조금 내렸고, 제일 위에 있는 Sol은 값이 그대로였습니다. Anthropic 쪽은 Opus 5가 Fable 5의 절반 값이고, Sonnet 5는 8월 말까지만 도입가를 붙여두고 그다음엔 올린다고 했습니다.

저는 내린 폭보다 내린 모양이 더 눈에 들어왔습니다. 싼 모델은 확 싸지고 비싼 모델은 그대로입니다. 값을 안 내린 쪽은 아직 성능 경쟁이 붙어 있는 쪽이고, 값이 확 싸진 쪽은 성능이 이미 충분해서 더는 값으로 버틸 필요가 없어진 쪽처럼 보였습니다.

단가 얘기는 며칠 전에 내 커밋의 92%가 AI를 거쳤고 대안 경로는 7배 느렸다는 걸 세어본 글에서 한참 했습니다. 그때 느낀 건 학습 설비 쪽에서 계산이 어긋나면 그 청구서는 늘 추론 단가와 지원 정책으로 돌아온다는 거였습니다. 그래서 8월의 가격 인하도 저한테는 싸졌다는 소식이라기보다 값이 언제든 바뀔 수 있다는 신호에 가까웠습니다.

순위 쪽은 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.36개

퍼센트로 적어두면 거리감이 잘 안 옵니다. 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위 사이도 태스크로 치면 한 개 남짓이라, 상위 셋이 전부 태스크 한두 개 안에 몰려 있었습니다.

에이전트 태스크 하나가 통과했다가 실패하는 데는 별게 필요 없습니다. 셸 명령 출력 순서가 조금 다르게 오거나, 타임아웃이 아슬아슬하게 걸리거나, 직전 단계에서 파일 시스템 상태가 살짝 달라져 있으면 됩니다. 같은 모델을 같은 조건에서 두 번 돌려도 그 정도 폭은 나올 것 같습니다. 그래서 0.4%p는 실력 차라기보다 그날의 순위로 보였습니다.

표를 다시 보다가 하나 더 걸렸습니다. 1위 기록은 xhigh effort이고 2위 기록은 max effort입니다. 추론에 힘을 얼마나 쓸지 정하는 설정이 서로 다릅니다. 두 기록은 같은 조건에서 나온 게 아닙니다. 각 진영이 자기 모델에서 제일 좋은 숫자가 나오는 설정을 골라 냈을 거고, 리더보드라는 게 원래 그런 식으로 돌아가는 것 같습니다. 다만 그렇게 만들어진 표를 A가 B보다 낫다로 읽으면 표가 하지 않은 말을 제가 대신 읽는 게 됩니다.

effort 설정은 답을 내기 전에 모델이 스스로 하는 추론의 양입니다. 높일수록 같은 질문에 토큰을 더 씁니다. 그러니 xhigh와 max가 가리키는 건 서로 다른 모델이 아니라 같은 모델을 얼마나 오래, 얼마나 비싸게 돌렸느냐입니다.

이걸 알고 나니 표가 다르게 읽혔습니다. 리더보드 한 줄은 이 모델이 이만큼 한다보다는 이 설정으로 이만큼 썼더니 이 점수가 나왔다에 가깝습니다. 그런데 쓴 양은 표에 없습니다. 89.5% 옆에 xhigh라는 라벨만 있고, 그 89개 태스크를 도는 데 토큰이 얼마나 들었는지는 안 나와 있습니다. 배수가 얼마인지는 저도 모릅니다. 그래도 추론을 늘리는 설정이 비용은 그대로 두고 점수만 올려줄 리는 없다고 봅니다.

성능이 모델에 고정된 성질이 아니라 조절하는 값이 됐다는 얘기이기도 합니다. 같은 모델을 얼마나 비싸게 돌릴지 제가 정하고, 그에 따라 점수가 오르내립니다. 그러면 어느 모델이 낫냐는 질문은 얼마를 쓸 거냐는 질문과 떨어질 수가 없는데, 리더보드는 둘을 떼어놓은 채로 답을 줍니다.

앞의 가격표를 다시 꺼내 보면 이게 좀 더 분명해집니다. 1위를 한 Sol은 7월 30일 인하에서 유일하게 값이 그대로였던 모델입니다. 그 단가에 xhigh를 걸고 89.5%가 나왔습니다. 같은 날 Luna는 입력 단가가 크게 떨어졌습니다. 하나는 최상단 성능을 제일 비싼 설정으로 밀어붙인 기록이고, 다른 하나는 아래쪽 단가가 내려앉은 일이라 같은 표에 놓고 볼 게 아니었습니다.

이렇게 보면 순위가 바뀌는 조건도 달라 보입니다. 2위가 1위가 되는 데 새 모델이 꼭 필요하지 않습니다. 같은 모델을 한 단계 높은 설정으로 다시 내기만 해도 태스크 하나쯤은 결과가 바뀔 수 있고, 태스크 하나면 순위가 바뀝니다. 다음 달에 순위가 바뀌었다는 소식이 와도 모델이 좋아진 건지 제출 조건이 올라간 건지 표만 봐서는 알 수가 없습니다.

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

이 표가 실제로 알려주는 건 상위 모델들이 이 벤치에서는 서로 구분이 안 된다는 것 정도였습니다. 그럼 제가 매일 느끼는 결과 차이는 표 바깥에서 오고 있다는 얘기가 됩니다.

제 하네스를 세어봤습니다

표 바깥이 어딘지는 안다고 생각했습니다. 하네스입니다.

내가 짠 건 하네스가 아니라 아웃터 루프였다에서 이 얘기를 한 번 한 적이 있습니다. 지각하고 추론하고 행동하고 관찰하는 이너 루프는 제가 짜는 게 아니라 하네스가 줍니다. 제가 짜는 건 그 루프를 언제 켜고, 무슨 일을 맡기고, 나온 결과를 무엇으로 검증하고, 어느 조건에서 멈추느냐입니다.

그러면 제가 실제로 짜놓은 게 얼마나 되는지 세어볼 만했습니다. 파일을 열어서 셌더니 에이전트가 6개, 스킬이 20개, 훅이 3개였습니다. 훅 세 개를 합치면 234줄인데 pre-commit-check.sh 하나가 166줄이고, session-start.sh 가 56줄, stop-reminder.sh 가 12줄입니다. 셋 중 하나가 거의 전부를 차지하고 있었습니다.

이 비율이 이상한가 싶어 들여다봤는데 이상하지 않았습니다. session-start.sh 는 세션을 시작할 때 파일 세 개를 컨텍스트 앞에 붙이는 일만 하고, stop-reminder.sh 는 다음 세션용 메모가 비어 있으면 한 줄 안내를 띄웁니다. 둘 다 편하자고 둔 장치입니다. pre-commit-check.sh 만 커밋을 실제로 막습니다. 뭔가를 막는 코드는 늘 길어집니다. 막을지 말지 정하는 데 필요한 예외 처리가 계속 붙기 때문입니다.

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

훅이 세 개뿐이면 적어 보일 수도 있는데, 며칠 전 keyv 침해가 .claude/settings.json에 SessionStart 훅을 심는 경로였다는 걸 확인하고 내 레포 여섯 곳의 훅 29개를 전부 세어봤을 때 생각이 좀 바뀌었습니다. 훅은 자동으로 실행되는 코드라 개수가 많으면 자동 실행 지점이 많다는 뜻이고, 그 하나하나가 다 들여다봐야 할 대상입니다. 적은 게 오히려 나을 수도 있다고 봅니다. 제 레포의 세 개는 적어도 각각 뭘 하는지는 제가 설명할 수 있습니다. 그런데 뭘 하는지 설명할 수 있다는 것과 그게 지금 제대로 일하고 있다는 건 다른 얘기였고, 그 차이를 바로 다음에 보게 됐습니다.

여기까지는 그냥 목록을 센 거였는데, 세다가 걸린 게 있었습니다.

두 달 반 동안 아무것도 안 한 게이트

AI_AUTOMATION.md 의 변경 이력을 훑다가 2026-07-29 행에서 "발행 게이트 제거" 라는 줄을 봤습니다. 그 게이트가 들어온 건 5월 중순이었습니다. 두 달 반 동안 돌았고, 도는 내내 아무것도 검증하지 않았습니다.

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

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

같이 붙어 있던 품질 게이트는 또 다른 식으로 틀려 있었습니다. 파일 전체에서 \bFAIL\b 를 찾아서 걸리면 막았는데, 체크리스트에 "FAIL 조건 없음" 이라고 적으면 그 줄에도 FAIL이 들어갑니다. 아무 문제 없는 글이 문제 없다고 적혀 있다는 이유로 못 나갔습니다.

둘을 합쳐 보면 실패가 정확히 반대 방향으로 나고 있었습니다. 나쁜 글은 파일에 판정: 합격 이라고 쓰기만 하면 통과하고, 좋은 글은 브리프 파일을 안 만들었거나 그 안에 FAIL이라는 단어가 스쳤으면 업로드가 안 됐습니다.

게이트가 정상 작업을 막기 시작하면 사람은 게이트를 고치지 않고 끕니다. 그 게이트에는 들어올 때부터 --skip-gate 플래그가 같이 붙어 있었습니다. 우회로가 처음부터 문에 달려 있었던 겁니다. 7월 29일에 게이트를 지우면서 이 플래그는 하위 호환으로 받되 무시하게 바꿨습니다.

두 달 반 동안 왜 한 번도 이상하다고 못 느꼈나. 이 질문을 꽤 오래 붙들고 있었는데, 답은 게이트 바깥에 있었습니다.

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

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

조용히 통과하는 게이트와 아예 없는 게이트는 로그가 똑같습니다. 둘 다 아무것도 출력하지 않습니다. 제가 deploy_to_blogger.py 를 직접 열어보기 전까지는 그 코드가 돌고 있는지 지워져 있는지 구분해줄 게 하나도 없었습니다. 매번 통과한다는 사실은 게이트가 일하고 있다는 증거처럼 보였는데, 같은 사실이 게이트가 아무것도 안 하고 있다는 증거이기도 했다는 건 한참 뒤에 알았습니다.

\bFAIL\b 오탐은 사정이 달랐습니다. 그건 소리를 냈습니다. 멀쩡한 글이 막히면 저는 바로 알았고, 바로 손을 썼습니다. 파일 문구를 바꾸거나 --skip-gate 를 붙이거나요. 두 실패가 같은 게이트 안에 나란히 있었는데 하나만 눈에 들어온 이유가 이거였습니다. 저를 성가시게 한 쪽만 제가 알아챘습니다. 저를 방해하지 않는 고장은 고장으로 접수가 안 됩니다. 시끄러운 실패는 짜증이 나지만 적어도 알려주기는 하는데, 조용한 실패는 아무 일도 없는 것처럼 보여서 오히려 고마운 쪽으로 느껴집니다. 두 달 반 동안 제가 그 게이트에 대해 가진 감정은 아마 고마움에 더 가까웠을 겁니다.

이번에 이게 다시 눈에 들어온 것도 게이트를 점검하러 가서가 아니었습니다. 리더보드 숫자가 이상해서 하네스에 뭐가 몇 개 있는지 세다가 변경 이력 한 줄에 걸린 겁니다. 다른 일을 하다 우연히 지나간 거라, 우연이 아니었으면 이걸 알아챌 방법이 제 레포에 있었는지는 아직 잘 모르겠습니다.

이 모양이 왜 낯익은가 했더니 며칠 전에 다른 쪽에서 한 번 봤던 것이었습니다. 계획서를 먼저 쓰고 그대로 구현시키는 방식이 왜 나한테 안 맞았는지 쓴 글에서 느낀 건, 승인받은 설계 문서가 있어도 그 문서가 틀렸으면 버그는 그대로 나간다는 거였습니다. 문서가 있다고 검증이 된 건 아닙니다. 그때는 개발 절차 얘기로 했는데, 발행 게이트는 같은 병을 코드로 굳혀둔 버전이었습니다. 서류가 있으면 통과하고, 서류에 뭐라고 적힐지는 서류를 쓴 쪽이 정합니다.

남겨둔 게이트

게이트를 다 걷어낸 건 아닙니다. pre-commit-check.sh 의 시크릿 스캔은 그대로 뒀습니다. 이걸 왜 남겼는지는 그 코드가 실제로 뭘 보는지를 보면 알 수 있습니다.

이 훅은 스테이지된 파일을 하나씩 열어서 정규식을 돌립니다. AKIA 로 시작하는 대문자 영숫자 16글자, ghp_ 로 시작하는 GitHub 토큰, xoxb- 같은 Slack 토큰, 사설키 헤더와 base64 본문이 같이 있는지 같은 걸 봅니다. 걸리면 커밋을 막고, 찾아낸 값은 절대 출력하지 않습니다. 출력하는 순간 그게 또 유출이니까요.

차이는 여기서 나옵니다. 파일에 AKIA 패턴이 있느냐 없느냐는 모델이 뭐라고 해도 안 바뀝니다. 에이전트가 이 파일엔 시크릿이 없다고 아무리 확신에 차서 적어도 grep은 그 문장을 안 읽고 파일을 읽습니다.

이 훅에서 나중에야 의미를 알게 된 부분도 있었습니다. 오탐을 줄이려고 붙여둔 장치 두 개입니다. 하나는 placeholder 허용목록입니다. 값에 EXAMPLE이나 FAKE, DUMMY, CHANGE_ME 같은 게 들어간 줄은 문서용 예시로 보고 넘깁니다. 실제로 발급된 시크릿에 EXAMPLE이 들어갈 일은 없으니까요. 다른 하나는 규칙마다 적용 범위를 달리 둔 겁니다. AKIA 처럼 모양이 뚜렷한 패턴은 모든 파일에 돌리고, password: 뒤에 뭔가 길게 붙어 있으면 잡는 식의 추측성 규칙은 코드와 설정 파일에만 돌립니다. 마크다운 문서에 예시 비밀번호가 적혀 있는 건 정상이라서요.

이 장치들이 왜 붙어 있는지 코드 주석에 적혀 있었는데, 읽고 좀 뜨끔했습니다. 죽은 게이트를 두고 제가 방금 한 말이 거기엔 처음부터 적혀 있었습니다. --no-verify 한 번이면 끝이라는 것, 탐지율만 올리고 오탐을 내버려둔 게이트는 결국 꺼진다는 것. \bFAIL\b 오탐과 똑같은 문제였는데 한쪽은 예외 처리를 붙여서 살아남았고 다른 쪽은 못 붙여서 죽었습니다.

그래서 기준이 하나 생겼습니다.

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

쓸 수 있으면 그건 게이트가 아니라 서류함입니다. 이걸로 제 레포의 검사 항목을 다시 봤습니다. 시크릿 패턴은 파일에 있거나 없거나라 검사받는 쪽이 쓸 수 없습니다. 글자 수, 이미지 개수, 내부링크 개수, placeholder 주석도 파일을 열어서 세면 나오는 값이라 마찬가지입니다. 반대로 판정: 합격 은 에이전트가 직접 적고, 0~100 품질 점수는 같은 모델이 매깁니다. 뒤의 두 개는 지웠고 앞의 것들은 남겼습니다. 발행 전에 길이, 히어로 이미지, 내부링크, placeholder 주석 이 넷을 세는 것도 같은 이유입니다. 넷 다 글을 쓴 쪽이 값을 바꿀 방법이 없습니다.

그렇다고 검사 대상만 잘 고르면 되는 것도 아니었습니다. 이 시크릿 스캔도 이미 두 번 실패했습니다. 가짜 시크릿 여섯 개를 넣고 돌려봤더니 탐지가 0건이었던 적이 있었고, 그걸 고친 다음엔 훅이 커밋되는 파일을 안 보고 워킹 트리를 보고 있었던 적도 있었습니다. 검사 대상은 맞았는데 검사 범위가 틀렸던 겁니다.

그 두 번은 고칠 수 있는 실패였습니다. 패턴을 더하고, 읽는 대상을 스테이지된 블롭으로 바꾸면 됐습니다. 판정: 합격 게이트는 그렇게 못 고칩니다. 문자열을 누가 쓰느냐를 바꾸지 않는 한 어떤 패턴을 더해도 같은 데로 돌아옵니다.

하루 1편

여기까지 쓰고 나서 좀 찜찜했습니다. 같은 모양이 하나 더 있었거든요.

이 레포에는 하루 최대 1편이라는 규칙이 있었습니다. L1 규칙, 그러니까 어기면 안 되는 규칙으로 들어가 있었습니다. 근거는 관측이었습니다. 산출물을 짧은 기간에 몰아서 낸 적이 있었고, 그 뒤에 품질 문제가 드러났습니다. 그런데 이 규칙을 받쳐줄 바깥 기준은 어디에도 없었습니다. 두 일이 같이 일어났다는 관측에서 레포가 앞의 것을 원인으로 정해버린 겁니다.

그때 산출물을 다시 세어보면 다른 숫자가 나옵니다. 길이 중앙값이 3,600자였고 이미지도 내부링크도 하나도 없었습니다. 몰아서 낸 시기는 이런 것들이 한꺼번에 드러난 계기였지 그것 자체가 원인은 아니었을 수도 있습니다. 진짜 원인이 뭐였는지는 지금도 모릅니다. 확실한 건 근거가 없는 관측을 L1 규칙으로 올려뒀고, 그 바람에 조건을 다 갖춘 글이 오늘 두 번째라는 이유로 못 나가고 있었다는 것뿐입니다.

8월 4일에 이 규칙을 없애고 글마다 조건을 확인하는 쪽으로 바꿨습니다. 시각을 세는 대신 네 숫자를 셉니다. 관측했던 숫자는 버리지 않고 문서에 그대로 남겨뒀고, 그 관측에서 규칙으로 건너가는 다리만 걷었습니다.

두 일을 겹쳐놓고 보니 둘 다 제가 뭔가를 검증하고 있다는 기분을 주는 장치였습니다. 판정: 합격 이 파일에 적혀 있으면 문체를 검사한 기분이 들고, 오늘 하나만 냈으면 품질을 지킨 기분이 듭니다. 그 기분이 실제 검증을 대신하는 동안 두 달 반이 지나갔습니다. 둘 다 처음 만들 때는 분명 이유가 있었고, 만든 사람도 저였습니다. 그래서 더 의심을 안 했던 것 같습니다. 남이 만든 규칙이면 한 번쯤 왜 이런 게 있냐고 물어봤을 텐데, 제가 만든 규칙은 이미 답을 안다고 생각하고 지나쳤습니다.

터미널로 들어온 스캐너

이 기준을 들고 있으니 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 를 먼저 열어볼 것 같습니다.

댓글

이 블로그의 인기 게시물

봉제인형 사진 한 장, ChatGPT와 Gemini의 다른 대답

구글은 서울에서 모델이 아니라 에이전트를 판다

AI 부업으로 3개월에 2달러 벌고 50달러를 썼다는 증언을 봤습니다. 그 위에 광고가 네 겹입니다