跳到正文
Claude.dev 开发者博客· Addy Osmani·· 4 天前精选AI 评分65

Claude Code mods 入门:从零构建 Token Weather

Getting started with Claude Code mods

AI 导读

Claude.dev 开发者博客发布 Claude Code mods 入门指南,mod 是运行在 Claude Code 会话内的 JavaScript 或 TypeScript 文件,可观察事件、改写行为或绘制终端与桌面端 UI,需 Claude Code 2.1.287 或更高版本,默认开启。

推荐理由

从零手写一个约 80 行的 Claude Code mod,并给出 hooks 链、状态持久化与测试的完整可复用路径。

正文 · AI 翻译

Mod 是一个小型 JavaScript 或 TypeScript 文件,运行在你的 Claude Code 会话中。它可以观察正在发生的事情、改变 Claude Code 的行为,或者在终端或桌面应用中绘制自己的 UI。你不需要学习 API 就能试用一个。运行 claude,然后描述你想要的 mod。当它询问时允许热重载,该 mod 会在回合结束时出现。

Claude Code 已经允许你大量改变它的行为方式:设置、权限规则、斜杠命令、技能和状态栏。Mod 走得更远:它们可以重写或替换 Claude Code 的行为,并绘制自定义 UI。在底层,mod 是随插件一起发布的钩子,每一个都能在会话中实时看到每一个事件。

这让 mod 成为一种让 Claude Code 适应你工作方式的方法。你可以添加一个你经常查看的读数,在你感到紧张的的命令前加一道防护,或者为你喜欢的变更阅读方式构建一个审查视图。

本指南从一个空文件夹开始构建一个 mod,Token Weather,一个绘制在提示符上方的上下文窗口实时预报。它大约 80 行。然后它会介绍两个更大的 mod,Blast Radius 和 Replay Theater,以展示 API 还能做什么。

图 AToken Weather、Blast Radius 和 Replay Theater,在终端会话中依次出现

A one-line terminal band cycling through three forecasts, a yellow sun for Clear, a blue umbrella for Showers and a pink lightning bolt for Storm, each with its token count out of 200k and a small bar chart of recent turns.

图 BToken Weather 的色带随上下文窗口填充:18% 时 Clear,67% 时 Showers,81% 时 Storm

Claude Code 2.1.287 或更高版本。 Mod 默认开启,所以无需打开任何东西。API 可能在不同版本之间变化。每次 Claude Code 加载一个 mod 时,它都会为你的构建把类型声明写入该 mod 的 .claude-plugin/types/ 文件夹,这些声明就是你所用版本的权威依据。

MOD 如何工作

Mod 是一个 Claude Code 插件,其行为存在于一个 JavaScript 或 TypeScript 模块中:

  • 该文件夹是一个普通插件,带有一个 .claude-plugin/plugin.json 清单。
  • hooks/hooks.json 指定 modules 下的一个模块。
  • 该模块导出 register(on, options)。在其中,on(event, matcher?, hook) 添加一个钩子。

每个钩子都有相同的形状:

代码JavaScript

on("tool.call", { tool: "Bash" }, async ($, e, next) => {
  // $    the mods API: ui, session, state, store, fs, process, clock, http, tool, command, model, ...
  // e    this event's input, as plain data
  // next passes e to the other plugins and then to Claude Code's own behavior
  return next(e);
});

钩子形成一条链,就像中间件。你的钩子运行,next(e) 将事件交给下一个插件,在底部 Claude Code 做它本来就会做的事。一个钩子可以做三件事之一:

钩子形成一条链

一个事件经过你的钩子,然后其他插件,然后 Claude Code

动作方式示例
观察const r = await next(e); /* look */ return r记录每一次文件编辑。在每个回合后读取一次。
重写return next({ ...e, command: safer })改变链中其余部分看到的内容。
应答return { deny: "…" } 而不调用 next拒绝一次工具调用。自己提供命令或工具。

这些事件涵盖工具调用、提交时的提示符、回合的开始和结束、会话的开始和结束、斜杠命令,以及 ui.render:界面绘制时的每一个部分。该模块运行在自己的沙箱中,没有 DOM 也没有 Node,因此它之外的一切都要通过 $。

这与设置钩子有何不同。 设置钩子为每个事件运行一个 shell 命令,并通过 stdin 和 stdout 传递 JSON。Mod 只加载一次并留在会话中。它可以保持状态、绘制随事件更新的 UI,并回调 Claude Code:打开一个窗格、运行一个进程、注册一个斜杠命令,或注册一个模型可以调用的工具。

Claude Code 自身就在使用它们。Claude Code 的一些自身功能就是以 mod 形式构建的,包括 AGENTS.md 支持以及对话旁边的 /diff 面板。它们的源码连同测试都放在公开的 anthropics/claude-code 仓库的 mods/ 目录下,因此你可以阅读团队是如何构建它们的。

构建你的第一个 MOD:TOKEN WEATHER

Token Weather 会在每一轮之后读取上下文窗口的占用程度,并在提示符上方绘制一行:一个天气图标、百分比、窗口已使用的 token 数、最近几轮的小图表,以及上一轮新增了多少。

已使用预报
低于 25%☀ 晴朗
25–49%☁ 多云
50–74%☂ 阵雨
75–89%☇ 暴风雨
90% 及以上↯ 即将压缩

下面是它在真实会话中的样子。每一轮都会读取更多文件,色带从 ☀ 晴朗逐渐变为 ☂ 阵雨,再到 ☇ 暴风雨:

图 CToken Weather 在一个完整终端会话中跨三轮的表现:先是 18%,然后 67%,再到 200k 窗口的 81%

捷径:让 Claude 来构建它

你可以跳过这六个步骤。Claude Code 知道如何编写 mod,因此你可以描述你想要的那个,然后让它来完成工作。用 claude 启动一个会话,并粘贴下面的提示词:

CODEText

Make me a Claude Code mod called token-weather: a live forecast of my context window, shown in the band above the prompt.

What it should show, on one line:
- A weather icon and word for how full the context window is: under 25% ☀ Clear (yellow), 25–49% ☁ Cloudy (cyan), 50–74% ☂ Showers (blue), 75–89% ☇ Storm (magenta), 90% and up ↯ Compact soon (red).
- The percentage used, then the tokens used out of the window, like "134.4k / 200k".
- A small chart of the last 12 turns, drawn with ▁▂▃▄▅▆▇█.
- How much the last turn added, like "▲ +98.3k last turn".

It should update after every turn.

Claude 会询问一次是否为该会话开启热重载。允许后,当 Claude 的回合结束时,色带就会出现在提示符上方。从那时起,每次更改都会就地重新加载,因此你可以不断要求微调(“让暴风雨从 70% 开始”,“在末尾加上美元成本”),并观察色带的变化。该 mod 只在此会话中加载,其文件夹稍后会被清理,所以若要保留它,请把文件夹复制出来,并像任何插件一样安装它(第 6 步)。

请注意,提示词只描述了你想要看到的内容。你不需要了解 API 就能编写一个。Claude Code 内置的 mod 编写指南涵盖了具体做法:把状态保存在哪里才能在重新加载后依然保留、如何用 claude plugin validate 检查插件,以及要挂钩哪些事件。修改“它应该显示什么”那几行,它就变成你的 mod,而不是我们的。

如果你更想先看看它是如何组合起来的,或者想检查 Claude 写了什么,请继续阅读。

第 1 步:创建文件夹

检查你的 Claude Code 版本是否足够新:

CODEShell

claude --version   # 2.1.287 or later

创建以下目录结构:

CODEText

token-weather/
├── .claude-plugin/
│   ├── plugin.json
│   └── types/            (written by Claude Code when it loads the mod)
├── hooks/
│   ├── hooks.json
│   └── token-weather.mjs
├── types/
│   └── index.d.ts        (added in step 3)
└── tests/
    └── token-weather.test.ts   (added in step 5)

.claude-plugin/plugin.json 是标准的插件清单:

CODEJSON

{
  "name": "token-weather",
  "version": "0.1.0",
  "description": "A live forecast of the context window, drawn above the prompt.",
  "author": { "name": "You" }
}

hooks/hooks.json 指向该模块。一个 mod 恰好有一个:

CODEJSON

{
  "modules": ["./token-weather.mjs"]
}

第 2 步:绘制一些东西

提示符正上方的那条色带是一个名为 AbovePrompt 的组件。Claude Code 自身不会在那里绘制任何内容,因此它是一个很好的首个目标。挂钩它的 ui.render 事件并返回一棵元素树:

CODEJavaScript

// 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" })],
    });
  });
}

这些元素不是全局变量。$.ui.resolve(e) 会返回正在绘制的界面的构造函数,因为 Claude Code 绘制的每个界面所支持的元素集略有不同。JSX 也可以使用,以 h 作为工厂函数。

在加载了该插件的情况下启动一个会话:

CODEShell

claude --plugin-dir ./token-weather

“☀ Clear skies”会出现在提示符上方。保持会话打开。该文件夹会被监视,因此每次保存都会就地重新加载模块,无需重启。这种快速反馈循环正是编写 mod 的大部分乐趣所在。

提示:一旦你了解了它的形态,就可以像捷径那样向 Claude 描述下一个 mod。它会将插件写入一个在同一会话中可热重载的文件夹。

第 3 步:读取真实数字并将它们保存在 $.state 中

$.session.usage() 返回与状态行相同的数字。context.tokens 是上一个响应所依据的输入,context.window 是模型的窗口,而 context.percent 是两者之比。该调用是免费的:只有当你请求 breakdown 时,它才会发送一个 token 计数请求。

在会话开始时以及每一轮之后读取一次:

CODEJavaScript

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;
});

两个钩子都先调用 next(e),然后再进行观察。两者都不会改变实际发生的情况。

把读数保存在哪里。 模块级的 let readings = [] 看起来是显而易见的选择,但热重载是一次全新的加载:register 会再次运行,session.start 会再次触发,模块变量也会重新开始。把历史记录放在 $.state 中。它在整个会话期间将命名值保存在宿主中,并且它们能在重载后保留下来。

CODEJavaScript

// Held by the host, so the history survives a hot reload of this file.
const readings = { plugin: "token-weather", key: "readings" };

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));
}

状态值在插件的 类型契约 中声明,这是一个由清单指向的小型 .d.ts 文件。添加 types/index.d.ts:

CODETypeScript

export type TokenWeatherReading = { tokens: number; window: number; percent: number };

declare module "claude-code" {
  interface PluginState {
    "token-weather": { readings: TokenWeatherReading[] };
  }
}

然后向 plugin.json 添加 "types": "./types/index.d.ts"。如果你跳过这一步,claude plugin validate 会阻止你并报出一个指明修复方法的错误:token-weather.readings is not declared: the manifest's types contract must name it in interface PluginState { … }。

作为回报,你可以免费获得重绘。在渲染钩子运行时创建的 $.state.get 会订阅该绘制,因此之后的每一次 $.state.set 都会重绘该条带。你永远不需要调用 $.ui.invalidate。

第 4 步:绘制预测

以下是整个模块:

CODEJavaScript

// 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);
}

有三个细节值得复制到你自己的 mod 中:

  • 组件的 props 在 e.props 上。 hasSurvey 告诉你某个 survey 需要该条带,因此钩子用 next(e) 让位给它。bodyColumns 是该条带的实际宽度,当有窗格停靠在对话记录旁边时,它比终端更窄。让树按这个宽度来调整大小。只有 e.component、e.surface、e.requestId 和 e.viewport 位于 e 的顶层。
  • 没有可绘制内容时就跳过。 返回 next(e) 会把条带还给 Claude Code 和其他 mod。
  • 使用单宽符号,而不是 emoji。 ☀ ☁ ☂ ☇ ↯ 在每种终端字体中都能对齐。

保存文件后,正在运行的会话会拾取它。在几轮读取大文件之后,条带会从 Clear 变为 Showers 再变为 Storm,正如本节开头的录像所示。

第 5 步:验证和测试

claude plugin validate 会以与 Claude Code 相同的方式读取清单和模块源代码,并报告该模块挂钩和调用了什么:

CODEText

$ 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

claude plugin test 会针对真实的 Claude Code 运行时运行插件的 *.test.ts 文件。测试用 on 注册的钩子会在链中 mod 之后 运行,并 stub 掉 Claude Code 会给出的响应,因此你可以精确控制 $.session.usage() 返回什么:

CODETypeScript

// 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();
  });
});

CODEText

$ claude plugin test ./token-weather
(pass) token-weather > the band follows the context window
 1 pass
 0 fail

该测试还会检查第 3 步中的重绘行为。条带会在 turn.complete 之后更新,而 mod 从未请求重绘。

mod 就是一个插件,因此它的发布方式相同。把它放进一个 marketplace,这可以简单到只是一个包含 .claude-plugin/marketplace.json 的文件夹:

CODEJSON

{
  "name": "my-mods",
  "owner": { "name": "You" },
  "plugins": [{ "name": "token-weather", "source": "./token-weather" }]
}

CODEShell

claude plugin marketplace add ./my-mods
claude plugin install token-weather@my-mods --scope user

分享你的 MOD

mod 是一个 Claude Code 插件,因此你像分享任何其他插件一样分享它,没有什么新东西需要学习。把 mod 放进一个带有 marketplace 文件的 GitHub 仓库,该仓库就成了你的 marketplace。任何人都可以从它安装,你也可以通过普通的 push 来更新它。

在 Claude Code 中安装需要三条命令:

CODEText

/plugin marketplace add your-org/my-mods
/plugin install token-weather@my-mods
/reload-plugins

重新加载后 mod 就会启动。如果它没有出现,请重启 Claude Code。

Mod 是在你机器上的 Claude Code 内运行的代码,拥有与 Claude Code 相同的访问权限,并且由它的发布者编写,而非 Anthropic。所以安装 mod 就像安装软件包一样:先阅读仓库,只从你信任的人那里安装。在你运行命令之前,什么都不会被安装。

Claude 目录接受包含 mod 的插件,你可以在 claude.ai/directory/manage 提交你的插件,这样人们无需你提供链接就能找到它。

另外两个 mod

Token Weather 只观察和绘制。接下来的两个 mod 会介入事件、打开面板并接收输入。

Blast Radius:在危险命令运行前查看它会改变什么

当 Claude 用 rm -rf、git reset --hard、git clean、强制推送或数据库迁移调用 Bash 时,Blast Radius 会拦住这次调用。它会算出该命令会触及什么,并打开一个带有 Proceed 和 Cancel 的面板。按 2,Claude 会收到带有原因的拒绝。按 1,命令会按原样运行。

图 DBlast Radius 拦住 rm -rf build,并列出它将删除的 9 个文件(1.1 MB)。Cancel 拒绝它;第二次尝试时,Proceed 运行它。

它使用三个钩子:在 Bash 上的 tool.call,以及在 Pane 和 AbovePrompt 上的 ui.render。它的核心是上表中的“answer”动作:

代码JavaScript

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());  // git status, git clean -n, du, ...
  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";         // too narrow for a pane: draw above the prompt

  while (held.decision === null && !next.signal.aborted) {
    await $.process.run(["sleep", "0.25"]);          // time inside $ calls doesn't count against the hook's time limit
  }
  if (held.decision === "proceed") return next(e);   // let it run
  return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
});

它教了什么:

  • 用 $.process.run 做试运行。报告来自工具自身的命令:git status --porcelain、git clean -n、git log HEAD..origin/main、showmigrations。参数以 argv 数组传入,因此路径中没有任何内容会作为 shell 代码运行。
  • 拦住一次调用。钩子每次分派有 10 秒自己的时间,但在 $ 调用内等待所花的时间不计入。循环等待短暂的 sleep 进程,直到某个按钮的 onPress 设定决定,并在 next.signal 中止(你按了 Esc)时放弃。
  • 带热键的按钮。Button({ label: "Proceed", hotkey: "1", onPress }) 可通过点击、Tab 加 Enter,或数字键来操作。
  • 降级到边栏。当终端足够宽时,它会在记录旁边停靠一个面板。当 $.ui.open 回答 isPlaced: false 时,同一份报告会绘制在提示符上方:
A terminal with no side pane: a yellow-bordered box above the prompt shows the command, the two files with uncommitted changes it would discard, and numbered Proceed and Cancel choices.
图 E在 120 列时,Blast Radius 在提示符上方的边栏中绘制 git reset --hard 的报告

它是安全网,不是权限系统。它读取命令文本,所以 $(…)、别名和调用 rm 的脚本都能绕过它。要硬性阻止,请使用权限规则。

Replay Theater:逐步查看上一轮的编辑

当一轮运行时,Replay Theater 记录每次 Edit 和 Write 调用:文件,以及修改前后的文本。当该轮结束时,提示符上方会出现一个提示。按 r(或输入 /replay),一个面板会一次一个 diff 地走过这些编辑,并带有一条编号步骤条和 Prev、Next 和 Close 按钮。

图 FReplay Theater:跨 3 个文件的 5 次编辑重命名后的提示,然后是面板中的步骤 1 到 5

它从不阻止或更改编辑。它只观察:

代码JavaScript

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;       // one replay per turn
  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" }));

它教了什么:

  • 配对事件。turn.start 和 turn.complete 将编辑括起来,使每轮成为一次回放,而 e.agentId 将子代理轮次排除在分组之外。
  • 注册斜杠命令。在 session.start 中使用 $.command.register,然后在 command.run 上应答它。
  • 读取文件。对于 Write,$.fs.read 会在写入落地前获取旧内容,因此 diff 是真实的。
  • 放置是界面的职责。在全屏下,面板停靠在右侧。在 80 列时,它内联打开在提示符上方。无论哪种方式,mod 都绘制同一棵树。
A tall terminal window with a magenta-bordered box above the prompt: a numbered step strip, the file greet.js, a one-line diff, and Prev, Next and Close buttons.
FIG G80 列的 Replay Theater,内联绘制在提示符上方

四个值得保留的习惯

  • 善用 Claude Code 为你编写的类型。每次它加载你的 mod 时,Claude Code 都会将你的构建声明写入 mod 的 .claude-plugin/types/ 文件夹,因此你的编辑器和 tsc -p 无需额外步骤即可工作。它们是每个事件、$ 上的每个方法以及每个元素 props 的参考。
  • 从 e.props 读取 props。hasSurvey、bodyColumns 以及其他内容都在那里,而不是在 e 本身上。
  • 为热重载做好规划。每次保存都会再次运行 register 和 session.start,因此请将数据保存在 $.state 中,而不是模块变量中。
  • 当绘图不显示时,查看日志。运行 claude --debug 并查找一行提示某个 hook 返回了无法通过验证的树。

你会为做什么 mod?

这里的三个 mod 各自源于一个问题:我的上下文有多满?、这条命令即将删除什么?以及Claude 刚刚改了什么?你的问题会有所不同,而这正是重点。一些可以开始的思路:

  • 一个来自 $.session.usage() 的成本或速率限制计量器,作为带有 $.ui.status 的状态行
  • 一个 prompt.submit hook,将你团队的约定添加到每个提示中
  • 一个窗格,列出 Claude 本次会话读取过的文件,作为它所见内容的实时地图
  • 一个专注计时器,在长时间回合结束时通过 $.ui.toast 发送 toast 通知
  • 一个针对你的技术栈调整的 tool.call 防护,例如生产 kubectl 上下文或 terraform apply

做了一个你现在每天都在用的 mod?把它发到 X 或 LinkedIn 上,附上它运行时的 GIF 或截图,让其他开发者看到可能性。将插件放入市场(分享你的 mod)并链接到它,这样任何喜欢它的人都能用三条命令安装它。

来源:Claude.dev 开发者博客 · claude.dev