anti-slop
규칙 15종을 제 코드 전부에 돌려 27건을 잡았고 한 건도 안 고치기로 했습니다. 대신 붙일 자리를 찾다가 제 유출 방어 게이트가 조용히 꺼질 수 있게 생긴 걸 봤습니다.
AI가 코드를 대충 쓸 때 나오는 나쁜 습관 15가지를 잡아 주는 검사 도구입니다. 제 코드 전부에 돌려 27건을 잡았고, 한 건도 안 고치기로 했습니다. 그런데 붙일 자리를 찾다가 훨씬 비싼 걸 봤습니다.
이 도구가 잡는 것 — 「알면서 모른 척하는 코드」
코드를 짤 때 「이 값이 무엇인지」를 미리 적어 두는 방식이 있습니다. 「여기엔 숫자가 들어온다」처럼요.
그러면 잘못된 걸 넣었을 때 만들 때 바로 걸립니다. 실행해 보기 전에 알 수 있습니다.
그런데 급하면 「아무거나 들어올 수 있다」고 적어 버리는 유혹이 있습니다. 그러면 검사가 안 걸리니 일단 넘어갑니다. 대신 나중에 실제로 돌릴 때 터집니다.
이 도구가 잡는 게 그겁니다 — 값이 뭔지 알면서 모른다고 적어 둔 자리.
| 막는 것 | 쉽게 말하면 |
|---|---|
| 「아무거나」로 흘려보내기 | 알면서 모른다고 적기 |
| 「무슨 키든 들어와도 됨」 | 상자에 라벨 안 붙이기 |
| 억지로 형태 바꾸기 | 틀렸는데 맞다고 우기기 |
| 「그냥 믿어라」 | 왜 안전한지 안 적고 안전하다고 하기 |
마지막 줄이 재미있습니다. 억지로 형태를 바꾸는 걸 «금지»하지 않고, 대신 «왜 안전한지 주석으로 적으라»고 강제합니다. 금지하면 우회하니까, 설명을 요구하는 쪽으로 갔습니다.
이 도구의 특이한 점 — 타입 검사기를 한 번도 안 부릅니다
기술적으로 특이한 부분입니다.
보통 이런 검사는 프로그램 전체를 이해해야 가능합니다. 느립니다.
이건 파일 한 장의 «생김새»만 봅니다. 무슨 뜻인지는 안 따지고 모양만 봅니다. 그래서 아주 빠릅니다.
대신 모양만 보니 놓치는 것도 있습니다. 빠름과 정확함을 맞바꾼 설계입니다.
배울 게 있던 건 규칙이 아니라 「배포하는 방식」이었습니다
제가 이 창고를 연 이유는 규칙이 아니었습니다. AI에게 결과를 알려 주는 «문구» 쪽이었습니다.
보통 검사 도구는 「이게 틀렸다」고만 말합니다. 사람은 그걸 읽고 어떻게 고칠지 생각합니다.
이 도구는 「이렇게 고쳐라」까지 씁니다. AI가 읽고 바로 행동할 수 있게 쓴 것입니다. 그 작업에 커밋 하나를 통째로 썼습니다.
또 하나 인상적이었던 것 — 설치 절차 안에 AI의 행동 제약을 박아 뒀습니다.
| 설치 절차에 적힌 것 | 무슨 사고를 막나 |
|---|---|
| 「네가 기억하는 판번호를 믿지 말고 조회해라」 | AI가 옛날 판번호를 기억으로 써넣는 사고 |
| 「규칙을 꺼서 검사를 통과시키지 마라」 | AI가 막히면 검사 자체를 끄는 사고 |
두 번째가 특히 좋습니다. 「테스트를 통과시켜라」고 하면 AI가 테스트를 지워 버리는 문제와 같은 종류인데, 그걸 절차 안에 미리 박아 뒀습니다.
자동 수정 기능을 전부 지웠습니다
이 도구에는 자동 고침이 0개입니다. 원래 있었는데 전부 지웠습니다.
이유가 있었습니다. 자동 고침이 「이 항목을 빼기」를 「이 항목을 «비어 있음»으로 설정하기」로 바꾼 사고가 있었습니다. 둘은 보기에 비슷한데 실행 결과가 다릅니다.
그 제보를 받고 16종 전부를 「알려만 주고 안 고침」으로 바꿨습니다.
→ 배운 것: 고쳐 주는 기능은 편한데, 틀리면 조용히 망가집니다. 알려만 주면 사람이 보고 판단합니다.
제 코드에 돌려 봤습니다 — 그런데 대상이 770줄이었습니다
먼저 잰 게 규칙이 아니라 적용할 면적이었습니다.
제 작업 폴더 전체에서 이 도구가 볼 수 있는 코드는 770줄이 전부였습니다. 나머지는 전부 다른 언어라 대상이 아닙니다.
참고로 이 도구는 대형 프로젝트를 겨냥해 만들어졌습니다. 외부 적용 사례 하나가 568,443줄이었습니다.
770줄 대 568,443줄. 이 차이가 판정을 좌우합니다.
그 770줄에 15종을 전부 켜고 돌렸습니다. 27건이 나왔고, 한 건도 안 고치기로 했습니다. 770줄짜리에 대형 프로젝트용 규칙을 걸 이유가 없습니다.
그런데 이 창고가 자기 자신에게는 면제돼 있었습니다
핵심 반증입니다. 이 창고는 자기 소스에 자기 검사 도구를 한 번도 돌린 적이 없습니다.
만들어 놓고 자기는 안 씁니다. 자기주장 열한 개 중 아홉 개가 안 버텼습니다.
그리고 붙일 자리를 찾다가 훨씬 비싼 걸 봤습니다
여기가 이 편의 본론입니다.
이 도구를 어디에 붙일지 보려고 제 배포 검사 체인을 열었습니다. 그러다 다른 걸 봤습니다.
제게는 공개하면 안 되는 내부 용어를 막는 검사가 있습니다. 그게 유일한 방어선입니다.
그 검사가 참조 파일 하나에 의존하고 있었습니다. 그리고 그 파일이 없으면 검사를 건너뜁니다. 그것도 출력 한 줄 없이 조용히요.
즉 파일 하나가 사라지면, 유출 방어가 아무 소리 없이 꺼집니다. 그리고 화면은 평소와 똑같습니다.
그래서 관통하는 한 줄
대상 쪽에서도 제 쪽에서도 같은 게 나왔습니다.
게이트는 «있는지»가 아니라 «도는지»로 센다.
저쪽은 만들어 놓고 자기한테 안 돌렸고, 제 쪽은 만들어 놨는데 조용히 꺼질 수 있게 생겼습니다. 둘 다 「있다」로 세면 통과입니다.
실제로 가져온 것 — 플러그인이 아니라 그 밑바닥
수확 셋을 적었는데 하나만 살아남았습니다. 그리고 그건 이 도구가 아니라 이 도구가 얹혀 있던 «바닥 검사기»였습니다.
제 사이트에는 일반적인 코드 검사기가 하나도 없었습니다. 제가 만든 검사 장치 셋은 전부 제 사이트 전용이라, 「안 쓰는 변수」 같은 일반적인 결함은 아무도 안 봤습니다.
그래서 바닥 검사기를 발행 절차에 넣었습니다. 넣자마자 제 파이프라인 코드에서 진짜 결함 2건을 잡았습니다.
나머지 둘은 축소했습니다. 확인해 보니 「제게 없다」고 적은 것이 이미 있었거나, 문제 방향이 반대였습니다.
여기부터는 자세한 기록입니다
규칙 15종을 제 코드 전부에 돌려 27건을 잡았고, 한 건도 안 고치기로 했습니다. anti-slop 은 TypeScript 에서 「타입 증거를 버리는」 구문 16종을 금지하는 Oxlint 플러그인입니다. 타입체커를 한 번도 부르지 않고 파일 한 장의 구문 트리만으로 판정하는 게 정체입니다. 제가 연 이유는 규칙이 아니라 에이전트가 읽고 바로 행동하도록 쓴 진단 문구 쪽이었는데, 붙일 자리를 찾으려 제 발행 게이트 체인을 열었다가 훨씬 비싼 걸 봤습니다 — 제가 공개하면 안 되는 내부 용어를 막는 유일한 스캔이, 참조 파일 하나가 사라지면 출력 한 줄 없이 건너뛰어지는 if 위에 서 있었습니다. 대상 쪽에서도 같은 게 나왔습니다. 이 레포는 자기 소스에 자기 플러그인을 한 번도 돌린 적이 없습니다. 관통선은 하나입니다 — 게이트는 존재가 아니라 실행으로 센다.
무엇을 하는 물건인가
「값이 무엇인지 알고 있는데 그 정보를 버리는 구문」을 금지하는 규칙 모음입니다. Oxlint 1.78 이 새로 연 JS 플러그인 API 위에 얹혀 있고, 아무 데나 켜는 범용 15종과 특정 프레임워크에서만 켜는 옵트인 1종으로 갈라져 있습니다.
| 무엇을 막나 | 어떤 구문 |
|---|---|
| 넓은 타입을 경계 밖으로 흘리기 | 파라미터·반환·타입 별칭 자리의 unknown · any · object · {} |
| 열린 사전 타입 | Record<string, unknown> 류 — 키가 무엇이든 통과한다 |
| 타입 세탁 | x as unknown as T 연쇄 단언 |
| 넓혔다가 도로 좁히기 | 알려진 값을 넓게 선언해 두고 나중에 단언으로 되돌리는 흐름 |
| 런타임으로 도망가기 | typeof 분기 · Reflect.get · Reflect.apply · 모듈 목킹 |
| 근거 없는 단언 | 단언 앞에 SAFETY: 주석으로 불변식을 진술하게 강제 |
특이한 건 이걸 하는 방법입니다 — 타입체커를 안 부릅니다. 파일 한 장의 구문 트리에서 최상위 선언만 훑어 타입 별칭 표를 직접 만든 뒤 그 표로 별칭을 재귀 치환합니다. Record · Partial 같은 빌트인 여덟 개는 이름을 상수로 박아 뒀고, 두 타입이 같은지도 소스에서 공백을 지운 문자열 비교로 근사합니다. 대가는 명확합니다 — import 한 타입도, 블록 안에서 선언한 별칭도 안 보입니다.
| 항목 | 실측 (2026-08-22) |
|---|---|
| 규모 | 파일 66개 · 62KB · 런타임 의존성 1개 |
| 별 · 포크 | 3,313 · 63 (피드가 08-20 에 찍은 값은 2,871 — 이틀에 +442) |
| 커밋 | 13개. 전부 08-12 ~ 08-18 · 릴리스 0 · 태그 0 |
| 기여자 | 1명. 그리고 PR 이 비활성화돼 있다 |
| 테스트 | 규칙 16종 중 3종은 테스트 파일이 아예 없다 |
| 자동 수정 | 0개. 오토픽스가 런타임 의미를 바꾼다는 제보를 받고 픽서를 전부 지웠다 |
배포도 보통이 아닙니다. npm 에 안 냅니다 — 패키지가 private 이고 진입점이 빌드 산출물이 아니라 소스 파일입니다. 설치는 에이전트 스킬이 소스 .ts 를 남의 저장소 안으로 복사하는 것이고, README 가 이유를 밝힙니다: 고정 의존성으로 두지 말고 복사해 읽고 팀 기준에 맞게 고쳐 쓰라는 것.
대표 기법 — 값나가는 건 규칙이 아니라 배포 쪽이다
벤더링 자체는 위에서 봤고, 그 위에 얹힌 다섯이 이렇습니다.
| 기법 | 무엇 |
|---|---|
| 스킬이 곧 설치 프로그램 | 결정론 코드는 21줄(복사 하나)뿐. 나머지 다섯 단계 — 패키지 매니저 판별 · 최신 버전 조회 · 설정 병합 · 규칙 활성화 · 검증 — 는 산문으로 적혀 LLM 이 수행한다 |
| 절차에 에이전트 행동 제약을 박는다 | 「네가 기억하는 버전을 믿지 말고 조회하라」, 「규칙을 억제하거나 unsafe cast 로 통과시켜 린트를 넘기지 말라」가 설치 절차의 한 항목 |
| 정본↔사본 바이트 동일성 CI 게이트 | 파일 목록과 utf8 내용을 둘 다 대조해 다르면 체인을 세운다. 벤더링의 대표 실패를 CI 로 봉쇄 |
| 오토픽스 전면 포기 | 픽서가 「키 생략」을 「키를 undefined 로 설정」으로 바꾼 사고 이후 16종 전부 report-only |
| 에이전트 겨냥 진단 문구 | 「이게 틀렸다」가 아니라 「경계에서 파싱하고 이름 있는 도메인 타입을 반환하라」. 커밋 하나를 통째로 이 작업에 썼다 |
제가 이 레포를 연 이유가 마지막 줄에 있습니다. 제 발행 게이트들의 1차 독자가 사람이 아니라 세션이라서요.
깨본 결과 — 자기주장 열하나 중 아홉이 안 버텼다
이 도구는 문서 수준에서 자기 자신에게 면제돼 있습니다. 이게 핵심 반증입니다.
- 저장소 어디에도 oxlint 설정 파일이 없습니다.
lint스크립트는 기본 규칙만 돌고, 플러그인은 자기 소스에 단 한 번도 적용된 적이 없습니다. - 그 결과 21줄짜리 공용 모듈 하나가 자기 규칙 최소 4종을 어깁니다 — 파라미터 자리의
unknown,typeof런타임 분기,as unknown as연쇄 단언, 그 단언 앞에SAFETY:주석 없음. 62KB 코드베이스라 「규모가 커서」로 방어되지 않습니다. - README 와 설치 절차는 사용자에게도 벤더링된 사본을 린트 제외 목록에 넣으라고 지시합니다.
핵심 규칙 하나는 빈 주석으로 뚫립니다. 단언에 근거를 적게 만드는 규칙의 검사가 SAFETY: 마커가 있는지만 보고 내용은 안 봅니다. // SAFETY: 한 줄이면 CI 가 정확히 이 규칙이 막으려던 캐스팅을 승인합니다. 설치 절차가 산문으로 금지한 「린트를 통과시키려고 타입을 세탁하는 짓」을 에이전트가 정확히 이 경로로 자동 수행할 수 있습니다. 미탐도 같은 모양입니다 — 별칭 표를 최상위에서만 만들어서 type Identity<T> = T 같은 투명 별칭을 한 번 거치면 규칙 여럿이 동시에 눈이 멉니다.
유일한 perf: 커밋은 상류 문서가 부정합니다. 규칙 15종을 새 API 로 옮기며 성능 개선이라 적었는데, 근거가 된 상류 공식 문서가 「지금 더 빠른가」에 대해 “지금은 아니다. 하지만 곧 그렇게 될 것이다” 라고 적어 놨습니다. 저장소 안에 전후 수치가 없고, 닷새 뒤 추가된 마지막 규칙은 구 API 로 되돌아가 있습니다. 배포 모델도 이미 우회당했습니다 — 뜯기 하루 전에 커뮤니티가 같은 규칙을 번들해 npm 에 올렸고, 그건 취향이 아니라 필요였습니다. oxlint 가 공유 설정에서 상대경로 플러그인 지정자를 지원하지 않아 공유 설정을 쓰는 조직은 벤더링으로 도입 자체가 불가능합니다.
버틴 것도 적어 둡니다. 정본↔사본 동일성 CI 게이트는 깰 거리가 없었고, 옵트인 규칙이 스스로 밝힌 범위 한계도 코드와 일치했습니다. 위험한 걸 하면서 한계를 적어 두는 쪽입니다. 나쁜 건 공급자입니다 — 08-18 이후 정지, 이슈 15건 전부 무응답, 그중 여섯은 패치까지 붙은 제보인데 PR 이 비활성이라 포크 링크로만 제출돼 있습니다.
내 환경에 대봤다 — 면적이 770줄이었다
먼저 잰 건 규칙이 아니라 적용 면적입니다. 제 작업 폴더 전체에서 손으로 쓴 TypeScript 는 개인 사이트 7개 파일 770줄이 전부입니다. 나머지 프로젝트는 파이썬·파워셸·CommonJS 라 TS 가 0개입니다. 이 도구는 대형 모노레포를 겨냥해 만들어졌습니다 — 외부 적용 보고 하나가 568,443줄·4,421파일 규모였습니다. 그 770줄에 15종을 전부 켜고 돌렸습니다.
27건 중 18건이 한 패턴입니다. 프레임워크가 라우트 파라미터를 「문자열이거나 없음」으로 주기 때문에 페이지마다 그걸 좁히는 단언이 한 줄씩 있습니다. 규칙은 그 18곳 전부에 주석을 요구하는데, 경계에서 강제되는 것이라 없앨 수 없고 주석을 18개 붙여도 코드는 한 글자도 안전해지지 않습니다. 7건 쪽은 키가 이미 닫혀 있는 사전 타입까지 잡습니다 — 외부 실측에서 150건 중 117건이 「고쳐도 타입이 동일」함이 컴파일러로 증명된 오탐군과 같은 종류입니다.
예상 반증도 하나 나왔는데, 나쁜 쪽으로 나왔습니다. 저는 이 린터가 사이트 템플릿 파일을 못 읽을 거라 가정했는데 실제로는 프런트매터를 파싱해 정상 보고합니다. 적용 면적이 16장 더 늘어난 셈인데 늘어난 만큼이 전부 위의 18건짜리 노이즈였습니다. 면적이 커진 것이 이득이 아니라 손해로 나온 사례입니다. 속도는 기각 사유가 아니었습니다 — 웜 260ms → 450ms 로 70% 늘지만 절대값이 무시할 수준입니다.
수확 셋을 적었고 하나만 살아남았다
실질 산출은 플러그인이 아니라 그 밑에 깔린 맨 oxlint 였습니다. 제 사이트에는 정식 린터가 하나도 없습니다 — eslint · prettier · biome · typescript 전부 미설치고 tsconfig.json 도 없습니다. 품질 게이트 세 개는 제가 짠 도메인 검사기라 미사용 변수 같은 범용 결함을 안 봅니다.
| 수확 후보 | 판정 | 사유 |
|---|---|---|
| 맨 oxlint 를 발행 체인에 편입 | 채택 | 무설정 실행이 exit 0 이라 오늘 넣어도 배포가 안 막힌다. 발행 파이프라인 코드에서 진짜 결함 2건을 즉시 잡았다 |
| 진단 문구를 지시문으로 | 축소 | 「13곳 전부 지시문이 없다」가 틀렸다. 두 곳은 이미 실행 명령까지 인쇄하고 나머지도 결과절을 달고 있다. 4줄 다듬기로 축소 |
| 정본↔사본 동일성 게이트 | 축소 · 방향 정정 | 겨냥한 실패 모드를 퍼블리시 절차가 이미 없애 놨다. 실제 감시 부재는 반대 방향이었다 |
✖ 죽은 링크 3건 — push 하지 않고 멈춥니다
✖ 죽은 링크 3건. 아래 주소를 고치거나 해당 문서를 만든 뒤 다시 돌릴 것 (링크를 지우는 것도 수정이다 — 자동으로 하지 않는다)
축소된 쪽에서 값을 치른 교훈이 나옵니다. 동기화 게이트는 「사본이 낡는 걸 못 잡는다」고 적었는데 실측하니 오늘 낡은 사본이 0건이었습니다. 조립 단계가 매번 전량 재복사를 해서 그 실패 모드가 이미 없어져 있었습니다. 더 나쁜 건 설계 쪽입니다 — 사본이 정본과 다른 이유의 본체가 제가 가정한 경로 치환이 아니라 대응표가 아예 존재하지 않는 용어 살균이라 「반나절」이라는 견적이 성립하지 않습니다. 그대로 켰으면 첫 실행에 허위경보 열 건이 떴을 겁니다. 채택한 쪽에도 정정이 붙었습니다 — 오탐을 2건이라 셌는데 실제로는 3건이었습니다. 한 파일을 빠뜨려 놓고 「전수」라고 적었습니다.
판정
| 항목 | 판정 |
|---|---|
| anti-slop 플러그인 도입 | 기각. 면적 770줄 · 27건 중 25건이 경계 노이즈와 기지 오탐 · 고유 명분 0건 |
| 맨 oxlint 편입 | 채택. 오늘 넣어도 발행이 안 막힌다 |
| 지시문 진단 · 동기화 게이트 | 축소. 절반은 이미 되어 있었고, 하나는 방향이 틀렸다 |
| 다시 여는 조건 | TS 를 1,500줄 이상 쓰는 프로젝트를 시작할 때 · 연쇄 단언 패턴이 내 코드에 실제로 나타날 때 · 상류가 오탐 두 규칙에 옵션을 붙일 때 · 타입 인지 린팅이 안정판이 될 때 |
공급자 리스크가 마지막 한 겹입니다. 벤더링이라 도입하면 방치된 패치를 제가 사본에 직접 발라야 하고, 그 순간부터 상류와 영구히 갈라지며 유지비를 제가 집니다. 별 3,313개는 이걸 하나도 가려 주지 않습니다.
이 조사가 실제로 바꾼 것은 남의 린터가 아니었다
수확을 붙일 자리를 찾으려고 제 퍼블리시 절차를 열었다가, 유출 방어의 단일 지점이 fail-open 인 걸 봤습니다. 그 절차에는 스캔이 두 번 들어갑니다 — 푸시 전과 푸시 후. 둘 다 이렇게 시작합니다.
if [ -f "<도메인 테이블맵 경로>" ]; then
grep -nE "\b(<테이블 이름 54종>)_[A-Za-z]" ...
fi
else 가 없습니다. 참조 파일이 사라지면 스캔이 통째로 건너뛰어지고, 출력은 한 줄도 안 나오며, 다음 줄에서 게이트는 그대로 통과합니다. 푸시 후 쪽은 grep 에 에러 무음 처리까지 붙어 이중 침묵입니다.
이게 왜 비싼지는 같은 문서가 스스로 밝혀 놨습니다. 제가 막아야 하는 이름 중 한 부류는 살균 전 이름과 살균 후 이름의 모양이 완전히 같습니다. 구조적 정규식으로 구분이 안 되니 이 부류는 패턴 목록 34개가 아니라 오직 저 테이블맵 조회만이 잡습니다. 그리고 이게 이론이 아닌 이유가 이번 조사에서 실물로 나왔습니다 — 공개용 사본과 정본을 대조해 보니 실제로 살균된 항목이 정확히 그 부류였습니다. 파일은 지금 살아 있습니다. 하지만 경로가 두 곳에 하드코딩돼 있고, 저는 이미 작업 폴더 구조를 한 번 통째로 갈아엎은 적이 있습니다.
여기서 값을 치른 교훈은 대상이 준 것의 반대쪽입니다. anti-slop 은 오토픽스 사고 이후 픽서를 전부 지웠습니다 — 틀리게 고치느니 안 고치겠다는 판단이고 옳습니다. 그런데 게이트는 반대입니다. 틀리게 울리는 것보다 안 울리는 쪽이 훨씬 위험합니다. 허위경보는 사람이 보고 지우지만 침묵은 아무도 안 봅니다.
못 한 것
- 그 한 줄을 아직 안 고쳤습니다. 찾기만 했습니다. 고치기 전에 퍼블리시 절차 전체를 다시 읽어 다른 fail-open 이 더 있는지부터 세야 해서 별건으로 남겼습니다.
- 커뮤니티 npm 포크를 안 돌려봤습니다. 「발행 하루짜리라 신뢰 근거가 없다」고 적고 건너뛰었는데, 그건 안 잰 이유이지 잰 결과가 아닙니다.
- 규칙 정밀도는 남의 숫자를 인용했습니다. 568,443줄 실측은 외부 사용자가 올린 것이고 제 측정 면적은 770줄뿐입니다. 「대형 코드베이스에서도 노이즈다」라고는 못 합니다 — 제가 말할 수 있는 건 제 코드에서 값이 0이었다는 것까지입니다.
- 상류에 아무것도 제출하지 않았습니다. 자기 규칙 4중 위반은 이슈 하나로 낼 만한데, PR 이 막혀 있고 제보 15건이 전부 무응답인 걸 보고 안 냈습니다. 판단이었지 확인은 아닙니다.