
안녕하세요, 미니지식공간입니다.
Claude Code mods는 2026년 10월 1일 공개된 확장 방식으로, 플러그인 디렉터리에 TypeScript 훅 모듈 하나를 넣어 Claude Code가 내보내는 이벤트를 가로채는 구조다. 이 글은 공식 문서의 파일 구조, register 시그니처, 이벤트 목록과 실행 제한을 코드 기준으로 정리한다.
hooks/hooks.json의modules배열이 훅 모듈을 가리키고, 그 모듈은register(on, options)를 export한다.on(이벤트명, matcher?, hook)으로 이벤트에 함수를 걸고,hook($, e, next)에서next(e)를 호출하면 미들웨어처럼 다음 핸들러로 넘어간다.- 훅 모듈 안에는 Node.js API도
setTimeout도 없다. 파일·프로세스·네트워크는 전부$네임스페이스를 통과한다.
| 항목 | 내용 |
|---|---|
| 발표일 | 2026-10-01 (Anthropic 공식 블로그) |
| 주체 | Anthropic |
| 필요 버전 | Claude Code 2.1.287 이상, 기본 활성화 |
| 작성 언어 | TypeScript / JavaScript (ES module) |
| 배포 단위 | 플러그인 (/plugin, Claude 디렉터리) |
| 동작 표면 | 터미널, 데스크톱 Code 탭 (WSL 데스크톱은 미지원) |
| 격리 | 없음 — Claude Code와 동일한 머신 접근 권한 |
| 사전 공개 | 출시 전 GitHub 이슈에 설계 공개 후 피드백 |
2.1.287 이전 버전에서 쓰였던 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS 환경 변수는 2.1.287 이후 무시된다. 즉 버전만 맞으면 별도 플래그 없이 바로 쓰이는 상태다.
mod는 플러그인 디렉터리에 파일 세 개가 들어간 형태다. 공식 문서가 제시한 트리는 다음과 같다.
first-mod/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.js
출처: https://code.claude.com/docs/en/plugins/mods/overview
눈여겨볼 지점은 plugin.json이다. 문서는 mods가 매니페스트에 추가로 요구하는 필수 필드가 없다고 적는다. mod임을 선언하는 곳은 매니페스트가 아니라 hooks/hooks.json의 modules 배열이다.
{ "modules": ["./register.js"] }
위 경로는 hooks.json 자신을 기준으로 한 상대 경로다. 같은 파일의 hooks 키 아래에 기존 설정형 hooks를 함께 둘 수도 있다. 훅 모듈의 확장자는 .js, .mjs, .cjs, .jsx, .ts, .mts, .cts, .tsx를 허용하며 ES module이어야 한다. $.state를 쓰거나 mods API에 네임스페이스를 추가하는 경우에만 매니페스트의 types가 가리키는 types/index.d.ts가 추가로 필요하다.
출처: https://code.claude.com/docs/en/plugins/mods/reference
공식 문서의 최소 예제는 도구 호출 횟수를 세어 스피너 옆에 붙이는 mod다. 아래는 문서에 실린 hooks/register.js 원문이다.
// The count, shared by the two hooks below
let calls = 0
// Claude Code calls this once when the mod loads
export function register(on) {
// Runs each time Claude is about to use a tool
on('tool.call', async ($, e, next) => {
calls += 1
// Ask Claude Code to draw the interface again, so the new count shows
$.ui.invalidate('ui.render')
// Let the tool run as usual
return next(e)
})
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}
출처: https://code.claude.com/docs/en/plugins/mods/overview
구조가 읽히는 지점이 몇 개 있다. register는 mod가 로드될 때 한 번 호출되고, 그 안에서 on을 여러 번 불러 이벤트마다 함수를 건다. 두 번째 인자로 넘긴 { component: 'Spinner' }가 matcher인데, 이벤트 필드에 대한 필터라서 스피너를 그릴 때만 훅이 들어온다. 문서가 적은 on의 형태는 on('tool.call', { tool: 'Bash' }, async ($, e, next) => next(e))이고, 반환된 등록 객체에는 오류 처리기를 지정하는 .catch(handler) 하나가 달려 있다.
훅이 받는 세 인자는 역할이 분명하다.
| 인자 | 역할 |
|---|---|
$ | mods API. 네임스페이스와 메서드를 전부 써서 호출한다 ($.fs.read('notes.md')) |
e | 이벤트 입력. 깊게 동결된 평문 데이터이므로 바꾸려면 복사본을 next에 넘긴다 |
next(e) | 다음 핸들러. 뒤의 훅들과 Claude Code의 기본 동작을 실행하고 결과로 resolve된다 |
next에는 부가 정보가 붙는다. next.signal은 이벤트가 버려질 때 중단되는 AbortSignal이고, next.origin은 이벤트를 낸 쪽의 { plugin, tier }다. Claude Code 자신은 { plugin: 'engine', tier: 'core' }로 표기된다. next.budget.remainingMs로 남은 시간 예산을 읽을 수 있고, next.to(e, tier)로 뒤쪽 tier로 건너뛸 수 있는데 이건 prependPlugins·appendPlugins에 올라간 mod만 호출할 수 있다.
실행 순서는 로드 순서다. 먼저 로드된 mod가 이벤트를 가장 먼저 보고 결과를 가장 마지막에 본다. 서로 다른 제작자의 mod를 겹쳐 쓸 수 있는 근거가 이 규칙이다.

문서가 나열한 이벤트는 범주별로 묶여 있다. 도구는 tool.call, tool.check, tool.describe, 프롬프트는 prompt.submit, prompt.compose, prompt.section, prompt.context 등, 턴은 turn.start, turn.step, turn.complete, 세션은 session.start, session.compact, session.send 등, 서브에이전트는 agent.offer, agent.spawn, 화면은 ui.render, ui.press, ui.input 등이다. 여기에 다른 mod를 다루는 plugin.register, engine.create와 텔레메트리 두 개가 더 있다.
반환값이 이벤트마다 정해져 있어서 사실상 이게 API 계약이다.
| 이벤트 | 반환 |
|---|---|
tool.call | next(e) / { deny: reason } / { result } |
tool.check | { decision } — allow, ask, deny |
prompt.submit | next({ ...e, text }) / next({ ...e, context }) / { drop: reason } |
command.run | { text } / {} / next(e) |
turn.step | yield* next(e) 또는 next({ ...e, model }), next({ ...e, effort }) |
agent.spawn | { model } / { deny: reason } |
plugin.register | { refuse: reason } |
turn.step과 process.spawn의 훅은 async generator이고 나머지는 async 함수다. 그래서 위 표의 turn.step 행만 yield*를 쓴다.
확장 범주가 두 개 더 있다. 하나는 기존 설정형 hooks를 받는 classic.<Event> 계열(classic.Stop, classic.PostToolUse 등)로, 이때 e는 그 훅의 stdin JSON이다. 다른 하나가 더 흥미로운데, mods API의 모든 메서드가 그 자체로 이벤트다. fs.read, model.complete, ui.open 같은 이름으로 걸 수 있고, 먼저 로드된 mod가 뒤에 오는 mod의 API 호출을 가로채 next(e), { deny: reason }, { value } 중 하나를 돌려줄 수 있다. 공식 블로그가 말한 "먼저 로드되는 mod가 다른 모든 mod의 호출을 기록하는 감사 로깅"이 이 메커니즘이다.
출처: https://code.claude.com/docs/en/plugins/mods/reference
session.start에서 등록하고 전용 이벤트에서 처리하는 패턴이 문서의 기본형이다. 슬래시 명령을 추가하는 예제는 다음과 같다.
on('session.start', async ($, e, next) => {
// Add /standup to the command list, with the description the user sees there
await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' })
return next(e)
})
// The matcher limits the hook to /standup, so other commands don't reach it
on('command.run', { command: 'standup' }, async ($, e) => {
// e.args is the text typed after the command name, or an empty string
return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }
})
출처: https://code.claude.com/docs/en/plugins/mods/api
도구도 같은 모양이다. 아래 예제에서 플러그인 이름이 my-mod일 때 최종 도구 이름이 mcp__my-mod__ticket이 되는 규칙에 주의할 만하다.
on('session.start', async ($, e, next) => {
await $.tool.register({
name: 'ticket',
// Claude decides when to call the tool from this description
description: 'Look up a ticket by its id and return its title and status',
// The arguments Claude has to send: one required string named id
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
})
return next(e)
})
// The full tool name is mcp__, the plugin's name, and the registered name
on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => {
// The tool's arguments are fields of e, so the id is e.id
const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id))
// Return a result either way, so Claude learns when the lookup failed
return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }
})
출처: https://code.claude.com/docs/en/plugins/mods/api
$ 네임스페이스와 실제 경계mods API는 네임스페이스 단위로 나뉘어 있다. 문서가 적은 목록은 $.plugin, $.ui, $.command, $.tool, $.agent, $.model, $.prompt, $.turn, $.session, $.config, $.settings, $.env, $.fs, $.store, $.state, $.clock, $.http, $.process, $.mcp, $.audio, $.telemetry다.
여기서 설계 의도가 드러나는 문장이 하나 있다. 훅 모듈 자체에는 Node.js API가 없고, setTimeout 같은 타이머 전역도 없으며, 자체적인 네트워크·파일 접근도 없다는 것이다. 대신 URL, TextEncoder, AbortController, crypto.subtle 같은 표준 웹 API는 쓸 수 있다. 파일을 읽으려면 $.fs.read, 명령을 돌리려면 $.process.run, 요청을 보내려면 $.http.fetch를 거쳐야 하고, 그 호출들이 다시 이벤트라서 다른 mod의 감시 대상이 된다.
제한값도 문서에 수치로 적혀 있다.
| 항목 | 한도 |
|---|---|
| 이벤트당 훅 실행 | 10초 (next와 $.clock.sleep 외 API 호출 시간 제외) |
.catch 처리기 | 1초 |
session.end 훅 전체 | 1.5초 |
$.process.run | 기본 30초, 최대 10분 |
$.model.complete maxTokens | 기본 1024, 최대 64,000 |
$.fs.read / write | 파일당 4 MiB |
$.store | 4 MiB JSON |
$.session.messages() | 최신 4,096건 |
$.ui.invalidate('ui.render') | 초당 10회 (터미널의 보이는 창 등은 30회) |
단, 이 경계를 격리로 읽으면 안 된다. Anthropic은 공식 블로그 도입부에서 mods가 샌드박스에 들어가지 않으며 Claude Code 자체와 같은 수준으로 머신에 접근한다고 못박았다. API를 통과하는 구조와 설치물의 신뢰 문제는 별개다. 플러그인 공급망에서 핀이 해석되는 지점이 실제 공격면이 됐던 사례는 Plugin4Shell 정리 글에 적어 둔 바 있다.
문서가 제시한 명령은 세 개다. 실행 없이 정적으로 들여다보는 쪽이 먼저다.
# 어떤 이벤트를 다루고 어떤 API를 호출하는지 hooks:/calls: 줄로 출력
claude plugin validate ./some-mod
# 플러그인 디렉터리를 한 세션에 로드하고, 저장할 때마다 훅 모듈 리로드
claude --plugin-dir ./some-mod
# *.test.ts / *.test.tsx 실행, 실패 시 exit 1
claude plugin test ./some-mod
출처: https://code.claude.com/docs/en/plugins/mods/overview
claude plugin validate에 --strict와 --json을 붙일 수 있고, 테스트 하나의 기본 제한은 5초다. 끄는 경로는 세 가지로, /plugin에서 플러그인 비활성화, --safe-mode 실행, ~/.claude/settings.json의 "disableAllHooks": true다.
조직 설정 키도 공개돼 있다. prependPlugins, appendPlugins, allowManagedModsOnly, allowModsToOverrideDenyRules, allowManagedHooksOnly, disableAllHooks, disableSideloadFlags, pluginConfigs이고, 환경 변수로는 CLAUDE_CODE_PLUGIN_DIRS, CLAUDE_CODE_PLUGIN_DIR_WATCH가 있다. Team·Enterprise 플랜과 관리 설정이 적용된 머신에서는 sec-default가 가장 먼저 로드돼 사용자 설치 mod가 권한 거부 규칙을 덮어쓰지 못하게 막는다.
내장 기능 일부도 이미 mod로 내려왔다. 문서에 이름이 있는 내장 mod는 cc-plugin-agents-md, cc-plugin-diff, cc-plugin-plugin-authoring, cc-plugin-sec-default, cc-plugin-telemetry, cc-plugin-you-should-know 여섯 개다. 공식 블로그는 /diff가 mod가 됐으므로 끄거나 자기 버전으로 교체할 수 있다고 적었다. 샘플 mod는 anthropics/claude-code-playground 저장소의 token-weather, blast-radius, replay-theater다. 2026년 10월 2일 외신 보도는 blast-radius가 rm -rf 같은 고위험 명령의 설명을 화면 오른쪽에 띄우고, replay-theater가 /replay로 한 턴의 파일 편집 diff를 단계별로 보여 준다고 전했다.
mods를 쓰려면 Claude Code 버전을 올려야 하나?
문서 기준으로 2.1.287 이상이 필요하고, 그 버전부터 기본 활성화다. 이전 버전에서 쓰였던 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS는 2.1.287 이후 무시된다.
기존 hooks 설정과 충돌하나?
충돌한다는 서술은 없다. 설정형 hooks는 hooks.json의 hooks 키에 그대로 둘 수 있고, mods에서도 classic.<Event> 계열 이벤트로 받을 수 있다. 공식 블로그는 hooks가 이벤트 재작성·새 UI·기능 대체를 못 한다는 점을 mods의 차별점으로 설명했다.
mod가 내 토큰이나 시크릿을 읽을 수 있나?
구조상 막혀 있지 않다. Anthropic은 mods가 샌드박스에 격리되지 않고 Claude Code와 같은 머신 접근 권한을 가진다고 공식 블로그에 명시했으며, 신뢰할 수 있는 출처의 mod만 설치하라고 권고했다. 조직 단위로는 마켓플레이스 차단과 allowManagedModsOnly 같은 설정이 대응 수단이다.
mods는 Claude Code를 "쓰는 도구"에서 "고쳐 쓰는 도구"로 옮기는 변경입니다. 이벤트 목록과 반환값 계약이 문서로 공개돼 있으니, 당장 만들 계획이 없어도 어떤 지점이 열려 있는지 한 번 훑어 두면 팀 표준을 정할 때 기준이 생깁니다. 반대로 설치 쪽은 격리가 없다는 전제에서 출처와 조직 설정을 먼저 정리해 두시길 권합니다.
본 글은 공개 자료를 바탕으로 정리했으며, 세부 내용·수치는 원 출처·공식 문서와 대조 확인을 권장합니다. 코드 스니펫은 전부 공식 문서 원문이며, 플랜별 과금 조건은 공식 자료에 기재돼 있지 않아 확인이 필요합니다.