OpenClaw
Status (chat UI path): ✅ Verified on OpenClaw
2026.4.18with helmdeck1b91f6c. Gateway chat UI athttp://localhost:18789sees the full 52-pack catalog (prefixedhelmdeck__) and SSE handshake succeeds. Status (CLI path): ⚠️ Regressed on OpenClaw≥ 2026.4.18.openclaw agent --agent main …does not load bundled MCP tools — only 24 built-in tools appear, nohelmdeck__*. Suspect upstream commit:0e7a992d(fix(agents): filter bundled tools through final policy). Use the chat UI for end-to-end agent runs until upstream fixes the CLI path. Last full end-to-end green: 2026-04-10 with OpenClaw2026.4.10+ helmdeckv0.6.0.Validation history: Originally validated via
scripts/validate-openclaw.sh— 9 packs tested through OpenClaw → SSE MCP → helmdeck round trip withopenrouter/autoas the LLM. Packs validated:http.fetch,browser.screenshot_url,web.scrape_spa,slides.render,browser.interact,github.list_prs,github.list_issues,github.search,repo.fetch+fs.listchain. Additionally validated via direct REST: full code-edit loop (repo.fetch→fs.write→fs.patch→fs.read→cmd.run→git.commit→repo.push) + all GitHub write packs (create_issue,post_comment,create_release) +python.run+node.run.Upgrading OpenClaw? Follow
openclaw-upgrade-runbook.md— covers thegit pullsequence, JWT / SKILLS.md preservation checks, and the regression triage steps for the CLI-path issue above.
Topology
OpenClaw is Topology A — both OpenClaw and helmdeck run as docker compose stacks on the same host, joined onto a shared bridge network so OpenClaw resolves helmdeck-control-plane by service-name DNS.
┌──── helmdeck_default network ─────────┐
│ helmdeck-control-plane:3000 │
│ ┌────────────────────────────┐ │
│ │ /api/v1/mcp/sse (MCP) │ │
│ │ /v1/chat/completions (LLM) │ │
│ └────────────────────────────┘ │
│ ▲ │
│ │ HTTP, JWT-protected │
│ openclaw-gateway:18789 │
└───────────────────────────────────────┘
Prerequisites
- Docker + docker compose v2
- Helmdeck cloned at
/root/helmdeck(or wherever) - ≥ 4 GB RAM, ≥ 2 CPUs (the install script preflight enforces this)
Tip: Helmdeck is on the official MCP Registry as
io.github.tosin2013/helmdeck. The cross-client registry walkthrough at Register helmdeck with your MCP client covers stdio install for any MCP-aware agent. The OpenClaw integration below is custom because OpenClaw runs as a separate container and uses a SKILL.md file in addition to the MCP bridge — theconfigure-openclaw.shscript handles both.
Setup at a glance
The full first-time wiring is six steps. Sections 1–6 below walk through them; this is the "are we sure we got everything?" cheat-sheet.
| # | Step | Where it lives |
|---|---|---|
| 1 | make install (helmdeck stack up) | scripts/install.sh |
| 2 | git clone https://github.com/openclaw/openclaw.git ~/openclaw + ./scripts/docker/setup.sh (OpenClaw stack up) | OpenClaw repo |
| 3 | Mint a helmdeck JWT for OpenClaw to send as MCP Authorization | curl against /api/v1/auth/login |
| 4 | Authenticate OpenClaw with an LLM provider — interactive, one-time | docker compose -f /root/openclaw/docker-compose.yml run --rm -it openclaw-cli models auth login --provider openrouter (paste API key when prompted; single line, -it required — see note below) |
| 5 | Pin a tool-capable model + wire MCP config + install SKILL.md + JWT refresh + network bridge | scripts/configure-openclaw.sh --model openrouter/<provider>/<model> --seed-identity (helmdeck repo) |
| 6 | Walk the Phase 5.5 code-edit loop in the chat UI to confirm | http://localhost:18789 |
⚠️ Steps 4 and 5 are separate.
configure-openclaw.sh(step 5) sets the model OpenClaw will use and wires every piece of helmdeck integration — but it can't paste an API key for you. Step 4 stores the OpenRouter / Bedrock / etc. API key under~/.openclaw/; step 5 pins which model OpenClaw asks that provider for. Helmdeck'sHELMDECK_OPENROUTER_API_KEYin.env.localis not the same thing — that wires helmdeck's own gateway, not OpenClaw's chat-UI loop. Theconfigure-openclaw.shscript now probes for the missing provider auth and fails fast with the exact command to run if step 4 was skipped.
💡 Copy-paste step 4 as a single line, with
-itto avoid the trailing-whitespace-after-\shell trap and the no-TTY error (Error: models auth login requires an interactive TTY):docker compose -f /root/openclaw/docker-compose.yml run --rm -it openclaw-cli models auth login --provider openrouterVerify auth landed with:
docker compose -f /root/openclaw/docker-compose.yml run --rm -T openclaw-cli models auth listYou should see your provider's profile in the Profiles block (e.g.
openrouter). If it showsProfiles: (none), the auth wizard didn't write — re-run the login command and watch for any TUI errors. The-Ton the verify command suppresses the TUI (read-only listing doesn't need a TTY).⚠️ OpenClaw 2026.5.6 API change: the
loginsubcommand now takes--provider <id>as a flag rather than as a positional argument. Older docs (and the helmdeckvalidate-openclaw.shscript) still show the bare positional form — those are stale and need updates.
1. Install helmdeck
git clone https://github.com/tosin2013/helmdeck.git
cd helmdeck
./scripts/install.sh
The script generates a .env.local with strong random secrets, builds every binary + the React UI, brings the compose stack up, polls /healthz, and prints the admin password. Save it.
2. Install OpenClaw
git clone https://github.com/openclaw/openclaw.git
cd openclaw
OPENCLAW_GATEWAY_BIND=lan ./scripts/docker/setup.sh
This builds the OpenClaw image, runs the onboarding flow, and brings up openclaw-gateway on port 18789. The setup script prints the gateway token at the end — save it.
OpenClaw's Control UI requires HTTPS or localhost (WebCrypto secure-context check). For remote access, the simplest path is an SSH tunnel from your workstation:
ssh -L 18789:localhost:18789 -L 3000:localhost:3000 root@<server>
Then open http://localhost:18789 and http://localhost:3000 in your browser — both are now treated as secure-context localhost.
3. Join the networks
Helmdeck ships an overlay file that merges OpenClaw's compose stack onto helmdeck's bridge network:
docker compose \
-f /root/openclaw/docker-compose.yml \
-f /root/helmdeck/deploy/compose/compose.openclaw-sidecar.yml \
up -d openclaw-gateway
After this, openclaw-gateway can resolve helmdeck-control-plane:3000 via DNS.
4. Configure helmdeck as an MCP server in OpenClaw
Two paths — pick whichever you prefer:
4a. Use the OpenClaw CLI (recommended — schema-validated)
docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
mcp set helmdeck '{"url":"http://helmdeck-control-plane:3000/api/v1/mcp/sse","headers":{"authorization":"Bearer <your-helmdeck-jwt>"}}'
The CLI writes to ~/.openclaw/openclaw.json and validates the shape against OpenClaw's config schema before saving — preferred over hand-editing because the schema occasionally shifts between OpenClaw releases.
4b. Edit ~/.openclaw/openclaw.json directly (advanced)
OpenClaw stores MCP servers at the top level of the config under mcp.servers, keyed by server name (NOT under each agent):
{
"gateway": { "...": "..." },
"agents": { "...": "..." },
"mcp": {
"servers": {
"helmdeck": {
"url": "http://helmdeck-control-plane:3000/api/v1/mcp/sse",
"headers": {
"authorization": "Bearer <your-helmdeck-jwt>"
}
}
}
}
}
Then restart openclaw-gateway to pick up the change:
docker compose -f /root/openclaw/docker-compose.yml restart openclaw-gateway
Mint the JWT
JWT=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<from install.sh>"}' | jq -r .token)
echo "$JWT"
Paste the value into the Authorization header above.
5. Configure OpenClaw's LLM provider
OpenClaw needs its own LLM credentials. The easiest path is OpenRouter (which is also what helmdeck routes to in the validation walkthrough):
docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
models auth login --provider openrouter
Follow the prompts to paste your OpenRouter API key. Then set the active model:
docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
models use openrouter/minimax/minimax-m2.7
Helmdeck-as-LLM-gateway path: OpenClaw's docs do not clearly document a custom OpenAI-compatible base URL escape hatch as of v0.6.0 of helmdeck. If we confirm via inspection of
models.jsonthat an arbitrarybase_urlworks, this section will gain a "Route OpenClaw's LLM through helmdeck" subsection that points OpenClaw athttp://helmdeck-control-plane:3000/v1/chat/completionsso the T607 success-rate panel lights up from OpenClaw runs. Until then, OpenClaw uses its OpenRouter key directly and helmdeck only sees the MCP tool calls.
5b. Confirm the catalog is visible from OpenClaw
Before opening the chat UI, smoke-test the MCP handshake from the CLI side. This proves the JWT works, the authorization header case is right, the network bridge is wired, and the model can see the tool catalog:
docker compose -f /root/openclaw/docker-compose.yml run --rm -T openclaw-cli agent \
--agent main \
--json \
--message "List every MCP tool whose name starts with helmdeck__. Just the names, one per line."
You should see the assistant reply listing the full helmdeck__* catalog — the 57 capability packs (47 without a gateway) plus the 3 async wrappers (helmdeck__pack-start, helmdeck__pack-status, helmdeck__pack-result) and the pipeline MCP tools.
🧩 Naming convention: MCP tool names can't contain dots, so helmdeck's
browser.screenshot_urlbecomeshelmdeck__browser-screenshot_urlover MCP. The mapping is mechanical —<family>.<action>→helmdeck__<family>-<action>. Pack reference pages list both forms.
If the response says "I don't have access to MCP tools" or returns 0 helmdeck tools:
- Check
docker logs helmdeck-control-plane | grep mcp/sse— should show recentGET /api/v1/mcp/sseentries. - Check the JWT in OpenClaw's MCP config didn't expire (default 7-day window from
configure-openclaw.sh); rotate with./scripts/configure-openclaw.sh --rotate-jwt. - Confirm the lowercase
authorizationheader survived any manual edits to~/.openclaw/openclaw.json(issue #1 workaround — Pascal-casedAuthorization401's against OpenClaw 2026.4+). - Confirm the network bridge is in place:
docker exec openclaw-openclaw-gateway-1 getent hosts helmdeck-control-planeshould print an IP. If it fails withbad address, thebaas-netnetwork attachment is missing — see "Network bridge survival" below.
Network bridge survival across rebuilds
The bundle-mcp connection requires openclaw-gateway to share the baas-net Docker network with helmdeck-control-plane. Runtime-only attachment (docker network connect) gets erased every time the container is recreated — compose up --build, compose down/up, host reboot, image refresh. Each erasure produces the same symptom: bundle-mcp probes start failing with getaddrinfo EAI_AGAIN (network gone) or 401 (stale token survived but the network didn't), the agent stops seeing helmdeck tools, and the operator gets pulled back into manual recovery.
The persistent fix is a compose override that declares the attachment in OpenClaw's lifecycle. configure-openclaw.sh installs it by default (added 2026-06-09):
ls /root/openclaw/docker-compose.override.yml # installed by configure-openclaw.sh
The override is at deploy/openclaw-baas-net.compose.yml — three-line declarative attachment that docker compose auto-loads alongside OpenClaw's main compose. Pass --skip-compose-override to configure-openclaw.sh if you'd rather manage the override yourself.
5c. Load the agent skills
The helmdeck pack catalog, schemas, error-handling rules, session-chaining contract, and freshness contract live in SKILLS.md. The configure-openclaw.sh script you ran in §4 already stamped this file into ~/.openclaw/skills/helmdeck/SKILL.md inside the OpenClaw gateway container, where the agent loads it automatically every turn.
v0.22.0 — routing & memory: the agent can now route, plan, and remember through helmdeck. See Route a request and read gap warnings, Decompose a multi-step request, Store agent facts, and Expose helmdeck memory to OpenClaw for the
memory_searchbridge. To run these orchestration packs on free models, read Run orchestration packs on free models.
You don't have to do anything here on a first install. This section is for the refresh case — after pulling a new helmdeck release that grew the catalog or updated the contracts, re-stamp the skill so OpenClaw sees the new content:
cd /path/to/helmdeck && ./scripts/configure-openclaw.sh
The script is idempotent — it only re-writes the stamped skill, leaving JWTs, network bridges, and MCP config untouched.
Verify by repeating the catalog smoke test from §5b — the response should include any newly-added pack names.
5d. OpenClaw canonical file roles (workspace layering)
OpenClaw separates the things that change per operator (your persona, your platforms, your projects) from the things that come from helmdeck (pack catalog, schemas, contracts) by splitting them across four canonical workspace files. Understanding the layering matters because mixing concerns leads to per-operator forks of helmdeck's shipped skills — exactly the problem #459 was filed to track and PR #477 closed differently.
Each agent has its own workspace directory at ~/.openclaw/workspace-<agent-id>/ containing:
| File | Role | Owned by | Example content |
|---|---|---|---|
SOUL.md | Voice + values + banned phrases | Operator | Voice posture ("first-person, terse, practitioner-voiced"), banned filler ("game-changer", "let's dive in"), editorial principles |
IDENTITY.md | Who the agent IS as a persona | Operator | Display name, emoji, expertise areas, audience |
USER.md | Who the operator IS | Operator | Domain, platforms, projects, geographic context, anything the agent should know about you |
AGENTS.md | Mechanism + workflow shape | Operator (often forked from a recipe) | Three-turn iterative pattern, tool allow-list, success criteria, model-specific prompting hints |
The four files are loaded into the system prompt at bootstrap. Each is capped at 12,000 characters (bootstrap injection limit); content beyond the cap is truncated, so concise > comprehensive.
Helmdeck's shipped skills at ~/.openclaw/skills/helmdeck/SKILL.md (and helmdeck-debug, tech-blog-publisher when operators add them locally) stay mechanism-only — the helmdeck pack vocabulary, schemas, contracts. They DON'T encode persona or operator-specific defaults. The persona layer lives in the workspace files; the mechanism layer in the skill. That split is what lets one helmdeck install support many agents with different personalities sharing the same pack catalog.
Multi-agent workspaces
A single helmdeck install can register many OpenClaw agents (openclaw.json → agents.list[]), each pointing at its own workspace directory with its own SOUL/IDENTITY/USER/AGENTS files. Common pattern (sanitized worked example using the Maya security-researcher persona — substitute your own):
~/.openclaw/
workspace-maya-blog/ ← Maya's persona on the default model
SOUL.md ← voice posture, banned phrases
IDENTITY.md ← display name, expertise, audience
USER.md ← Maya's domain (security research)
AGENTS.md ← three-turn iterative workflow
workspace-maya-gemma-4/ ← same persona, Gemma-4-tuned variant
SOUL.md ← (reused from above)
IDENTITY.md ← (reused from above)
USER.md ← (reused from above)
AGENTS.md ← Gemma-4 prompting shape
...
Each workspace's AGENTS.md tunes the workflow to the model's prompting style (per the per-model profiles in models/). The persona files (SOUL/IDENTITY/USER) stay reusable across model variants when the operator wants the same voice + identity on multiple models — only AGENTS.md changes per-model.
Worked example
The Gemma 4 iterative workflow recipe walks a sanitized Maya-persona example end-to-end — what each canonical file contains, how the AGENTS.md adapts to Gemma 4's role-turn-conversational format vs gpt-oss's Harmony format, and how the same three-turn iterative shape ports across models.
Why this matters operationally
The empirical work captured in models/*.yaml community_traces[] arrays consistently shows that per-use-case AGENTS.md hardening is more impactful than per-model profile guidance alone (PR #481 captures one such trace: a Tier C agent on nemotron-3-super-120b-a12b:free with a docs-only baseline AGENTS.md fired 24 pack calls without reaching the deposit step; a hardened variant with explicit tool whitelist + async-pattern bounds + tighter invalidation conditions fired 7 calls and completed deposit+verify with all_present: true on the same prompt).
The takeaway: helmdeck's shipped skill stays compact and stable; the operator-tuned workspace AGENTS.md is where reliability comes from. Treating SOUL/IDENTITY/USER/AGENTS as separate layers (rather than dumping everything into one IDENTITY.md) is what makes that tuning maintainable across many agents.
6. Walk the Phase 5.5 code-edit loop
Open http://localhost:18789 in your browser, paste the OpenClaw gateway token into Settings, then send a chat prompt:
Use the helmdeck packs to:
repo.fetchgit@github.com:<me>/<fixture-repo>.gitusing vault credentialgh-deploy-key.fs.listthe clone for*.mdfiles.fs.readthe README and propose a one-line edit.fs.patchto apply the edit (literal search-and-replace).cmd.rungo test ./...(or any project check) in the clone.git.commitwith messagechore: helmdeck integration smoke.repo.pushback toorigin.
Pass criteria:
- The new commit lands on the remote branch.
- The Audit Logs panel in the helmdeck UI (
http://localhost:3000) shows one entry per pack call, in order. - The SSH private key never appears in OpenClaw's chat transcript — only the
${vault:gh-deploy-key}placeholder.
If all three hold, update the status banner at the top of this file to ✅ with today's date + the helmdeck version, and flip the matching row in README.md.
Known issue: header key MUST be lowercase authorization
Status: Confirmed against OpenClaw 2026.4.10 +
@modelcontextprotocol/sdk@1.29.0+eventsource@3.0.7. Filed upstream as a draft issue atdocs/integrations/openclaw-upstream-issue.md.
If you write the helmdeck MCP server config with capital-A Authorization:
{ "url": "...", "headers": { "Authorization": "Bearer <jwt>" } }
…OpenClaw's bundle-mcp will fail to connect to helmdeck with:
[bundle-mcp] failed to start server "helmdeck" (.../api/v1/mcp/sse): Error: SSE error: Non-200 status code (401)
Helmdeck's audit log shows the request as GET /api/v1/mcp/sse → 401.
Why
OpenClaw's buildSseEventSourceFetch (/app/dist/content-blocks-k-DyCOGS.js) merges the user's headers over the SDK's headers as a plain JS object via spread:
return fetchWithUndici(url, {
...init,
headers: { ...sdkHeaders, ...headers } // sdkHeaders from Headers iteration → lowercase keys
});
The MCP SDK returns headers as a Headers instance, and iterating it yields lowercase keys per the spec — so sdkHeaders ends up with authorization. When the user config has Authorization (capital), the spread produces a plain object with two distinct keys:
{ accept: "text/event-stream", authorization: "Bearer <jwt>", Authorization: "Bearer <jwt>" }
Undici then constructs a Headers list from that object using append, which comma-joins duplicates (per the Fetch spec) into:
Authorization: Bearer <jwt>, Bearer <jwt>
Helmdeck's bearer-token parser (and any standards-compliant parser) rejects this malformed header with 401.
Workaround (until upstream fix)
Use lowercase authorization as the key in your OpenClaw helmdeck config:
docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
mcp set helmdeck '{"url":"http://helmdeck-control-plane:3000/api/v1/mcp/sse","headers":{"authorization":"Bearer <jwt>"}}'
This makes OpenClaw's spread merge into a single authorization entry, which undici then sends as a single well-formed Authorization header.
Upstream fix (proposed)
OpenClaw's buildSseEventSourceFetch should construct a Headers instance and use .set() (which is case-insensitive and replaces) instead of plain-object spread:
function buildSseEventSourceFetch(headers) {
return (url, init) => {
const merged = new Headers(init?.headers ?? {});
for (const [k, v] of Object.entries(headers)) merged.set(k, v);
return fetchWithUndici(url, { ...init, headers: merged });
};
}
This eliminates the case-collision regardless of how the user wrote the key.
Webhook callback — push pack results to OpenClaw
Heavy packs (slides.narrate, research.deep, content.ground) automatically return a SEP-1686 task envelope so the JSON-RPC request never blocks. Most clients SHOULD just poll, but if you want true push semantics — the LLM's next turn fires when a pack completes, no polling at all — wire up the webhook receiver bundled in examples/webhook-openclaw/.
Architecture
┌─────────────────┐ 1. tools/call slides.narrate
│ OpenClaw LLM ├─────────────────────────────────┐
└─────────────────┘ ▼
┌──────────────────┐
│ helmdeck-control │
│ -plane (MCP) │
└────────┬─────────┘
3. POST /helmdeck-callback │
X-Helmdeck-Signature: sha256= │ (60-180s
┌─────────────────────────────────┘ later)
▼
┌─────────────────┐ 4. POST /chat/inject
│ helmdeck-callback ├──────────┐
│ (Node, ~50 LOC) │ ▼
└─────────────────┘ ┌──────────────────┐
│ OpenClaw chat- │
│ injection API │
└──────────────────┘
│
5. LLM sees: "[helmdeck] Pack
completed. Video: <url>"
Step 2 (helmdeck task envelope returns immediately) and step 3 (helmdeck POSTs the result when done) are independent — the LLM doesn't have to wait or poll.
Setup
-
Run the receiver alongside helmdeck and OpenClaw:
# in your docker-compose overrideservices:helmdeck-callback:build: ./examples/webhook-openclawenvironment:OPENCLAW_INJECT_URL: http://openclaw-openclaw-gateway-1:3210/api/chat/injectWEBHOOK_SECRET: ${HELMDECK_WEBHOOK_SECRET}networks: [helmdeck_default, openclaw_default] -
Set the shared secret in your
.env.local:HELMDECK_WEBHOOK_SECRET=$(openssl rand -hex 32) -
Tell the LLM to use the webhook in your prompt. SKILLS.md teaches the LLM the pattern; for an explicit prompt:
Render this Marp deck as a narrated video. Use webhook_url=http://helmdeck-callback:8080/doneand webhook_secret=<your-secret>. Don't poll — I'll get notified when it's ready.---marp: true---# My Deck<!-- Welcome to my deck. -->The future is now. -
What you'll see: the LLM calls
slides.narrateonce, gets back "task started" (SEP-1686 envelope), and a few minutes later you see a fresh chat message:system: [helmdeck] Pack
slides.narratecompleted. Video artifact:slides.narrate/<key>/video.mp4— open athttp://localhost:3000/artifacts/...The LLM can then respond to that message in its normal turn cycle.
Generic spec
For non-OpenClaw clients (custom A2A bridges, Slack bots, anything else) the same webhook fires with the same payload. See docs/integrations/webhooks.md for the wire contract.
Troubleshooting
origin not allowed (use HTTPS or localhost secure context)— OpenClaw's Control UI requires a secure context. Use the SSH tunnel from step 2, not the public IP.- OpenClaw can't reach
helmdeck-control-plane:3000— confirm the network overlay is applied:docker network inspect helmdeck_defaultshould listopenclaw-gatewayas a member. 401 unauthorizedon every tool call — JWT expired or wrong scope. Mint a new one and update~/.openclaw/openclaw.json.tools/listreturns nothing — check that the helmdeck Pack Registry is populated:curl -H "Authorization: Bearer $JWT" http://localhost:3000/api/v1/packsshould list dozens of packs. If empty, the control plane hasn't registered the built-ins (checkdocker compose logs control-plane).
References
Related ADRs
The agent-runtime and session-layer decisions that shape OpenClaw integration:
- ADR-001 — Sidecar pattern for browser isolation
- ADR-004 — Ephemeral stateless browser sessions
- ADR-011 — Tiered isolation (Docker / gVisor / Firecracker)
- ADR-028 — WebRTC live session streaming
- ADR-029 — Four-tier agent memory API
- ADR-031 — Object store: Garage default + pluggable S3
- ADR-032 — Artifact explorer + MCP inline image content
- ADR-033 — GitHub webhook listener
- ADR-039 — Universal memory delivery layer
- ADR-040 — Persistent repos volume + cross-session clone reuse
- ADR-047 — Pipeline routing + memory
- ADR-048 — Memory write surface + OpenClaw memory-corpus bridge