From 72883fa02c8ca0fdd8db1567f88745e3595a4612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Miji=C4=87?= Date: Sat, 24 Oct 2026 22:39:08 +0200 Subject: [PATCH 1/3] docs: add official Mods reference copies --- docs/mods/official/README.md | 21 + docs/mods/official/api.md | 416 ++++++++++++++ docs/mods/official/create.md | 366 ++++++++++++ docs/mods/official/events.md | 393 +++++++++++++ docs/mods/official/gallery.md | 399 +++++++++++++ docs/mods/official/interface.md | 880 +++++++++++++++++++++++++++++ docs/mods/official/overview.md | 254 +++++++++ docs/mods/official/reference.md | 408 +++++++++++++ docs/mods/official/test.md | 412 ++++++++++++++ docs/mods/official/troubleshoot.md | 333 +++++++++++ 10 files changed, 3882 insertions(+) create mode 100644 docs/mods/official/README.md create mode 100644 docs/mods/official/api.md create mode 100644 docs/mods/official/create.md create mode 100644 docs/mods/official/events.md create mode 100644 docs/mods/official/gallery.md create mode 100644 docs/mods/official/interface.md create mode 100644 docs/mods/official/overview.md create mode 100644 docs/mods/official/reference.md create mode 100644 docs/mods/official/test.md create mode 100644 docs/mods/official/troubleshoot.md diff --git a/docs/mods/official/README.md b/docs/mods/official/README.md new file mode 100644 index 00000000..5a048a97 --- /dev/null +++ b/docs/mods/official/README.md @@ -0,0 +1,21 @@ +# Official Claude Code Mods documentation + +These are unmodified Markdown copies of Anthropic's official Claude Code Mods documentation, downloaded on +24 October 2026. They are kept here as a local reference for relay's Mods development. Anthropic owns the +source documentation. + +The canonical documentation index is . The online pages and their native +Markdown versions are the authoritative sources; the declarations generated for the installed Claude Code build +take precedence when they differ, as described in the creation guide. + +| Local copy | Canonical Markdown source | +|---|---| +| [Mods overview](overview.md) | | +| [Create a mod](create.md) | | +| [Use the mods API](api.md) | | +| [React to events](events.md) | | +| [Draw in the interface](interface.md) | | +| [Mods API reference](reference.md) | | +| [Test a mod](test.md) | | +| [Troubleshoot mods](troubleshoot.md) | | +| [Sample mods gallery](gallery.md) | | diff --git a/docs/mods/official/api.md b/docs/mods/official/api.md new file mode 100644 index 00000000..9110f22c --- /dev/null +++ b/docs/mods/official/api.md @@ -0,0 +1,416 @@ +> ## Documentation Index +> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt +> Use this file to discover all available pages before exploring further. + +# Use the mods API + +> Call the mods API from a Claude Code mod to add commands and tools, call a model, run work on a timer, message other sessions, and reach files and the network. + +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](https://code.claude.com/docs/en/plugins/mods/events.md) decide when a hook runs, and the mods API is what the hook calls once it does. + +Build your [first mod](https://code.claude.com/docs/en/plugins/mods/create.md) before you start here. For every method, see [mods API methods](https://code.claude.com/docs/en/plugins/mods/reference.md#mods-api-methods) or read [the types for your build](https://code.claude.com/docs/en/plugins/mods/create.md#get-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`](https://code.claude.com/docs/en/plugins/mods/reference#session) 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`](https://code.claude.com/docs/en/plugins/mods/reference#commands-and-configuration) for its name. This example adds a `/standup` command that takes an optional number of days: + +```javascript theme={null} +on('session.start', async ($, e, next) => { + // Add /standup to the command list, with the description the user sees there + await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' }) + return next(e) +}) + +// The matcher limits the hook to /standup, so other commands don't reach it +on('command.run', { command: 'standup' }, async ($, e) => { + // e.args is the text typed after the command name, or an empty string + return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' } +}) +``` + +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](https://code.claude.com/docs/en/plugins/mods/interface.md#pick-where-to-draw) 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`](https://code.claude.com/docs/en/plugins/mods/events#guard-or-change-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: + +```javascript theme={null} +on('session.start', async ($, e, next) => { + await $.tool.register({ + name: 'ticket', + // Claude decides when to call the tool from this description + description: 'Look up a ticket by its id and return its title and status', + // The arguments Claude has to send: one required string named id + inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, + }) + return next(e) +}) + +// The full tool name is mcp__, the plugin's name, and the registered name +on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => { + // The tool's arguments are fields of e, so the id is e.id + const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id)) + // Return a result either way, so Claude learns when the lookup failed + return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status } +}) +``` + +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. + +> [!TIP] +> +> When [MCP tool search](https://code.claude.com/docs/en/mcp.md#scale-with-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`](https://code.claude.com/docs/en/plugins/mods/reference#tools) to the registration to [load the full tool upfront](https://code.claude.com/docs/en/mcp.md#exempt-a-server-from-deferral). 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: + +| In the request | `$.model.complete` | `$.model.fork` | +| :- | :- | :- | +| Model | The `model` you pass | The session's model | +| System prompt | A short [attribution block](https://code.claude.com/docs/en/llm-gateway-protocol.md#system-prompt-attribution-block), then your `system` if you pass one | The session's system prompt | +| Messages | One user message, your `prompt` | The conversation so far, then your `prompt` as a user message | +| CLAUDE.md and other project context | Not included | Included, as in the conversation's last request | +| Tools | None | Claude's tools, which the model can't call | + +A fork repeats the conversation's last request, so the Claude API serves most of it from the [prompt cache](https://code.claude.com/docs/en/prompt-caching.md) 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](https://code.claude.com/docs/en/plugins/mods/create.md#get-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](#add-a-command), by asking a small model to label the text typed after it: + +```javascript theme={null} +on('command.run', { command: 'triage' }, async ($, e) => { + const r = await $.model.complete({ + model: 'haiku', + // The system prompt sets the job, and the prompt carries the text to label + system: 'Reply with one word: bug, feature, or question.', + prompt: e.args, + // One word needs few tokens, and the call gives up after 15 seconds + maxTokens: 20, + timeoutMs: 15000, + }) + // r.text exists only when the model answered, so check r.isAnswered first + const label = r.isAnswered ? r.text.trim() : 'unknown' + return { text: 'Label: ' + label } +}) +``` + +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](https://code.claude.com/docs/en/plugins/mods/create.md#get-the-types-for-your-build) list the other options, such as `effort`, and the [limits](https://code.claude.com/docs/en/plugins/mods/reference.md#limits) give the `maxTokens` default. + +### Use prompt caching + +`$.model.complete` supports the Claude API's [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching). The API caches the start of a request, called the prefix, up to a [cache breakpoint](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints) 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`](#choose-between-prompt-and-system). + +> [!NOTE] +> +> 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](#send-one-prompt) 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: + +```javascript theme={null} +on('command.run', { command: 'triage' }, async ($, e) => { + const r = await $.model.complete({ + model: 'haiku', + prompt: [ + // Identical on every call, so it forms the cached prefix + { text: RULES, cache: true }, + // Changes on every call, so it goes after the breakpoint + { text: e.args }, + ], + }) + return { text: 'Label: ' + (r.isAnswered ? r.text.trim() : 'unknown') } +}) +``` + +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`](https://code.claude.com/docs/en/prompt-caching#choose-the-ttl-yourself) to `1h`. +* **Breakpoints per request**: the API accepts [up to four](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#when-to-use-multiple-breakpoints), 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](https://code.claude.com/docs/en/amazon-bedrock.md), [Claude Platform on AWS](https://code.claude.com/docs/en/claude-platform-on-aws.md), [Google Cloud's Agent Platform](https://code.claude.com/docs/en/google-vertex-ai.md), [Microsoft Foundry](https://code.claude.com/docs/en/microsoft-foundry.md), or an [LLM gateway](https://code.claude.com/docs/en/llm-gateway)**: use `prompt`. Claude Code begins the system prompt with an [attribution block](https://code.claude.com/docs/en/llm-gateway-protocol.md#system-prompt-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](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#tracking-cache-performance). `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`](#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: + +* **The prefix is too short**: the API doesn't cache a prefix under the model's [minimum length](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#cache-limitations), and it returns no error +* **Prompt caching is disabled**: when a [`DISABLE_PROMPT_CACHING` variable](https://code.claude.com/docs/en/prompt-caching#disable-prompt-caching) applies to the model, Claude Code removes the breakpoints and sends the text uncached +* **Your gateway strips `cache_control`**: a gateway can [remove the field and still return success](https://code.claude.com/docs/en/prompt-caching.md#where-the-cache-lives) +* **Another mod rewrites the start of the text**: Claude Code then [sends it without breakpoints](#what-a-model-complete-hook-receives) + +### What a `model.complete` hook receives + +If you hook the [`model.complete`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-calls) 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](#use-prompt-caching). 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](https://code.claude.com/docs/en/plugins/mods/reference.md#limits) 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: + +```javascript theme={null} +on('session.start', async ($, e, next) => { + // Call the function every 60,000 milliseconds, starting one minute from now + $.clock.every(60_000, async () => { + const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state']) + // Replace the line under the prompt with the latest summary + $.ui.status('checks: ' + summarize(status.stdout)) + }) + // Return without waiting for the timer, so the session starts right away + return next(e) +}) +``` + +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](https://code.claude.com/docs/en/plugins/mods/troubleshoot.md#read-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: + +| Call | What the user sees | +| :- | :- | +| `$.ui.status(text)` | One line under the prompt that stays until you change it. It starts with `⚠` and the mod's name, as in `⚠ my-mod: checks: 3 passing`. | +| `$.ui.toast(text)` | A toast notification with the mod's name that disappears after a few seconds. It's a box at the top right in [fullscreen rendering](https://code.claude.com/docs/en/fullscreen.md), and one line at the right under the prompt in the classic renderer. | +| `$.ui.log(text)` | A dim line in the transcript that Claude doesn't read. It starts with `●` and the mod's name, as in `● my-mod: build finished`. | +| `$.ui.notify(text)` | A notification from their terminal, outside Claude Code, sent the way their [`preferredNotifChannel`](https://code.claude.com/docs/en/settings-reference#preferrednotifchannel) setting says. The mod's name is its title unless you pass a non-empty `{ title }`, which is shown as you give it. | + +`$.ui.notify` is the only one of these that answers. It resolves to `{ isSent: true, channel }` when the user's channel wrote the notification to their terminal. `channel` is the setting's value that sent it, such as `kitty` or `terminal_bell`. The `terminal_bell` channel rings the bell and shows neither your text nor the title. + +Otherwise the call resolves to `{ isSent: false, reason }`: + +| `reason` | Meaning | +| :- | :- | +| `disabled` | The user set `preferredNotifChannel` to `notifications_disabled` | +| `no-channel` | The setting is `auto` and their terminal has no notification Claude Code can send | +| `no-surface` | The session has no terminal, as in a `-p` run, the Agent SDK, the desktop app, the VS Code extension, or a cloud session. Also the answer to a call made before the session starts, while your mod is loading, and to a call made while Claude Code is exiting | +| `refused` | Another mod's hook on `ui.notify` answered in Claude Code's place without sending | + +The call rejects instead of resolving when `text` isn't a string, when another mod's hook on `ui.notify` returns `{ deny }`, or when writing to the terminal fails. + +Your mod can't choose the channel, and Claude Code doesn't limit how often a mod notifies. The user's setting is the only switch. + +When the call reaches a running session, the user's [`Notification` hooks](https://code.claude.com/docs/en/hooks#notification) run with the type `plugin_notification`. They run even when the answer is `disabled`, and in a session with no terminal, where the answer is `no-surface`. No hook runs for a call made before the session starts or while Claude Code is exiting. The hook your mod called `$.ui.notify` from is left out of that run, so a `classic.Notification` hook that notifies isn't called again for its own notification. + +A terminal doesn't report whether it showed the notification, so `isSent: true` means it was sent, not that the user saw it. + +This example notifies when the main conversation's turn ends without an interruption, and falls back to a toast when no notification with text went out. It doesn't wait for the notification before passing the event on, because the user's `Notification` hooks run first and can take a while: + +```javascript theme={null} +on('turn.complete', ($, e, next) => { + const isMainTurn = e.agentId === undefined && !e.isAborted + const fallBack = () => $.ui.toast('Turn finished') + if (isMainTurn) { + $.ui.notify('Turn finished').then(sent => { + const hasText = sent.isSent && sent.channel !== 'terminal_bell' + if (!hasText) fallBack() + }, fallBack) + } + return next(e) +}) +``` + +In the [VS Code extension](https://code.claude.com/docs/en/plugins/mods/interface.md#draw-in-the-vs-code-extension), status lines and toasts show above the prompt box instead, and log lines in the conversation. + +### 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`](https://code.claude.com/docs/en/plugins/mods/reference#the-hook-function) 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. + +### Calls and background work after the user interrupts a turn + +The reason `the session was stopped and nobody has sent input since` in a result or error means the user stopped a running turn, such as with Esc or **Stop**. After that interruption, calls that start new work in the session are refused until the user's next input, such as a message at the prompt. + +Apart from `$.prompt.submit` and `$.command.run`, this handling of calls and background work after an interruption requires Claude Code v2.1.304 or later. + +#### Retry a refused or dropped call + +Make a refused or dropped call again from a [`turn.complete`](https://code.claude.com/docs/en/plugins/mods/events#follow-a-turn) hook. Calls stay refused until the user's next input, and nothing queues or repeats them for you. + +When a call's result or error contains one of these strings, keep its arguments for a later retry: + +* **`the session was stopped and nobody has sent input since`**: the call was refused +* **`was removed from the queue before it ran`**: the prompt or command left the queue without running and was dropped, for example when the user stopped the turn + +In this example, call `submitPrompt` from your background job in place of [`$.prompt.submit`](#start-a-turn-from-a-background-job). Put the `pendingText` variable and the `submitPrompt` function above [`register`](https://code.claude.com/docs/en/plugins/mods/reference#files), and the `turn.complete` hook inside it, because [validation](https://code.claude.com/docs/en/plugins/mods/create.md#check-what-claude-code-reads-from-your-mod) refuses `$` passed to a function declared inside `register`. When the result or the error carries one of those strings, `submitPrompt` keeps the text in `pendingText` for the `turn.complete` hook to submit again: + +```javascript theme={null} +// The text of the last refused or dropped prompt, or null +let pendingText = null + +async function submitPrompt($, text) { + let reason + try { + const result = await $.prompt.submit({ text }) + reason = result?.drop ?? '' + } catch (error) { + reason = String(error?.message ?? error) + } + if (reason.includes('the session was stopped and nobody has sent input since') || + reason.includes('was removed from the queue before it ran')) { + pendingText = text + } + // Any other result or error isn't retried +} + +on('turn.complete', async ($, e, next) => { + if (pendingText !== null) { + const text = pendingText + // Empty pendingText before retrying, so that a second run of this hook + // doesn't submit the same text. + pendingText = null + // Not awaited: the hook would stay open until the new turn starts + submitPrompt($, text) + } + return next(e) +}) +``` + +Before the user's next input, `turn.complete` can fire for the interrupted turn itself and for a subagent's turn, and calls made then are refused again. In that case `submitPrompt` keeps the text for a later `turn.complete`. + +For a call that rejects, such as `$.command.run`, catch the error and test its message the way `submitPrompt` tests `reason`. + +#### What refused calls return + +In the results, `` stands for `the session was stopped and nobody has sent input since`: + +| Some refused calls | Result until the user's next input | +| :- | :- | +| `$.prompt.submit` | Resolves with `{ drop: '' }` and queues no prompt | +| `$.command.run` | Rejects with an error whose message ends with `` | +| [`$.agent.spawn`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods) | Resolves with `{ deny: '' }` and starts no subagent | +| `$.session.send` to a subagent that finished | Resolves with `{ isDelivered: false, reason: 'Agent "" was not resumed: .' }`, where `` is the subagent's id. A subagent that's still running receives the message. | +| `$.session.send` to an [agent team](https://code.claude.com/docs/en/agent-teams.md) teammate | Resolves with `{ isDelivered: false, reason: 'Teammate "" was not messaged: .' }`, where `` is the teammate's name | +| [`$.tool.call`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods) of Bash with `run_in_background: true`, Monitor, or CronCreate | Resolves with `{ deny: ' was not run: it starts background work, a schedule or a goal, and ' }`, where `` is the tool's name | + +Along with these `$.session.send` and `$.tool.call` results, the user can see the [`Stopped:` toast](https://code.claude.com/docs/en/plugins/mods/troubleshoot#stopped-type-a-message-to-continue). + +#### Work your mod started before the interruption + +After the user stops a running turn, for example with Esc, some work your mod had already started is dropped and some waits for the user's next input. + +| Work | What happens until the user's next input | What to do | +| :- | :- | :- | +| A prompt from `$.prompt.submit` still in the queue | Dropped. The call resolves with `{ drop: 'the prompt was removed from the queue before it ran' }`. | Submit it again as [Retry a refused or dropped call](#retry-a-refused-or-dropped-call) shows | +| A command from `$.command.run` still in the queue | Dropped. The call rejects with an error whose message includes `the command was removed from the queue before it ran`. | Run it again as [Retry a refused or dropped call](#retry-a-refused-or-dropped-call) shows | +| A one-time job scheduled with a tool such as CronCreate that comes due before the user's next input | Starts no turn then, and runs after that input | Nothing needed | +| A recurring job scheduled with a tool such as CronCreate that comes due before the user's next input | Starts no turn then. The job loses that run and keeps its schedule. | Nothing needed | +| An event from a monitor your mod started | The monitor keeps running. An event the monitor raises before the user's next input is dropped, except the notice that the monitor ended, which arrives after that input. | Nothing can be done | +| A subagent your mod spawned and later resumed with `$.session.send` that finishes before the user's next input | Its finish notice waits, and reaches the conversation after that input | Nothing needed | + +When a scheduled job comes due or a monitor raises an event before the user's next input, the user can see the [`Stopped:` toast](https://code.claude.com/docs/en/plugins/mods/troubleshoot#stopped-type-a-message-to-continue). + +## 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](https://code.claude.com/docs/en/agent-teams.md). 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](#add-a-command), by asking the session whose id you type after it for a status: + +```javascript theme={null} +on('command.run', { command: 'ping' }, async ($, e) => { + // e.args is the session id typed after /ping + const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' }) + // The call resolves either way, so check isDelivered to learn what happened + if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason) + // An empty result prints nothing in this session's transcript + return {} +}) +``` + +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: + +| Event | Fires when | Useful fields | +| :- | :- | :- | +| `session.receive` | A message arrives for this session, before Claude reads it | `e.text`, and `e.origin.kind`, such as `peer` or `peer-send-message` for another session or agent, `task-notification`, or `scheduled-trigger`. Return `{ consumed: reason }` to keep it from Claude. | +| `session.send` | A message is about to leave, from the SendMessage tool or a mod | `e.to`, `e.text`, and `e.origin.kind`, which is `model` or `plugin` | + +A session set to [refuse inbound messages](https://code.claude.com/docs/en/cross-session-messaging.md#control-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: + +| Namespace | What it does | +| :- | :- | +| `$.fs` | `read(path)`, `write(path, text)`, `exists(path)`, `stat(path)`, and `list(path)` work on files and directories | +| `$.process` | `run(['git', 'status'])` starts a command and resolves when it exits. `spawn` streams a long-running command's output. | +| `$.http` | `fetch(url, init)` over `http` or `https`. It resolves to `{ status, ok, headers, text }` once the body is read. | +| `$.store` | A JSON key-value store of your plugin's own, kept between sessions | +| `$.env` | `get` and `set` environment variables. Write the name as a string literal. | +| `$.settings` | `read` what the settings files and managed policy hold | +| `$.session` | `messages()` returns the transcript as a list of `{ role, text, toolUses }`. Also the working directory, model, and more. [`usage()`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods) returns context window use and plan limits. | +| `$.mcp` | `call` a tool on a connected MCP server | + +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](https://code.claude.com/docs/en/plugins/mods/reference.md#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](https://code.claude.com/docs/en/plugins/mods/events.md#the-order-mods-run-in) 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 + +* [React to events](https://code.claude.com/docs/en/plugins/mods/events.md): hook tool calls, prompts, and turns +* [Draw in the interface](https://code.claude.com/docs/en/plugins/mods/interface.md): show what your mod collects in a pane or above the prompt +* [Test a mod](https://code.claude.com/docs/en/plugins/mods/test.md): stub any of these calls in a test +* [Mods reference](https://code.claude.com/docs/en/plugins/mods/reference.md): events, mods API methods, and limits diff --git a/docs/mods/official/create.md b/docs/mods/official/create.md new file mode 100644 index 00000000..7aa10d4d --- /dev/null +++ b/docs/mods/official/create.md @@ -0,0 +1,366 @@ +> ## Documentation Index +> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt +> Use this file to discover all available pages before exploring further. + +# Create a mod + +> Have Claude write a Claude Code mod from a description, or write one yourself that counts tool calls and adds a command. Learn the reload and validate loop. + +A mod is a Claude Code [plugin](https://code.claude.com/docs/en/plugins/overview.md) with an entry file, called the hooks module: a JavaScript or TypeScript file whose functions Claude Code calls when events happen. To make one: + +* **Ask Claude to write it**: [describe what you want](#ask-claude-for-a-mod) in a Claude Code session +* **Write it yourself**: [follow the tutorial](#write-a-mod-yourself) to learn how a mod's code works. You don't need Node.js, a bundler, or a build step, because Claude Code loads `.js` and `.ts` files directly. + +If you haven't decided whether a mod is the right tool, read the [comparison on the overview](https://code.claude.com/docs/en/plugins/mods/overview.md#compare-mods-settings-hooks-skills-and-mcp-servers) first. + +> [!NOTE] +> +> Use Claude Code v2.1.287 or later. In your shell, run `claude --version` to check. To see whether mods can load for you, see [Check whether mods can load](https://code.claude.com/docs/en/plugins/mods/troubleshoot.md#check-whether-mods-can-load). + +## Ask Claude for a mod + +Describe the mod you want in an interactive Claude Code session, and Claude writes it. Claude works from a built-in [skill](https://code.claude.com/docs/en/skills.md) named `plugin-authoring`, which tells it where to write the mod, which events and methods your version has, and how the mod gets loaded. Claude can load the skill when you ask for a mod, or you can load it yourself by running `/plugin-authoring` at the Claude Code prompt. + +The mod runs once you approve it, except in [sessions where a mod Claude writes can't load](#sessions-that-skip-the-approval). + +**1. Describe the mod** + +Ask for the mod in your own words, for example `make a mod that shows the current git branch above the prompt`. Claude writes the mod in a directory of its own in the session's mods folder, which is `~/.claude/dev-mods/` followed by the session's ID. A mod's full path looks like `~/.claude/dev-mods/3f2a9c1e-5b7d-4e8a-9c21-6d0f4b8a7e13/git-branch/`. + +> [!NOTE] +> +> In the `default` and `acceptEdits` [permission modes](https://code.claude.com/docs/en/permission-modes.md), Claude Code asks you to approve each of the mod's files before Claude creates it, because the mods folder is a [protected path](https://code.claude.com/docs/en/permission-modes.md#protected-paths). Approve each file as it comes up. + +**2. Approve the mod** + +When Claude saves the first file, Claude Code asks whether to enable hot reloading for the session. Hot reloading runs the mods Claude writes in this session and picks up each later change. + +Choose one of these answers: + +* **Enable for this session**: the mods in the session's mods folder load when the turn ends, and reload at the end of each turn that changes them. Your answer lasts for the session, including after you resume it. +* **Not now**: nothing loads for now. The files stay where Claude wrote them, and the mods load the next time that session starts. To keep a mod from ever loading, delete its directory. + +**3. Check that the mod loaded** + +Run `/plugin` at the Claude Code prompt and press Tab until the **Installed** tab is selected. It lists the mod, and you can turn it off there. + +**4. Try the mod** + +Use what you asked for. For the example prompt, the current branch name appears above the prompt box. If the mod doesn't do what you wanted, tell Claude what to change. The mod reloads at the end of each turn that changes its files, so you can try the change as soon as Claude finishes. + +### Use the mod in other sessions + +A mod Claude wrote loads only in the session that made it, and Claude Code deletes that session's mods folder once it's older than [`cleanupPeriodDays`](https://code.claude.com/docs/en/settings-reference#cleanupperioddays). To keep the mod, copy its directory out of the mods folder to a place of your own, such as `~/mods/git-branch`. Then choose how to load it: + +* **In a session you start**: in your shell, run `claude --plugin-dir ~/mods/git-branch` +* **For other people**: [add it to a marketplace](#share-your-mod) so they can install it + +### Sessions where a mod Claude writes can't load + +A mod Claude writes loads only after you approve it, in a trusted workspace where mods are allowed to run. In these sessions it doesn't load: + +* **Nobody is there to approve**: the session can't show you a prompt, as in a `claude -p` run or [`dontAsk` mode](https://code.claude.com/docs/en/permission-modes) +* **The workspace isn't trusted**: you haven't accepted the trust prompt for the directory +* **Mods are disabled**: you started with `--safe-mode` or `--bare`, you set `disableAllHooks`, or your organization's [managed settings block it](https://code.claude.com/docs/en/plugins/mods/admin.md#choose-how-much-to-allow) + +## Write a mod yourself + +In this tutorial you build a mod named `first-mod` that counts the tool calls Claude makes, shows the count beside the spinner while Claude works, and adds a `/tally` command that prints it. You then read the type declarations Claude Code writes beside your mod and run `claude plugin validate`. Together they show you the events and methods your version offers and what Claude Code reads from your code. + +This recording shows the finished mod. The spinner counts tool calls, `/tally` prints the count, and an edit to the code takes effect while the session runs: + +