Skip to main content
The mods API is the set of methods a mod calls to act: add commands and tools, call a model, run work between events, and reach the file system, processes, and the network. Every hook receives it as its first argument, $, 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 a session.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 handle command.run for its name. This example adds a /standup command that takes an optional number of days:
After the session starts, /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 of mcp__, 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:
When you ask about a ticket, Claude can call 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.
When MCP tool search defers a registered tool, Claude sees its name but not its description until it searches for it. If Claude should consider the tool on every turn, add isDeferred: false to the registration to load the full tool upfront. The field requires Claude Code v2.1.293 or later, and earlier versions ignore it.

Call a model

A mod can send its own requests to a model for a small job such as classifying or summarizing text. $.model.complete sends your prompt on its own, and $.model.fork({ prompt }) sends the current conversation with your prompt at the end. This table compares what each request contains: A fork repeats the conversation’s last request, so the Claude API serves most of it from the prompt cache while the conversation is still cached. Both calls use the session’s credentials, so they bill to the user’s plan, API key, or cloud provider. The types for your build document every $.model method.

Send one prompt

Pass model and prompt to $.model.complete. prompt becomes the user message. To give the model instructions, such as a role or an output format, also pass system, which becomes the system prompt. This hook answers a /triage command, registered as a command, by asking a small model to label the text typed after it:
When you run /triage the export button does nothing, the mod sends that text to the model and prints its answer, such as Label: bug. 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 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.

Use prompt caching

$.model.complete supports the Claude API’s prompt caching. The API caches the start of a request, called the prefix, up to a cache breakpoint that you set. When every call starts with the same long static content, such as instructions or reference material, set a breakpoint at the end of that content. Later calls then read it from the cache instead of paying the full input price for it. To set a breakpoint, pass prompt as an array of { text } blocks instead of a string, and add cache: true to the last block of the static content. Claude Code sends that block with the API’s cache_control field. system takes the same array form. To decide between them, see Choose between prompt and system.
Arrays of blocks require Claude Code v2.1.292 or later. Earlier versions reject an array in prompt with an error that ends with takes { model, prompt } (host check), and they leave an array in system out of the request.
This version of the /triage hook sends a long set of labeling rules before the text to label, with a breakpoint after the rules. RULES is a string of your own:
The TTL and the number of breakpoints have these limits:
  • TTL: a cache entry lasts five minutes after its last use. The TTL comes from the user’s Claude Code settings, not from the call. For one hour, set subagentPromptCacheTtl to 1h.
  • Breakpoints per request: the API accepts up to four, and one more comes back as an api-error in r.reason

Choose between prompt and system

Put the static content that your calls share at the start of prompt unless you know that your requests go directly to the Claude API:
  • Directly to the Claude API, with an API key or a Claude subscription: either field works
  • Through Amazon Bedrock, Claude Platform on AWS, Google Cloud’s Agent Platform, Microsoft Foundry, or an LLM gateway: use prompt. Claude Code begins the system prompt with an attribution block whose fingerprint comes from the start of the user message. The api.anthropic.com endpoint strips that block before caching. Other endpoints receive it as part of the prompt, so a breakpoint in system can miss when prompt starts differently.
  • In a mod that other people run: use prompt, because you don’t choose their provider
system comes before prompt in the prefix, so a breakpoint in prompt covers system too, and a call with a different system misses the cache.

Check for cache hits

The result of $.model.complete has a usage object with the API’s cache fields. usage.cache_creation_input_tokens counts the tokens the call wrote to the cache, and usage.cache_read_input_tokens counts the tokens it read from the cache. Expect a write on the first call and reads on later calls within the TTL. If every call writes and none reads, the prefix differs between calls or the calls are further apart than the TTL. For a prefix that differs, see Choose between prompt and system. If both fields stay at zero on calls the model answered, nothing was cached. Check for each of these causes:

What a model.complete hook receives

If you hook the model.complete event to inspect or change other mods’ requests, read the text from these fields:
  • e.prompt: always a string. When the caller passed an array, it’s the blocks’ text concatenated in order.
  • e.system: a string built the same way, or absent when the caller passed no system
  • e.promptBlocks and e.systemBlocks: the caller’s arrays, each present when the caller passed an array for that field
Claude Code sends the strings your hook passes to next and uses the arrays you pass with them to place cache breakpoints. It keeps the leading blocks that still match the start of the string, with their breakpoints, and sends the rest of the string with no breakpoint. For example, next({ ...e, prompt: e.prompt + NOTE }) keeps the caller’s breakpoints, and a hook that changes the start of prompt removes them.

Run work in the background

Work that outlives one event, such as checking on something once a minute, runs on a timer you start from session.start. A hook itself runs for one event and has a time limit on its own execution 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 session starts as usual. A minute later, a line appears under the prompt with a ⚠, 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

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, to one of this session’s subagents, or to a teammate in its agent team. It can also observe the messages that arrive and leave. To send one, call $.session.send({ to, text }), which makes the same delivery the SendMessage tool makes. Set to by who receives the message:
  • Another of your sessions: { sessionId }
  • A subagent or teammate: { agentId }, with an id from $.agent.list()
  • The sender of a message you received: the string address that 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:
When the message is queued, nothing appears in your session, and the other session’s Claude reads Status? One line. When nothing was delivered, a toast notification gives the reason. session.receive and session.send 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 as setTimeout, 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, processes, and requests have a few rules of their own:
  • Paths: a relative path resolves against the working directory of the session, or of the subagent whose event the hook is handling
  • $.fs.list: returns one directory’s entries as { name, kind, size, isLink } and isn’t recursive
  • $.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 in try and catch.
  • $.http.fetch: follows up to five redirects. On a redirect to a different origin, it keeps only the accept, accept-language, content-type, and user-agent request headers you set and drops the rest, so a request that depends on another header, such as Authorization, can fail after that redirect. The limits give its timeout and body sizes.
Every one of these calls is itself an event, named for its namespace and method without the $., 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. A mod can refuse your $.process.spawn call after the command has produced output or exited, and nothing the command did is undone. The call then rejects with a message that ends with one of these strings and the refusing mod’s reason:
  • $.process.spawn started, and a plugin withheld its result:: the refusing mod hadn’t read the command’s output to the end. Claude Code stops the command if it’s still running.
  • $.process.spawn ran, and a plugin withheld its result:: the refusing mod had read the command’s output to the end, so the command had exited

Next steps