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:
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: theagent and send_message tools (and the older spawn_worker family) are given to every agent automatically. See Tools → Sub-agents for what the agent can do with them.
Per-agent settings
Each agent carries an optionalsubagents block on its config, saved verbatim to agents.json:
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.
Desktop has no Sub-agents card yet:
delegation, allowedTypes, allowedModels, maxDepth and modelAliases are set through the Management API (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.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:
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: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:
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.
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:
- 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).
<dataDir> as a placeholder — for example throughout 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.
Upgrading from an older version? The first time Dash starts, it automatically moves your existing data into
~/.dash — no action needed. See Troubleshooting if anything looks missing.Environment variables
Dash needs no environment variables for normal Desktop use. These are available for advanced and standalone scenarios:
Provider API keys are not read from environment variables when you use Desktop — they live in the encrypted credential store (see Secrets). When running the HQ standalone, you can supply them as environment variables (e.g.
ANTHROPIC_API_KEY); see AI Providers.
For details on which models each provider supports and how routing works, see AI Providers.
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:~/.dash/gateway for its data and creates agents, channels, and credentials through its Management API. The HQ accepts these flags:
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, 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 adash-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 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) 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.
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”.
Plugins
Plugins let you drop extra skills into any Dash agent without touching the config file. The HQ discovers plugins from theplugins/ 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.
For a step-by-step guide to discovering, enabling, trusting, and using plugins, see Plugins. This section is the detailed reference: directory layout, the
config.json schema, hook events, and the provider catalog schema.Directory layout
skills/— one subdirectory per skill, each with aSKILL.mdfile in standard frontmatter format (name,description, body).commands/— flat<name>.mdfiles. Each becomes a/<plugin>:<command>slash command (see below).agents/— flat<name>.mdfiles (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 below). Code execution, so it requirestrusted..mcp.json— extra MCP servers to register with the HQ. Code execution, so it requirestrusted(see below).providers/— flat<provider>.jsonLLM provider catalogs that add selectable models. Credential-bearing, so they requiretrusted(see LLM providers below).bin/— executables prepended to the HQ’sPATH. Code execution, so it requirestrusted.
.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:
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:
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
Aconfig.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:
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
Eachcommands/<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
Eachagents/<name>.md file (Claude Code subagent format) is a sub-agent type an agent can launch with the agent tool. 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 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 ahooks/hooks.json file in the Claude Code hooks format. Hooks run shell commands, so they require "trusted": true; an untrusted plugin’s hooks are skipped.
Supported events:
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.
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 ahookSpecificOutputfield (e.g.{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"..."}}or{"hookSpecificOutput":{"additionalContext":"..."}}). - Exit
2— blocking error. Standard error becomes the block reason.
${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 moreproviders/<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.
Each catalog file is a single JSON object:
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:
off, minimal, low, medium, high, and xhigh:
"off": nullmeans 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". nullon any other level means the model does not accept it.
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
[plugins] loaded 'my-skills' (skills) and the agent’s load_skill tool (see Tools — requires load_skill in the agent’s tools list) can reference any skill from that plugin.
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 rather than loaded into the main session.