도구는 실행하고 결과를 관찰합니다.
next(e)로 원래 실행을 이어갑니다. 실행 결과를 받은 뒤 로그나 통계를 남길 수 있습니다. 관찰은 원래 동작을 유지합니다.
CLAUDE CODE MODS · 한국어 · 처음 배우는 사람을 위해
도구 호출을 바꾸고, 정보를 표시하고, 편집을 검토합니다. Claude Code Mods의 실제 기능을 스크롤하며 익힙니다.
스크롤하면 예시가 바뀝니다.
중간에 만나는 질문에서는 새로운 상황에 적용해보세요.
Mods는 Claude Code 안에서 이벤트를 받는 JavaScript·TypeScript 확장입니다. Claude가 Bash를 호출할 때, 내 함수가 실행 전에 입력을 보고 다음 처리를 선택합니다.
const r = await next(e); // 실행 결과를 관찰 return r;
next(e)로 원래 실행을 이어갑니다. 실행 결과를 받은 뒤 로그나 통계를 남길 수 있습니다. 관찰은 원래 동작을 유지합니다.
명령을 수정한 새 이벤트를 next에 전달합니다. 수정은 Claude의 도구 호출이 실제로 받을 입력을 바꾸는 기능입니다.
직접 응답은 next를 호출하지 않고 이벤트에 맞는 결과를 반환합니다. tool.call에 deny를 반환하면 이 호출은 원래 도구 실행으로 넘어가지 않습니다.

행동이 실제 실행으로 넘어가는 지점은 어디일까요?
Token Weather는 컨텍스트 사용량을 날씨와 그래프로 보여주는 Mod입니다. Claude가 참고하는 대화·파일·도구 결과의 양을 매 턴 확인할 수 있습니다. 토큰은 모델이 글을 처리하는 작은 조각 단위입니다.
$.session.usage()
예시 창은 200k 토큰. 36.1k를 참고하면 반올림해 18%, 맑음입니다.
134.4k / 200k = 67.2%. 표시값은 67%, 소나기입니다. 마지막 턴 증가량은 98.3k입니다.
162k / 200k = 81%, 폭풍입니다. 예보는 상태 표시이며, 모드가 자동으로 압축한다는 뜻은 아닙니다.

계기판의 경고와 실제 조작은 같은 기능일까요?
Token Weather의 동작은 완료 이벤트 → 사용량 읽기 → 상태 저장 → 프롬프트 위 렌더입니다. “턴”은 Claude가 한 번의 작업을 진행하고 마치는 단위입니다. 화면 표시에 실제 세션 데이터를 연결합니다.
await next(e)$.session.usage() → $.state.setui.render → $.state.geton("turn.complete", async ($, e, next) => {
const r = await next(e);
if (!e.agentId) await takeReading($);
return r;
});turn.complete에서 먼저 await next(e)를 호출합니다. 완료 처리가 끝난 뒤 읽어야 최신 사용량을 관찰합니다.
$.session.usage()로 읽고 $.state.set으로 저장합니다. e.agentId가 없는 메인 턴만 측정해 하위 작업이 섞이지 않게 합니다.
ui.render 안에서 $.state.get으로 읽으면 그 화면이 상태 변경을 구독합니다. 이 예제에는 별도의 invalidate 호출이 필요 없습니다.

값의 정확성뿐 아니라 어떤 사건을 기록했는지도 중요합니다.
실행 중인 Claude Code에서 Mod를 수정하면 코드를 다시 로드해 바로 확인할 수 있습니다. UI 코드는 바뀌어도 같은 세션의 최근 12턴 이력을 유지하려면, 모듈 변수와 호스트 상태를 구분해야 합니다.
let history = [];
let history = []는 지금 로드된 모듈의 기억입니다. 핫 리로딩은 코드를 저장하면 새 모듈로 교체하는 과정입니다.
register가 다시 실행되고 session.start도 발생합니다. 모듈 변수는 초기화되어 이전 기록을 잃습니다.
호스트가 같은 세션에서 상태를 보관하므로 새 모듈도 같은 이력을 읽습니다. 세션을 넘는 저장이 필요하면 지속 저장용 API를 따로 검토합니다.

교체되는 쪽과 계속 살아 있는 쪽을 구분해보세요.
Blast Radius는 감지한 위험 명령의 실행을 보류하고, 바뀔 파일을 보고 사용자가 진행·취소를 선택하게 합니다. Claude가 쓰던 Bash 호출 흐름에 확인 UI를 끼워 넣는 Mod입니다.
on("tool.call", { tool: "Bash" }, …)rm -rf build를 바로 실행하지 않고 tool.call에서 보류합니다. 웹페이지에서는 설명용 시뮬레이션만 보여줍니다.
원문 터미널 예시에서는 파일 9개, 총 1.1 MB를 표시합니다. 삭제할 대상이 예상과 같은지 사용자가 확인합니다.
진행이면 원래 명령을 next(e)에 넘깁니다. 취소면 거부를 반환합니다. 이 보조 장치는 권한 규칙을 대체하지 않습니다.

판단을 돕는 장치와 실행 권한을 제한하는 장치의 역할을 나눠보세요.
Replay Theater는 Claude의 Edit·Write 호출을 기록하고 지난 턴의 변경 전후를 보여주는 Mod입니다. 최종 diff만 보는 대신 편집 한 단계씩 이동하며 검토합니다. 기록 열람은 실제 파일 복원과 별개입니다.
turn.start → pending = []
turn.start에서 이번 턴의 편집 목록을 준비합니다. Edit / Write 호출의 파일과 변경 전후를 기록합니다.
기록한 뒤 next(e)로 전달합니다. Write는 반영 직전 $.fs.read로 기존 내용을 읽습니다. 관찰은 편집을 되돌리는 기능이 아닙니다.
turn.complete 뒤 /replay 명령이나 패널로 확인합니다. 넓은 화면은 옆 패널, 좁은 화면은 인라인으로 같은 내용을 보여줍니다.

화면의 시간 이동과 파일 시스템의 시간 이동을 구분해보세요.
API를 모두 외울 필요는 없습니다. Claude에게 원하는 표시를 설명하고, 지금 배운 원리로 구현을 검토하세요.
turn.complete → ui.render프롬프트 위에서 컨텍스트 추세를 확인합니다. 사용량을 표시하며 자동 압축을 수행하지 않습니다.
tool.call → 진행 / 취소감지한 위험 명령을 보류하고 영향 보고서를 보여줍니다. 진행을 선택해야 원래 호출로 넘깁니다.
Edit·Write 기록 → /replay파일 변경을 순서대로 검토합니다. UI에서 이전 단계로 이동해도 실제 파일은 복원되지 않습니다.
세 예제의 영상·전체 Token Weather 구현·나머지 두 예제의 핵심 훅을 아래에서 이어서 읽습니다. 공식 문서가 연결하는 완성 예제 저장소에서는 설치 가능한 플러그인 묶음을 확인할 수 있습니다.
프롬프트 위 표시줄에 컨텍스트 사용량을 날씨처럼 보여주는 token-weather Claude Code 모드를 만들어줘. 한 줄에 표시할 내용: - 25% 미만 ☀ Clear(노랑), 25–49% ☁ Cloudy(청록), 50–74% ☂ Showers(파랑), 75–89% ☇ Storm(자홍), 90% 이상 ↯ Compact soon(빨강). - 사용률과 토큰 사용량 / 창 크기. 예: 134.4k / 200k. - 최근 12턴의 작은 그래프: ▁▂▃▄▅▆▇█. - 마지막 턴의 증가량. 예: ▲ +98.3k last turn. 세션 시작과 메인 턴 완료 뒤에 갱신해줘. 재로딩에도 같은 세션의 이력이 유지되도록 $.state를 사용하고, 상태 타입 계약도 선언해줘. 검증과 테스트를 실행해줘.
문서 기준 Claude Code 2.1.287 이상입니다. API가 달라질 수 있으므로 로드 시 생성되는 타입 선언을 확인하세요.
claude --version claude --plugin-dir ./token-weather claude plugin validate ./token-weather claude plugin test ./token-weather
Claude가 작성한 개발 Mod는 핫 리로딩을 승인하면 해당 파일을 바꾼 턴이 끝날 때 로드·재로딩됩니다. /plugin에서 실제 로드 여부를 확인하세요. 세션별 임시 폴더는 정리될 수 있으므로 계속 쓰려면 자신의 폴더로 복사하고 설치합니다.
.claude-plugin/plugin.json이름·버전·작성자. 상태를 쓰면 types 경로도 선언합니다.
hooks/hooks.jsonmodules에 모듈 하나를 연결합니다.
hooks/token-weather.mjsregister에서 이벤트 훅을 등록합니다.
types/index.d.tsPluginState에 token-weather.readings를 선언합니다.
tests/token-weather.test.ts사용량을 제어하며 화면이 갱신되는지 확인합니다.
모드에는 DOM과 Node가 없습니다. 외부 기능은 $ API로 사용합니다. e.props에서 화면 속성을 읽고, 그릴 내용이 없거나 설문이 자리를 쓰면 next(e)로 넘깁니다.
현재 빌드의 .claude-plugin/types/를 기준으로.
너비는 e.props.bodyColumns를 기준으로.
재로딩에도 남길 값은 $.state로.
화면이 안 보이면 claude --debug로.
| Claude Code 사용 환경 | Mod 훅 | Mod UI |
|---|---|---|
| 터미널 · IDE의 통합 터미널 · JetBrains | 실행 | 표시 |
| Desktop의 Code 탭 (WSL 제외) | 실행 | 표시 · 터미널 전용 요소 제외 |
| Desktop WSL 세션 | 미지원 | 미지원 |
| VS Code 확장 채팅 패널 | 실행 | 표시 안 됨 |
| claude -p · Agent SDK | 실행 | 표시 안 됨 |
| Remote Control의 웹·모바일 | 로컬 세션에서 실행 | 로컬 터미널에 표시 |
| 클라우드 세션 | 플러그인이 전달된 경우 실행 | 표시 안 됨 |
터미널은 claude --version으로 2.1.287 이상인지, Desktop은 Code 탭의 /status에서 내장 Claude Code 2.1.286 이상인지 확인합니다. UI Mod를 VS Code 채팅 패널에서 테스트하면 “설치됐는데 보이지 않는다”는 상황이 생깁니다.
/plugin-authoring을 먼저 실행합니다./plugin의 Installed 탭과 활성 Mod 목록을 봅니다. 코드가 생성된 것과 실행 중인 것은 다릅니다.설치 Mod 하나를 멈추려면 /plugin에서 비활성화합니다. --safe-mode는 다른 사용자 지정 기능도 함께 멈추므로 개별 Mod를 끄는 것보다 범위가 넓습니다.
근거: 지원 환경 · 공식 제작·재로딩 흐름

이 문제에서 판단이 필요한 시점은 실행 전인가요, 실행 후인가요?
다음 파일을 my-mods/.claude-plugin/marketplace.json에 두고, 같은 폴더에 token-weather/를 둡니다.
{
"name": "my-mods",
"owner": { "name": "You" },
"plugins": [{ "name": "token-weather", "source": "./token-weather" }]
}claude plugin marketplace add ./my-mods claude plugin install token-weather@my-mods --scope user
앞에서는 행동을 이해했습니다. 이제 파일을 연결하고, 값을 저장하고, 테스트로 확인하는 과정까지 이어갑니다. 코드는 짧은 덩어리로 나누어 역할을 설명합니다.
이름을 외우기보다 앞에서 본 역할에 이름을 붙여보세요. 각 용어는 코드에서 무엇을 찾아야 하는지 알려주는 표지입니다.
플러그인은 설치·배포 단위, 모듈은 구현 파일, 훅은 이벤트별 처리 함수입니다. 모드 하나는 hooks.json의 modules에 모듈 하나를 연결합니다.
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
// $: Claude Code 기능을 사용하는 API
// e: 이번 이벤트의 입력 데이터
// next: 다음 훅, 마지막에는 원래 동작으로 전달
return next(e);
});화면, 세션, 상태, 파일 등 외부 기능에 접근합니다.
이번 도구, 명령, 화면 속성 같은 입력입니다.
입력을 다음 훅으로 전달하고 결과를 받습니다.
호출 내 훅 → 다른 플러그인 → Claude Code
결과 내 훅 ← 다른 플러그인 ← Claude Code
await next(e) 뒤에 기록하면 실행 후 관찰입니다. next 없이 결과를 반환하면 이 훅이 직접 답합니다. 명령 거부뿐 아니라 슬래시 명령어나 도구의 처리도 직접 맡을 수 있습니다.
| 사건 | 예시 이름 | 어울리는 목적 |
|---|---|---|
| 도구를 호출함 | tool.call | 실행 전 확인, 파일 변경 기록 |
| 프롬프트 제출 | prompt.submit | 요청에 팀 규칙 덧붙이기 |
| 작업 시작·완료 | turn.start / turn.complete | 턴 단위 기록, 완료 후 측정 |
| 세션 시작·종료 | session.start 등 | 초기화, 명령 등록, 세션 수명 관리 |
| 슬래시 명령 실행 | command.run | /replay 같은 직접 처리 |
| 화면을 그림 | ui.render | 표시줄·패널의 요소 반환 |
모듈은 DOM·Node API를 직접 쓰는 웹앱이 아닙니다. 브라우저의 document나 Node의 fs 대신 Claude Code가 제공하는 API를 호출합니다. 파일은 $.fs, 프로세스는 $.process, HTTP는 $.http처럼 $를 통해 사용합니다.
$.ui$.session$.state$.store$.fs$.process$.clock$.http$.tool$.command$.model어떤 메서드와 인수를 지원하는지는 현재 빌드가 생성한 타입 선언으로 확인합니다. 이름만 보고 지원 기능을 추측하지 마세요.
셸 명령형 훅은 이벤트마다 프로그램을 실행하고 표준 입력·출력으로 JSON을 주고받습니다. 현재 설정 훅에는 HTTP·프롬프트 등 다른 유형도 있습니다.
한 번 로드되어 세션에 남습니다. 상태를 유지하고, UI를 그리며, 패널·프로세스·명령·도구를 다룰 수 있습니다.
설정·권한 규칙·슬래시 명령어·스킬·상태 표시줄은 기존 확장 방법입니다. Mods는 이벤트 처리 코드를 로드해 동작을 수정·대체하고 UI까지 그리는 확장입니다. Claude Code의 AGENTS.md 지원과 /diff 패널 일부도 모드로 구현되어 있습니다. 공개 저장소의 mods/ 소스와 테스트에서 구현 방식을 볼 수 있습니다.
| 원하는 결과 | 먼저 볼 기능 | 이유 |
|---|---|---|
| 작업 절차·반복 지시를 Claude에게 알려주기 | Skill | SKILL.md를 Claude가 읽고 적용합니다. |
| 기존 검사 스크립트를 특정 시점에 실행 | 설정 훅 | PreToolUse 같은 이벤트에 외부 처리를 연결합니다. |
| 외부 서비스에 접근할 새 도구 제공 | MCP 서버 | Claude가 사용할 도구를 추가합니다. |
| 입력 위 표시줄·검토 패널·직접 처리 명령 | Mods | 이벤트 처리와 Claude Code UI를 코드로 연결합니다. |
| 위 기능들을 함께 설치·배포 | Plugin | Mods·스킬·훅·MCP를 담는 배포 단위입니다. |
설정 훅의 PreToolUse와 Mod의 tool.call은 서로 다른 이벤트 이름입니다. 스크립트형 설정 훅의 표준 입출력 계약을 Mod 함수에 그대로 복사하지 않습니다. Mod에서는 register로 등록하고 $, e, next를 사용합니다.
근거: 설정 훅 · Skills · Plugins · Mods 이벤트/API

Claude에게 지시를 읽히는 것과 Claude Code의 UI를 직접 확장하는 것은 다릅니다.
처음부터 복잡한 기능을 만들지 않습니다. 연결이 맞는지 작은 글자 하나로 확인한 뒤, 실제 측정값을 연결합니다.
먼저 claude --version으로 문서 기준 2.1.287 이상인지 확인합니다. 모드는 기본 활성화되어 별도 스위치를 켤 필요가 없습니다.
token-weather/.claude-plugin/plugin.json플러그인 이름·버전token-weather/hooks/hooks.json구현 파일을 가리키는 연결표token-weather/hooks/token-weather.mjs실제 동작.claude-plugin/types/모드를 로드할 때 Claude Code가 생성types/index.d.ts3단계에서 만드는 내 상태 계약tests/token-weather.test.ts5단계에서 만드는 동작 시험{
"name": "token-weather",
"version": "0.1.0",
"description": "A live forecast of the context window, drawn above the prompt.",
"author": {
"name": "You"
}
}{
"modules": ["./token-weather.mjs"]
}modules에는 모듈 하나를 지정합니다. 이 상대 경로는 hooks.json이 있는 위치에서 구현 파일을 찾습니다.
// hooks/token-weather.mjs
export function register(on) {
on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
const { Box, Text } = $.ui.resolve(e);
return Box({
paddingX: 1,
children: [Text({ color: "yellow", bold: true, children: "☀ Clear skies" })],
});
});
}AbovePrompt는 프롬프트 바로 위의 빈 표시줄입니다. $.ui.resolve(e)로 현재 화면에 맞는 Box와 Text 생성자를 받고, 요소 트리를 반환합니다. 요소 생성자는 전역이 아닙니다.
claude --plugin-dir ./token-weather
세션을 계속 열어두면 저장할 때마다 모듈이 재로딩됩니다. “Storm 기준을 70%로 바꿔줘”, “끝에 달러 비용을 추가해줘”처럼 같은 세션에서 수정 결과를 확인할 수 있습니다.
| 값 | 정확히 뜻하는 것 |
|---|---|
context.tokens | 마지막 응답을 생성할 때 사용한 입력 토큰 수 |
context.window | 모델의 컨텍스트 창 크기 |
context.percent | 창 크기에 대한 사용률 |
$.session.usage()는 상태 표시줄과 같은 수치를 반환합니다. 문서 기준 이 호출에는 비용이 들지 않으며, breakdown을 요청할 때만 토큰 계산 요청을 보냅니다. 200k는 예시 창 크기이며 모든 모델의 고정값이 아닙니다.
export type TokenWeatherReading = {
tokens: number;
window: number;
percent: number;
};
declare module "claude-code" {
interface PluginState {
"token-weather": { readings: TokenWeatherReading[] };
}
}모듈 구현은 JavaScript로 작성해도 상태 계약은 .d.ts 파일로 선언합니다. readings는 위 구조의 측정값 배열입니다.
"types": "./types/index.d.ts"
plugin.json 객체 안에 types 필드를 추가합니다. 한 줄만으로 독립된 JSON 파일을 만드는 것이 아닙니다.
“상태 계약에 token-weather.readings가 선언되어 있지 않다”는 뜻입니다. 타입 계약과 매니페스트의 types 연결을 확인합니다. 이를 생략하면 claude plugin validate가 중단됩니다.

자동 생성된 API 안내와 내 데이터 구조 선언은 다릅니다.
약 80줄의 Token Weather를 여섯 덩어리로 읽습니다. 다음 코드 블록을 순서대로 합치면 전체 모듈이 됩니다. 코드의 기능은 원문과 같습니다.
// Token Weather: a live forecast of the context window, above the prompt.
const HISTORY = 12;
const BARS = "▁▂▃▄▅▆▇█";
const FORECAST = [
{ upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
{ upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
{ upTo: 75, icon: "☂", word: "Showers", color: "blue" },
{ upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
{ upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
];
// Held by the host, so the history survives a hot reload of this file.
const readings = { plugin: "token-weather", key: "readings" };
export function register(on) {
on("session.start", async ($, e, next) => {
const result = await next(e);
await takeReading($);
return result;
});
on("turn.complete", async ($, e, next) => {
const result = await next(e);
if (!e.agentId) {
await takeReading($); // main-loop turns only, not subagents
}
return result;
});
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
const { value: history = [] } = await $.state.get(readings);
if (e.props.hasSurvey || history.length === 0) {
return next(e);
}
const { Box, Text } = $.ui.resolve(e);
return band(Box, Text, history, e.props.bodyColumns);
});
}
async function takeReading($) {
const { context } = await $.session.usage();
if (!context?.window) return;
const tokens = context.tokens ?? 0;
const percent = context.percent ?? Math.round((tokens / context.window) * 100);
const { value: history = [] } = await $.state.get(readings);
await $.state.set(readings,
[...history, { tokens, window: context.window, percent }].slice(-HISTORY));
}
function band(Box, Text, history, columns) {
const now = history[history.length - 1];
const f = FORECAST.find((b) => now.percent < b.upTo);
const parts = [
Text({ color: f.color, bold: true, children: `${f.icon} ${f.word}` }),
Text({ children: ` ${now.percent}% of context` }),
Text({ dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
];
if (columns >= 60) {
parts.push(Text({ dimColor: true, children: " last turns " }));
parts.push(Text({ color: f.color, children: sparkline(history) }));
if (history.length > 1) {
parts.push(Text({ dimColor: true, children: trend(history) }));
}
}
return Box({ flexDirection: "row", paddingX: 1, children: parts });
}
function sparkline(history) {
const top = Math.max(...history.map((r) => r.tokens), 1);
return history.map((r) =>
BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
}
function trend(history) {
const delta = history[history.length - 1].tokens - history[history.length - 2].tokens;
if (delta === 0) return " steady";
return delta > 0 ? ` ▲ +${short(delta)} last turn` : ` ▼ ${short(-delta)} last turn`;
}
function short(n) {
if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
return String(n);
}값이 어디까지일 때 어떤 예보를 보여줄지 정합니다. upTo는 상한이며 비교는 “미만”입니다. 25%는 맑음이 아니라 흐림입니다. Infinity는 마지막 구간에 상한을 두지 않는 값입니다.
// Token Weather: a live forecast of the context window, above the prompt.
const HISTORY = 12;
const BARS = "▁▂▃▄▅▆▇█";
const FORECAST = [
{ upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
{ upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
{ upTo: 75, icon: "☂", word: "Showers", color: "blue" },
{ upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
{ upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
];
// Held by the host, so the history survives a hot reload of this file.
const readings = { plugin: "token-weather", key: "readings" };세션 시작과 메인 턴 완료 뒤에 측정합니다. 화면을 그릴 때는 저장한 값을 읽습니다. 설문이 표시줄을 쓰거나 이력이 비었으면 next(e)로 자리를 양보합니다.
export function register(on) {
on("session.start", async ($, e, next) => {
const result = await next(e);
await takeReading($);
return result;
});
on("turn.complete", async ($, e, next) => {
const result = await next(e);
if (!e.agentId) {
await takeReading($); // main-loop turns only, not subagents
}
return result;
});
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
const { value: history = [] } = await $.state.get(readings);
if (e.props.hasSurvey || history.length === 0) {
return next(e);
}
const { Box, Text } = $.ui.resolve(e);
return band(Box, Text, history, e.props.bodyColumns);
});
}창 크기가 없으면 계산하지 않습니다. ??는 값이 null이나 undefined일 때 기본값을 선택합니다. 새 측정값을 붙인 뒤 slice(-HISTORY)로 마지막 12개만 남깁니다.
async function takeReading($) {
const { context } = await $.session.usage();
if (!context?.window) return;
const tokens = context.tokens ?? 0;
const percent = context.percent ?? Math.round((tokens / context.window) * 100);
const { value: history = [] } = await $.state.get(readings);
await $.state.set(readings,
[...history, { tokens, window: context.window, percent }].slice(-HISTORY));
}먼저 예보·사용률·토큰을 그립니다. 실제 표시줄 너비가 60열 이상일 때만 최근 턴 그래프와 증감량을 덧붙입니다. 패널이 열리면 표시줄은 전체 터미널보다 좁아질 수 있습니다.
function band(Box, Text, history, columns) {
const now = history[history.length - 1];
const f = FORECAST.find((b) => now.percent < b.upTo);
const parts = [
Text({ color: f.color, bold: true, children: `${f.icon} ${f.word}` }),
Text({ children: ` ${now.percent}% of context` }),
Text({ dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
];
if (columns >= 60) {
parts.push(Text({ dimColor: true, children: " last turns " }));
parts.push(Text({ color: f.color, children: sparkline(history) }));
if (history.length > 1) {
parts.push(Text({ dimColor: true, children: trend(history) }));
}
}
return Box({ flexDirection: "row", paddingX: 1, children: parts });
}최근 이력에서 가장 큰 토큰 수를 기준으로 높이를 나눕니다. 가장 높은 막대가 항상 컨텍스트 100%를 뜻하지는 않습니다. 절대 사용률은 옆의 %에서 읽습니다.
function sparkline(history) {
const top = Math.max(...history.map((r) => r.tokens), 1);
return history.map((r) =>
BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
}마지막 두 턴의 차이를 표시합니다. 변화가 없으면 steady, 증가면 ▲, 감소면 ▼입니다. short는 큰 수를 k 또는 M 단위로 줄이고 소수 한 자리까지 표시합니다.
function trend(history) {
const delta = history[history.length - 1].tokens - history[history.length - 2].tokens;
if (delta === 0) return " steady";
return delta > 0 ? ` ▲ +${short(delta)} last turn` : ` ▼ ${short(-delta)} last turn`;
}
function short(n) {
if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
return String(n);
}e.component · e.surface · e.requestId · e.viewporte.props.hasSurvey · e.props.bodyColumnshasSurvey는 설문이 표시줄을 쓰려 한다는 신호입니다. bodyColumns는 현재 표시줄의 실제 너비입니다. 그릴 내용이 없을 때도 next(e)로 다음 구현에 넘깁니다. ☀ ☁ ☂ ☇ ↯ 같은 한 칸 기호를 써 터미널 정렬을 맞춥니다.

그래프와 백분율이 각각 무엇을 분모로 삼는지 확인해보세요.
선이 연결되었다고 전등이 켜지는 것은 아닙니다. 파일과 선언이 맞는지 검사하는 일과, 입력을 바꾸었을 때 화면이 바뀌는지 시험하는 일은 다릅니다.
claude plugin validate ./token-weather
> types ./types/index.d.ts declares state: token-weather.readings
> ./token-weather.mjs hooks: session.start, turn.complete,
ui.render{component=AbovePrompt}
> ./token-weather.mjs calls: $.session.usage (via takeReading),
$.state.get, $.state.set (via takeReading), $.ui.resolve
> ./token-weather.mjs state writes: token-weather.readings
> ./token-weather.mjs state reads: token-weather.readings
√ Validation passed매니페스트와 모듈 소스를 읽어 훅, API 호출, 상태 읽기·쓰기를 보여줍니다. 여기의 통과 메시지는 원문 예시이며, 이 웹페이지가 실제 플러그인을 실행한 결과가 아닙니다.
claude plugin test는 실제 Claude Code 런타임에서 *.test.ts를 실행합니다. 테스트의 on 훅은 모드 뒤에 등록되어 호스트 응답을 대신합니다. 외부 값에 운을 맡기지 않고 원하는 입력으로 동작을 시험합니다.
// tests/token-weather.test.ts
import { describe, expect, test } from "claude-code/testing";
describe("token-weather", () => {
test("the band follows the context window", async ($, on) => {
// Hooks registered here run after the mod and stub what Claude Code would answer.
let tokens = 36_100;
on("session.start", ($, e) => ({ cwd: e.cwd }));
on("session.usage", () => ({
value: {
startedAt: 0,
rateLimits: [],
context: { tokens, window: 200_000, percent: Math.round(tokens / 2_000) },
},
}));
on("turn.complete", () => ({ text: "" }));
await $.session.start({ surface: "terminal", isInteractive: true, cwd: "/work" } as any);
const ui = await $.ui.mount({
plugin: "token-weather",
surface: "terminal",
component: "AbovePrompt",
props: { hasSurvey: false, isWorking: false, maxRows: 10, bodyColumns: 120 },
} as any);
expect(await ui.find({ type: "Text", text: /Clear/ })).toBeDefined();
tokens = 134_400;
await $.turn.complete({ reason: "answer", answer: "ok", durationMs: 1 } as any);
expect(await ui.find({ type: "Text", text: /Showers/ })).toBeDefined();
expect(await ui.find({ type: "Text", text: /67% of context/ })).toBeDefined();
expect(await ui.find({ type: "Text", text: /▲ \+98\.3k last turn/ })).toBeDefined();
await ui.unmount();
});
});시작 화면, 변경된 예보, 사용률, 증감량을 각각 확인합니다. 마지막에는 ui.unmount()로 정리합니다.
$ claude plugin test ./token-weather (pass) token-weather > the band follows the context window 1 pass 0 fail
별도 다시 그리기 요청이 없어도 turn.complete 뒤 화면이 바뀌는지 확인합니다. 이 테스트는 3단계에서 설명한 상태 구독과 자동 갱신도 검증합니다.
모드를 로드할 때 생성되는 .claude-plugin/types/를 기준으로 이벤트·API·props를 확인합니다. 편집기와 tsc -p에서도 사용할 수 있습니다.
hasSurvey와 bodyColumns는 e 자체가 아니라 e.props 안에 있습니다.
register와 session.start가 다시 실행됩니다. 이력 초기화와 중복 등록 문제를 살펴봅니다.
claude --debug로 반환한 요소 트리가 검증에 실패했는지 읽습니다.

연결 확인과 입력 변화에 대한 결과 확인은 다릅니다.
앞에서 본 실행 전 확인을 코드로 읽습니다. 위험을 분류하고, 영향을 측정하고, 결정을 기다린 다음에 원래 실행을 넘깁니다.
rm -rfgit reset --hardgit clean강제 push데이터베이스 마이그레이션Blast Radius는 Bash의 tool.call, Pane과 AbovePrompt의 ui.render라는 세 훅을 사용합니다. 분류 결과가 없으면 바로 next(e), 위험 후보이면 확인을 거칩니다. 진행을 선택하면 원래 명령을 그대로 넘깁니다. 취소하면 Claude에게 거부 사유를 전달합니다.
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
const risk = classify(String(e.command ?? ""));
if (risk === null) return next(e); // everything else runs as normal
const report = await measure($, risk, await $.session.cwd());
held = { command: e.command, risk, report, decision: null };
const opened = await $.ui.open({
id: "blast-radius", title: "Blast Radius", focus: true,
});
if (!opened.isPlaced) held.where = "band";
while (held.decision === null && !next.signal.aborted) {
await $.process.run(["sleep", "0.25"]);
}
if (held.decision === "proceed") return next(e);
return {
deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.`,
};
});classify, measure, held와 렌더 훅은 모드의 다른 부분에 정의됩니다. 이 코드는 핵심 흐름을 설명하는 원문 발췌이며 독립 실행 가능한 전체 모드가 아닙니다.
classify 명령 텍스트를 읽고 위험을 분류합니다.
measure 작업 위치에서 영향을 측정합니다.
held 보류한 명령과 사용자 결정을 묶습니다.
$.ui.open 확인 패널을 엽니다.
while 결정이나 중단 신호까지 기다립니다.
return 진행은 next(e), 나머지는 거부로 응답합니다.
보고서는 git status --porcelain, git clean -n, git log HEAD..origin/main, du, showmigrations 같은 도구의 결과에서 얻습니다. 명령마다 제공하는 확인 기능을 선택합니다.
await $.process.run(["git", "clean", "-n"]);
원문 핵심 원리는 argv 배열입니다. 셸 문자열을 조립해 경로 내용을 실행 코드로 만드는 방식과 구분합니다.
문서 기준 훅은 이벤트 처리마다 자체 실행 시간 10초가 주어집니다. $ API 호출 안에서 기다리는 시간은 여기에 포함되지 않습니다. 그래서 짧은 sleep 프로세스로 기다리고, 버튼의 onPress가 결정을 바꿉니다. Esc로 next.signal이 중단되면 대기를 끝냅니다.
Button({ label: "Proceed", hotkey: "1", onPress })Proceed는 1, Cancel은 2로 선택합니다. 클릭뿐 아니라 Tab과 Enter, 숫자 단축키로도 조작할 수 있습니다.
대화 옆 패널에 표시.
isPlaced: false면 표시줄 사용.
120열 터미널의 원문 화면에서는 git reset --hard 영향 보고서가 프롬프트 위에 표시됩니다. 특정 전체 화면 너비만으로 배치를 단정하지 않고 실제 배치 결과를 확인합니다.

명령 텍스트를 보기 때문에 $(…), 별칭, 내부에서 rm을 호출하는 스크립트는 감지를 피할 수 있습니다. 이 모드는 보조 안전장치입니다. 원문은 필수 차단을 권한 규칙과 구분해 설계하라고 설명합니다. 현재 공식 문서에서는 Bash 패턴 규칙에도 다른 호출 형태를 놓치는 한계가 있으며, Mod가 권한 판단에 개입할 수 있다고 명시합니다. 규칙 하나나 이 미리보기만으로 모든 실행 경로의 차단을 보장하지 않습니다.

판단 내용은 유지하고 표현할 위치를 바꿀 수 있습니다.
한 번의 작업에서 파일 여러 개를 바꾸더라도 기록은 한 턴 단위로 묶습니다. 기록 수집, 검토 준비, 명령 실행의 역할을 나눠봅니다.
Replay Theater는 Edit와 Write 호출의 파일 경로, 변경 전후 텍스트를 기록합니다. Write는 덮어쓰기가 반영되기 직전 $.fs.read로 기존 파일 내용을 읽어야 실제 이전 값과 새 값을 비교할 수 있습니다.
on("tool.call", async ($, e, next) => {
if (EDIT_TOOLS.has(e.tool)) {
state.pending.push(...(await stepsFor($, e))); // old/new text → diff
}
return next(e); // the edit runs untouched
});
on("turn.start", ($, e, next) => {
if (!e.agentId) state.pending = [];
return next(e);
});
on("turn.complete", async ($, e, next) => {
const r = await next(e);
if (!e.agentId && state.pending.length) state.replay = state.pending;
return r;
});
on("session.start", async ($, e, next) => {
const r = await next(e);
await $.command.register({
name: "replay", description: "Step through the last turn's file edits",
});
return r;
});
on("command.run", { command: "replay" }, async ($, e) => ({
text: (await openReplay($)) ? "Replaying" : "No edits",
}));EDIT_TOOLS, stepsFor, state, openReplay는 모드의 다른 부분에서 정의됩니다. 발췌문의 state 객체를 재로딩에 안전한 저장소 선언과 혼동하지 마세요. 이 코드만으로 완성된 플러그인이 되는 것은 아닙니다.
turn.start 메인 턴의 임시 편집 목록을 비웁니다.
tool.call 변경을 기록하고 원래 편집을 넘깁니다.
turn.complete 편집이 있으면 이번 턴을 리플레이로 확정합니다.
session.start /replay 명령을 등록합니다.
command.run 명령을 직접 처리해 패널을 엽니다.
“replay라는 명령이 있다”는 메뉴를 준비합니다.
사용자가 그 명령을 실행했을 때 응답합니다.
턴이 끝나면 프롬프트 위 안내가 나타납니다. r을 누르거나 /replay를 입력하면 번호가 붙은 단계 표시줄과 Prev, Next, Close가 있는 패널을 엽니다. 편집이 없으면 No edits로 응답합니다.
패널이 오른쪽에 붙습니다.
프롬프트 위에 인라인으로 열립니다.
모드는 같은 요소 트리를 그리고, 화면이 배치를 담당합니다. e.agentId로 서브에이전트 턴이 메인 기록과 섞이지 않게 합니다. 이 모드는 편집을 관찰하며 막거나 바꾸지 않습니다.


비교할 옛 값이 사라지는 시점을 생각해보세요.
앞에서는 무엇이 가능한지 배웠습니다. 마지막에는 유지·설치·신뢰를 확인하고, 새로운 문제에 이벤트와 API를 연결합니다.
claude로 시작해 원하는 모드를 설명하면 API를 미리 알지 않아도 제작을 요청할 수 있습니다. “무엇을 보여줄지”를 설명하세요. 내장 작성 가이드가 이벤트 선택, 재로딩 상태 보관, validate 사용법을 안내합니다.
핫 리로딩을 허용하면 턴이 끝난 뒤 표시줄이 나타나고, 이후 수정이 바로 반영됩니다. 이 빠른 방법은 현재 세션에만 로드되며 폴더가 나중에 정리될 수 있습니다. 계속 쓰려면 폴더를 다른 곳에 복사해 일반 플러그인처럼 설치합니다.
앞의 제작 요청에서 표시 요구사항을 바꾸면 내 모드가 됩니다. 예보 임계값이나 비용 표시 같은 작은 수정부터 시작하세요.
my-mods/.claude-plugin/marketplace.json설치 목록my-mods/token-weather/완성 플러그인 폴더{
"name": "my-mods",
"owner": { "name": "You" },
"plugins": [{ "name": "token-weather", "source": "./token-weather" }]
}claude plugin marketplace add ./my-mods claude plugin install token-weather@my-mods --scope user
@ 앞은 플러그인 이름, 뒤는 마켓플레이스 이름입니다. 목록을 추가하는 단계와 실제 설치를 구분합니다.
마켓플레이스 파일과 플러그인을 GitHub 저장소에 올리면 저장소가 마켓플레이스가 됩니다. 일반 push로 업데이트할 수 있습니다. 설치한 사용자는 다음 세 명령으로 추가·설치·재로딩합니다.
/plugin marketplace add your-org/my-mods /plugin install token-weather@my-mods /reload-plugins
your-org/my-mods는 자신의 조직·저장소 이름으로 바꿉니다. 재로딩하면 모드가 시작됩니다. 나타나지 않으면 Claude Code를 재시작합니다.
모드는 내 컴퓨터의 Claude Code 안에서 실행되는 코드이고 Claude Code와 같은 접근 권한을 갖습니다. 작성자는 Anthropic이 아니라 해당 배포자입니다. 저장소를 읽고 신뢰할 수 있는 사람이 만든 모드를 설치하세요. 설치 명령을 실행하기 전에는 아무것도 설치되지 않습니다.
Claude 디렉터리 제출에서 모드가 포함된 플러그인을 등록할 수도 있습니다. 매일 쓰는 모드가 되었다면 X나 LinkedIn에 GIF·스크린샷과 마켓플레이스 링크를 공유해 다른 사람이 가능성을 살펴보고 설치할 수 있게 합니다.
$.session.usage() → $.ui.status표시줄에 비용·사용 한도를 보여줍니다. 컨텍스트 비율과 계정 한도를 구분하세요.
prompt.submit입력을 수정해 다음 처리에 넘깁니다.
도구 호출 관찰 → 패널세션에서 읽은 파일을 목록으로 모아 표시합니다.
turn.complete → $.ui.toast작업 완료 시 알림을 표시합니다.
tool.callkubectl 컨텍스트나 terraform apply 같은 스택별 확인 장치를 만듭니다.
“언제 개입할까, 무엇을 넘길까, 어디에 남길까”를 먼저 정합니다. 그 다음에 이벤트·상태·UI를 선택하면 API 이름에 끌려 기능을 만드는 일을 줄일 수 있습니다.
세션의 커밋하지 않은 변경을 패널에서 검토합니다. 직접 명령 처리와 UI 확장이 한 기능으로 묶인 사례입니다.
프로젝트 CLAUDE.md가 없을 때 AGENTS.md의 프로젝트 지시를 읽습니다. Mods는 시각적 패널뿐 아니라 Claude에게 전달할 정보에도 개입합니다.
턴이 끝나면 다음 요청을 최대 3개 제안합니다. 1·2·3은 해당 요청을 입력 초안에 넣고, 0은 닫습니다. 제안 선택은 요청 자동 제출과 다릅니다.
공식 Mods 카탈로그의 시연·기능 설명 · 카탈로그가 연결하는 Next steps 소스
카탈로그는 실제 사용 모습을, API 문서는 구현 계약을 확인하는 데 씁니다. 예제 저장소 링크는 공식 문서에서 확인했으며, 이 페이지의 코드 검토는 원문에 실린 구현을 기준으로 합니다.

기능을 보여주는 것과 설치 가능한 형태로 제공하는 것은 다릅니다.
객관식 정답은 원리를 확인하는 출발점입니다. 자신의 요구사항으로 모드를 만들고 검증하면 이해를 실제 능력으로 옮길 수 있습니다.
튜토리얼의 코드와 최신 문서는 버전이 다를 수 있습니다. 현재 레퍼런스는 v2.1.290 기준이며, 내 Claude Code가 생성한 .claude-plugin/types/ 선언을 우선합니다. 원문 내용은 유지하되 최신 설명과 달라진 부분은 본문에서 구분했습니다.
시점실행 전 판단인가,
실행 후 관찰인가.
전달그대로 넘기는가,
바꾸는가, 여기서 답하는가.
보관재로딩되어도
남아야 하는 값인가.