Dùng mods API
Mods API là tập method mà một mod gọi để hành động: thêm command và tool, gọi model, chạy tác vụ giữa các event, và truy cập file system, process và mạng. Mỗi hook nhận nó làm tham số đầu tiên, $, với các method được nhóm theo namespace như $.ui và $.fs. Event quyết định khi nào hook chạy, còn mods API là thứ hook gọi khi nó chạy.
Hãy xây dựng mod đầu tiên trước khi đọc trang này. Để xem mọi method, xem phương thức mods API hoặc đọc type cho bản build của bạn.
Thêm command hoặc tool
Phần tiêu đề “Thêm command hoặc tool”Mod có thể thêm command để user chạy và tool để Claude gọi. Hãy đăng ký cả hai trong một hook session.start. Claude Code chờ hook này chạy xong rồi mới tới prompt đầu tiên, nên những gì bạn đăng ký có sẵn ngay từ turn đầu.
Thêm command
Phần tiêu đề “Thêm command”Command dành cho user. Đăng ký nó, rồi xử lý command.run theo tên của nó. Ví dụ này thêm command /standup nhận một số ngày tùy chọn:
on('session.start', async ($, e, next) => { // Thêm /standup vào danh sách command, kèm mô tả mà user thấy ở đó await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' }) return next(e)})
// Matcher giới hạn hook ở /standup, nên các command khác không tới đâyon('command.run', { command: 'standup' }, async ($, e) => { // e.args là text gõ sau tên command, hoặc chuỗi rỗng return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }})Sau khi session bắt đầu, /standup xuất hiện kèm mô tả trong danh sách bạn thấy khi gõ /. argumentHint hiện trong ô prompt sau khi bạn gõ command và một dấu cách, như /standup [days]. Khi bạn chạy /standup 3, hook thứ hai trả về Summary for the last 3 day(s): ..., và transcript hiển thị đoạn text đó sau tên plugin. Hook không bao giờ gọi next, vì command này không có hành vi nào khác ngoài hành vi của bạn.
Đoạn text bạn trả về được in ra transcript và Claude đọc được nó. Để không in gì, như với một command chỉ mở pane, hãy trả về {}. Để command chạy được cả khi Claude đang làm việc, thêm immediate: true khi đăng ký.
Hãy chọn tên mà không command tích hợp sẵn nào dùng. Gõ / trong một session để xem các tên đó. $.command.register ném lỗi nếu tên đã bị dùng, với thông báo như "/focus" refused: it is the built-in /focus. Hook ném lỗi sẽ bị bỏ qua, nên phần còn lại của hook session.start cũng không chạy. Hãy đăng ký command ở cuối hook đó, hoặc bọc lời gọi trong try và catch.
Thêm tool
Phần tiêu đề “Thêm tool”Tool dành cho Claude. Đăng ký nó với một tên, một mô tả để Claude đọc, và một JSON Schema cho input. Claude thấy nó dưới một tên dài hơn, gồm mcp__, tên plugin của bạn, hai dấu gạch dưới, và tên bạn đã đăng ký. Bạn xử lý các call tới nó trong một hook tool.call được lọc theo tên đầy đủ đó. Ví dụ này, từ một plugin tên my-mod, đăng ký ticket, nên tên đầy đủ là mcp__my-mod__ticket. Nó cho Claude một tool để tra cứu ticket trong một issue tracker:
on('session.start', async ($, e, next) => { await $.tool.register({ name: 'ticket', // Claude dựa vào mô tả này để quyết định khi nào gọi tool description: 'Look up a ticket by its id and return its title and status', // Tham số Claude phải gửi: một chuỗi bắt buộc tên là id inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, }) return next(e)})
// Tên tool đầy đủ là mcp__, tên plugin, rồi tên đã đăng kýon('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => { // Tham số của tool là các field của e, nên id là e.id const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id)) // Luôn trả về kết quả, để Claude biết cả khi tra cứu thất bại return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }})Khi bạn hỏi về một ticket, Claude có thể gọi mcp__my-mod__ticket với id của nó. Hook thứ hai lấy ticket và trả về nội dung response, Claude đọc nó như kết quả của tool. Khi server trả về mã lỗi, Claude đọc được Lookup failed with status kèm con số.
Gọi model
Phần tiêu đề “Gọi model”Mod có thể tự đặt câu hỏi cho một model, nằm ngoài cuộc hội thoại, cho những việc nhỏ như phân loại hay tóm tắt một đoạn text. $.model.complete gửi một prompt tới model bằng credential của session và trả về câu trả lời. Lời gọi này không có lịch sử hội thoại.
Hook này trả lời command /triage, đã được đăng ký như một command, bằng cách nhờ một model nhỏ gắn nhãn cho đoạn text gõ phía sau:
on('command.run', { command: 'triage' }, async ($, e) => { const r = await $.model.complete({ model: 'haiku', // System prompt đặt nhiệm vụ, còn prompt chứa đoạn text cần gắn nhãn system: 'Reply with one word: bug, feature, or question.', prompt: e.args, // Một từ cần rất ít token, và lời gọi bỏ cuộc sau 15 giây maxTokens: 20, timeoutMs: 15000, }) // r.text chỉ có khi model đã trả lời, nên kiểm tra r.isAnswered trước const label = r.isAnswered ? r.text.trim() : 'unknown' return { text: 'Label: ' + label }})Khi bạn chạy /triage the export button does nothing, mod gửi đoạn text đó tới model và in câu trả lời, như Label: bug. Cuộc hội thoại của Claude không nằm trong request. Khi model không trả lời, nhãn là unknown.
Lỗi từ Claude API không làm lời gọi bị reject, nên hãy kiểm tra r.isAnswered, và đọc r.reason khi nó là false. Lời gọi chỉ reject với request mà Claude Code không chịu gửi, như một model bị tổ chức của bạn chặn. Type cho bản build của bạn liệt kê các option khác, như effort, còn phần giới hạn cho biết giá trị mặc định của maxTokens.
$.model.fork({ prompt }) thì đặt một câu hỏi dựa trên cuộc hội thoại hiện tại, với cùng model và system prompt, nên Claude API phục vụ phần lớn request từ prompt cache.
Các lời gọi này dùng gói (plan) hoặc API key của user.
Chạy tác vụ nền
Phần tiêu đề “Chạy tác vụ nền”Những việc kéo dài hơn một event, như kiểm tra một thứ gì đó mỗi phút, chạy trên một timer mà bạn khởi động từ session.start. Bản thân một hook chỉ chạy cho một event và có giới hạn thời gian cho thời gian thực thi của chính nó. Thời gian chờ next hoặc chờ một lời gọi mods API không bị tính, trừ $.clock.sleep. $.clock.every và $.clock.after thay thế cho setInterval và setTimeout, với độ trễ tính bằng mili giây đặt trước: $.clock.after(5000, fn) gọi fn một lần, năm giây sau. Mỗi hàm trả về một timer có method cancel(), và await $.clock.now() cho thời gian hiện tại tính bằng mili giây.
Hook này tra cứu các check của một pull request mỗi phút một lần và hiển thị kết quả dưới ô prompt. summarize là một hàm của riêng bạn, biến output JSON của lệnh thành vài từ:
on('session.start', async ($, e, next) => { // Gọi hàm mỗi 60.000 mili giây, bắt đầu từ một phút sau $.clock.every(60_000, async () => { const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state']) // Thay dòng dưới ô prompt bằng bản tóm tắt mới nhất $.ui.status('checks: ' + summarize(status.stdout)) }) // Trả về mà không chờ timer, để session bắt đầu ngay return next(e)})Session bắt đầu như bình thường. Một phút sau, một dòng xuất hiện dưới ô prompt với ⚠, tên mod, rồi checks: và bản tóm tắt của bạn. Dòng này được thay mới mỗi phút sau đó. Callback của timer chạy ngoài mọi event, nên nó tiếp tục chạy giữa các turn và không khởi động turn nào. Nếu callback ném lỗi, lỗi được ghi vào debug log và timer vẫn chạy lại ở chu kỳ tiếp theo.
Hiển thị thông tin mà không bắt đầu turn
Phần tiêu đề “Hiển thị thông tin mà không bắt đầu turn”Một tác vụ nền có thể cho user thấy thông tin mà không cần bắt đầu turn. Mỗi lời gọi dưới đây đặt text ở một nơi khác nhau:
| Lời gọi | User thấy gì |
|---|---|
$.ui.status(text) | Một dòng dưới ô prompt, giữ nguyên cho đến khi bạn đổi nó. Dòng bắt đầu bằng ⚠ và tên mod, như ⚠ my-mod: checks: 3 passing. |
$.ui.toast(text) | Một thông báo toast ở góc trên bên phải, có tên mod phía trên text, biến mất sau vài giây |
$.ui.log(text) | Một dòng chữ mờ trong transcript mà Claude không đọc. Dòng bắt đầu bằng ● và tên mod, như ● my-mod: build finished. |
Bắt đầu turn từ tác vụ nền
Phần tiêu đề “Bắt đầu turn từ tác vụ nền”Khi một tác vụ nền phát hiện điều gì đó cần Claude chú ý, nó có thể bắt đầu một turn bằng cách gửi prompt qua $.prompt.submit({ text }). Claude đọc đoạn text sau một câu nêu tên mod của bạn là người gửi. Để gửi như lời của chính user, không có câu đó, thêm asUser: true. Lời gọi chờ đến khi session rảnh rồi mới bắt đầu một turn mới. Nó trả về khi turn đó bắt đầu, nên đừng await nó trong một handler chạy lúc Claude đang làm việc.
Dừng tác vụ nền
Phần tiêu đề “Dừng tác vụ nền”Timer dừng khi module reload. Với tác vụ chạy lâu bên trong một hook, next.signal là một AbortSignal sẽ bị abort khi event mà hook đang xử lý bị bỏ dở, ví dụ khi user ngắt, nên hãy truyền nó cho mọi thứ chạy lâu.
Gửi và nhận tin nhắn giữa các session
Phần tiêu đề “Gửi và nhận tin nhắn giữa các session”Mod có thể gửi một tin nhắn dạng text thuần tới một session khác của bạn hoặc tới một subagent của session này, và quan sát các tin nhắn đến và đi. $.session.send({ to, text }) gửi một tin nhắn, theo cùng cơ chế mà tool SendMessage dùng. to là { sessionId } cho một session, { agentId } cho một subagent lấy từ $.agent.list(), hoặc chuỗi địa chỉ mà một tin nhắn nhận được gửi đến từ đó. Lời gọi trả về khi tin nhắn đã vào hàng đợi, với { isDelivered: true }. Khi không gửi được, nó trả về { isDelivered: false, reason }, và reason cho biết lý do.
Hook này trả lời command /ping, đã được đăng ký như một command, bằng cách hỏi trạng thái của session có id bạn gõ phía sau:
on('command.run', { command: 'ping' }, async ($, e) => { // e.args là id của session gõ sau /ping const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' }) // Lời gọi luôn trả về, nên kiểm tra isDelivered để biết chuyện gì đã xảy ra if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason) // Kết quả rỗng không in gì vào transcript của session này return {}})Khi tin nhắn đã vào hàng đợi, không có gì xuất hiện trong session của bạn, và Claude ở session kia đọc được Status? One line.. Khi không gửi được, một thông báo toast cho biết lý do.
session.receive và session.send cho phép mod quan sát các tin nhắn. Trả về next(e) từ cả hai để mỗi tin nhắn đi qua nguyên vẹn:
| Event | Kích hoạt khi | Các field hữu ích |
|---|---|---|
session.receive | Một tin nhắn đến session này, trước khi Claude đọc nó | e.text, và e.origin.kind, như peer hoặc peer-send-message cho session hay agent khác, task-notification, hoặc scheduled-trigger. Trả về { consumed: reason } để giữ tin nhắn lại không cho Claude đọc. |
session.send | Một tin nhắn sắp được gửi đi, từ tool SendMessage hoặc từ một mod | e.to, e.text, và e.origin.kind, là model hoặc plugin |
Một session được đặt để từ chối tin nhắn đến sẽ từ chối tin nhắn trước khi session.receive được kích hoạt, nên hook không bao giờ thấy nó. Một tin nhắn đang chờ bạn duyệt thì tới hook trước, nên mod có thể đọc một tin nhắn mà bạn chưa duyệt. next(e) của hook sẽ reject khi tin nhắn không được chuyển tới.
Tên người gửi trên một tin nhắn nhận được là do người gửi tự viết, nên đừng dựa vào nó để ra quyết định.
Truy cập file, process và mạng
Phần tiêu đề “Truy cập file, process và mạng”Mod truy cập file system, process và mạng thông qua mods API, với cùng quyền của user đang chạy Claude Code. Bản thân hooks module không có Node.js API, không có các hàm timer toàn cục như setTimeout, và không tự truy cập mạng hay file được. Các API chuẩn của JavaScript và web như URL, TextEncoder, AbortController và crypto.subtle thì có sẵn. Mỗi namespace dưới đây phụ trách một loại truy cập:
| Namespace | Chức năng |
|---|---|
$.fs | read(path), write(path, text), exists(path), stat(path) và list(path) thao tác trên file và thư mục |
$.process | run(['git', 'status']) khởi chạy một lệnh và trả về khi lệnh kết thúc. spawn stream output của một lệnh chạy lâu. |
$.http | fetch(url, init) qua http hoặc https. Nó trả về { status, ok, headers, text } sau khi đã đọc xong body. |
$.store | Một key-value store JSON của riêng plugin, giữ lại giữa các session |
$.env | get và set biến môi trường. Viết tên biến dưới dạng string literal. |
$.settings | read nội dung của các settings file và managed policy |
$.session | messages() trả về transcript dưới dạng danh sách { role, text, toolUses }. Ngoài ra còn có thư mục làm việc, model, và nhiều thứ khác. usage() trả về mức dùng context window và giới hạn của gói. |
$.mcp | call một tool trên một MCP server đang kết nối |
File và process có vài quy tắc riêng:
- Đường dẫn: đường dẫn tương đối được tính từ thư mục làm việc của session
$.fs.list: trả về các mục của một thư mục dưới dạng{ name, kind, size, isLink }và không đệ quy$.process.run: nhận một danh sách tham số và không dùng shell. Nó trả về{ exitCode, stdout, stderr }bất kể exit code là gì. Nó reject nếu chương trình không khởi chạy được hoặc vẫn đang chạy khi hết timeout, mặc định là 30 giây, nên hãy bọc nó trongtryvàcatch.
Mỗi lời gọi trên bản thân cũng là một event, đặt tên theo namespace và method nhưng bỏ $., như fs.read cho $.fs.read. Một mod đứng trước trong chuỗi có thể quan sát, viết lại hoặc từ chối lời gọi của bạn, và đây chính là cách một tổ chức giới hạn những gì mod được truy cập.
Đọc tiếp
Phần tiêu đề “Đọc tiếp”- Phản ứng với event: hook vào tool call, prompt và turn
- Vẽ giao diện bằng mod: hiển thị những gì mod thu thập trong pane hoặc phía trên prompt
- Kiểm thử mod: stub bất kỳ lời gọi nào ở trên trong test
- Tham chiếu Mods: event, phương thức mods API và giới hạn
Bài tiếp theo: Kiểm thử mod - Viết test tự động kích hoạt event, stub câu trả lời của Claude Code và bấm nút, không cần session.