Bỏ qua để đến nội dung

Tạo một mod

Mod là một plugin của Claude Code có một file entry, gọi là hooks module: một file JavaScript hoặc TypeScript chứa các hàm mà Claude Code gọi khi có event xảy ra. Có hai cách để tạo mod:

  • Nhờ Claude viết: mô tả thứ bạn muốn trong một session Claude Code
  • Tự viết: làm theo hướng dẫn để hiểu code của một mod hoạt động thế nào. Bạn không cần Node.js, bundler hay bước build nào, vì Claude Code nạp trực tiếp file .js và .ts.

Nếu bạn chưa chắc mod có phải công cụ phù hợp không, hãy đọc phần so sánh ở trang tổng quan trước.

Mô tả mod bạn muốn trong một session Claude Code tương tác, và Claude sẽ viết nó. Claude làm việc dựa trên một skill tích hợp sẵn tên là plugin-authoring, skill này cho Claude biết nên ghi mod vào đâu, phiên bản Claude Code của bạn có những event và method nào, và mod được nạp ra sao. Claude có thể tự nạp skill khi bạn yêu cầu viết mod, hoặc bạn tự nạp bằng cách chạy /plugin-authoring tại prompt của Claude Code.

Mod sẽ chạy ngay khi bạn duyệt nó, trừ ở những session không nạp được mod do Claude viết.

Yêu cầu mod bằng lời của bạn, ví dụ make a mod that shows the current git branch above the prompt. Claude ghi mod vào một thư mục riêng bên trong thư mục mods của session, tức là ~/.claude/dev-mods/ cộng với ID của session. Đường dẫn đầy đủ của một mod sẽ có dạng ~/.claude/dev-mods/3f2a9c1e-5b7d-4e8a-9c21-6d0f4b8a7e13/git-branch/.

Khi Claude lưu file đầu tiên, Claude Code hỏi có bật hot reloading cho session này không. Hot reloading sẽ chạy các mod Claude viết trong session này và tự cập nhật theo mỗi thay đổi sau đó.

Chọn một trong hai câu trả lời:

  • Enable for this session: các mod trong thư mục mods của session được nạp khi turn kết thúc, và được nạp lại khi kết thúc mỗi turn có thay đổi chúng. Lựa chọn này giữ nguyên trong suốt session, kể cả khi bạn resume session.
  • Not now: tạm thời không nạp gì. Các file vẫn nằm ở nơi Claude đã ghi, và các mod sẽ được nạp vào lần tiếp theo session đó khởi động. Để một mod không bao giờ được nạp, hãy xóa thư mục của nó.

Chạy /plugin tại prompt của Claude Code và nhấn Tab cho đến khi tab Installed được chọn. Tab này liệt kê mod, và bạn có thể tắt nó ở đó.

Dùng thứ bạn đã yêu cầu. Với prompt ví dụ ở trên, tên branch hiện tại sẽ xuất hiện phía trên ô prompt. Nếu mod chưa làm đúng ý, hãy bảo Claude cần sửa gì. Mod được nạp lại vào cuối mỗi turn có thay đổi file của nó, nên bạn có thể thử thay đổi ngay khi Claude làm xong.

Mod do Claude viết chỉ được nạp trong session đã tạo ra nó, và Claude Code sẽ xóa thư mục mods của session đó khi nó cũ hơn cleanupPeriodDays. Để giữ mod lại, hãy copy thư mục của nó ra khỏi thư mục mods tới một chỗ của riêng bạn, ví dụ ~/mods/git-branch. Sau đó chọn cách nạp:

  • Trong một session bạn khởi động: trong shell, chạy claude --plugin-dir ~/mods/git-branch
  • Cho người khác dùng: thêm nó vào một marketplace để họ có thể cài

Những session không nạp được mod do Claude viết

Phần tiêu đề “Những session không nạp được mod do Claude viết”

Mod do Claude viết chỉ được nạp sau khi bạn duyệt, trong một workspace được tin cậy (trusted) và cho phép mods chạy. Trong các session sau, nó không được nạp:

  • Không có ai để duyệt: session không thể hiện prompt cho bạn, như khi chạy claude -p hoặc ở mode dontAsk
  • Workspace chưa được tin cậy: bạn chưa chấp nhận trust prompt cho thư mục đó
  • Mods bị tắt: bạn khởi động với --safe-mode hoặc --bare, bạn đặt disableAllHooks, hoặc managed settings của tổ chức chặn nó

Trong phần hướng dẫn này, bạn sẽ xây dựng một mod tên first-mod: đếm số tool call Claude thực hiện, hiển thị con số đó cạnh spinner trong lúc Claude làm việc, và thêm command /tally để in con số ra. Sau đó bạn đọc các file khai báo type mà Claude Code ghi cạnh mod của bạn và chạy claude plugin validate. Hai thứ này cho bạn biết phiên bản của bạn có những event và method nào, và Claude Code đọc được gì từ code của bạn.

Video dưới đây cho thấy mod hoàn chỉnh. Spinner đếm số tool call, /tally in ra con số, và một chỉnh sửa trong code có hiệu lực ngay trong lúc session đang chạy:

Bạn sẽ viết ba file:

first-mod/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.js

Tạo hai thư mục chứa các file:

Bash hoặc Zsh
mkdir -p first-mod/.claude-plugin first-mod/hooks
PowerShell
New-Item -ItemType Directory -Force first-mod\.claude-plugin, first-mod\hooks

Mod là một plugin, và mod cần có manifest. Manifest của mod này không có field nào đặc biệt. Lưu nội dung sau thành first-mod/.claude-plugin/plugin.json:

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" }
}

Bước 3: Cho Claude Code biết code của bạn nằm ở đâu

Phần tiêu đề “Bước 3: Cho Claude Code biết code của bạn nằm ở đâu”

Khi nạp một plugin, Claude Code đọc file hooks/hooks.json của plugin. Key modules trong file đó chỉ ra đường dẫn tới code của bạn, và chính việc có key này khiến plugin trở thành một mod. Ghi một đường dẫn, tương đối so với hooks.json. Ở đây nó trỏ tới register.js, file bạn sẽ viết ở bước tiếp theo.

Lưu nội dung sau thành first-mod/hooks/hooks.json:

first-mod/hooks/hooks.json
{
"description": "The first-mod hooks module",
"modules": ["./register.js"]
}

File này là code của mod, gọi là hooks module. Khi mod được nạp, Claude Code gọi hàm register mà file export ra và truyền vào một hàm tên là on. Mỗi lần gọi on sẽ đăng ký một event handler, gọi là hook, cho event được nêu tên.

Lưu nội dung sau thành first-mod/hooks/register.js:

first-mod/hooks/register.js
// Biến đếm, dùng chung cho các hook bên dưới
let calls = 0
// Claude Code gọi hàm này một lần khi mod được nạp
export function register(on) {
// Chạy khi session bắt đầu, trước prompt đầu tiên của bạn
on('session.start', async ($, e, next) => {
// Thêm command /tally
await $.command.register({
name: 'tally',
description: 'Show how many tool calls Claude has made',
})
// Để session bắt đầu như bình thường
return next(e)
})
// Chạy mỗi khi Claude sắp dùng một tool
on('tool.call', async ($, e, next) => {
calls += 1
// Yêu cầu Claude Code vẽ lại giao diện để hiện con số mới
$.ui.invalidate('ui.render')
// Để tool chạy như bình thường
return next(e)
})
// Chạy khi bạn gõ /tally, và chỉ khi đó, nhờ matcher
on('command.run', { command: 'tally' }, async () => {
// Đoạn text sẽ in ra transcript
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
// Chạy mỗi khi Claude Code vẽ spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Giữ nguyên spinner của Claude Code, thêm con số vào sau chữ của nó
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}

File này lưu con số trong biến calls và đăng ký bốn hook:

  • session.start chạy khi session bắt đầu, trước prompt đầu tiên của bạn, và chạy lại mỗi khi mod được reload. Nó thêm command /tally vào Claude Code.
  • tool.call chạy mỗi khi Claude sắp dùng một tool. Nó cộng calls thêm một và yêu cầu Claude Code vẽ lại giao diện.
  • command.run chạy khi bạn gõ /tally. Nó trả về đoạn text cần in.
  • ui.render chạy mỗi khi Claude Code vẽ spinner. Nó thêm con số vào sau chữ của spinner.

Phần Mod ví dụ hoạt động thế nào giải thích ba tham số mà mỗi hook nhận và giá trị mỗi hook trả về.

Khởi động Claude Code với cờ --plugin-dir, cờ này nạp một thư mục plugin cho một session mà không cần cài đặt:

Terminal window
claude --plugin-dir ./first-mod

Nhờ Claude làm việc gì đó cần vài tool call, ví dụ list the files here and read the README. Trong lúc Claude làm việc, chữ của spinner sẽ có thêm một con số tăng dần phía sau, kiểu Thinking · tool calls: 2…. Khi Claude làm xong, gõ /tally rồi nhấn Enter. Transcript sẽ hiện first-mod: Claude has made 2 tool calls since this mod loaded, với con số của riêng bạn. Claude Code tự thêm tên plugin vào trước đoạn text của command.

Để kiểm tra command mà không cần session tương tác, hãy chạy nó ở chế độ non-interactive:

Terminal window
claude -p "/tally" --plugin-dir ./first-mod
first-mod: Claude has made 0 tool calls since this mod loaded

Nếu /tally không có trong danh sách command, nghĩa là module chưa được nạp. Xem Tìm hiểu vì sao mod không làm gì.

Bước 7: Sửa code trong lúc session đang chạy

Phần tiêu đề “Bước 7: Sửa code trong lúc session đang chạy”

Để session mở. Trong register.js, đổi ' · tool calls: ' thành ' · tools used: ' trong hook ui.render rồi lưu lại. Dòng được tô sáng là dòng thay đổi:

first-mod/hooks/register.js
// Chạy mỗi khi Claude Code vẽ spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Giữ nguyên spinner của Claude Code, thêm con số vào sau chữ của nó
return next({ ...e, props: { ...e.props, suffix: ' · tools used: ' + calls + '…' } })
})

Một dòng trong transcript báo first-mod đã reload và liệt kê các hook của nó, và spinner lần sau sẽ dùng text mới, kiểu Thinking · tools used: 1….

Mỗi hàm bạn truyền cho on là một hook, tức là một event handler. Claude Code truyền cho mọi hook cùng ba tham số:

  • Mods API, đặt tên là $: mọi method mà mod có thể gọi để tác động ra bên ngoài, được nhóm theo namespace như $.ui và $.command
  • Event, đặt tên là e: input của event dưới dạng dữ liệu thuần, ví dụ tên và tham số của một tool call
  • Handler kế tiếp, đặt tên là next: một hàm chuyển event tiếp cho các mod khác rồi tới hành vi mặc định của Claude Code, và trả về kết quả

Các hook trong first-mod xử lý event theo những cách sau:

  • Quan sát: hook session.start đăng ký command, còn hook tool.call đếm call và yêu cầu vẽ lại. Cả hai đều trả về next(e), nên session bắt đầu và tool chạy như bình thường.
  • Trả lời: hook command.run trả về kết quả của riêng nó và không bao giờ gọi next. Tham số thứ hai của on, { command: 'tally' }, là một bộ lọc, gọi là matcher, nên hook chỉ chạy cho /tally.
  • Viết lại: hook ui.render gọi next với một bản copy của e có suffix chứa con số, nên Claude Code vẽ spinner như thường lệ, kèm text của bạn sau chữ

Claude Code theo dõi thư mục được nạp bằng --plugin-dir và hot-reload hooks module khi có file trong đó thay đổi. Mỗi lần reload, register chạy lại, nên calls trở về 0 và /tally bắt đầu đếm lại từ đầu. Để giữ một giá trị qua các lần reload, xem Lưu state.

Khi một mod đã được nạp, bạn có thể nhờ Claude sửa nó, kiểm tra code của bạn với type definitions của phiên bản đang dùng, liệt kê các event và lời gọi mà Claude Code tìm thấy trong mod, và viết test cho nó.

Để sửa một mod bạn đã có, khởi động session với --plugin-dir trỏ tới thư mục của mod, để những gì Claude viết được nạp ngay trong cùng session:

Terminal window
claude --plugin-dir ./first-mod

Sau đó yêu cầu thay đổi, ví dụ add a /tally-reset command to this mod that sets the tally back to zero. Claude sẽ sửa hooks module, chạy claude plugin validate và sửa những lỗi nó báo. Thư mục bạn nạp bằng --plugin-dir là một protected path, nên ở mode default và acceptEdits bạn sẽ được hỏi để duyệt từng chỉnh sửa Claude làm trên mod. Bảng protected paths cho biết kết quả với các permission mode khác.

Các file Claude lưu trong turn sẽ được reload khi turn kết thúc, nên bạn có thể thử /tally-reset ngay khi Claude làm xong.

Lấy type definitions cho phiên bản của bạn

Phần tiêu đề “Lấy type definitions cho phiên bản của bạn”

Mỗi lần Claude Code nạp hoặc reload một mod từ thư mục bạn truyền vào --plugin-dir, hoặc một mod Claude đã viết cho bạn, nó ghi các file khai báo TypeScript, đuôi .d.ts, vào .claude-plugin/types/ bên trong thư mục của mod. Các file này mô tả chính xác các event, method của mods API và element có trong phiên bản Claude Code bạn đang chạy, nhờ đó editor có thể gợi ý (autocomplete) và kiểm tra type cho hook của bạn. Để xem các khai báo online, đọc mods/types/claude-code.d.ts trong repository Claude Code, dòng đầu tiên của file ghi phiên bản đã sinh ra nó. Thư mục chứa các file sau:

Đường dẫnKhai báo gì
claude-code/index.d.tsMọi event cùng input và result của nó, mọi namespace và method của mods API, và các element mà mỗi surface vẽ được
claude-code-tools/index.d.tsInput và result của các tool tích hợp sẵn, để khi kiểm tra e.tool === 'Bash' thì type của e được thu hẹp lại
claude-code-mcp/index.d.tsInput của các MCP tool đang kết nối vào lần cuối bạn lưu một file trong mod
index.d.ts trong thư mục mang tên một pluginNhững gì plugin đó thêm vào mods API. Có một thư mục cho mỗi plugin mà plugin.json của bạn liệt kê trong dependencies.
tsconfig.jsonCác compiler option phù hợp với hooks module

Nếu mod của bạn chưa có tsconfig.json riêng, Claude Code sẽ thêm một file ở thư mục gốc của mod, kế thừa (extend) file được sinh ra, nhờ đó editor và lệnh tsc -p ./first-mod kiểm tra type cho mod mà không cần thiết lập gì thêm.

Event và method có thể thay đổi giữa các bản phát hành, nên khi có mâu thuẫn, hãy tin các file này hơn bất kỳ trang tài liệu nào, kể cả trang này.

claude-code/index.d.ts là tài liệu tham chiếu đầy đủ nhất cho bản build của bạn, có chú thích và ví dụ cho mọi method của mods API. Để tra cứu, tìm theo tên trong file, ví dụ 'tool.call'.

Kiểm tra Claude Code đọc được gì từ mod

Phần tiêu đề “Kiểm tra Claude Code đọc được gì từ mod”

Để xem mod của bạn theo cách Claude Code nhìn thấy nó, mà không cần chạy code hay khởi động session, hãy dùng claude plugin validate. Lệnh này kiểm tra manifest và chạy cùng phép phân tích tĩnh (static analysis) trên mã nguồn hooks module mà Claude Code chạy khi nạp mod. Trong shell, chạy nó trên thư mục của mod:

Terminal window
claude plugin validate ./first-mod

Với first-mod, output có các dòng sau.

❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
❯ ./register.js calls: $.command.register, $.ui.invalidate
✔ Validation passed

Dòng hooks: liệt kê các event mà module của bạn hook vào, mỗi event kèm bộ lọc trong ngoặc nhọn. Dòng calls: liệt kê mọi method của mods API mà nó gọi. Module nào đọc hoặc đặt biến môi trường sẽ có thêm dòng env reads: và env writes:, còn module dùng $.state có thêm state reads: và state writes:.

Nếu một event bạn định xử lý không có trong dòng đầu tiên, Claude Code cũng sẽ không gọi hook đó. Nguyên nhân thường gặp là viết sai tên event, khi đó lệnh báo lỗi kiểu "tool.calls" is not an event.

Tuân thủ các quy tắc sau để static analysis tìm được mọi hook và mọi lời gọi:

  • Viết đầy đủ mỗi lời gọi mods API: $, namespace, rồi method, như $.store.get('notes'). Bạn có thể truyền $ cho một hàm khai báo ở top level của cùng file, và với một hàm tên loadNotes, dòng calls: sẽ ghi $.store.get (via loadNotes). Truyền $ cho một method, một hàm định nghĩa bên trong hook, hoặc một hàm import từ file khác của bạn sẽ khiến validate thất bại. Các hàm read và update mà $.state dùng là những import duy nhất được nhận $. Không gán $ hay một namespace của nó cho biến, không destructure, không truy cập bằng tên tính toán động. const ui = $.ui sẽ lỗi với $.ui is used as a value.
  • Viết tên event trong mỗi lời gọi on dưới dạng string literal, như 'tool.call'. Một biến, hoặc một vòng lặp qua danh sách tên, sẽ lỗi với the event name passed to on() is not a string literal.
  • Bên trong register, không khai báo thêm biến hay tham số nào tên on. Validate sẽ lỗi với "on" is declared again (shadowed).
  • Chỉ import từ các file bên trong thư mục plugin, bằng đường dẫn tương đối. Import “trần” (bare import) duy nhất được phép là claude-code, dùng cho type và một vài helper.
  • Dùng khai báo import ở đầu file, như import { name } from './file.js'. Một import() động sẽ lỗi với a dynamic import(); a hooks module imports its own files with an import declaration.
  • Viết mọi file dưới dạng ES module, dùng import chứ không dùng require. Phần tham chiếu liệt kê các đuôi file Claude Code nạp được.

Bạn có thể viết test tự động cho mod và chạy chúng từ shell bằng claude plugin test, không cần session, đăng nhập hay mạng. Một test sẽ kích hoạt (fire) các event mà hook của bạn xử lý và kiểm tra hook đã làm gì.

Test dưới đây kích hoạt hai tool call, chạy /tally, và kiểm tra rằng câu trả lời đếm đủ cả hai. Lưu thành first-mod/tests/first-mod.test.ts:

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) => {
// Trả lời mỗi tool call thay cho Claude Code, nên không tool nào thực sự chạy
on('tool.call', () => ({ result: 'ok' }))
// Kích hoạt hai tool call, hook tool.call của mod sẽ đếm chúng
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Chạy /tally và kiểm tra đoạn text mà hook của nó trả về
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})

Trong shell, chạy test từ thư mục first-mod:

Terminal window
claude plugin test

Output ghi tên từng test và kết quả pass hay fail, kèm thời gian chạy sẽ khác nhau mỗi lần:

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]

Trang Kiểm thử mod hướng dẫn stub một lời gọi model hoặc store, và kiểm thử timer cũng như phần vẽ giao diện.

Mod là một plugin, nên bạn quản lý phiên bản trong manifest, và người khác cài, cập nhật nó bằng các lệnh /plugin. Cách chia sẻ phụ thuộc vào đối tượng:

Trước khi chia sẻ, hãy kiểm tra name của plugin: claude plugin validate sẽ báo lỗi với tên trông giống tên của chính Anthropic, ví dụ tên bắt đầu bằng claude-. Event và method có thể thay đổi giữa các bản phát hành, nên README là nơi để ghi rõ bạn đã test với phiên bản Claude Code nào.

Hãy tiếp tục phát triển trên thư mục gốc với --plugin-dir, đừng sửa bản đã cài. Claude Code cache plugin đã cài theo version, nên chỉnh sửa của bạn sẽ không tới được bản đã cài cho đến khi bạn tăng version và cài lại.

Bài tiếp theo: Vẽ giao diện bằng mod - Vẽ pane, band phía trên prompt, nút bấm, ô nhập liệu và lưu state.