> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dashsquad.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> How Dash agents, data, and credentials are configured — all through Desktop, with a standalone HQ option.

Dash is configured through **Desktop**. There are no config files to hand-edit — you deploy agents, connect providers, and set up messaging apps from the desktop app, and Desktop saves everything for you.

This page explains what you can configure, where Dash stores its data, and the few environment variables and CLI flags available when you run the HQ on its own.

## Squad member settings

Each agent is configured in Desktop's **Deploy** wizard (and editable later from the agent's **Configuration** tab). An agent has these settings:

| Setting           | Required | Description                                                                                                                                                                                              |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Model             | yes      | The model the agent uses, in `provider/model` format (e.g. `anthropic/claude-sonnet-4-20250514`). The dropdown is populated live from the providers you've connected. See [AI Providers](/ai-providers). |
| System prompt     | yes      | Instructions sent with every request that shape the agent's behavior.                                                                                                                                    |
| Tools             | no       | Which capabilities the agent can use. See [Tools](/tools) for the full list.                                                                                                                             |
| Max tokens        | no       | Maximum response length in tokens (default: `4096`).                                                                                                                                                     |
| Working directory | no       | Where the agent's file and shell tools operate. Defaults to `~/.dash/workspaces/<agent-id>`.                                                                                                             |
| Fallback models   | no       | Backup models to try if the primary model is unavailable. See [Squad](/agents#fallback-models).                                                                                                          |
| Extended thinking | no       | Let the model reason before responding. See [Extended thinking](/extended-thinking).                                                                                                                     |
| Skills            | no       | Reusable instructions the agent can discover and load. See [Skills](/skills).                                                                                                                            |

Your agents are saved to `~/.dash/gateway/agents.json` and restored automatically when the HQ restarts.

## Sub-agents

Any agent can hand a self-contained task to a **sub-agent** — a child agent with its own context, its own tool set, and its own transcript — and either wait for its report or let it run in the background. This is **on by default**: the `agent` and `send_message` tools (and the older `spawn_worker` family) are given to every agent automatically. See [Tools → Sub-agents](/tools#sub-agents) for what the agent can do with them.

### Per-agent settings

Each agent carries an optional `subagents` block on its config, saved verbatim to `agents.json`:

| Setting         | Default                | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`       | `true`                 | Whether this agent gets the sub-agent tools at all. When unset, the older `swarm.enabled` is read instead; when both are set, `subagents.enabled` wins.                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `delegation`    | derived from the model | `auto` — delegate proactively; `explicit` — only when you ask for it. This is **guidance written into the system prompt, not an enforced limit**: an `explicit` agent still holds the sub-agent tools and can launch a sub-agent whenever it decides to. Unset means it follows the agent's model, as its **provider's** catalog rates it: a model the provider lists as top-tier (`anthropic/claude-opus-…`, `google/gemini-…-pro`, `openai/o3-pro`, …) gets `auto`, everything else gets `explicit` — including the same model reached through an aggregator such as OpenRouter, which rates its own list separately. |
| `allowedTypes`  | *(all)*                | The sub-agent types this agent may launch. An entry that matches no type is reported as a warning, not an error.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `allowedModels` | *(unset)*              | **Extra** models a sub-agent may be given, in addition to the parent's own model and its fallback models — the allowed set is the union of all three. It cannot be used to withhold the parent's model, so setting it to a cheaper model does not stop a sub-agent asking for the parent's expensive one. When unset, a child can use the parent's model and fallbacks.                                                                                                                                                                                                                                                 |
| `maxConcurrent` | `8`                    | Most sub-agents this agent may run **at once**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `maxPerTurn`    | `24`                   | Most sub-agents this agent may launch **in one turn**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `maxRunSeconds` | `1800`                 | Wall-clock limit for a single sub-agent (30 minutes).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `maxDepth`      | `3`                    | How deep sub-agents may nest. A direct child is depth 1; `0` stops this agent spawning at all.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `modelAliases`  | *(none)*               | Short names a sub-agent's `model` may use, as `{"sonnet": "anthropic/claude-sonnet-4-6"}` — what lets an agent file copied from Claude Code, whose frontmatter says `model: sonnet`, resolve. Keys are bare names (a value with a `/` in it is already a model id and is used as-is). No aliases ship by default: which model `sonnet` means depends on the provider you use. An alias that is not in the map is not an error — the sub-agent runs on the parent's model and the tool result says so.                                                                                                                   |

Messages to a running child are capped separately, by `swarm.maxSteersPerWorker` (default `10`); the count spans a child's whole life, resumes included, for as long as the HQ keeps that child in memory. The count is not written to disk, so a child resumed after an HQ restart starts from zero again.

<Note>
  Desktop has no Sub-agents card yet: `delegation`, `allowedTypes`, `allowedModels`, `maxDepth` and `modelAliases` are set through the [Management API](/api-reference) (`PUT /agents/:id`). Its **Configuration → Swarm** card edits the caps and the on/off switch — the switch reads and writes `subagents.enabled`, showing the same state the HQ acts on, so an agent that has neither block appears (correctly) as enabled.
</Note>

A change to either block **takes effect on the agent's next message** — the HQ rebuilds the agent so the new limits, delegation mode, and enabled state apply to the next turn.

### HQ defaults

The HQ enforces a **global** ceiling across every agent and supplies the per-agent defaults above. Each can be overridden with an environment variable set where the HQ runs:

| Setting                         | Default | Environment variable                  | Description                                                                                                                           |
| ------------------------------- | ------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `maxConcurrentWorkersGlobal`    | `16`    | `SUBAGENTS_MAX_CONCURRENT_GLOBAL`     | Hard ceiling on sub-agents running **at once across all agents** on this HQ. A launch that would exceed it fails until others finish. |
| `defaults.maxConcurrentWorkers` | `8`     | `SUBAGENTS_MAX_CONCURRENT`            | Per-agent default used when an agent's own `maxConcurrent` is unset.                                                                  |
| `defaults.maxWorkersPerRun`     | `24`    | `SUBAGENTS_MAX_PER_TURN`              | Per-agent default for `maxPerTurn`.                                                                                                   |
| `defaults.maxRunSeconds`        | `1800`  | `SUBAGENTS_MAX_RUN_SECONDS`           | Per-agent default for `maxRunSeconds`.                                                                                                |
| `defaults.maxSteersPerWorker`   | `10`    | `SWARM_DEFAULT_MAX_STEERS_PER_WORKER` | Per-agent default for messages per child. This one has no `SUBAGENTS_` name.                                                          |

The older names — `SWARM_MAX_CONCURRENT_WORKERS_GLOBAL`, `SWARM_DEFAULT_MAX_CONCURRENT_WORKERS`, `SWARM_DEFAULT_MAX_WORKERS_PER_RUN`, `SWARM_DEFAULT_MAX_RUN_SECONDS` — are still read for one release. When both are set, the `SUBAGENTS_` value wins. There is **no environment variable for nesting depth**; set `subagents.maxDepth` per agent.

Values must be **positive whole numbers** — anything else is ignored with a warning in the HQ log, and the built-in default is used instead. When Desktop manages the HQ for you, set the variable in the environment you launch Desktop from — the HQ inherits it. Restart the HQ (or Desktop) to apply a change.

A sub-agent is a conversation of its own, so its transcript sits beside your other conversations on disk, under `~/.dash/gateway/sessions/<agent-name>/<subagent-id>/`. Transcripts are never auto-deleted. (Older `.swarm/` directories from previous releases are no longer written to, and are left alone — delete them by hand if you want the space.)

### Sub-agent types

A **type** is a markdown file with YAML frontmatter, in Claude Code's subagent format — the frontmatter configures the agent, the body is its system prompt:

```markdown theme={null}
---
name: code-reviewer
description: Reviews a diff for correctness bugs. Use after implementing a task.
tools: read, grep, find, bash
disallowedTools: write, edit
model: inherit
skills:
  - dash-conventions
maxTurns: 40
background: false
isolation: worktree
---
You are a senior reviewer...
```

`name` and `description` are required; `name` must match `^[a-z0-9][a-z0-9-]*$` — lowercase letters, digits and hyphens only. `tools` is a comma-separated list of Dash tool names, MCP patterns (`mcp__<server>`, `mcp__<server>__<tool>`, `mcp__*`), or `agent(<type>, …)` to limit which types this agent may launch in turn; omit it to inherit the parent's grant **after** the skill-management, connector-management and Projects tools, and the four legacy swarm tools, have been dropped — those are parent-only however the agent is configured, so "everything the parent has" never includes them. `disallowedTools` is subtracted first. `model` takes a `provider/model` id or `inherit`. `skills` names skills to preload into the prompt. `maxTurns` stops the agent once it has made more than that many tool calls and reports what it has so far (it can be resumed) — the call that trips the limit still runs to completion, so the agent stops after `maxTurns + 1` calls rather than before. `background: true` **forces** this type to detach: a caller passing `run_in_background: false` cannot bring it back into the foreground, though a caller can always add background to a type that does not set it. `isolation: worktree` gives the agent its own git checkout of the workspace, cut from the workspace's current commit. Claude Code keys Dash does not act on — `permissionMode`, `hooks`, `mcpServers`, `memory`, `effort`, `color`, `experimental` — are **silently ignored**: nothing is written to the HQ log, so a setting that appears to do nothing is one of these rather than a file that failed to load.

Definitions are read from four places. Where the same name appears twice, the **first** match here wins:

| Order | Where                                                                   | Named             |
| ----- | ----------------------------------------------------------------------- | ----------------- |
| 1     | `<workspace>/.dash/agents/*.md`, then `<workspace>/.claude/agents/*.md` | by its `name`     |
| 2     | `~/.dash/gateway/subagents/<agent-name>/*.md`                           | by its `name`     |
| 3     | A plugin's `agents/*.md`                                                | `<plugin>:<name>` |
| 4     | The built-ins (`general-purpose`, `Explore`, `Plan`)                    | by name           |

Plugin types are always namespaced, so they can never shadow anything; a workspace file whose `name` is `explore` does replace the built-in `Explore`. Because the `name` grammar is lowercase-only, that is how you spell it — a file with `name: Explore` is rejected, not applied.

<Warning>
  **A sub-agent can never hold a tool or an MCP server its parent does not have.** A definition's `tools` list is intersected with the parent's own tools, and its MCP access with the parent's assigned servers. Asking for more does not grant it — and if the intersection comes out empty, the launch fails rather than running a tool-less agent.
</Warning>

The HQ reads these files once and caches them per agent. A file you add or edit by hand is picked up the next time the agent's config is saved, a plugin reloads, or the HQ restarts. The Management API also exposes them: `GET /agents/:id/subagent-types` lists everything an agent can launch (including which definition shadowed which), and `GET`/`PUT`/`DELETE /agents/:id/subagent-definitions/:name` read and write the per-agent directory. A `PUT` validates before it writes, so an unparseable definition is rejected rather than silently ignored.

## Data directory

Everything Dash stores on disk lives under a single root — `~/.dash` by default — organized into clearly named subfolders:

```
~/.dash/
├── gateway/      # HQ data: credentials, agents, sessions, skills, databases
├── desktop/      # Desktop app data: settings, conversations
├── logs/         # mc.log and gateway.log
└── workspaces/   # one folder per agent (where its files live)
```

This is the same location whether you launch Dash through Desktop or run the HQ on its own, so your agents, credentials, and conversations are always in one predictable place. You can open any of these folders in Finder or your editor.

Two of these subfolders are **data directories** in their own right — each is the root a single component reads and writes:

* The **HQ's** data directory is `~/.dash/gateway` (credentials, agents, sessions, skills, databases, and plugins).
* **Desktop's** data directory is `~/.dash/desktop` (app settings and conversations).

Where this page writes `<dataDir>` as a placeholder — for example throughout [Plugins](#plugins) — it always means the **HQ's** data directory, `~/.dash/gateway`, since the HQ is the component that loads those files.

To store everything somewhere else (for example, a separate disk or multiple profiles), set `DASH_HOME` to a directory of your choosing. Running the HQ with `--data-dir <path>` overrides just the HQ's own folder; `MC_DATA_DIR` overrides just Desktop's.

<Note>
  Upgrading from an older version? The first time Dash starts, it automatically moves your existing data into `~/.dash` — no action needed. See [Troubleshooting](/troubleshooting#my-data-moved-after-updating) if anything looks missing.
</Note>

## Environment variables

Dash needs no environment variables for normal Desktop use. These are available for advanced and standalone scenarios:

| Variable                          | Description                                                                                                                         |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `DASH_HOME`                       | Root directory for everything Dash stores on disk (default: `~/.dash`).                                                             |
| `MC_DATA_DIR`                     | Override just the Desktop app's data folder (used by tests and QA).                                                                 |
| `POOL_MAX_SIZE`                   | Maximum number of agent runtimes kept in memory by the HQ (default: `200`).                                                         |
| `SUBAGENTS_MAX_CONCURRENT_GLOBAL` | Ceiling on sub-agents running at once across **all** agents (default: `16`). Was `SWARM_MAX_CONCURRENT_WORKERS_GLOBAL`, still read. |
| `SUBAGENTS_*`, `SWARM_DEFAULT_*`  | Per-agent sub-agent cap defaults — see [HQ defaults](#hq-defaults) for the full list.                                               |
| `NODE_ENV`                        | Set to `production` to quiet the HQ's verbose request logging.                                                                      |

Provider API keys are **not** read from environment variables when you use Desktop — they live in the encrypted credential store (see [Secrets](/secrets)). When running the HQ standalone, you can supply them as environment variables (e.g. `ANTHROPIC_API_KEY`); see [AI Providers](/ai-providers#configuring-credentials).

<Note>
  For details on which models each provider supports and how routing works, see [AI Providers](/ai-providers).
</Note>

## Running the HQ standalone

Desktop spawns and manages the HQ for you. To run it on its own — for a headless server or Docker — start it directly:

```bash theme={null}
npm run gateway
```

It uses `~/.dash/gateway` for its data and creates agents, channels, and credentials through its [Management API](/api-reference). The HQ accepts these flags:

| Flag                       | Description                                                      |
| -------------------------- | ---------------------------------------------------------------- |
| `--management-port <port>` | Port for the Management API (default: `9300`).                   |
| `--channel-port <port>`    | Port for the WebSocket channel server (default: `9200`).         |
| `--token <token>`          | Bearer token required on the Management API.                     |
| `--chat-token <token>`     | Token required on the channel server (`ws://…/ws/chat?token=…`). |
| `--data-dir <path>`        | Override the HQ's data directory (default: `~/.dash/gateway`).   |
| `--verbose`                | Enable debug logging.                                            |

There are no `--config` or `--secrets` flags, and the HQ does not read any JSON config file — agents, channels, and credentials are managed at runtime through the [Management API](/api-reference), which is exactly what Desktop uses.

## Remote HQ runtime

Desktop can also use an HQ that runs somewhere else, such as a VPS.
Open **Settings → General → HQ** to choose where Desktop should connect.

The HQ row shows the active connection. Click **Change HQ** to switch.

**Use this computer** is the default. Desktop starts the HQ on this
machine and talks to it over loopback.

**Connect existing HQ** connects Desktop to an HQ URL you already
have. Enter the management URL, chat URL, management token, chat token, and relay
credential if your relay requires one. Click **Test connection** first. Desktop only enables **Use this HQ** after the HQ answers successfully.
URLs are saved in Desktop's settings. Tokens and relay credentials stay
in the OS keychain.

**Self-host on a VPS** is the advanced path. It connects over SSH, installs Dash
on the server, creates a `dash-gateway` systemd user service, starts the HQ
with your relay settings, tests the deployed HQ, and then saves the resulting
relay endpoint in Desktop. You need a VPS that already has Node.js, npm,
git, and systemd user services available.

## Credentials

Provider API keys and messaging tokens are encrypted at rest. The HQ keeps them in `~/.dash/gateway/credentials.enc` (AES-256-GCM); HQ access tokens live in your OS keychain. See [Secrets](/secrets) for the full picture of how credentials are stored and delivered to agents.

## Speech

Speech-to-text (dictation) and text-to-speech (read aloud) configuration is persisted at
`<dataDir>/speech.json`. Unset fields fall back to the defaults: OpenRouter's
`openai/whisper-large-v3` for transcription, and OpenRouter's `minimax/speech-2.8-turbo`
with the `English_expressive_narrator` voice for speech. Voice ids are model-specific (for
example `English_radiant_girl` for MiniMax, `af_heart` for Kokoro, `en-US-Harper:MAI-Voice-2` for
MAI-Voice-2); an unknown voice is rejected by the provider.

Speech doesn't introduce a new credential type — it reads the same `openrouter` provider API key
your LLM configuration already stores. Add or update that credential and speech becomes available
without any extra setup.

PCM-only models (currently Google's Gemini TTS) are served to Read aloud as WAV; MP3-only models
(MiniMax) are never asked for PCM.

The configuration always reports a `realtime` slot for live back-and-forth conversation, alongside
the transcription and speech providers. Today it's always unavailable — no configured provider
offers realtime speech yet — but its presence in the config and API responses is intentional,
reserved for a future release.

### Voice mode

Hands-free voice mode (see [Dash for iPhone and iPad](/ios#voice-mode)) speaks each sentence as
soon as it's ready, either as a continuous PCM stream or as one MP3 clip per sentence, depending on
the text-to-speech model. `hexgrad/kokoro-82m`, `microsoft/mai-voice-2-flash`, and
`microsoft/mai-voice-2` stream PCM at a fixed sample rate, and Google's Gemini TTS streams PCM
because it's PCM-only; every other model, including the default MiniMax voice, sends one MP3 clip
per sentence.

<Note>
  An HQ upgrade that adds speech and an app upgrade that expects it need to be deployed
  together. An older HQ rejects a client's request to set dictation language to "Auto".
</Note>

## Plugins

Plugins let you drop extra skills into any Dash agent without touching the config file. The HQ discovers plugins from the `plugins/` directory inside the **HQ's data directory** — that is, `~/.dash/gateway/plugins/`. Throughout this section, `<dataDir>` is shorthand for that HQ data directory (`~/.dash/gateway`), not Desktop's `~/.dash/desktop`.

<Note>
  For a step-by-step guide to discovering, enabling, trusting, and using plugins, see [Plugins](/plugins). This section is the detailed reference: directory layout, the `config.json` schema, hook events, and the provider catalog schema.
</Note>

### Directory layout

```
<dataDir>/                  # the HQ's data directory: ~/.dash/gateway
└── plugins/
    ├── config.json
    └── <plugin-name>/
        ├── .claude-plugin/
        │   └── plugin.json          (optional)
        ├── skills/
        │   └── <skill-name>/
        │       └── SKILL.md
        ├── commands/
        │   └── <command-name>.md    (optional)
        ├── agents/
        │   └── <agent-name>.md      (optional)
        ├── hooks/
        │   └── hooks.json           (optional, trusted only)
        ├── .mcp.json                (optional, trusted only)
        ├── providers/
        │   └── <provider>.json      (optional, trusted only)
        └── bin/                     (optional, trusted only)
            └── <executable>
```

Each plugin follows the Claude Code layout:

* **`skills/`** — one subdirectory per skill, each with a `SKILL.md` file in standard frontmatter format (`name`, `description`, body).
* **`commands/`** — flat `<name>.md` files. Each becomes a `/<plugin>:<command>` slash command (see below).
* **`agents/`** — flat `<name>.md` files (Claude Code subagent format). Each becomes a sub-agent type this plugin's agents can launch (see below).
* **`hooks/hooks.json`** — event hooks that run shell commands at key points in a run (see [Hooks](#hooks) below). Code execution, so it requires `trusted`.
* **`.mcp.json`** — extra MCP servers to register with the HQ. Code execution, so it requires `trusted` (see below).
* **`providers/`** — flat `<provider>.json` LLM provider catalogs that add selectable models. Credential-bearing, so they require `trusted` (see [LLM providers](#llm-providers) below).
* **`bin/`** — executables prepended to the HQ's `PATH`. Code execution, so it requires `trusted`.

The `.claude-plugin/plugin.json` manifest is optional; when present it can set `name`, `displayName`, `description`, and `version`. When absent, the plugin's directory name is used as its name.

### Enabling a plugin

Create or edit `<dataDir>/plugins/config.json`:

```json theme={null}
{
  "<plugin-name>": { "enabled": true }
}
```

Plugins not listed in `config.json`, or listed with `"enabled": false`, are ignored.

### Trusting a plugin

`enabled` is enough for markdown components — `skills/` and `commands/`, which never run code. Components that **execute code or handle credentials** — `hooks/hooks.json`, `.mcp.json` servers, `bin/` executables, and `providers/` LLM catalogs — additionally require `"trusted": true`:

```json theme={null}
{
  "<plugin-name>": { "enabled": true, "trusted": true }
}
```

An enabled-but-untrusted plugin still contributes its skills and commands, but its hooks, MCP servers, `bin/` directory, and provider catalogs are skipped. Only set `trusted` for plugins whose code you have reviewed and trust to run inside the HQ process.

### Loading a plugin from a path

A `config.json` entry can point at a plugin directory anywhere on disk with a `path` field, instead of requiring the plugin to live under `<dataDir>/plugins/`. This is handy for local development — you can keep a plugin in your own working tree and load it without copying it in:

```json theme={null}
{
  "my-dev-plugin": { "path": "/abs/or/relative/dir", "trusted": true }
}
```

A `path` entry is **enabled automatically** — you do not need `"enabled": true`. It still requires `"trusted": true` to run code components (hooks, MCP servers, `bin/` executables, and provider catalogs); skills and commands load from `enabled` alone, just like any other plugin.

### Commands

Each `commands/<name>.md` file becomes a slash command named `/<plugin>:<name>`. For example, a plugin called `my-skills` with `commands/triage.md` exposes `/my-skills:triage`. The command body is the markdown content; optional frontmatter (`name`, `description`) overrides the command name and supplies a description. The slash name is `<plugin>:<frontmatter-name>` when `name:` frontmatter is present, otherwise `<plugin>:<filename-without-.md>`. Commands need only `enabled`.

### Plugin sub-agent types

Each `agents/<name>.md` file (Claude Code subagent format) is a **sub-agent type** an agent can launch with the [`agent` tool](/tools#sub-agents). A plugin called `my-skills` with `agents/reviewer.md` contributes the type `my-skills:reviewer`; the namespaced name is `<plugin>:<frontmatter-name>` when `name:` frontmatter is present, otherwise `<plugin>:<filename-without-.md>`. Like commands, these are markdown — they need only `enabled`, not `trusted`.

**This changed.** A plugin's `agents/*.md` used to be a *loadable specialist*: it ran in the main conversation, was invoked as `/<plugin>:<agent>` or through `load_skill`, and its `tools` frontmatter was advisory. It is now a **spawnable type instead** — it runs in its own context with its own transcript, its `tools` and `model` frontmatter are **enforced**, and it is no longer a slash command or loadable through `load_skill`. Enforced still means bounded: a sub-agent never receives a tool or MCP server its parent lacks. See [Sub-agent types](#sub-agent-types) for the full file format and where else definitions can live.

### MCP servers and executables

A trusted plugin's `.mcp.json` is read at startup and its servers are registered with the HQ's MCP manager and persisted alongside your other MCP servers. Registration is fail-isolated: a server that fails to start is logged and skipped without affecting the rest of the HQ. A trusted plugin's `bin/` directory is prepended to the HQ's `PATH`, so its executables resolve ahead of system binaries.

### Hooks

A trusted plugin can register **hooks** — shell commands that run at key points in an agent run — via a `hooks/hooks.json` file in the [Claude Code hooks format](https://docs.claude.com/en/docs/claude-code/hooks). Hooks run shell commands, so they require `"trusted": true`; an untrusted plugin's hooks are skipped.

Supported events:

| Event              | When it fires                                                                                    | What it can do                                      |
| ------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
| `SessionStart`     | Before the model runs, at the start of a run                                                     | Add context to the system prompt                    |
| `UserPromptSubmit` | After an inbound channel message passes routing, before the agent runs                           | Block the message, or prepend context to the prompt |
| `PreToolUse`       | Before a tool executes (matched by tool name)                                                    | Allow or deny the tool call                         |
| `PostToolUse`      | After a tool executes (matched by tool name)                                                     | Block the result, or append context                 |
| `Stop`             | After the run completes                                                                          | Cleanup (its output is logged, not injected)        |
| `SubagentStart`    | When a [sub-agent](/tools#sub-agents) starts (matched by its **role**, see below)                | Observe launches — log, notify, audit               |
| `SubagentStop`     | When a sub-agent finishes, fails, is cancelled, or hits its turn limit (matched by its **role**) | Observe results — log, notify, audit                |

A sub-agent's **role** — what these two matchers match on — is the `name` the `agent` call passed, and the **type name** (`general-purpose`, `Explore`, `Plan`, `my-plugin:reviewer`) when no `name` was given. On the legacy `spawn_worker` path it is the `role` argument. It is never the `description`.

Each event maps to an array of matcher groups; each group has an optional `matcher` (a tool name, a `|`-separated list, or a regex — omit or use `*` to match everything) and a `hooks` array of `{ "type": "command", "command": "...", "timeout": <seconds> }` entries.

```json theme={null}
{
  "PreToolUse": [
    {
      "matcher": "bash",
      "hooks": [
        { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/guard.sh" }
      ]
    }
  ],
  "UserPromptSubmit": [
    {
      "hooks": [
        { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/inject-context.sh" }
      ]
    }
  ]
}
```

Hook commands receive the Claude Code event payload (`tool_name`, `tool_input`, `prompt`, `session_id`, `cwd`, …) as JSON on **stdin**, and communicate back through their exit code and stdout:

* **Exit `0`** — allowed. To deny, add context, or replace tool input, print a JSON object with a `hookSpecificOutput` field (e.g. `{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}` or `{"hookSpecificOutput":{"additionalContext":"..."}}`).
* **Exit `2`** — blocking error. Standard error becomes the block reason.

The string `${CLAUDE_PLUGIN_ROOT}` in a command is substituted with the plugin's absolute directory, so hooks can reference scripts bundled with the plugin. `${CLAUDE_PROJECT_DIR}` and `${CLAUDE_PLUGIN_DATA}` (a per-plugin scratch directory) are also available.

**Fail-open by design.** A hook that errors, times out, exits non-zero (other than `2`), or prints malformed JSON is ignored — the run proceeds as if the hook had allowed it. Hooks never crash the HQ.

**Where each event fires.** `SessionStart`, `PreToolUse`, `PostToolUse`, and `Stop` fire on **every** agent run, whether the run was reached through Desktop chat or an inbound channel. `UserPromptSubmit` fires **only on the channel path** (Telegram, WhatsApp); it does not run for Desktop chat messages. `SubagentStart` and `SubagentStop` fire around every sub-agent's lifecycle, however it was launched; their stdin payload carries `worker_id`, `role`, and (on stop) the terminal `status` (`done`, `failed`, `cancelled`, or `max_turns`). They are observe-only — a Subagent hook cannot block a sub-agent, and its output is logged, not injected. A sub-agent that an HQ restart interrupted is marked terminal at boot without firing these hooks.

### LLM providers

A trusted plugin can add a whole **LLM provider** — a base URL, a wire protocol, and a list of models — through one or more `providers/<provider>.json` catalog files. Once loaded, its models appear in Desktop's model dropdown as `<id>/<model-id>` and any agent can be pointed at them, exactly like the built-in providers.

<Warning>
  A provider catalog requires `"trusted": true`. A provider sees **every prompt** you send to its models, **receives your API key**, and **chooses the network endpoint** requests go to. Only add provider plugins you trust with all three.
</Warning>

Each catalog file is a single JSON object:

```json theme={null}
{
  "id": "myllm",
  "label": "My LLM",
  "credentialPrefix": "myllm-api-key",
  "baseUrl": "https://api.myllm.example/v1",
  "api": "openai-completions",
  "models": [
    {
      "id": "my-fast",
      "name": "My Fast",
      "contextWindow": 128000,
      "maxTokens": 8192,
      "reasoning": false,
      "cost": { "input": 0.5, "output": 1.5, "cacheRead": 0, "cacheWrite": 0 }
    }
  ]
}
```

| Field                  | Required | Description                                                                                                                                                             |
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                   | yes      | Provider id and the `<id>/<model>` prefix. Also the credential lookup name.                                                                                             |
| `label`                | yes      | Human-readable provider name.                                                                                                                                           |
| `baseUrl`              | yes      | API base URL the HQ calls.                                                                                                                                              |
| `api`                  | yes      | Wire protocol: `openai-completions` or `anthropic-messages`.                                                                                                            |
| `credentialPrefix`     | yes      | Prefix the API key is stored under (e.g. `myllm-api-key`).                                                                                                              |
| `models[]`             | yes      | Statically-known models. Each needs `id`, `contextWindow`, `maxTokens`; `name`, `reasoning`, `thinkingLevelMap`, `cost`, `input`, `headers`, and `compat` are optional. |
| `dynamicModels`        | no       | OpenRouter-style "accept any model id" — see below.                                                                                                                     |
| `dynamicModelDefaults` | no       | `{ "contextWindow", "maxTokens" }` applied to dynamically-accepted ids.                                                                                                 |
| `placeholderKey`       | no       | Stand-in key for keyless local servers — see below.                                                                                                                     |

**Reasoning models.** Some models refuse a request that does not ask for
reasoning, and others want a specific value. Add `thinkingLevelMap` to those
models so the HQ sends something they accept:

```json theme={null}
{
  "id": "my-reasoner",
  "contextWindow": 272000,
  "maxTokens": 128000,
  "reasoning": true,
  "thinkingLevelMap": { "off": null, "high": "HIGH" }
}
```

The keys are `off`, `minimal`, `low`, `medium`, `high`, and `xhigh`:

* `"off": null` means the model **requires** reasoning. The HQ then picks
  the lowest level it does support instead of turning reasoning off.
* `"off": "none"` means reasoning can be turned off, but the provider wants to
  be told so explicitly (the GPT-5 family works this way).
* Any other string remaps that level to the value the provider expects, e.g.
  `"high": "HIGH"`.
* `null` on any other level means the model does not accept it.

Leave `thinkingLevelMap` out if the model is happy either way. If you see an
error complaining about a missing or invalid reasoning/`reasoning_effort` value,
this field is what to set.

**Storing the API key.** Save it in the credential store under `<id>-api-key:<name>`, for example `myllm-api-key:default`. The simplest way is Desktop's **Settings → AI Providers** screen, which writes that key for you; you can also set it directly with the HQ's credentials API. The first key stored for a provider id wins.

**Accept-any-model providers.** Set `"dynamicModels": true` together with `"dynamicModelDefaults": { "contextWindow": ..., "maxTokens": ... }` to allow any model id the provider supports — useful for aggregators like OpenRouter where the catalog can't list everything. Listed `models` still appear in the dropdown; an unlisted id is accepted on demand and sized from the defaults. (Without `dynamicModelDefaults`, an unlisted id is rejected — the HQ can't size it.)

**Keyless local servers.** For a local server that needs no real key (such as Ollama), set `"placeholderKey": "ollama"` (any non-empty string). The HQ uses it whenever no real key is stored, so the provider authenticates out of the box.

If you already run an OpenAI- or Anthropic-compatible endpoint (OpenRouter, OpenClaw, a self-hosted gateway, …), porting it is just filling in this catalog — `baseUrl`, `api`, and the model list.

### Example

```bash theme={null}
# Create the plugin directory and a skill
mkdir -p /path/to/data/plugins/my-skills/skills/summarize

cat > /path/to/data/plugins/my-skills/skills/summarize/SKILL.md <<'EOF'
---
name: summarize
description: Summarize any text concisely
---
Summarize the provided text in three sentences or fewer.
EOF

# Enable the plugin
echo '{ "my-skills": { "enabled": true } }' \
  > /path/to/data/plugins/config.json
```

When the HQ starts, it logs `[plugins] loaded 'my-skills' (skills)` and the agent's `load_skill` tool (see [Tools](/tools) — requires `load_skill` in the agent's tools list) can reference any skill from that plugin.

<Note>
  Skills, commands, sub-agent types, hooks, MCP servers, `bin/` executables, and LLM providers are active. A plugin's `agents/*.md` is a sub-agent type (see above), launched with the [`agent` tool](/tools#sub-agents) rather than loaded into the main session.
</Note>
