Mod 만들기
기준일: 2026-10-23
공식 기준: Create a mod
개요
mod는 진입 파일(hooks module)을 가진 Claude Code 플러그인입니다. 이벤트가 나면 Claude Code가 그 파일의 함수를 호출하는 JavaScript/TypeScript 파일입니다.
- Claude에게 쓰게 하기: 세션에서 원하는 것을 설명
- 직접 쓰기: 튜토리얼로 코드 동작 학습. Claude Code가
.js/.ts를 직접 로드하므로 Node.js, 번들러, 빌드 단계가 필요 없음
mod가 맞는 도구인지 아직 모르겠다면 먼저 개요의 비교를 읽으세요. Claude Code v2.1.287 이상이 필요합니다 (claude --version). 로드 가능 여부는 Check whether mods can load.
핵심 개념
| 개념 | 설명 |
|---|---|
| hooks module | hooks/register.js. register(on)을 export |
hooks.json |
modules 키로 코드 경로를 가리킴. 이 키가 있어야 mod |
on(event, [matcher], fn) |
hook 등록. 이벤트 이름은 문자열 리터럴 |
| hook 인자 | $(mods API), e(이벤트), next(다음 핸들러) |
--plugin-dir |
설치 없이 한 세션만 플러그인 디렉터리 로드, 파일 변경 시 hot reload |
claude plugin validate |
매니페스트와 hooks module 정적 분석 |
claude plugin test |
세션·로그인·네트워크 없는 자동 테스트 |
상세
Claude에게 mod 요청
대화형 세션에서 원하는 mod를 설명하면 Claude가 씁니다. Claude는 내장 skill plugin-authoring을 따릅니다 (어디에 쓰는지, 내 버전의 이벤트·메서드, 로드 방식). mod를 요청하면 로드하거나 프롬프트에서 /plugin-authoring으로 직접 로드할 수 있습니다. 승인하면 실행됩니다 (로드되지 않는 세션 제외).
- mod 설명: 예
make a mod that shows the current git branch above the prompt. Claude는 세션의 mods 폴더~/.claude/dev-mods/+ 세션 ID 아래 자체 디렉터리에 씁니다. 전체 경로 예:~/.claude/dev-mods/3f2a9c1e-5b7d-4e8a-9c21-6d0f4b8a7e13/git-branch/.default와acceptEdits권한 모드에서는 mods 폴더가 보호 경로라 파일마다 생성 전 승인을 묻습니다. - mod 승인: 첫 파일을 저장할 때 세션에 hot reloading을 켤지 묻습니다. Enable for this session이면 세션 mods 폴더의 mod가 턴 종료 시 로드되고, 변경한 턴마다 리로드됩니다 (세션을 resume해도 답 유지). Not now면 지금은 로드되지 않고 파일은 남으며 그 세션이 다음에 시작할 때 로드됩니다. 영원히 막으려면 디렉터리를 삭제하세요.
- 로드 확인:
/plugin에서 Tab으로 Installed 탭. 여기서 끌 수도 있음 - 써보기: 예제라면 프롬프트 상자 위에 현재 브랜치 이름. 원하는 대로 안 되면 Claude에게 수정을 요청. 파일을 바꾼 턴이 끝날 때 리로드됨
다른 세션에서 쓰기
Claude가 쓴 mod는 만든 세션에서만 로드되고, 그 세션의 mods 폴더는 cleanupPeriodDays보다 오래되면 삭제됩니다. 보관하려면 디렉터리를 ~/mods/git-branch 같은 곳에 복사한 뒤:
- 내가 시작하는 세션:
claude --plugin-dir ~/mods/git-branch - 다른 사람: 마켓플레이스에 추가
Claude가 쓴 mod가 로드되지 않는 세션
승인 후, 신뢰된 workspace에서 mod가 허용될 때만 로드됩니다. 로드되지 않는 경우:
- 승인할 사람이 없음:
claude -p,dontAsk모드 등 프롬프트를 못 띄우는 세션 - workspace 미신뢰: 디렉터리 신뢰 프롬프트를 수락하지 않음
- mod 비활성:
--safe-mode/--bare로 시작,disableAllHooks설정, 조직 managed settings가 차단
직접 mod 작성
first-mod는 Claude의 도구 호출 수를 세어 작업 중 스피너 옆에 표시하고 이를 출력하는 /tally 명령을 추가합니다. 이후 Claude Code가 mod 옆에 쓰는 타입 선언을 읽고 claude plugin validate를 실행합니다. 세 파일을 씁니다.
first-mod/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.js
1. 디렉터리 생성
mkdir -p first-mod/.claude-plugin first-mod/hooks
PowerShell:
New-Item -ItemType Directory -Force first-mod\.claude-plugin, first-mod\hooks
2. 매니페스트 first-mod/.claude-plugin/plugin.json (특수 필드 없음, mod도 매니페스트가 필요):
{
"name": "first-mod",
"version": "0.1.0",
"description": "Counts Claude's tool calls, shows the count beside the spinner, and adds a /tally command",
"author": { "name": "Your Name" }
}
3. 코드 위치 알리기 first-mod/hooks/hooks.json. Claude Code는 플러그인을 로드할 때 hooks/hooks.json을 읽습니다. modules 키가 코드 경로를 주며, 이 키가 있어야 플러그인이 mod가 됩니다. hooks.json 기준 상대 경로 하나를 적습니다.
{
"description": "The first-mod hooks module",
"modules": ["./register.js"]
}
4. 코드 first-mod/hooks/register.js. mod가 로드되면 Claude Code가 export된 register 함수를 호출하며 on 함수를 넘깁니다. on 호출마다 이벤트용 핸들러(hook)가 등록됩니다.
// The count, shared by the hooks below
let calls = 0
// Claude Code calls this once when the mod loads
export function register(on) {
// Runs when the session starts, before your first prompt
on('session.start', async ($, e, next) => {
// Add the /tally command
await $.command.register({
name: 'tally',
description: 'Show how many tool calls Claude has made',
})
// Let the session start as usual
return next(e)
})
// 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 when you type /tally, and only then, because of the matcher
on('command.run', { command: 'tally' }, async () => {
// The text to print in the transcript
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
// 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 + '…' } })
})
}
hook 4개:
session.start: 세션 시작(첫 프롬프트 전)과 mod 리로드마다 실행./tally명령 추가tool.call: 도구 사용 직전.calls를 올리고 재그리기 요청command.run:/tally입력 시. 출력할 텍스트를 반환ui.render: 스피너를 그릴 때. 단어 뒤에 카운트 추가
5. 로드: --plugin-dir은 설치 없이 한 세션만 플러그인 디렉터리를 로드합니다.
claude --plugin-dir ./first-mod
6. 써보기: list the files here and read the README처럼 도구 호출이 몇 번 필요한 일을 시키면 스피너 단어 뒤에 Thinking · tool calls: 2…처럼 올라가는 카운트가 붙습니다. 끝난 뒤 /tally+Enter를 입력하면 transcript에 first-mod: Claude has made 2 tool calls since this mod loaded(본인 카운트)가 나옵니다. Claude Code가 명령 텍스트 앞에 플러그인 이름을 붙입니다. 대화형 세션 없이 명령을 확인:
claude -p "/tally" --plugin-dir ./first-mod
first-mod: Claude has made 0 tool calls since this mod loaded
/tally가 명령 목록에 없으면 module이 로드되지 않은 것입니다. Find out why a mod does nothing.
7. 세션이 도는 중 코드 변경: 세션을 열어 둔 채 register.js의 ui.render hook에서 ' · tool calls: '를 ' · tools used: '로 바꿔 저장합니다.
// 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: ' · tools used: ' + calls + '…' } })
})
transcript에 first-mod가 리로드됐다는 줄과 hook 목록이 나오고, 다음 스피너는 Thinking · tools used: 1…처럼 새 텍스트를 씁니다.
예제가 동작하는 방식
on에 넘기는 함수가 hook입니다. 모든 hook은 같은 세 인자를 받습니다.
- mods API
$: mod가 바깥에 닿는 모든 메서드,$.ui·$.command같은 namespace - 이벤트
e: 도구 호출의 이름과 인자 같은 이벤트 입력(평범한 데이터) - 다음 핸들러
next: 이벤트를 다른 mod, 이어서 Claude Code 자체 동작에 넘기고 결과를 반환
처리 방식:
- Observe:
session.starthook은 명령을 등록하고tool.callhook은 호출을 세고 재그리기를 요청. 둘 다next(e)를 반환하므로 세션이 시작되고 도구가 평소대로 실행됨 - Answer:
command.runhook은 자체 결과를 반환하고next를 호출하지 않음.on의 두 번째 인자{ command: 'tally' }는 matcher라는 필터라/tally일 때만 실행 - Rewrite:
ui.renderhook은suffix에 카운트를 담은e의 복사본으로next를 호출해 평소 스피너 단어 뒤에 텍스트를 붙임
Claude Code는 --plugin-dir로 로드한 디렉터리를 감시해 파일이 바뀌면 hooks module을 hot reload합니다. 리로드마다 register가 다시 실행되므로 calls가 0으로 초기화되고 /tally가 다시 셉니다. 리로드 사이에 값을 유지하려면 Keep state.
mod 계속 개발
Claude로 mod 수정
기존 mod를 바꾸려면 mod 디렉터리를 --plugin-dir로 가리켜 세션을 시작하세요. 그래야 Claude가 쓴 것이 같은 세션에 로드됩니다.
claude --plugin-dir ./first-mod
예: add a /tally-reset command to this mod that sets the tally back to zero. Claude가 hooks module을 고치고 claude plugin validate를 실행해 보고된 문제를 고칩니다. --plugin-dir로 로드한 디렉터리는 보호 경로라 default/acceptEdits에서는 Claude의 편집마다 승인을 묻습니다. 그 턴에 저장한 파일은 턴이 끝나면 리로드되므로 Claude가 끝나자마자 /tally-reset을 써볼 수 있습니다.
내 버전의 타입 정의
인터랙티브 세션에서 --plugin-dir로 mod를 로드하거나 Claude가 쓴 mod를 로드하면, Claude Code가 mod의 .claude-plugin/types/ 디렉터리에 TypeScript 선언 파일을 씁니다. 실행 중인 버전의 정확한 이벤트·mods API 메서드·요소를 기술해 에디터가 자동완성과 타입 검사를 합니다.
| 경로 | 선언하는 것 |
|---|---|
claude-code/index.d.ts |
모든 이벤트와 입력·결과, 모든 mods API namespace와 메서드, 각 surface가 그릴 수 있는 요소 |
claude-code-tools/index.d.ts |
내장 도구의 입력·결과 (e.tool === 'Bash' 검사로 e가 좁혀짐) |
claude-code-mcp/index.d.ts |
mod 파일을 마지막 저장했을 때 연결돼 있던 MCP 도구의 입력 |
플러그인 이름 디렉터리의 index.d.ts |
그 플러그인이 mods API에 더하는 것. plugin.json의 dependencies에 나열한 플러그인마다 하나 |
tsconfig.json |
hooks module에 맞는 컴파일러 옵션 |
mod에 자체 tsconfig.json이 없으면 생성된 것을 확장하는 파일을 mod 루트에 추가하므로 에디터와 tsc -p ./first-mod가 추가 설정 없이 타입 검사합니다. 이벤트와 메서드는 릴리스 간에 바뀔 수 있으니, 이 파일들과 페이지 내용이 다르면 파일을 믿으세요. claude-code/index.d.ts가 내 빌드에서 가장 완전한 레퍼런스이며 모든 메서드에 주석과 예제가 있습니다 (예: 'tool.call' 검색).
Claude Code가 mod에서 읽는 것 확인
코드를 실행하거나 세션을 시작하지 않고 claude plugin validate를 사용합니다. 매니페스트를 검사하고, 로드 시 Claude Code가 하는 것과 같은 hooks module 소스 정적 분석을 합니다.
claude plugin validate ./first-mod
first-mod 출력:
❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
❯ ./register.js calls: $.command.register, $.ui.invalidate
✔ Validation passed
hooks: 줄에서 hook한 이벤트(중괄호 안에 필터)를, calls:에서 호출하는 모든 mods API 메서드를 확인합니다. 환경 변수를 읽거나 설정하면 env reads:/env writes: 줄이, $.state를 쓰면 state reads:/state writes: 줄이 나옵니다. 동작을 거부할 수 있는 hook마다 gating hook without .catch: tool.call 같은 줄이 나와 .catch 핸들러 유무를 알려 줍니다. 첫 줄에 의도한 이벤트가 없으면 Claude Code도 그 hook을 호출하지 않으며, 보통 이벤트 이름 오타이고 "tool.calls" is not an event 같은 오류로 보고됩니다.
정적 분석이 모든 hook과 호출을 찾도록 지킬 규칙:
- mods API 호출을 완전히 씁니다:
$, namespace, 메서드 순 ($.store.get('notes')).$는 같은 파일 최상위에 선언한 함수에 넘길 수 있고,loadNotes라는 함수라면calls:줄에$.store.get (via loadNotes)로 나옵니다. 메서드, hook 안에 정의한 함수, 다른 파일에서 import한 함수에$를 넘기면 검증 실패 ($.state가 쓰는read/update함수는 받을 수 있는 import).$나 그 namespace를 변수에 대입, 구조 분해, 계산된 이름으로 인덱싱 금지.const ui = $.ui는$.ui is used as a value로 실패 - 각
on호출의 이벤트 이름은'tool.call'같은 문자열 리터럴. 변수나 이름 목록 반복은the event name passed to on() is not a string literal로 실패 register안에on이라는 두 번째 변수/매개변수 금지."on" is declared again (shadowed)로 실패- 플러그인 디렉터리 안 파일만 상대 경로로 import. 허용되는 bare import는
claude-code(타입과 일부 helper)뿐 - 파일 맨 위의
import선언 사용 (import { name } from './file.js'). 동적import()는a dynamic import(); a hooks module imports its own files with an import declaration로 실패 - 모든 파일을 ES module로 작성 (
require아님). Claude Code가 로드하는 확장자는 reference
mod 테스트
세션·로그인·네트워크 없이 셸에서 claude plugin test로 자동 테스트를 실행합니다. 테스트가 hook이 처리하는 이벤트를 발생시키고 hook이 한 일을 검사합니다. 도구 호출 2회 → /tally → 응답이 둘 다 센지 확인. first-mod/tests/first-mod.test.ts:
import { expect, test } from 'claude-code/testing'
test('/tally reports the tool calls the mod has seen', async ($, on) => {
// Answer each tool call in Claude Code's place, so no tool runs
on('tool.call', () => ({ result: 'ok' }))
// Fire two tool calls, which the mod's tool.call hook counts
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Run /tally and check the text its hook returns
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})
first-mod 디렉터리에서:
claude plugin test
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [22.87ms]
1 pass
0 fail
Ran 1 test across 1 file. [0.19s]
모델 호출/store stub, timer·그리기 테스트는 Test a mod.
mod 공유
mod는 플러그인이므로 매니페스트에서 버전을 관리하고 /plugin 명령으로 설치·업데이트합니다.
- 몇 사람: 플러그인 디렉터리나
.zip전달 (Share a plugin without a marketplace) - 팀: 자체 마켓플레이스에 등록 (예: 플러그인마다 디렉터리를 둔 private 저장소). 저장소 작업자 전체에 추가하려면 저장소 설정에 등록
- 조직 전체: 관리자가 managed settings로 조직의 mod를 설치
- 누구나: 마켓플레이스 저장소를 공개하거나 Anthropic 디렉터리에 제출
공유 전 name을 확인하세요. claude plugin validate는 claude-로 시작하는 등 Anthropic 자체 이름처럼 보이는 이름을 실패시킵니다. 이벤트와 메서드가 릴리스 간에 바뀔 수 있으니 README에 테스트한 Claude Code 버전을 적으세요. 개발은 설치된 사본이 아니라 --plugin-dir 디렉터리로 계속하세요. 설치된 플러그인은 버전별로 캐시되므로 버전을 올려 다시 설치해야 편집이 설치본에 반영됩니다.
체크리스트
- Claude Code v2.1.287 이상
-
plugin.json,hooks/hooks.json(modules),hooks/register.js작성 -
claude --plugin-dir ./first-mod로 로드해 hot reload 확인 -
claude plugin validate의hooks:/calls:가 의도와 일치 -
claude plugin test로 테스트 통과 - 공유 전
name과 README의 테스트 버전 확인
함께 보기
- Mods overview, Manage mods for your organization
- Draw in the interface, React to events, Use the mods API
- Test a mod, Troubleshoot a mod
- Read the source of built-in mods
기준일
- 문서 작성·동기화 기준일: 2026-10-23 (v2.1.309 반영)
- 원문: https://code.claude.com/docs/en/plugins/mods/create