카테고리: IT·활용 팁

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' 가 나오면 모드 사용 가능

예시 화면: PowerShell에서 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_HOOKS during early access, remove it. Claude Code v2.1.287 and later ignores it, so setting it to 0 doesn'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

예시 화면: /plugin Installed 탭에 '1 mod active · first-mod' 줄과 Built-in 목록, 맨 아래 cc-plugin-you-should-know가 disabled로 보이는 화면

💬 내장 모드 중 네 개(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.open

calls: 줄에서 눈여겨볼 것들이에요.

보이면 뜻
$.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 - 일반 플러그인 매니페스트와 똑같아요. 특별한 키가 없어요.

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 키가 있어야 "모드"가 돼요. 경로는 이 파일 기준 상대 경로, 하나만.

JSON
{
  "description": "The first-mod hooks module",
  "modules": ["./register.js"]
}
  • modules : 이게 없으면 validate는 통과하는데 hooks: 줄이 안 나와요 (= 아무것도 안 하는 플러그인). 7장 에러 2번
  • 설정 훅(PreToolUse 같은 것)도 이 파일에 같이 적을 수 있어요. 한 플러그인에 모드와 설정 훅이 공존 가능

📌 2단계 - 코드 (register.js)

JavaScript
// 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 --plugin-dir ./first-mod 세션에서 스피너가 'Thinking · tool calls: 3…'으로 세고, /tally 결과와 reloaded 줄이 보이는 화면

  • 세션 없이 확인 : 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-mod reloaded 줄이 뜨고 다음 스피너부터 바뀐 글자가 나와요. 리로드 때 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

예시 화면: VS Code에서 register.js를 열어 두고 아래 터미널에 claude plugin validate ./first-mod 결과로 hooks·calls 두 줄과 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로 저장해요.

TypeScript
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 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]
  • on('tool.call', () => ({ result: 'ok' })) : 테스트 안에서 Claude Code 자리를 대신 하는 가짜 응답. 이게 있어서 ls가 실제로 실행되지 않아요
  • $.tool.call(…) : 이벤트를 일으키는 쪽. 모드의 tool.call 훅이 이걸 받아 calls를 올려요
  • 모델 호출·$.store·타이머·화면 그리기도 같은 방식으로 가짜를 꽂아 테스트할 수 있어요 (공식 "Test a mod" 문서)

💡 6. 한 걸음 더 - 위험한 명령 막기 (직접 답하기 패턴)

"지켜보기"만 해 봤으니 "직접 답하기"도 하나 볼게요. git push --force를 거부하는 모드예요. 파일 구조는 5장과 같고 register.js만 이렇게.

JavaScript
// 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 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를 먼저 열어 보세요. 다음에는 디스코드 봇 시리즈에서 만든 기능을 패널로 그리는 모드를 하나 만들어 볼까 해요. 기대해주세요 🙌

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그