$, with the methods grouped in namespaces such as $.ui and $.fs. Events decide when a hook runs, and the mods API is what the hook calls once it does.
Build your first mod before you start here. For every method, see mods API methods or read the types for your build.
Add a command or a tool
A mod can add a command for the user to run and a tool for Claude to call. Register both in asession.start hook. Claude Code waits for that hook before the first prompt, so what you register is available from the first turn.
Add a command
A command is for the user. Register it, then handlecommand.run for its name. This example adds a /standup command that takes an optional number of days:
/standup appears with its description in the list you see when you type /. The argumentHint shows in the prompt after you type the command and a space, as in /standup [days]. When you run /standup 3, the second hook returns Summary for the last 3 day(s): ..., and the transcript shows that text after the plugin’s name. The hook never calls next, because the command has no behavior other than yours.
The text you return prints in the transcript and Claude reads it. To print nothing, as a command that only opens a pane does, return {}. To let the command run while Claude is working, add immediate: true to the registration.
Pick a name that no built-in command uses. Type / in a session to see them. $.command.register throws for a taken name, with a message such as "/focus" refused: it is the built-in /focus. A hook that throws is skipped, so the rest of your session.start hook doesn’t run either. Register commands last in that hook, or wrap the call in try and catch.
Add a tool
A tool is for Claude. Register it with a name, a description Claude reads, and a JSON Schema for its input. Claude sees it under a longer name made ofmcp__, your plugin’s name, two underscores, and the name you registered. You handle its calls in a tool.call hook filtered to that full name. This example, from a plugin named my-mod, registers ticket, so the full name is mcp__my-mod__ticket. It gives Claude a tool that looks up a ticket in an issue tracker:
mcp__my-mod__ticket with its id. The second hook fetches the ticket and returns the response body, which Claude reads as the tool’s result. When the server answers with an error status, Claude reads Lookup failed with status and the number.
Call a model
A mod can ask a model a question of its own, outside the conversation, for a small job such as sorting or summarizing a piece of text.$.model.complete sends one prompt to a model with your session’s credentials and resolves to the reply. It has no conversation history.
This hook answers a /triage command, registered as a command, by asking a small model to label the text typed after it:
/triage the export button does nothing, the mod sends that text to the model and prints its answer, such as Label: bug. Claude’s conversation isn’t part of the request. When the model doesn’t answer, the label is unknown.
A Claude API failure doesn’t reject the call, so check r.isAnswered, and read r.reason when it’s false. The call rejects only for a request Claude Code won’t send, such as a model your organization blocks. The types for your build list the other options, such as effort, and the limits give the maxTokens default.
$.model.fork({ prompt }) asks one question over the current conversation instead, with the same model and system prompt, so the Claude API serves most of it from the prompt cache.
These calls use the user’s plan or API key.
Run work in the background
Work that outlives one event, such as checking on something once a minute, runs on a timer you start fromsession.start. A hook itself runs for one event and has a time limit of 10 seconds of its own running time. Time spent waiting on next or on a mods API call doesn’t count, except a $.clock.sleep. $.clock.every and $.clock.after take the place of setInterval and setTimeout, with the delay in milliseconds first: $.clock.after(5000, fn) calls fn once, five seconds from now. Each returns a timer with a cancel() method, and await $.clock.now() gives the time in milliseconds.
This hook looks up a pull request’s checks once a minute and shows the result under the prompt. summarize is a function of your own that turns the command’s JSON output into a few words:
⚠, the mod’s name, and then checks: and your summary. It’s replaced once a minute after that. The timer’s callback runs outside any event, so it keeps running between turns and doesn’t start one. If the callback throws, the error goes to the debug log and the timer runs again at the next interval.
Show something without starting a turn
A background job can show the user something without starting a turn. Each of these calls puts text in a different place:Start a turn from a background job
When a background job finds something that needs Claude’s attention, it can start a turn by submitting a prompt with$.prompt.submit({ text }). Claude reads the text after a sentence that names your mod as the sender. To send it as the user’s own words, without that sentence, add asUser: true. The call waits until the session is idle and then starts a new turn. It resolves when that turn starts, so don’t await it in a handler that runs while Claude is working.
Stop background work
Background work stops in two ways. Timers stop when the module reloads. For long-running work inside a hook,next.signal is an AbortSignal that aborts when the event your hook is handling is abandoned, for example when the user interrupts, so pass it to anything long-running.
Send and receive messages between sessions
A mod can send a plain-text message to another of your sessions or to one of this session’s subagents, and observe the messages that arrive and leave.$.session.send({ to, text }) sends one, the same delivery the SendMessage tool makes. to is { sessionId } for a session, { agentId } for a subagent from $.agent.list(), or the string address a received message came from. The call resolves once the message is queued, with { isDelivered: true }. When nothing was delivered it resolves with { isDelivered: false, reason }, and reason says why.
This hook answers a /ping command, registered as a command, by asking the session whose id you type after it for a status:
Status? One line. When nothing was delivered, a small box at the top right gives the reason and disappears after a few seconds.
Two events let a mod observe the messages. Return next(e) from both to pass each message through unchanged:
A session set to refuse inbound messages refuses a message before
session.receive fires, so a hook never sees it. A message that’s held for your approval reaches the hook first, so a mod can read a message you haven’t approved yet. The hook’s next(e) rejects when the message isn’t delivered.
The sender’s name on a received message is whatever the sender wrote, so don’t base a decision on it.
Reach files, processes, and the network
A mod reaches the file system, processes, and the network through the mods API, with the same permissions as the user running Claude Code. The hooks module itself has no Node.js APIs, no timer globals such assetTimeout, and no network or file access of its own. Standard JavaScript and web APIs such as URL, TextEncoder, AbortController, and crypto.subtle are available. Each namespace below covers one kind of access:
Files and processes have a few rules of their own:
- Paths: a relative path is under the session’s working directory
$.fs.list: returns one directory’s entries as{ name, kind, size, isLink }and doesn’t descend into subdirectories$.process.run: takes an argument list and uses no shell. It resolves to{ exitCode, stdout, stderr }whatever the exit code. It rejects if the program can’t start or is still running at the timeout, which is 30 seconds by default, so wrap it intryandcatch.
$., such as fs.read for $.fs.read. A mod earlier in the chain can observe, rewrite, or refuse your call, which is how an organization restricts what mods reach.
Next steps
- React to events: hook tool calls, prompts, and turns
- Draw in the interface: show what your mod collects in a pane or above the prompt
- Test a mod: stub any of these calls in a test
- Mods reference: every event, every mods API method, and the limits