Tác giả bài gốc: Addy Osmani (Member of Technical Staff). Đăng ngày 01/10/2026 trên claude.dev.
Mod là một file JavaScript hoặc TypeScript nhỏ chạy bên trong session Claude Code của bạn. Nó có thể theo dõi những gì đang diễn ra, thay đổi hành vi của Claude Code hoặc vẽ giao diện riêng, trong terminal hay ứng dụng desktop. Bạn không cần học API để thử một mod. Chạy claude, rồi mô tả mod bạn muốn. Cho phép tự động tải lại khi có thay đổi (hot reload) khi được hỏi, và mod sẽ xuất hiện khi lượt chạy kết thúc.
Claude Code vốn đã cho bạn thay đổi rất nhiều hành vi: thiết lập, quy tắc cấp quyền, slash command, skill và dòng trạng thái (status line). Mod đi xa hơn: chúng có thể viết lại hoặc thay thế những gì Claude Code làm, và vẽ giao diện tùy chỉnh. Bên dưới, mod là các hook được đóng gói trong plugin, và mỗi mod đều nhận được mọi event trong session ngay khi chúng xảy ra.
Nhờ vậy, mod là cách để điều chỉnh Claude Code cho phù hợp với cách bạn làm việc. Bạn có thể thêm một bảng thông số mình luôn muốn xem, đặt một guard trước những lệnh khiến bạn lo lắng, hoặc tạo một giao diện review theo cách bạn thích đọc các thay đổi.
Hướng dẫn này sẽ tạo một mod từ thư mục trống: Token Weather, một bản dự báo trực tiếp về context window, được vẽ phía trên prompt. Mod này dài khoảng 80 dòng. Sau đó, chúng ta sẽ tìm hiểu hai mod lớn hơn, Blast Radius và Replay Theater, để thấy API còn làm được gì. Code hoàn chỉnh của cả ba nằm trong anthropics/claude-code-playground.
Video (Terminal): Token Weather, Blast Radius và Replay Theater lần lượt xuất hiện trong một session terminal
Video (Desktop): Token Weather, Blast Radius và Replay Theater lần lượt xuất hiện trong Claude Code trên desktop
Video: Dải Token Weather khi context window dần đầy: Clear ở 18%, Showers ở 67%, Storm ở 81%
Claude Code 2.1.287 trở lên. Mod được bật mặc định, nên bạn không cần bật gì thêm. API có thể thay đổi giữa các bản phát hành. Mỗi khi Claude Code tải một mod, nó ghi các khai báo kiểu cho bản build của bạn vào thư mục .claude-plugin/types/ của mod, và đó là nguồn tham chiếu chuẩn cho phiên bản bạn đang dùng.
Mod hoạt động như thế nào
Mod là một plugin Claude Code có hành vi nằm trong một module JavaScript hoặc TypeScript:
- Thư mục là một plugin thông thường, có manifest
.claude-plugin/plugin.json. hooks/hooks.jsonchỉ định một module trongmodules.- Module export
register(on, options). Bên trong hàm đó,on(event, matcher?, hook)thêm một hook.
Mọi hook đều có cùng dạng:
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
// $ API của mod: ui, session, state, store, fs, process, clock, http, tool, command, model, ...
// e đầu vào của event này, dưới dạng dữ liệu thuần
// next chuyển e cho các plugin khác rồi đến hành vi gốc của Claude Code
return next(e);
});
Các hook tạo thành một chuỗi, giống middleware. Hook của bạn chạy, next(e) chuyển event cho plugin tiếp theo, và ở cuối chuỗi, Claude Code làm những gì nó vốn sẽ làm. Một hook có thể thực hiện một trong ba việc:
Hình: Một event đi qua hook của bạn, rồi qua các plugin khác, rồi đến Claude Code. Mỗi hook gọi next(e) để chuyển tiếp event, và kết quả đi ngược trở lại. Hook tự trả lời sẽ return mà không gọi next.
| Cách xử lý | Cách làm | Ví dụ |
|---|---|---|
| Quan sát | const r = await next(e); /* look */ return r |
Ghi lại mọi lần sửa file. Đọc thông số sau mỗi lượt. |
| Viết lại | return next({ ...e, command: safer }) |
Thay đổi dữ liệu mà phần còn lại của chuỗi nhận được. |
| Trả lời | return { deny: "…" } mà không gọi next |
Từ chối một tool call. Tự xử lý một lệnh hoặc một tool. |
Các event bao gồm tool call, prompt khi được gửi, lúc bắt đầu và kết thúc lượt, lúc bắt đầu và kết thúc session, slash command, và ui.render: mọi phần của giao diện khi được vẽ. Module chạy trong một môi trường cô lập (sandbox) riêng, không có DOM và không có Node, nên mọi tương tác với bên ngoài đều đi qua $.
Khác gì với hook trong thiết lập? Hook trong thiết lập chạy một lệnh shell cho mỗi event và truyền JSON qua stdin và stdout. Mod được tải một lần và tồn tại suốt session. Nó có thể giữ state, vẽ giao diện cập nhật khi event xảy ra, và gọi ngược vào Claude Code: mở một khung (pane), chạy một process, đăng ký slash command, hoặc đăng ký một tool mà model có thể gọi.
Chính Claude Code cũng dùng mod. Một số tính năng của Claude Code được xây dựng dưới dạng mod, bao gồm hỗ trợ AGENTS.md và pane /diff bên cạnh cuộc hội thoại. Mã nguồn của chúng, cùng các test, nằm trong thư mục mods/ của repo công khai anthropics/claude-code, nên bạn có thể đọc để xem đội ngũ xây dựng chúng như thế nào.
Tạo mod đầu tiên: Token Weather
Token Weather đọc mức độ đầy của context window sau mỗi lượt và vẽ một dòng phía trên prompt: biểu tượng thời tiết, phần trăm, số token đã dùng trên tổng dung lượng window, một biểu đồ nhỏ của các lượt gần đây, và lượng token mà lượt vừa rồi thêm vào.
| Đã dùng | Dự báo |
|---|---|
| dưới 25% | ☀ Trời quang |
| 25–49% | ☁ Nhiều mây |
| 50–74% | ☂ Mưa rào |
| 75–89% | ☇ Giông bão |
| từ 90% trở lên | ↯ Sắp cần nén context |
Đây là mod trong một session thực tế. Mỗi lượt đọc thêm file, và dải hiển thị dần đầy, từ ☀ Clear đến ☂ Showers rồi ☇ Storm:
Video (Terminal): Token Weather trong một session terminal đầy đủ qua ba lượt: 18%, rồi 67%, rồi 81% của window 200k
Video (Desktop): Token Weather trong Claude Code trên desktop qua ba lượt: 10%, rồi 54%, rồi 77% của window 200k
Cách nhanh hơn: để Claude tạo mod
Bạn có thể bỏ qua sáu bước. Claude Code biết cách viết mod, nên bạn có thể mô tả mod mình muốn và để nó làm. Bắt đầu một session bằng claude rồi dán prompt dưới đây:
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 sẽ hỏi một lần xem có bật hot reload cho session không. Cho phép, và dải hiển thị sẽ xuất hiện phía trên prompt khi lượt của Claude kết thúc. Từ đó, mọi thay đổi đều được tải lại ngay tại chỗ, nên bạn có thể tiếp tục yêu cầu chỉnh sửa (“cho Storm bắt đầu từ 70%”, “thêm chi phí tính bằng đô la ở cuối”) và xem dải hiển thị thay đổi. Mod chỉ được tải trong session này, và thư mục của nó sẽ được dọn đi sau đó, nên nếu muốn giữ lại, hãy sao chép thư mục ra ngoài và cài như bất kỳ plugin nào (Bước 6).
Để ý rằng prompt chỉ mô tả những gì bạn muốn thấy. Bạn không cần biết API để viết một mod. Hướng dẫn viết mod tích hợp trong Claude Code giải thích cách làm: lưu state ở đâu để không mất khi tải lại, cách kiểm tra plugin bằng claude plugin validate, và nên gắn hook vào event nào. Thay các dòng “What it should show” và đó sẽ là mod của bạn, không phải của chúng tôi.
Nếu bạn muốn xem cách lắp ghép mod trước, hoặc muốn kiểm tra những gì Claude đã viết, hãy đọc tiếp.
Bước 1: Tạo thư mục
Kiểm tra phiên bản Claude Code của bạn đã đủ mới:
claude --version # 2.1.287 trở lên
Tạo cấu trúc sau:
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 là một manifest plugin tiêu chuẩn:
{
"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 trỏ đến module. Một mod có đúng một module:
{
"modules": ["./token-weather.mjs"]
}
Bước 2: Vẽ thứ gì đó
Dải ngay phía trên prompt là một component tên AbovePrompt. Bản thân Claude Code không vẽ gì ở đó, nên đây là mục tiêu khởi đầu tốt. Gắn hook vào event ui.render của nó và trả về một cây phần tử:
// 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" })],
});
});
}
Các phần tử không phải biến toàn cục. $.ui.resolve(e) trả về các constructor cho bề mặt giao diện (surface) đang được vẽ, vì mỗi surface mà Claude Code vẽ lên hỗ trợ một tập phần tử hơi khác nhau. JSX cũng dùng được, với h làm factory.
Bắt đầu một session có tải plugin:
claude --plugin-dir ./token-weather
“☀ Clear skies” xuất hiện phía trên prompt. Giữ session mở. Thư mục được theo dõi, nên mỗi lần lưu đều tải lại module ngay tại chỗ, không cần khởi động lại. Vòng phản hồi nhanh đó là phần lớn lý do khiến việc viết mod thú vị.
Mẹo: Khi đã nắm được cấu trúc, hãy mô tả mod tiếp theo cho Claude theo cách trong lối tắt. Nó sẽ viết plugin vào một thư mục được hot reload trong cùng session.
Bước 3: Đọc số liệu thật và lưu trong $.state
$.session.usage() trả về cùng các số liệu như status line. context.tokens là lượng đầu vào mà phản hồi gần nhất dựa vào để trả lời, context.window là dung lượng window của model, còn context.percent là tỷ lệ giữa hai giá trị đó. Lời gọi này miễn phí: nó chỉ gửi request đếm token nếu bạn yêu cầu breakdown.
Đọc thông số khi session bắt đầu và sau mỗi lượt:
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($); // chỉ các lượt của vòng lặp chính, không phải subagent
}
return result;
});
Cả hai hook đều gọi next(e) trước rồi mới quan sát. Không hook nào thay đổi những gì diễn ra.
Lưu các lần đọc thông số ở đâu? Một biến let readings = [] ở cấp module có vẻ là lựa chọn hiển nhiên, nhưng hot reload là tải mới hoàn toàn: register chạy lại, session.start được kích hoạt lại, và các biến của module được khởi tạo lại từ đầu. Thay vào đó, hãy lưu lịch sử trong $.state. Nó giữ các giá trị có tên ở host trong suốt session, và chúng không mất khi tải lại.
// Được host lưu giữ, nên lịch sử không mất khi file này được hot reload.
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));
}
Một giá trị state được khai báo trong hợp đồng kiểu (type contract) của plugin: một file .d.ts nhỏ mà manifest trỏ đến. Thêm types/index.d.ts:
export type TokenWeatherReading = { tokens: number; window: number; percent: number };
declare module "claude-code" {
interface PluginState {
"token-weather": { readings: TokenWeatherReading[] };
}
}
Sau đó thêm "types": "./types/index.d.ts" vào plugin.json. Nếu bỏ qua bước này, claude plugin validate sẽ chặn bạn bằng một lỗi nêu rõ cách sửa: token-weather.readings is not declared: the manifest's types contract must name it in interface PluginState { … }.
Đổi lại, bạn được tự động vẽ lại giao diện. Một lời gọi $.state.get trong lúc render hook chạy sẽ đăng ký cho phần giao diện đó theo dõi state, nên mọi lời gọi $.state.set về sau đều vẽ lại dải hiển thị. Bạn không bao giờ phải gọi $.ui.invalidate.
Bước 4: Vẽ dự báo
Đây là toàn bộ module:
// Token Weather: dự báo trực tiếp về context window, phía trên 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" },
];
// Được host lưu giữ, nên lịch sử không mất khi file này được hot reload.
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($); // chỉ các lượt của vòng lặp chính, không phải subagent
}
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);
}
Có ba chi tiết đáng áp dụng vào mod của bạn:
- Props của component nằm trong
e.props.hasSurveycho biết một khảo sát muốn dùng dải hiển thị, nên hook nhường chỗ bằngnext(e).bodyColumnslà chiều rộng thực của dải, nhỏ hơn chiều rộng terminal khi có một pane nằm cạnh lịch sử hội thoại. Hãy định kích thước cây phần tử theo giá trị này. Chỉe.component,e.surface,e.requestIdvàe.viewportnằm ở cấp cao nhất củae. - Chuyển tiếp khi không có gì để vẽ. Trả về
next(e)sẽ giao lại dải hiển thị cho Claude Code và các mod khác. - Dùng ký hiệu rộng một ô ký tự, không dùng emoji. ☀ ☁ ☂ ☇ ↯ thẳng hàng trong mọi font terminal.
Lưu file và session đang chạy sẽ nhận thay đổi. Sau vài lượt đọc các file lớn, dải hiển thị chuyển từ Clear sang Showers rồi Storm, như trong video ở đầu phần này.
Bước 5: Kiểm tra tính hợp lệ và chạy test
claude plugin validate đọc manifest và mã nguồn của module theo đúng cách Claude Code sẽ đọc, rồi báo các event mà module gắn hook và các hàm nó gọi:
$ 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 chạy các file *.test.ts của plugin trên runtime Claude Code thật. Các hook mà test đăng ký bằng on chạy sau mod trong chuỗi và giả lập câu trả lời của Claude Code, nên bạn kiểm soát chính xác những gì $.session.usage() trả về:
// 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) => {
// Các hook đăng ký ở đây chạy sau mod và giả lập câu trả lời của Claude Code.
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();
});
});
$ claude plugin test ./token-weather
(pass) token-weather > the band follows the context window
1 pass
0 fail
Test cũng kiểm tra hành vi vẽ lại từ bước 3. Dải hiển thị cập nhật sau turn.complete mà mod không hề yêu cầu vẽ lại.
Bước 6: Chia sẻ
Mod là plugin, nên cách phát hành cũng giống nhau. Đưa nó vào một marketplace, có thể đơn giản chỉ là một thư mục chứa .claude-plugin/marketplace.json:
{
"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
Để so sánh phiên bản của bạn với một bản hoàn chỉnh, hãy xem mod Token Weather đầy đủ trong anthropics/claude-code-playground.
Chia sẻ mod của bạn
Mod là một plugin Claude Code, nên bạn chia sẻ nó giống như bất kỳ plugin nào khác, không phải học thêm gì mới. Đưa mod vào một repo GitHub kèm file marketplace, và repo đó trở thành marketplace của bạn. Ai cũng có thể cài từ đó, và bạn cập nhật bằng một lần push thông thường.
Việc cài đặt cần ba lệnh trong Claude Code:
/plugin marketplace add your-org/my-mods
/plugin install token-weather@my-mods
/reload-plugins
Mod bắt đầu chạy khi bạn tải lại. Nếu không thấy nó xuất hiện, hãy khởi động lại Claude Code.
Mod là code chạy bên trong Claude Code trên máy bạn, có cùng quyền truy cập như Claude Code, và do người phát hành viết chứ không phải Anthropic. Vì vậy, hãy cài mod như cách bạn cài package: đọc repo trước và chỉ cài từ những người bạn tin tưởng. Không có gì được cài cho đến khi bạn chạy lệnh.
Danh mục Claude chấp nhận các plugin có chứa mod, và bạn có thể gửi mod của mình tại claude.ai/directory/manage, để mọi người tìm thấy nó mà không cần bạn gửi link.
Hai mod nữa
Token Weather chỉ theo dõi và vẽ giao diện. Hai mod tiếp theo can thiệp vào event, mở pane và nhận đầu vào.
Code đầy đủ của cả hai nằm trong anthropics/claude-code-playground: Blast Radius và Replay Theater, mỗi mod có một README giải thích cách nó được xây dựng.
Blast Radius: xem lệnh rủi ro sẽ thay đổi gì trước khi chạy
Khi Claude gọi Bash với rm -rf, git reset --hard, git clean, một lệnh force push hoặc một migration database, Blast Radius giữ lại lời gọi. Nó xác định lệnh sẽ tác động đến những gì và mở một pane với Tiếp tục và Hủy. Nhấn 2 và Claude nhận được lời từ chối kèm lý do. Nhấn 1 và lệnh chạy nguyên như đã viết.
Video (Terminal): Blast Radius giữ lại rm -rf build và liệt kê 9 file (1.1 MB) mà lệnh sẽ xóa. Hủy từ chối lệnh; ở lần thử thứ hai, Tiếp tục cho lệnh chạy.
Video (Desktop): Trên desktop, Blast Radius giữ lại rm -rf build và liệt kê 9 file (498 KB) mà lệnh sẽ xóa. Hủy từ chối lệnh; ở lần thử thứ hai, Tiếp tục cho lệnh chạy.
Mod dùng ba hook: tool.call cho Bash, và ui.render cho Pane và AbovePrompt. Cốt lõi của nó là cách xử lý “trả lời” trong bảng ở trên:
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
const risk = classify(String(e.command ?? ""));
if (risk === null) return next(e); // mọi lệnh khác chạy như bình thường
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"; // quá hẹp cho pane: vẽ phía trên prompt
while (held.decision === null && !next.signal.aborted) {
await $.process.run(["sleep", "0.25"]); // thời gian bên trong lời gọi $ không tính vào giới hạn thời gian của hook
}
if (held.decision === "proceed") return next(e); // cho lệnh chạy
return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
});
Những gì bạn học được:
$.process.runđể chạy thử mà không thay đổi dữ liệu (dry run). Báo cáo lấy từ chính các lệnh của tool:git status --porcelain,git clean -n,git log HEAD..origin/main,showmigrations. Các tham số được truyền dưới dạng mảng argv, nên không có phần nào trong đường dẫn bị chạy như code shell.- Giữ lại một lời gọi. Mỗi lần được gọi xử lý event (dispatch), hook có 10 giây thời gian thực thi riêng, nhưng thời gian chờ bên trong một lời gọi
$không được tính. Vòng lặp chờ bằng các processsleepngắn cho đến khionPresscủa một nút đặt quyết định, và dừng chờ khinext.signalbị hủy (bạn nhấn Esc). - Nút có phím tắt.
Button({ label: "Proceed", hotkey: "1", onPress })dùng được bằng cách click, dùng Tab rồi Enter, hoặc nhấn phím số. - Chuyển về dải hiển thị khi không đủ chỗ. Terminal đặt pane cạnh lịch sử hội thoại khi đủ rộng. Khi
$.ui.opentrả vềisPlaced: false, cùng báo cáo đó được vẽ phía trên prompt:

Ở chiều rộng 120 cột, Blast Radius vẽ báo cáo cho git reset –hard trong dải phía trên prompt
Đây là một lớp bảo vệ bổ sung, không phải hệ thống cấp quyền. Nó đọc văn bản lệnh, nên $(…), alias và script gọi rm có thể vượt qua nó. Hãy dùng quy tắc cấp quyền nếu muốn chặn triệt để.
Replay Theater: xem lần lượt các chỉnh sửa của lượt gần nhất
Trong khi một lượt đang chạy, Replay Theater ghi lại mọi lời gọi Edit và Write: file cùng nội dung trước và sau. Khi lượt kết thúc, một gợi ý xuất hiện phía trên prompt. Nhấn r (hoặc gõ /replay) và một pane sẽ lần lượt hiển thị các chỉnh sửa, mỗi lần một diff, với một dải bước được đánh số và các nút Trước, Tiếp và Đóng.
Video (Terminal): Replay Theater: gợi ý sau một lần đổi tên gồm 5 chỉnh sửa trên 3 file, rồi các bước 1 đến 5 trong một pane
Video (Desktop): Replay Theater trên desktop: gợi ý sau một lần đổi tên gồm 6 chỉnh sửa trên 4 file, rồi các bước 1 đến 6 trong một pane
Mod không bao giờ chặn hoặc thay đổi một chỉnh sửa. Nó chỉ quan sát:
on("tool.call", async ($, e, next) => {
if (EDIT_TOOLS.has(e.tool)) state.pending.push(...(await stepsFor($, e))); // nội dung cũ/mới → diff
return next(e); // chỉnh sửa chạy mà không bị can thiệp
});
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; // một lần phát lại cho mỗi lượt
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" }));
Những gì bạn học được:
- Ghép cặp event.
turn.startvàturn.completeđánh dấu đầu và cuối để gom các chỉnh sửa thành một lần phát lại cho mỗi lượt, còne.agentIdloại các lượt của subagent khỏi nhóm đó. - Đăng ký slash command. Dùng
$.command.registertrongsession.start, rồi trả lời lệnh trongcommand.run. - Đọc file. Với một lời gọi Write,
$.fs.readlấy nội dung cũ ngay trước khi thao tác ghi diễn ra, nên diff phản ánh thay đổi thật. - Bố trí là việc của surface. Ở chế độ toàn màn hình, pane nằm bên phải. Ở chiều rộng 80 cột, nó mở ngay trong luồng giao diện phía trên prompt. Dù cách nào, mod cũng vẽ cùng một cây phần tử.

Replay Theater ở chiều rộng 80 cột, được vẽ ngay trong luồng giao diện phía trên prompt
Bốn thói quen đáng giữ
- Dựa vào các kiểu mà Claude Code viết sẵn cho bạn. Mỗi khi tải mod của bạn, Claude Code ghi các khai báo cho bản build bạn đang dùng vào thư mục
.claude-plugin/types/của mod, nên editor vàtsc -phoạt động mà không cần thêm bước nào. Chúng là tài liệu tham chiếu cho mọi event, mọi phương thức trên$và props của mọi phần tử. - Đọc props từ
e.props.hasSurvey,bodyColumnsvà các props còn lại nằm ở đó, không nằm trực tiếp trêne. - Thiết kế để hỗ trợ hot reload. Mỗi lần lưu đều chạy lại
registervàsession.start, nên hãy giữ dữ liệu trong$.state, không phải trong biến của module. - Khi giao diện không xuất hiện, hãy đọc log. Chạy
claude --debugvà tìm dòng báo rằng một hook trả về cây phần tử không hợp lệ.
Bạn sẽ tùy chỉnh gì bằng mod?
Ba mod ở đây, mỗi mod xuất phát từ một câu hỏi: context của mình đầy đến đâu?, lệnh này sắp xóa những gì?, và Claude vừa thay đổi gì? Câu hỏi của bạn sẽ khác, và đó chính là mục đích. Một vài ý tưởng để bắt đầu:
- Một bộ hiển thị chi phí hoặc giới hạn tần suất (rate limit) từ
$.session.usage(), dưới dạng status line với$.ui.status - Một hook
prompt.submitthêm các quy ước của đội bạn vào mọi prompt - Một pane liệt kê các file Claude đã đọc trong session này, như một bản đồ cập nhật trực tiếp về những gì nó đã xem
- Một bộ đếm thời gian tập trung gửi thông báo ngắn (toast) bằng
$.ui.toastkhi một lượt dài kết thúc - Một guard
tool.callđược điều chỉnh cho stack của bạn, chẳng hạn context kubectl của môi trường production hoặcterraform apply
Chia sẻ những gì bạn tạo ra
Bạn đã tạo một mod mà giờ dùng hằng ngày? Đăng nó lên X hoặc LinkedIn kèm GIF hoặc ảnh chụp lúc nó đang chạy, để các developer khác thấy những gì có thể làm được. Đưa plugin vào một marketplace (Chia sẻ mod của bạn) và dẫn link đến đó, để bất kỳ ai thích nó đều có thể cài bằng ba lệnh.