Claude Code 'Claude Mods' 출시! 플러그인이 Claude Code 자체를 고친다 - v2.1.287부터 기본 켜짐·내장 'You should know' 모드·커뮤니티 모드 75개·register(on) 한 파일로 첫 모드 만들기 🧩
한눈에 보기
Claude Code v2.1.287부터 플러그인이 Claude Code의 화면·툴 호출·명령어를 안에서 직접 고치는 'Claude Mods'가 기본으로 켜졌어요. 따로 설정할 것 없이 버전만 맞으면 되고, 내장 모드 'You should know'는 /plugin에서 켜요. register(on) 함수 하나짜리 파일로 툴 호출 횟수를 세는 첫 모드를 만들고 validate·test까지 돌려 봅니다.
- Claude Code v2.1.287(2026-10-01 체인지로그) · 공식 모드 문서(code.claude.com/docs/en/plugins/mods) 기준 2026-10-02 확인
- 모드는 v2.1.287부터 기본 켜짐 - 얼리 액세스 때 쓰던 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS는 이제 무시되니 settings.json에서 지우기
- 모드 = plugin.json + hooks.json(modules) + register.js 세 파일. 훅은 on('이벤트', matcher, async ($, e, next) => …) 한 모양
- 모드는 내 권한으로 파일·프로세스·네트워크에 닿고 샌드박스가 없음 - 설치 전 claude plugin validate로 hooks:·calls: 줄 확인
목차20개 항목
안녕하세요! Mojangcam입니다 😎
10월 1일에 올라온 Claude Code v2.1.287 체인지로그에 짧은 한 줄이 있었어요. "Added Claude Mods: plugins may now modify deeper behavior." 플러그인이 이제 더 깊은 동작을 고칠 수 있다는 건데, 실제로 열어 보니 생각보다 큰 변화더라고요. 지금까지 플러그인은 Claude에게 스킬(지시문)·MCP 도구·훅 스크립트를 바깥에서 붙여 주는 역할이었다면, 모드(Mod)는 Claude Code 프로세스 안에서 돌아가는 자바스크립트 함수예요. 스피너 글자를 바꾸고, 툴 호출을 가로채서 막고, 프롬프트 옆에 내 패널을 그릴 수 있어요.
이번 글에서 다루는 건 이 네 가지예요.
- 모드가 뭔지, 기존 훅·스킬·MCP와 뭐가 다른지 - 비교표 하나로
- 켜는 법 - 결론부터 말하면 v2.1.287이면 켤 게 없어요. 얼리 액세스 때 환경변수를 넣어 두신 분은 오히려 지워야 해요
- 내장 모드 6개와 'You should know' - 긴 작업 중에 옆에서 지켜보다가 놓친 걸 알려 주는 모드
- 첫 모드 만들기 -
register(on)함수 하나짜리 파일로 툴 호출 횟수를 세고/tally로 보여 주는 모드. 검증(validate)·테스트(test)까지
Claude Code 입문 글에서 설치하고 오토 모드 글에서 권한 모드를 익히셨다면, 이번 편은 "Claude Code를 내 손에 맞게 깎는" 단계예요. 자바스크립트를 많이 몰라도 따라올 수 있게 코드는 한 줄씩 풀어 둘게요 🙌
📌 확인한 환경
- Claude Code v2.1.287 (2026-10-01 체인지로그 기준 최신) - 공식 체인지로그와 모드 문서(code.claude.com/docs/en/plugins/mods) 기준으로 정리했어요
- 이 글을 준비한 라즈베리파이에는 v2.1.286이 설치돼 있었어요. 그래서 모드 로딩·
/tally실행 화면은 문서 기준 예시 화면이고, 발행 전에 윈도우 PC에서 v2.1.287로 올려 캡처할 예정이에요 - 코드는 공식 문서의
first-mod예제를 그대로 썼어요 (이벤트·메서드 이름은 릴리스마다 바뀔 수 있다고 문서가 못 박고 있으니, 글의 버전을 꼭 봐 주세요)
버전부터 맞추고 시작할게요. 모드를 쓸 수 있는 상태인지는 claude plugin test로 확인해요. 모드가 없는 아무 폴더에서 돌리면 되고, 출력에 no hooks module to load가 들어 있으면 "모드는 켜져 있는데 이 폴더엔 모드가 없다"는 뜻이라 정상이에요.
claude update
claude --version # 2.1.287 이상
claude plugin test # 'no hooks module to load' 가 나오면 모드 사용 가능
🧩 1. 모드가 뭔가요 - 훅·스킬·MCP와 뭐가 다른가
Claude Code는 이벤트가 생길 때마다 등록된 함수를 불러요. Claude가 툴을 쓰려 할 때, 내가 프롬프트를 보낼 때, 스피너를 그릴 때 같은 순간이요. 모드는 그 순간에 끼어드는 함수 묶음이고, 함수는 세 가지 중 하나를 해요.
| 할 수 있는 일 | 뜻 | 예 |
|---|---|---|
| 지켜보기 (Observe) | 이벤트를 그대로 통과시키면서 기록만 | 툴 호출 횟수 세기 |
| 고쳐 쓰기 (Rewrite) | 이벤트 내용을 바꿔서 넘김 | 스피너 글자 뒤에 숫자 붙이기, 프롬프트 앞뒤 공백 지우기 |
| 직접 답하기 (Answer) | 원래 동작을 안 돌리고 내가 결과를 돌려줌 | git push --force 명령 거부하기 |
기존에 있던 것들과 비교하면 이래요. 공식 문서의 표를 옮겼어요.
| 모드 (Mod) | 설정 훅 (Settings hook) | 스킬 (Skill) | MCP 서버 | |
|---|---|---|---|---|
| 정체 | Claude Code 프로세스 안에서 도는 자바스크립트 함수 | 설정 파일에 적은 셸 명령·HTTP·프롬프트 | Claude가 읽는 SKILL.md | Claude에게 도구를 주는 외부 프로세스 |
| 바꿀 수 있는 것 | 툴 호출·프롬프트·명령어·턴·화면 | 툴 호출 허용 여부·인자·결과, 추가 컨텍스트 | Claude가 아는 것 | Claude가 쓸 수 있는 도구 |
| 화면에 그리기 | 가능 (패널·프롬프트 위 띠·버튼·입력창) | 불가 | 불가 | 불가 |
| 뭘 쓰나 | JS 또는 TS | 스크립트 + settings.json | 마크다운 | 아무 언어의 서버 |
| 고를 때 | 패널·명령어·이벤트 고쳐 쓰기가 필요할 때 | 이미 있는 스크립트로 막기·기록만 할 때 | 같은 지시를 자꾸 붙여 넣고 있을 때 | 외부 시스템에 닿아야 할 때 |
하나씩 뜯어볼게요 💡
- 설정 훅은 없어지지 않아요 : 문서가 "nothing about them is deprecated"라고 분명히 적었어요. 기존
PreToolUse훅 스크립트는 그대로 돌아가요. 다만 Claude Code가 둘 다 "훅"이라고 부르니, 모드 문서에서 "hook"은 모드의 함수, 설정 파일 쪽은 "settings hook"이라고 구분해요 - 모드만 할 수 있는 것 : 화면 그리기, 툴 호출을 붙잡아 두고 사용자에게 물어보기, 요청 하나를 다른 모델로 보내기, Claude 턴 없이 즉시 실행되는
/명령어, 훅끼리 변수 공유(한 훅이 센 숫자를 다른 훅이 화면에 표시) - 어디서 도나 : 터미널의
claude와 데스크톱 앱 Code 탭에서는 훅도 돌고 화면도 그려요. VS Code 확장 채팅 패널,claude -p, 클라우드 세션에서는 훅은 돌지만 화면은 안 그려져요. WSL 세션(데스크톱 앱)은 플러그인 자체가 안 돼요 - Claude Code 자신도 모드로 만들어졌어요 :
/diff패널, AGENTS.md 읽기 같은 기능이 내장 모드예요. 3장에서 봐요
🛠️ 2. 켜는 법 - 사실 켤 게 없어요 (그리고 지울 게 하나 있어요)
v2.1.287부터 모드는 기본으로 켜져 있어요. 설정 키도, 환경변수도 필요 없어요. claude --version이 2.1.287 이상이면 끝이에요.
여기서 주의할 게 하나 있어요. 정식 출시 전 얼리 액세스 기간에는 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS라는 환경변수로 켰었거든요. 커뮤니티 디렉터리(4장) 안내문에도 아직 이 변수를 settings.json의 env에 넣으라고 적혀 있어요. 그런데 공식 문서는 이렇게 말해요.
If you set
CLAUDE_CODE_ENABLE_FUNCTION_HOOKSduring early access, remove it. Claude Code v2.1.287 and later ignores it, so setting it to0doesn't keep mods off.
- 넣어 둔 분 : 지우세요. 있어도 해는 없지만,
0으로 바꿔도 모드가 꺼지지 않아요. 끄려면 아래 방법을 써야 해요 - 지금 처음 쓰는 분 : 아무것도 넣지 마세요
끄고 싶을 때 - 범위별로 세 가지
| 범위 | 방법 | 같이 꺼지는 것 |
|---|---|---|
| 모드 하나 | /plugin → Installed 탭에서 그 플러그인 끄기(또는 삭제) |
없음 |
| 이번 세션의 설치 모드 전부 | claude --safe-mode로 시작 |
내 다른 커스터마이징도 빠짐 |
| 모든 세션의 설치 모드 전부 | ~/.claude/settings.json에 "disableAllHooks": true |
설정 훅·커스텀 상태줄도 멈춤. 조직이 관리하는 건 계속 돎 |
disableAllHooks는 모드만 멈추고 그 플러그인의 스킬·명령어·MCP 서버는 그대로 로드돼요. 그리고 내장 모드는 이 설정들로 꺼지지 않아요. 내장 모드는 각자 끄는 스위치가 따로 있어요 (3장 표).
지금 어떤 모드가 로드됐나 보기
세션에서 /plugin을 치면 탭 아래에 흐린 글씨로 한 줄이 떠요. 예를 들면 1 mod active · first-mod. 설치한 모드가 여기 안 보이면 로드가 안 된 거예요 (원인은 7장 "자주 나는 에러").
💬 회사 계정이라면 : Team·Enterprise로 로그인했거나 관리 설정이 깔린 PC에는 내장 가드
cc-plugin-sec-default가 항상 먼저 돌아요. 관리자가allowManagedModsOnly를 켜 두면 내가 설치한 모드는 하나도 안 돌고, 디버그 로그에mods are limited to your organization's by policy줄이 남아요. 이건 내 설정으로 못 풀어요.
🧩 3. 내장 모드 6개와 'You should know'
/plugin → Installed 탭의 Built-in 아래에 Claude Code 자신이 쓰는 모드가 보여요. 업데이트·삭제는 못 하고 끄기만 돼요.
/plugin에 보이는 이름 |
하는 일 | 끄는 법 |
|---|---|---|
cc-plugin-agents-md |
AGENTS.md를 프로젝트 지시로 읽음 |
/plugin에서 끄기 |
cc-plugin-diff |
/diff 명령을 맡아 변경 사항 패널을 그림 |
끄면 /diff는 남고 내장 기본 동작이 답함 |
cc-plugin-plugin-authoring |
Claude에게 모드 작성용 plugin-authoring 스킬을 줌 (모드 코드는 없음) |
/plugin에서 끄기 |
cc-plugin-sec-default |
조직이 관리하는 설정을 사용자 모드로부터 보호하는 가드 | 못 끔 (관리 설정에서만 순서 조정) |
cc-plugin-telemetry |
Claude Code와 내장 모드의 분석 기록 전송 | /plugin에서 끄거나 DISABLE_TELEMETRY |
cc-plugin-you-should-know |
긴 작업 중 옆에서 지켜보는 사이드 에이전트. 놓치기 쉬운 걸 찾으면 프롬프트 위에 메모 | 기본 꺼짐. 아래 명령으로 켬 |
'You should know' 켜기
체인지로그 원문은 "a built-in mod where a side agent watches your back and flags things you or Claude might miss"예요. Claude가 긴 작업을 하는 동안 다른 에이전트가 따라 읽다가, 알아 둘 게 있으면 프롬프트 박스 위에 알림 한 줄을 띄워 줘요.
/plugin enable cc-plugin-you-should-know@builtin@builtin: 내장 플러그인을 가리키는 마켓플레이스 이름이에요. 빠뜨리면 못 찾아요- 보이는 조건 : 체인지로그에 "for first-party sessions with telemetry on"이라고 적혀 있어요. Anthropic 계정으로 직접 로그인한 세션에서 텔레메트리를 끄지 않았을 때
/plugin→ Installed → Show disabled에 나타나요. Bedrock·Vertex 같은 서드파티 프로바이더로 쓰시거나DISABLE_TELEMETRY를 켜 두셨으면 목록에 없을 수 있어요 - 끄는 건 다시
/plugin에서 disable

💬 내장 모드 중 네 개(
diff·agents-md·sec-default·telemetry)는 소스가 공개돼 있어요 (GitHub anthropics/claude-code 저장소의mods/폴더). 각각 완전한 플러그인이라 테스트까지 들어 있으니, 5장 첫 모드를 만든 다음 읽어 보면 "진짜 모드는 이렇게 쓰는구나"가 보여요. 특히diff는 버튼·스크롤이 있는 패널의 교과서예요.
🧩 4. 커뮤니티 모드 75개 둘러보기 - 그리고 설치 전에 꼭 할 것
공식 디렉터리는 아직 없고, claude-mods.com이라는 비공식 커뮤니티 디렉터리가 먼저 생겼어요. 사이트 스스로 "independent, unofficial directory"라고 밝히고 있고, Anthropic과는 관계가 없어요. 10월 2일 기준 75개가 올라와 있고, 분류는 이래요.
| 분류 | 개수 | 예 |
|---|---|---|
| Dashboards | 20 | 토큰·컨텍스트 사용량 패널 |
| Interface | 12 | firstmate-calm - 툴 행을 숨기고 스피너를 돛단배로 |
| Games | 10 | cc-arcade - Claude가 일하는 동안 토큰 안 쓰고 하는 게임 9종 |
| While you wait | 10 | 기다리는 동안 보여 주는 것들 |
| Context & memory | 8 | 세션 간 메모 |
| Security | 7 | pii-guard - 툴 출력에서 개인정보 가리기 |
| Automation | 7 | |
| Other | 1 |
재미있는 게 많지만, 설치 버튼을 누르기 전에 모드가 뭘 할 수 있는지를 꼭 알아 두셔야 해요. 공식 문서의 경고를 그대로 옮기면 이래요.
- 모드는 내 권한으로 내 PC에서 돌아요. 내 계정이 읽고 쓸 수 있는 파일은 다 읽고 쓸 수 있고, 프로그램을 실행하고, 네트워크 요청을 보낼 수 있어요
- 환경변수와 설정 파일을 읽어요. 거기 API 키가 있으면 그것도요
- 내가 보내는 모든 프롬프트와 모든 툴 호출을 봐요. 바꿀 수도 있고, 내가 친 것처럼 프롬프트를 보낼 수도 있어요
- 권한 프롬프트가 뜨기 전에 툴 호출을 승인해 버릴 수 있어요
- 샌드박스가 없어요. Bash 샌드박스를 켜도 모드가 띄운 프로세스는 그 밖에서 돌아요
- 바꿀 수 없는 건 딱 하나, 권한 프롬프트 자체예요. 프롬프트가 보여 주는 내용은 못 속여요
🛠️ 설치 전에 코드 안 돌리고 확인하기
저장소를 받아서 claude plugin validate를 돌리면, 실행하지 않고도 그 모드가 어떤 이벤트를 받고 어떤 API를 부르는지 두 줄로 나와요.
git clone https://github.com/<작성자>/<모드-저장소> some-mod
claude plugin validate ./some-mod ❯ ./register.js hooks: session.start, tool.call, ui.render{component=Pane}
❯ ./register.js calls: $.fs.read, $.http.fetch, $.store.set, $.ui.opencalls: 줄에서 눈여겨볼 것들이에요.
| 보이면 | 뜻 |
|---|---|
$.fs.read, $.fs.write |
내가 읽고 쓸 수 있는 어느 파일이든 읽고 씀 |
$.process.run, $.process.spawn |
내 권한으로 프로그램 실행 |
$.http.fetch |
네트워크 요청 |
$.env.get, $.settings.read |
환경변수·설정 읽기 (API 키가 있을 수 있음). env reads: 줄에 변수 이름이 나옴 |
$.model.complete |
내 플랜·API 키로 모델 호출 |
$.prompt.submit |
프롬프트를 대신 보냄 (내 말처럼 보낼 수도) |
hooks: 줄의 tool.call과 prompt.submit은 "모든 툴 호출·모든 프롬프트를 보고 바꿀 수 있다"는 뜻이고, tool.check는 "권한 프롬프트 전에 승인·거부할 수 있다"는 뜻이에요. 게임 모드가 $.http.fetch와 $.fs.write를 부른다면 왜 필요한지 README에서 확인하고 설치하세요.
설치 자체는 일반 플러그인과 같아요. 디렉터리의 각 항목이 두 줄 명령(마켓플레이스 추가 + 설치)을 보여 줘요.
/plugin marketplace add <작성자>/<마켓플레이스-저장소>
/plugin install <모드-이름>@<마켓플레이스-이름>세션이 열린 채로 셸에서 설치했다면 /reload-plugins를 한 번 쳐 주세요.
💬 디렉터리 안내문의 "settings.json에
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: 1을 넣으라"는 1단계는 얼리 액세스 때 안내예요. v2.1.287 이상에서는 건너뛰세요 (2장).
📌 5. 첫 모드 만들기 - 툴 호출을 세고 /tally로 보여 주기
공식 튜토리얼의 first-mod를 그대로 따라가 볼게요. Node.js도, 번들러도, 빌드 단계도 필요 없어요. Claude Code가 .js·.ts 파일을 직접 읽어요.
📌 준비물
- Claude Code v2.1.287 이상 (
claude --version) - 텍스트 에디터 (VS Code 등)
- 자바스크립트 기초 -
async/await와{ ...e }펼치기 문법 정도만 알면 돼요. 모르셔도 아래에서 풀어 드려요
📌 1단계 - 폴더와 파일 세 개
first-mod/
├── .claude-plugin/
│ └── plugin.json ← 플러그인 이름표 (매니페스트)
└── hooks/
├── hooks.json ← "내 코드는 register.js 야" 라고 알려 주는 파일
└── register.js ← 진짜 코드 (훅 모듈)mkdir -p first-mod/.claude-plugin first-mod/hooks(윈도우 PowerShell은 New-Item -ItemType Directory -Force first-mod\.claude-plugin, first-mod\hooks)
first-mod/.claude-plugin/plugin.json - 일반 플러그인 매니페스트와 똑같아요. 특별한 키가 없어요.
{
"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" }
}first-mod/hooks/hooks.json - 여기 modules 키가 있어야 "모드"가 돼요. 경로는 이 파일 기준 상대 경로, 하나만.
{
"description": "The first-mod hooks module",
"modules": ["./register.js"]
}modules: 이게 없으면validate는 통과하는데hooks:줄이 안 나와요 (= 아무것도 안 하는 플러그인). 7장 에러 2번- 설정 훅(
PreToolUse같은 것)도 이 파일에 같이 적을 수 있어요. 한 플러그인에 모드와 설정 훅이 공존 가능
📌 2단계 - 코드 (register.js)
// Claude Code v2.1.287+ / first-mod/hooks/register.js
// 아래 훅들이 함께 쓰는 변수 (모듈이 다시 로드되면 0으로 돌아가요)
let calls = 0
// 모드가 로드될 때 Claude Code 가 한 번 호출해요
export function register(on) {
// 세션이 시작될 때 (첫 프롬프트 전에) 실행
on('session.start', async ($, e, next) => {
// /tally 명령어 등록
await $.command.register({
name: 'tally',
description: 'Show how many tool calls Claude has made',
})
// 세션은 평소처럼 시작
return next(e)
})
// Claude 가 툴을 쓰기 직전마다 실행
on('tool.call', async ($, e, next) => {
calls += 1
// 화면을 다시 그리라고 요청 (새 숫자가 보이게)
$.ui.invalidate('ui.render')
// 툴은 평소처럼 실행
return next(e)
})
// /tally 를 입력했을 때만 실행 (두 번째 인자 matcher 때문)
on('command.run', { command: 'tally' }, async () => {
// 대화창에 출력할 글자
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
// Claude Code 가 스피너를 그릴 때마다 실행
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// 원래 스피너는 그대로 두고, 단어 뒤에 숫자만 덧붙여요
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}하나씩 뜯어볼게요 💡
register(on): 모드가 로드될 때 Claude Code가 딱 한 번 부르는 함수.export가 꼭 있어야 해요. 인자로 받는on이 "이 이벤트에 이 함수를 걸어 줘"예요on('이벤트 이름', [matcher], 훅 함수): 이벤트 이름은 따옴표로 적은 문자열이어야 해요. 변수에 담아 넘기면 검증에서 막혀요. 가운데 matcher는 필터예요.{ command: 'tally' }는 "/tally일 때만",{ component: 'Spinner' }는 "스피너를 그릴 때만"- 훅 함수의 인자 세 개
($, e, next): 모든 훅이 똑같아요$: 모드 API. 바깥세상(화면·명령어·파일·네트워크)에 닿는 유일한 통로.$.ui.…,$.command.…처럼 항상$+ 네임스페이스 + 메서드를 풀네임으로 써요.const ui = $.ui처럼 변수에 담으면 검증 실패 - 이렇게 강제하는 덕분에 4장의calls:줄을 코드를 안 돌리고도 뽑을 수 있는 거예요e: 이벤트 내용. 툴 이름, 인자, 스피너의 props 같은 읽기 전용 데이터예요 (고치려고 대입하면 에러)next: "다음으로 넘겨" 함수. 다른 모드들을 거쳐 Claude Code 원래 동작까지 가고, 결과를 돌려줘요
- 세 가지 패턴이 다 들어 있어요 :
session.start·tool.call훅은next(e)를 그대로 돌려주니 지켜보기,ui.render훅은next({ ...e, props: {…} })로 복사본을 넘기니 고쳐 쓰기,command.run훅은next를 안 부르고{ text }를 돌려주니 직접 답하기 $.ui.invalidate('ui.render'): "화면 다시 그려 줘". 이걸 안 부르면calls는 올라가도 스피너 글자는 다음 그리기 때까지 안 바뀌어요{ ...e, props: { ...e.props, suffix: … } }:e를 통째로 복사하되props만 바꾸고,props안에서도suffix만 바꾼 새 객체. 원본e는 건드리지 않아요
📌 3단계 - 로드하고 써 보기
설치하지 않고 폴더째 한 세션에만 로드하는 플래그가 있어요.
claude --plugin-dir ./first-mod툴을 몇 번 쓰게 시켜 보세요. 예를 들어 list the files here and read the README. Claude가 일하는 동안 스피너가 Thinking · tool calls: 2…처럼 세고, 끝난 뒤 /tally를 치면 first-mod: Claude has made 3 tool calls since this mod loaded가 떠요. 앞의 first-mod:는 Claude Code가 플러그인 이름을 붙여 준 거예요.

- 세션 없이 확인 :
claude -p "/tally" --plugin-dir ./first-mod→first-mod: Claude has made 0 tool calls since this mod loaded - 핫 리로드 : 세션을 켜 둔 채
register.js의' · tool calls: '를' · tools used: '로 고쳐 저장해 보세요. 대화창에first-modreloaded 줄이 뜨고 다음 스피너부터 바뀐 글자가 나와요. 리로드 때register가 다시 실행되니calls는 0으로 돌아가요 (값을 유지하려면$.state·$.store를 써요) - 매번 플래그 치기 귀찮으면 : 공식 문서 기준으로 "다른 사람에게 주려면 마켓플레이스에 올려 설치"가 정석이고, 혼자 쓸 때는
--plugin-dir로 계속 개발하라고 권해요. 설치본은 버전별로 캐시돼서 파일을 고쳐도 반영이 안 되거든요
📌 4단계 - 검증(validate)
claude plugin validate ./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:줄에 내가 건 이벤트가 빠져 있으면 Claude Code도 그 훅을 안 불러요. 보통 오타예요 ("tool.calls" is not an event처럼 알려 줘요)- 검증이 잡는 규칙:
$를 변수에 담기·구조 분해 금지, 이벤트 이름은 문자열 리터럴,register안에서on이라는 변수 다시 선언 금지, import는 플러그인 폴더 안 파일만 상대 경로로 (require·동적import()불가), 이름이claude-로 시작하는 플러그인은 거부 - 타입 자동 생성 :
--plugin-dir로 로드할 때마다.claude-plugin/types/안에 지금 내 버전의 이벤트·메서드·요소를 적은.d.ts파일을 써 줘요. VS Code에서 자동 완성이 되고,tsc -p ./first-mod로 타입 검사도 돼요. 문서보다 이 파일을 믿으라고 공식 문서가 적어 뒀어요
📌 5단계 - 테스트(test)
세션도, 로그인도, 네트워크도 없이 셸에서 도는 테스트예요. 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) => {
// 툴 호출을 Claude Code 대신 여기서 답해서, 실제 툴은 돌지 않게
on('tool.call', () => ({ result: 'ok' }))
// 툴 호출 두 번 발생 → 모드의 tool.call 훅이 셈
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// /tally 를 실행하고 돌아온 글자를 확인
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})cd first-mod
claude plugin testtests/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]on('tool.call', () => ({ result: 'ok' })): 테스트 안에서 Claude Code 자리를 대신 하는 가짜 응답. 이게 있어서ls가 실제로 실행되지 않아요$.tool.call(…): 이벤트를 일으키는 쪽. 모드의tool.call훅이 이걸 받아calls를 올려요- 모델 호출·
$.store·타이머·화면 그리기도 같은 방식으로 가짜를 꽂아 테스트할 수 있어요 (공식 "Test a mod" 문서)
💡 6. 한 걸음 더 - 위험한 명령 막기 (직접 답하기 패턴)
"지켜보기"만 해 봤으니 "직접 답하기"도 하나 볼게요. git push --force를 거부하는 모드예요. 파일 구조는 5장과 같고 register.js만 이렇게.
// Claude Code v2.1.287+ / guard/hooks/register.js
// git push --force 를 막는 모드
export function register(on) {
// matcher 로 Bash 호출만 받으니 e.command 가 셸 명령이에요
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/git push .*--force/.test(e.command)) {
// next 를 부르지 않고 return 하면 이 명령은 실행되지 않아요
return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
}
// 나머지 명령은 권한 검사 → Bash 로 평소처럼
return next(e)
})
}{ tool: 'Bash' }matcher 덕분에e.command가 셸 명령 문자열이에요.Edit이면e.file_path처럼 툴 인자가e의 필드로 와요. matcher는 배열({ tool: ['Edit', 'Write'] })이나 정규식({ tool: /^mcp__github__/ })도 돼요{ deny: '…' }: 명령은 안 돌고, 권한 프롬프트도 안 뜨고, Claude는 이 문장을 툴 결과로 읽어요. 그래서 Claude가 다음에 뭘 하면 되는지를 문장에 적어 주는 게 좋아요- 고정된 명령·경로라면 모드보다 권한 규칙(
Bash(git push --force*)deny)이 더 간단해요. 모드는 "현재 브랜치가 main인가"처럼 그 순간의 상태로 판단해야 할 때 쓰는 거예요 (tool.check이벤트 +$.process.run(['git', 'branch', '--show-current'])) - 훅이 터지면 열려 버려요 : 훅이 예외를 던지거나 10초를 넘기면 Claude Code는 그 훅을 건너뛰고 명령을 실행해요. 막는 모드는
on(…).catch(async ($, e, next) => ({ deny: '…' }))를 붙여 닫힌 상태로 실패하게 만드세요 - 순서 : 관리 설정의
PreToolUse훅이 모드보다 먼저 돌고, 그 차단은 최종이에요. 내settings.json의PreToolUse훅은 모드 뒤에 도니까,next를 안 부르는 모드가 있으면 그 훅은 실행조차 안 돼요
💬 Claude에게 시켜도 돼요. 세션에서 "make a mod that shows the current git branch above the prompt"처럼 말하면, 내장
plugin-authoring스킬이 지금 버전의 이벤트·메서드 목록을 읽고 모드를 써 줘요. 첫 파일을 저장할 때 "이 세션에서 핫 리로드를 켤까요?"라고 딱 한 번 물어보고, 켜면 턴이 끝날 때 로드돼요. 다만claude -p나dontAsk권한 모드처럼 물어볼 사람이 없는 세션에서는 로드되지 않아요. 이 글을 쓴 자동 세션이 딱 그 경우라, 저도 이 흐름은 문서로만 확인했어요.
📌 7. 자주 나는 에러·헷갈리는 점
1. 모드를 넣었는데 아무 일도 안 일어나요
- 원인 : (가)
claude --version이 2.1.287 미만 (나)/plugin의mods active줄에 이름이 없음 = 모듈이 로드 안 됨 (다) 처음 여는 폴더라 신뢰 프롬프트에 아직 답을 안 함 (라)--safe-mode로 시작 - 해결 : 업데이트 →
claude plugin validate ./폴더로 오타 확인 → 그래도 안 되면claude --debug-file ./mod-debug.log --plugin-dir ./first-mod로 열고 다른 터미널에서tail -f ./mod-debug.log | grep first-mod.hooks module first-mod@inline not loaded:뒤에 이유가 적혀요
2. validate는 통과하는데 hooks: 줄이 안 나와요
- 원인 :
hooks/hooks.json에modules키가 없거나 오타 - 해결 :
"modules": ["./register.js"]추가
3. "tool.calls" is not an event
- 원인 : 이벤트 이름 오타. 단수
tool.call이에요 - 해결 : 이름 고치기. 전체 이벤트 목록은
.claude-plugin/types/claude-code/index.d.ts에서'tool.call'처럼 검색
4. $.ui is used as a value
- 원인 :
const ui = $.ui처럼$나 네임스페이스를 변수에 담음 - 해결 : 매번
$.ui.log(…)처럼 풀네임으로.$를 넘길 수 있는 건 같은 파일 최상위에 선언한 함수뿐
5. on("session.start") is registered twice without a matcher
- 원인 : matcher 없이 같은 이벤트에
on을 두 번 호출 - 해결 : 세션 시작 때 할 일은 훅 하나에 모으기
6. /tally를 쳤는데 first-mod registered /tally but no command.run hook answered it
- 원인 :
command.run훅이 없거나, matcher의 명령 이름이 다르거나, 훅이next(e)를 돌려줌. 또는 훅이hook skipped로 건너뛰어짐 - 해결 : matcher를
{ command: 'tally' }로, 반환은{ text: … }로
7. 파일을 고쳤는데 반영이 안 돼요
- 원인 : 설치한 플러그인을 고치고 있음. 설치본은 버전별 캐시를 써요
- 해결 : 개발은
--plugin-dir ./first-mod로. 배포본은plugin.json의version을 올리고 다시 설치
8. hooks: mods that run in the hooks worker are off for this session: it crashed 3 times
- 원인 : 설치 모드들이 공유하는 워커 스레드가 3번 죽었고 범인을 못 찾음.
await없는 무한 루프처럼 스레드를 막는 훅이 흔한 원인 - 해결 : 의심되는 모드를 끄고
/reload-plugins
9. 오토 모드에서 a hook changed this call's input after the model wrote it로 거부돼요
- 원인 : 서버 측 분류기가 검토한 뒤에 모드(또는 설정 훅)가 툴 입력을 바꿔서, 검토 결과가 실제 실행될 내용과 달라짐
- 해결 : 매번 바꾸는 모드라면 끄거나, 오토 모드를 벗어나 직접 승인
📌 관련 글
- Claude Code 입문 - 설치부터 첫 실행까지 - 아직 안 깔았다면 여기부터
- Claude Code 오토 모드가 기본값이 됐어요 - 7장 9번의 "서버 측 분류기"가 뭔지
- 클로드 코드 모델 변경·effort 설정 완전 정리 -
$.model.complete의model: 'haiku'같은 별칭 - 클로드 Opus 5.5 출시! Claude Code 기본 모델 교체 - v2.1.280 변화
- MCP 새 스펙(2026-07-28) 총정리 - 모드 비교표의 MCP 서버 쪽 이해하기
- AI 코딩 프롬프트 잘 쓰는 법 - Claude에게 모드를 시킬 때도 통하는 요령
📌 마무리
오늘은 Claude Code v2.1.287의 Claude Mods를 정리했어요. 요약하면 이래요.
- 모드는 Claude Code 안에서 도는 자바스크립트 함수. 설정 훅·스킬·MCP가 못 하는 화면 그리기·툴 호출 붙잡기·즉시 명령어를 해요
- v2.1.287이면 켤 게 없고, 얼리 액세스 변수
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS는 지워요. 끄는 건/plugin·--safe-mode·disableAllHooks - 내장 모드 'You should know'는
/plugin enable cc-plugin-you-should-know@builtin - 커뮤니티 모드는 재밌지만 내 권한으로 샌드박스 없이 돌아요. 설치 전
claude plugin validate로calls:줄을 꼭 보세요 - 첫 모드는 파일 세 개 +
register(on)하나.--plugin-dir로 로드하고validate·test로 확인
이벤트·메서드 이름은 릴리스마다 바뀔 수 있다고 문서가 반복해서 말하니, 글의 코드가 안 돌면 .claude-plugin/types/ 안의 .d.ts를 먼저 열어 보세요. 다음에는 디스코드 봇 시리즈에서 만든 기능을 패널로 그리는 모드를 하나 만들어 볼까 해요. 기대해주세요 🙌
