'이걸 원한다면'에서 테트리스까지, 12일
Claude Code Mods는 Claude Code 엔진 안에서 실행되며 엔진의 이벤트를 함수로 훅하는 TypeScript 모듈이다. Claude Code를 이끄는 Boris Cherny는 다섯 단어로 이를 발표했다: "Claude mods are landing now." 이 글은 2,400개의 좋아요를 모았고, 그 시점에 이미 누군가는 터미널 안에 테트리스를 만들어 두었다. 테트리스, 둠, 그리고 Claude가 테스트를 실행하는 동안 자라나는 펫까지, 모두 프롬프트 위에 렌더링되며, 토큰은 0개다.
발표 글은 어떤 문서 페이지도 링크하지 않는다. 대신 12일 전 Anthropic 엔지니어가 연 GitHub 이슈를 링크하는데, 거기엔 한 가지 조건이 적혀 있었다. Alice Poteat는 이 기능이 출시될지 여부는 커뮤니티의 반응이 좌우할 가능성이 크다고 썼다. 177개의 댓글이 달린 뒤, 사람들은 플래그로 감춰진 바이너리를 두드려 보고, 타임아웃을 측정하고, 그 위에 게임을 만들었다.
| Measure | Value |
|---|---|
| Likes on the announcement | 2,400 |
| Comments on the GitHub issue | 177 |
| Typed surface in this week's build | 10,700 lines |
| Events on nouns (issue-thread build) | 84 on 19 |
| Mods on GitHub inside 48 hours | 31 |
이 글을 쓰는 시점 기준 문서 페이지는 404를 반환한다. 이 기사는 Claude Code를 확장하는 네 가지 방법과, 의도적으로 만들고 부순 유용한 Mod 하나, 그리고 출시하기 전에 아직 불안정한 부분을 다룬다.
Mod는 중간에 있는 함수다
소스 트리는 이를 한 문장으로 정의한다: Mod는 동작이 hooks module 안에 있는 Claude Code plugin이다. 하나의 register 항목이 엔진의 이벤트를 함수로 훅한다. 디스크 위에서 보면, 이는 plugin 폴더 하나, 정확히 하나의 모듈을 지정하는 hooks manifest 하나, 그리고 그 모듈 자체다.
모든 hook은 세 가지의 함수다: 모든 side effect가 통과하는 문 $, 이벤트, 그리고 continuation, 즉 당신 아래에 있는 체인의 나머지. hook은 middleware처럼 중첩된다. 먼저 등록된 hook이 그 이벤트를 소유하며, 그 아래 어떤 것도 이를 막을 수 없다. 그 순서는 설정되는 것이지, 설치 순서로 정해지지 않는다. Alice Poteat의 말을 빌리면, 순서는 설치 시점이 아니라 설정으로 정해진다.
주변 접근권 같은 건 없다. Mod가 하는 모든 일은 $를 통과하므로, 관리자는 어떤 이벤트든 감사(audit)하고, 허용하고, 거부하고, 기록할 수 있다. 한 댓글 작성자는 이렇게 요약했다: 플러그인이 한 일은 정확히 그것이 실행한 호출들뿐이다. *에 걸린 hook 하나가 모든 이벤트를 보므로, 감사 로그는 함수 하나로 끝난다. Mod는 화면을 그릴 수도 있는데, 인터페이스가 React이기 때문이다. Bun 위에서 프로세스 내부(in process)로 실행되며, 99번째 백분위수 기준 50마이크로초가 걸린다. 환경 변수 하나로 켜고 끌 수 있다. $ 위에 실제로 무엇이 있는지는 아직 초기 액세스 파트너들과 함께 설계되는 중이다.
Claude Code를 확장하는 네 가지 방법, 그리고 각각의 용도
shell hook은 엔진이 정해진 순간에 호출하는 스크립트다. 표준 입력으로 JSON을 받고 종료 코드(exit code)로 답한다. 이 답변에는 상한선이 있다: 컨텍스트로 돌려줄 수 있는 건 8,000자, 일부 hook은 2,000자다. Pratham의 말을 빌리면, Windows에서는 이런 hook들이 종종 이상하게 깨진다.
plugin은 하나의 상자다. 매니페스트 하나가 skill, agent, hook, MCP 서버를 한데 묶는다. 마켓플레이스에서 설치하거나 디스크의 폴더에서 바로 불러올 수 있고, 명령어 하나가 무엇이든 실행되기 전에 그 상자를 검증한다.
skill은 모델이 필요할 때 읽는 산문(prose)이다. 본문은 일치하는 prompt가 들어올 때만 로드된다. 여전히 동작을 바꾸는 가장 저렴한 방법이다.
Mod는 같은 상자에 파일 하나가 더 있을 뿐이다: 모듈 하나를 지정하는 hooks manifest, 그리고 그 모듈은 TypeScript로 작성되고, 타입이 있으며, 프로세스 내부에서, 모든 엔진 이벤트에 대해 동작한다. 차이는 그게 전부다.
| Mechanism | What it is | Runs | Ceiling |
|---|---|---|---|
| Shell hook | Script called at a fixed moment | Subprocess, exit code | 8,000 chars back (2,000 for some hooks) |
| Plugin | Bundle of skills, agents, hooks, MCP servers | Installed or loaded from disk | Validated before it runs |
| Skill | Prose loaded on a matching prompt | In the model's context | Cheapest change |
| Mod | Plugin plus a TypeScript hooks module | In process, on every event | Typed, early access |
이 타입들은 슬래시 명령어 하나에서 나오는데, 이 명령어는 $가 제공하는 것의 전체 목록을 그대로 당신의 프로젝트에 적어 넣는다. 기존 shell hook들은 폐기된 게 아니라 감싸졌을(wrapped) 뿐이다. 어느 초기 빌드에서, Spencer Morley는 이 wrapper가 로드에 실패하고도 여전히 선언된 채로 남아 있는 것을 지켜보았다. 경험칙은 이렇다: Claude가 아는 것을 바꾸려면 skill을 쓰고, 어떤 순간에 스크립트를 실행하려면 shell hook을, 번들을 배포하려면 plugin을, 엔진 안에 자리 잡으려면 Mod를 쓴다. shell hook은 그 플래그가 꺼져 있는 모든 곳에서 여전히 동작한다.
Anthropic 자체의 세 가지, 소스로 읽기
바이너리 안에는 Mod 세 개가 내장되어 있고 그 소스는 GitHub에 있다: 보안 기본값 하나, diff 패널 하나, 그리고 telemetry 하나.
보안 기본값은 가장 바깥쪽에 자리 잡는다. managed settings가 설정된 머신에서, 혹은 Team이나 Enterprise 조직에서는, 사용자가 설치한 그 무엇도 이보다 위에 있을 수 없다. 이 hook은 12개의 이벤트를 훅하고, 각 hook은 세 가지 동작 중 하나를 한다: user tier를 통과시키거나, user tier의 호출자를 이름으로 거부하거나, 그냥 지나간다. 이는 fail closed 방식이며, 문서 주석도 두 단어로 그렇게 밝힌다. 스레드가 신경 쓴 부분이 바로 여기다: 관리자가 $에서 권한 하나를 제거하면, 그 아래에 등록된 어떤 것도 그것을 호출할 수 없다. 한 댓글 작성자의 말을 빌리면, 이는 플러그인에게 무언가를 하지 말라고 부탁하는 것과는 완전히 다른 범주의 이야기다.
diff는 transcript 옆에 놓이는 패널로, 세션의 커밋되지 않은 변경 사항을 파일 단위로 보여주며 Claude가 편집할 때마다 갱신된다. 세션 시작 시 등록되며 27개의 helper 파일에 걸쳐 있어, 장난감 수준이 아니다.
telemetry는 엔진 생성 내부에서 $에 noun 하나를 추가한다. 아래에 있는 것을 await한 뒤 그것과 자기 자신을 함께 반환한다. 내부 빌드에서만 실행된다.
README는 소스에서 하나를 실행하고 소스에서 하나를 테스트하라고 안내한다. 오늘 빌드의 help는 validate, eval, details를 나열한다. test 항목의 help는 여전히 응답하는데도, 목록에는 test가 없다. 내장 티어는 당신의 사본도 거부한다: 같은 이름으로 plugin을 배포해 봐야, 바이너리는 자기 자신의 것을 로드한다.
42줄: 모델로부터 비밀을 숨기는 Mod
타입을 적어 넣는 슬래시 명령어는 이 빌드 기준으로 11,700줄을 생성한다: 23개의 noun에 걸친 84개의 이벤트다. tool call hook은 결과 또는 거부로만 응답하며, 텍스트로는 답하지 않는다. 이 부분은 core에서 정해진다.
이 Mod는 파일 세 개로 이루어진다: plugin manifest, 한 줄짜리 hooks manifest, 그리고 모듈 자체. 모듈은 42줄이다. 아래에 있는 것을 await하고, 결과를 스크럽(scrub)한 뒤 돌려준다. 네 가지 패턴이 두 가지 벤더 키 형태, GitHub 토큰, 그리고 key, secret, token이라는 이름의 변수에 할당된 모든 값을 커버한다.
validate는 실행되기 전에 소스를 읽는다. 그 모듈이 훅하는 이벤트와 $에서 호출하는 단 하나의 것을 이름으로 알려준다. 유일한 경고는 author 누락뿐이다. 플래그를 켠 채 디스크에서 로드하면, 테스트 파일에는 애초에 가짜로 만들어진 키 두 개가 들어 있고, 모델은 "redacted"라고 읽는다. 모델 자신의 말을 빌리면, 값들이 redacted 상태로 돌아왔기 때문에 그 안에 무엇이 있는지 볼 수 없다는 것이다.
| Measure | Value |
|---|---|
| Module length | 42 lines |
| Hop latency, worker included | 28 ms |
| Tokens added to the session | 0 |
엔진은 이 hop을 hooks module에 의해 resolved된 것으로 기록한다. 인벤토리에는 skill도, agent도, 상시 작동하는 것도 전혀 없다. 한 가지 한계는 남는다: 모델로부터 숨겨진 것이 화면으로부터도 숨겨진 것은 아니다. transcript에는 여전히 그 tool이 출력한 내용이 보인다. Ship Notes의 Max가 한 줄로 지적했듯, 그건 다른 Mod의 몫이다.
부숴보기: 느린 가드는 우회된 가드다
같은 Mod에 한 줄을 추가한다: 아래에 있는 것을 호출하기 전에 15초 동안 sleep. 10초 뒤, 엔진은 이를 포기하고 hook이 예산을 초과해 스킵되었다고 보고하며, 그 아래에 있는 것이 대신 실행된다. 명령어는 어쨌든 실행되어 "hi"를 출력한다.
sleep을 throw로 바꿔도: 574밀리초, 같은 판정, 스킵, 명령어는 실행된다.
| Case | Time | Verdict |
|---|---|---|
| Healthy hook | 28 ms | Resolved |
| Throw | 574 ms | Skipped, command ran |
| Hang | over 10 s | Skipped, command ran |
두 실패 모두 같은 단어로 끝난다: 스킵됨. 스레드는 일주일 전 이미 이 비대칭성을 측정해 두었다. 로드 시점에 없는 capability는 fail closed다. 예산을 넘긴 hook은 fail open이다: 시끄럽지만 우회된다는 것이, Spencer Morley의 말이다. 이유 없이 그냥 막히면 모델은 다른 tool로 넘어갈 뿐이다. Pratham은 모델이 다른 tool을 골라 어쨌든 파일을 쓰는 것을 지켜보았다.
지금 테이블 위에 놓인 답은 catch다. Alice Poteat는 hook의 반환값에 catch를 두자고 제안하는데, 이는 시간을 너무 끌거나 throw할 때 실행된다. 다시 쓰인 write는 여전히 모델에게 알려줄 문구가 필요하다: 모델은 캐싱 때문에 자신이 쓰려고 했던 것을 그대로 보게 되므로, 컨텍스트 줄 하나를 덧붙여야 한다. 예산은 곧 격리이기도 하다. hooks worker는 별도로 실행되며, 그것이 죽으면 엔진은 이를 재기동하고 해당 세션의 hook을 꺼버린다. 해법은 더 긴 예산이 아니라 명시적으로 선언된 catch다.
Mod의 48시간, 그리고 한 문장으로 얻는 것
이틀 만에: 프롬프트 위에서 테트리스와 다른 게임 일곱 개를 Claude가 작업하는 동안 플레이할 수 있게 되었고, 토큰은 0개다. 1993년 원작 둠은 자체 프로세스로 실행되며, Mod는 로컬 HTTP를 통해 여기에 접근해 초당 10번씩 다시 그린다. 스피너 안에는 숨쉬는 듯한 페이서가 있고, hooks worker 안에서 260,000개의 파라미터로 돌아가는 스토리 모델이 API 토큰 없이 실행된다.
레지스트리는 validate로 31개의 Mod를 스캔했다. 그중 14개는 host 프로세스를 실행할 수 있다. 13개는 모든 tool call을 볼 수 있다.
커뮤니티 스레드의 데모 8은 한 문장이면 모델이 비밀을 읽기 전에 숨기는 plugin을 작성해 준다고 약속했다. 우리는 그걸 직접 요청해 보았다.
| Measure | Value |
|---|---|
| Time | 4 min |
| Turns | 34 |
| Cost | $1.23 |
| Lines (plus a test file) | 190, against our 42 |
| Tests | 4 pass in a third of a second |
결과물은 무엇을 숨겼는지 종류별로 표시하고, 깨끗하게 검증을 통과하며, API 키도 모델 호출도 필요 없다. 한 댓글은 이렇게 정리했다: allowlist가 진짜 제품이고, 테트리스는 그냥 데모다.
결론: 지금 만들고, 나중에 출시하라
레지스트리 자체의 수치가 경고다. 31개 중 14개의 Mod가 host 프로세스를 실행할 수 있는데, 검증 수단은 정적 footprint 하나뿐이다. eval의 help 문구 자체가 이를 말해준다: 테스트 통과가 보안 검증은 아니라고.
| Signal | Value |
|---|---|
| Events on the 8th | 20 |
| Events on the 15th | 84 |
| CLI versions in a fortnight | 14 |
| Hacker News points | 2 |
| Replies to the compatibility question | 0 |
10초짜리 예산은 타입이 아니라 런타임 안에 산다고, Marat는 지적했다. 체인지로그 줄도, 문서 페이지도, 출시 포스트도 없다. 한 경쟁자는 Anthropic이 이를 베꼈다고 주장한다: Ahmad Awais는 Command Code의 mods를 가리키며 그들의 예제 mod를 직접 작성한 당사자이니, 이 발언은 그 점을 감안해서 받아들여야 한다. "early access, APIs may change"라는 문구가 달린 포스트는 357개의 좋아요를 모았다.
이미 hook을 작성하고 있고 감사 로그, redaction, 패널을 원한다면 지금 하나 만들어라. 팀에 배포하기 전이라면 기다려라, 계약(contract)이 아직 쓰이지 않았으니까. 같은 개방성 덕분에, fail open하는 예산, MCP call 타입 불일치, wrapper 실패가 모두 며칠 만에 사용자들에 의해 발견되었다.
AIDive