cd /news/ai-agents/cowork-self-hosted-task-queue-and-da… · home › topics › ai-agents › article
[ARTICLE · art-169640] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Cowork – Self-hosted task queue and dashboard for Claude, Codex, and AI agents

An open-source project called Cowork, published on GitHub by developer slashman413, offers a self-hosted MCP server and web dashboard that routes coding tasks across Claude Code, Hermes, Gemini and Ollama through automatic fallback chains when an agent hits a rate limit. Cowork stores all state as plain JSON and Markdown files with no Postgres, Redis or SQLite database, ships 285+ built-in specialist personas across 19 departments, and lets additional machines join as remote workers with a one-line Node command. The project positions itself against CrewAI, AutoGPT, OpenHands and LangGraph by natively supporting Claude Code and Hermes command-line agents and adding quota-aware failover the comparison table marks as manual or absent in the others.

read30 min views3 publishedNov 14, 2026
Cowork – Self-hosted task queue and dashboard for Claude, Codex, and AI agents
Image: Michielbdejong (auto-discovered)

Self-hosted MCP server & Web Dashboard with auto-fallback chains, remote fleet execution, and zero-database storage.

⚡ Quick Start • 💡 Why Cowork? • ⚔️ Competitive Comparison • 📖 Documentation • 🌐 Live Architecture • 🤝 Join as a Remote Agent • ⭐ Star on GitHub

You are using Claude Code for development. Two hours into a sprint, you hit the dreaded rate limit:

Error: You have exceeded your current quota. Please wait 4 hours and 12 minutes.

Your workflow stalls. Your terminal hangs. You switch to Gemini CLI or a local Hermes instance, but you lose your entire task state, intermediate patches, and execution context.

Cowork solves this in one command. It is an open-source control plane that connects all your coding agents into one unified task inbox:

  • 🔀 Smart Fallback Chains: Route tasks from Claude Code ➔ Hermes ➔ Gemini ➔ Ollama. When an API limits out, Cowork transparently hands the work to the next agent in line.
  • 🖥️ Single-Pane-of-Glass Dark Dashboard: Live CPU/GPU/memory thermals, running agent status, interactive calendar routines, and direct artifact downloads.
  • 📁 Zero-Database Simplicity: Pure JSON and Markdown on disk. No Postgres, Redis, or SQLite migrations to maintain.
  • ⚡ One-Command Remote Workers: Harness GPUs or models across laptops, Mac Studios, and cloud boxes:COWORK_URL=https://hub:6868 HOST=mac-studio node cowork/deploy/remote-agent-client.mjs
  • 🎭 285+ Built-in Specialist Personas: Out-of-the-box routing across 19 departments (Frontend, SRE, Security, Penetration Testing, Data Engineering).
Feature Cowork CrewAI / AutoGPT OpenHands LangGraph
Primary Focus Multi-CLI / Multi-Agent Control Plane LLM Python Scripts Headless Dev Container Graph Logic Framework
Supports Claude Code & Hermes Native CLIs ✅ Native ❌ No ❌ No ❌ No
Auto Quota / Rate-Limit Fallback Chains ✅ Built-in ❌ Manual ❌ No
Self-Hosted Dark Web UI & Hardware Telemetry ✅ Instant ✅ Web UI
Model Context Protocol (MCP) Streamable Server ✅ First-class ❌ No ❌ Custom
Storage Engine Plain Files / JSON SQLite/Postgres Docker Volume Postgres/Memory
Multi-Machine Remote Cluster Joining ✅ 1-Line Node CLI ❌ Complex ❌ Docker swarm ❌ Complex
Pre-loaded Agent Personas 285+ Personas 0 (DIY) 0 (Generalist) 0 (DIY)

Jump to: Quick Start · Connecting agents · Dispatcher · Agents · Vocabulary · Thinking level · MCP tools

One dark-themed single pane of glass for the whole operation. The Overview shows live host metrics (CPU / GPU / memory / temp), active workers, open tasks and personas, a 7-day quality panel (pass rate, retries, wait-input, timeouts), the latest agent rulings, the dispatcher's role → model chains, and a real-time activity feed.

The sidebar groups every view: Operations (Overview, Portal, Tasks, Recipes, Calendar, Goals), AI Fleet (Routing, Agents) and System (Workers, Configuration). Integrations (skills, MCP servers and persona packs) have their own page, /integrations.html. The Tasks, Routing, Agents and Workers views are shown in context throughout this README.

Calendar — scheduled tasks and recurring routines by week Goals — long-running goals with phases, decisions and budget
Recipes — saved tasks you re-run with one click Integrations — audited git repos installed as skills / MCP / persona packs / portal cards
Portal — one-click launcher for local web services Obsidian Vault — the shared, readable knowledge base
Configuration — every setting, editable live
Platform Agents Format
Claude Code ~285 .md with YAML frontmatter
Antigravity (AGY) Built-in + skills SKILL.md
Hermes Agent 39 skills SKILL.md + triggers
Gemini CLI Converted from source .md subagents
Copilot Same as Claude .md agents
Codex Converted .toml agents
Cursor Converted .mdc rules
+ 7 more See the agency-agents persona pack Various formats
  • Node.js ≥ 20 (tested with v22)
  • npm ≥ 10
  • Nothing else to clone. Personas and skills are integrations : on first run, once the server is up, it provisions the default integrations in the background —msitarzewski/agency-agents (persona pack the router picks from per task),garrytan/gbrain ,addyosmani/agent-skills ,anthropics/skills ,vercel-labs/agent-browser ,backnotprop/pstack ,pbakaus/impeccable ,Nutlope/hallmark ,multica-ai/andrej-karpathy-skills . Check them withGET /api/integrations/defaults (or the Integrations page); override the list withintegrations.defaults in config.

From the repo root of your clone:

deploy/install-server.sh

deploy/install-skill.sh            # auto-detects Claude Code / Hermes / Antigravity

install-server.sh generates a systemd unit with this machine's real node path and working dir (nothing to hand-edit), or pass --foreground to launch in the terminal and --build-only to stop after building. install-skill.sh copies the right per-client skill into place and, for Claude Code, merges mcpServers.cowork into ~/.claude.json non-destructively (add --url http://<host>:6868 for a remote server; --client claude|hermes|agy|all to target one). The numbered steps below are the same thing, broken out for when you want to do it by hand.

docker run -d -p 6868:6868 -v cowork-data:/data ghcr.io/cowork/cowork:1.0.1
docker logs <container>   # first start prints the generated API key once

The image (amd64 + arm64) runs the MCP server, dashboard and task inbox with the local dispatcher off: agents connect to http://<host>:6868/mcp, and machines offer their agents with deploy/remote-agent-client.mjs (see JOIN-AS-AN-AGENT.md). Config and all state live in the /data volume. /api and /mcp always require a key: pass your own with -e COWORK_API_KEY=…, or the server generates one on first start, saves it to /data/api-key and prints it once. Clients send Authorization: Bearer <key> (the agent client reads COWORK_API_KEY); open http://<host>:6868/?apiKey=<key> once to log the dashboard in. Images are built from v* tags by the release workflow on a digest-pinned base.

Once the hub is up, open Integrations (or GET /api/integrations/defaults) to check that the default persona and skill packs finished provisioning. Until the persona pack is installed, the router has no personas to pick from.

cd cowork/server
npm install

Config lives outside the repo. The tracked cowork/config.json is a sanitized template only. On first run the server copies it to ~/.cowork/config.json — your real per-server config (host binding, registered agents, chains). Edit that copy, not the template; the server also persists all live dashboard/API edits there, so your personal host/agent settings never touch the repo. Override the location with the COWORK_CONFIG env var.

Edit ~/.cowork/config.json to match your environment:

{
  "server": {
    "port": 6868,          // ← Change the port here
    "host": "0.0.0.0",     // ← Bind address (0.0.0.0 = all interfaces)
    "name": "cowork-mcp",
    "version": "1.0.0",
    "apiKey": null          // ← Set a string to require API key auth (see below for null)
  },
  "paths": {
    "inbox": "./inbox",
    "status": "./.status",
    "decisions": "./decisions",
    "artifacts": "./artifacts",
    "inputs": "./inputs"
  }
}

All paths are relative to cowork/ (the parent of server/). Use ~ for home directory paths (e.g., ~/.claude/agents).

Setting Default Description
server.port 6868 HTTP port for MCP endpoint + Web UI
server.host 0.0.0.0 Bind address ( 127.0.0.1 for local-only)
server.apiKey null API key for /api and /mcp. When null and host is not loopback, a key is generated intoapi-key beside the config and printed once
server.allowUnauthenticated false Serve a non-loopback host with no key (also COWORK_ALLOW_NO_AUTH=1 ). Only for a trusted, firewalled network
server.blockedClients — Client denylist enforced regardless of the API key: {ips: [...], names: [...]} . A matching source IP gets 403 on every path; anames substring (case-insensitive) in the User-Agent /X-Cowork-Client header, MCPclientInfo.name , or tool-call identity args (agent_name ,agent_id ,capabilities ,agents[].id ) gets 403 on/api //mcp . Matching agents are disabled so tasks stop routing to them. Seeserver/src/core/client-guard.ts
paths.agencyAgents unset Optional. Pins the roster to one local persona checkout; when unset, personas come from every installed persona-pack integration
integrations.defaults built-in list Repos provisioned once on first start (persona pack + skill packs)

| platforms.*.enabled | true | Enable/disable individual platforms | | platforms.*.agentsDir | varies | Where platform-specific agents live on disk | | services.*.enabled | false | Enable/disable service health monitoring |

cd cowork/server
npm run dev

npm run build
npm start

You should see:

Multi-Agent Cowork MCP Server Started HTTP API: http://0.0.0.0:6868/api MCP Endpoint: http://0.0.0.0:6868/mcp


Open **[http://localhost:6868](http://localhost:6868)** in your browser.

For programmatic access, wire the MCP connection into your agent clients:

**Scripted (recommended):** `deploy/install-skill.sh` installs the `cowork` coordination
skill and wires the MCP connection for every agent client detected on the box — one command
instead of hand-editing each config:

deploy/install-skill.sh # auto-detect all clients, localhost server deploy/install-skill.sh --client claude # just Claude Code deploy/install-skill.sh --client all --url http://cowork-host:6868 # remote server, every client


It drops the per-client skill (canonical copies in `deploy/skills/`) into the client's live
skill dir and, for Claude Code, merges the `mcpServers.cowork` entry into `~/.claude.json`
without disturbing your other servers. The manual per-client config is below for reference.

Add to your MCP configuration (`~/.claude.json` or project-level):

{ "mcpServers": { "cowork": { "url": "http://localhost:6868/mcp", "transport": "streamable-http" } } }


Then in Claude Code, the agent can call:

Use the cowork MCP tools: register_agent, create_task, get_roster, etc.


Add to your AGY MCP settings:

{ "mcpServers": { "cowork": { "url": "http://localhost:6868/mcp" } } }


**agy quota meter / token refresh.** To mint Antigravity access tokens, the usage
probe and `deploy/remote-agent-client.mjs` need Google OAuth client values. These are
the public values that ship with the agy and Gemini CLIs. They are not committed. Copy
[`.env.example`](https://github.com/slashman413/cowork/blob/main/.env.example) to `~/.cowork/oauth-clients.env`, fill it in, and run
`chmod 600`. Alternatively, export the same variables in the service environment;
process env takes precedence. If no values are set, the agy meter only works while a
cached CLI access token is still valid.

Configure the MCP endpoint in Hermes:

{ "mcp_endpoints": { "cowork": "http://localhost:6868/mcp" } }


The server speaks standard MCP over Streamable HTTP. Any client that supports
MCP can connect to `http://localhost:6868/mcp`.

The server includes a **dispatcher** that turns queued tasks into real agent runs.
Output becomes the task `result`; the full transcript is saved as `result.md` in the
task's artifacts dir.

Every task lands in the **Task Inbox**, tagged with the division/agent that ran it and the
agent it ran on, with its output artifacts attached and filterable by status
(done · in-progress · pending · wait-input · failed).

An unassigned task is routed in **two stages** by an orchestrator/classifier agent
(default `qwen3.8-27b` via Hermes, `orchestration.classifier`):

1. **Division** — pick 1 of 19 divisions (testing, engineering, security, …). With`orchestration.workRoles.enabled` the same reply also names a**work role** (`mechanical` ,`feature` ,`bug-fix` ,`perf-issue` ,`research` ,`prose` ,`hardest` ),
which sets the task's`context.tier` and`context.thinking` unless the task set them
(thinking is capped by`workRoles.budget` : small = medium, medium = high,
large = xhigh, unlimited = max).
2. **Persona** — pick 1 of ~285 personas in that division. The chosen persona's`.md` file becomes the system prompt. If the router names no persona (empty reply,
classifier off), a keyword matcher (`core/persona-match.ts` ) picks one; if the reply
can't be parsed at all, the task goes to`classifier.fallbackRole` (`generalist` ).

A persona shapes the **prompt** only; the agent that runs it comes from the fallback
chain (below). Skip the router by targeting directly: `context.persona: "<persona-slug>"`
or a special-executor name. Tag a task `manual` to never auto-execute.

A task **pinned** to an agent (`context.agent`) skips agent selection. If that agent runs a
small model (≤ 70B parameters, parsed from the model id, e.g. `qwen3.8-27b`) and the task
has no persona, the router still runs first so the pinned agent gets a persona. Override
per agent with `orchestration.agents.<id>.personaRouting: "always" | "never"`.

- **Special executors** (`config.json → orchestration.roleChains` ) — only`orchestrator` ,`generalist` ,`video` ; each is`{description, agents: [...]}` with its own chain.`video` runs the ComfyUI**LTX** pipeline
(`deploy/video-pipeline.sh` ; LTX only — never Wan/Hunyuan/SVD).
- **Personas** — the ~285 personas from the installed persona-pack integrations
(default:`msitarzewski/agency-agents` , cloned under`~/.cowork/integrations/` ),
grouped into 19 divisions. They have no chain of their own: per-division and
per-persona chains were removed on 2026-11-13, and`divisionChains` /`personaChains` in an old config are ignored.

- **Global default** —`orchestration.defaultChain` ; drag-reorder in the**Routing** view (`PUT /api/chains/default` ).
- **Tier lists** —`orchestration.tiers.<mechanical|standard|judgment>` (`GET /api/chains/tiers` ,`PUT /api/chains/tier/:tier` ). A task with a`context.tier` (set by hand or by its work role) tries its tier's agents first, then the default chain.
- **Special executors** —`orchestration.roleChains.<role>.agents` (`/api/roles-config/:name` ).

A chain runs top → bottom: the task runs on `chain[0]`; on failure the dispatcher
**hands over** to `chain[1]`, then `[2]`… recording the failed agent on the task
each attempt, until success or the chain is exhausted. Pin a single task to one
agent with `context.agent: "<id>"`.

The **Routing** view edits the special-executor chains, the tier lists and the default
chain: drag to reorder or `+ add agent…`. Changes apply live, with no restart.

| Term | Meaning | Where you see it | 
|---|---|---|
| **Agent** | One model on one CLI on one machine: the thing that runs a task. Example: `local-cc-opus-5-5` . | `orchestration.agents` ,`context.agent` ,`/api/agents` , Agents page | 
| **Thinking** | How hard an agent reasons on a run. A parameter of the run, never part of an agent id. | `context.thinking` ,`agents.<id>.thinking` ,`orchestration.defaultThinking` | 
| **Persona** | A roster specialist the run takes on. It shapes the prompt, not the agent choice. Example: `engineering-sre` . | `context.persona` ,`get_roster` , persona-pack integrations | 
| **Work role** | The kind of work the router judged a task to be; sets its tier + thinking. Example: `bug-fix` . | `orchestration.workRoles` ,`context.tier` | 
| **Role** | A named chain of agents for a built-in job ( `orchestrator` ,`generalist` ,`video` ). | `orchestration.roleChains` , Routing page | 
| **Worker** | A connected client that registers agents and claims their tasks. | `/api/workers` , Workers page,`register_agent` | 

`register_agent`, `deregister_agent` and their `agent_id` / `agent_name` arguments keep their names so
connected clients don't break: `agent_id` is the **worker's** registration id, not an id from `agents`.

Agent ids no longer carry a level: `local-agy-gemini-3.8-flash` replaces the `-high`, `-medium` and `-low`
variants. A run's level comes from the first of these that is set:

1. the task's `context.thinking`
2. the agent's `thinking`
3. `orchestration.defaultThinking`
4. `medium`

Levels are `low`, `medium`, `high`, `xhigh` and `max`. Set one on the New task, Edit, Re-run, Reassign and
Continue dialogs, per agent on the Agents page, or through the API:

curl -X POST "$COWORK/api/inbox" -H 'Content-Type: application/json' -d '{ "title": "...", "description": "...", "from": {"platform": "cli", "agent": "me"}, "to": {}, "context": {"agent": "local-cc-opus-5-5", "thinking": "high"} }' curl -X PATCH "$COWORK/api/agents/local-cc-opus-5-5/thinking" -H 'Content-Type: application/json' -d '{"thinking": "high"}'


| exec | How the level is applied | 
|---|---|
| claude | `--effort <level>` | 
| codex | `-c model_reasoning_effort="<level>"` (`max` runs as`xhigh` ) | 
| agy | model slug `<model>-<level>` (`xhigh` and`max` run as`high` ;`gpt-oss-120b` only has`medium` ) | 
| hermes | `--reasoning <level>` , only when a level was set explicitly (`xhigh` and`max` run as`high` ) | 
| dsh, ollama, script | no reasoning control, so the level is ignored | 

A hub upgraded from the `brain` naming migrates itself: on start, the server detects the old config shape, backs up
the config and state to `backups/agent-naming-boot-*.tar.gz` next to the config, and runs
`scripts/migrate-agent-naming.mjs` before  anything. So `git pull && deploy/redeploy.sh` is enough. Local
clients still on `cowork-local-brain@` keep working under that unit; `deploy/cutover-agent-naming.sh` moves them to
`cowork-local-agent@`.

Clients on the old contract keep working: the hub translates their old field names at the door
(`server/legacy-aliases.json`, read only by `server/src/legacy-wire.ts`) and answers with `agent_aliases` so they
can update. `GET /api/legacy-wire` counts how often that still happens. Once it stays at zero, delete those two files.

`config.json → orchestration.agents` (`GET /api/agents`) names each agent a chain
can reference. The **Agents** view lists every registered agent — its platform
(`claude` / `agy` / `hermes`), underlying model, location, and whether it was
`auto`-registered by a connecting client — with one-click deregister:

| Alias | Location | Runs | 
|---|---|---|
| `local-hermes-qwen3.8-27b` | local | Hermes on that model | 
| `local-dsh-qwen3-8-27b` | local | DeepSeek Harness ( `dsh` ) on that model | 
| `local-cc-opus-5-5` /`-sonnet-5-5` /`-fable-5-1` | local | Claude Code on that model | 
| `local-agy-*` /`local-comfy-ltx` | local | Antigravity/Gemini / ComfyUI-LTX video | 
| `local-codex-gpt-6-luna` /`-gpt-5-6-terra` /`-gpt-5-6-luna` /`-default` | remote | Codex on this host, claimed by the codex client | 
| `local-script-eval-harness` | local | The `eval-prompt-change` harness (`exec: script` ) | 
| `remote-ai-code-gen-cc-*` | remote | Claude Code on the ai-code-gen machine | 
| `remote-<host>-cc-sonnet-5-5` | remote | Claude Code on another machine | 

This table is an example. Read the live set with `GET /api/agents`.

Agent ids carry the **model version** (`local-cc-opus-5-5`, not `local-cc-opus`) so the
model an agent runs is readable from its id; only `-default` agents, which follow the
account default, are unversioned.

**Naming convention — the prefix names the machine, `location` names the runner.**
The `local-`/` remote-` id prefix says **which machine the agent executes on**:
`local-` = this host (it can see `/home/wayne`, `~/.priv`, the GPU), `remote-` =
another machine (`remote-<host>-…`). The separate `location` field says **who
starts it**: `local` = the dispatcher spawns it, `remote` = a client claims it from
the inbox. The two usually agree, but not always — `local-codex-*` runs on this
host yet has `location: remote` because the codex client claims its tasks. Code must
read `location`, never infer it from the prefix. DeepSeek
agents are retired — any id matching `/deepseek/i` is scrubbed from the registry on
load (see `DENYLISTED_AGENT_RE`), so never put `deepseek` in a new id.

**Renamed ids keep working.** `AGENT_ID_ALIASES` in `server/src/config.ts` (plus an
optional `orchestration.agentAliases` in config) maps retired ids to their canonical
id. Old ids in an on-disk config, a task's `context.agent` pin, or a client's
registration all resolve to the canonical agent. A client that still declares an
old id keeps receiving its tasks under that id (`clientId` on the agent), until it
re-declares the canonical one. Renames (2026-10-07, and codex again on 2026-11-03):

| Retired id | Canonical id | 
|---|---|
| `local-ha-qwen3-8-27b` ,`local-ha-qwen38-27b` ,`local-ha-qwen3.8-27b` | `local-hermes-qwen3.8-27b` | 
| `remote-codex-default` /`-gpt-5-6-terra` /`-gpt-5-6-luna` /`-gpt-6-luna` | `local-codex-default` /`-gpt-5-6-terra` /`-gpt-5-6-luna` /`-gpt-6-luna` (2026-11-03: the prefix names the machine, so the 10-07`local-codex-*` →`remote-codex-*` rename was reversed) | 
| `remote-ai-code-gen-cc-opus` /`-sonnet` /`-fable` | `remote-ai-code-gen-cc-opus-4-8` /`-sonnet-5` /`-fable-5` | 

**Local** agents the dispatcher spawns here. **Remote** agents it leaves `pending`
and **publishes the agent id onto the task's `context.agent`** so that machine's
client can discover and claim it (the client filters `list_inbox` for tasks whose
`context.agent` is one of its own ids). If no client claims a remote rung within
`orchestration.remoteGraceMs` (default 60 s), the dispatcher advances to the next
agent in the chain, so a clientless remote rung never stalls a task.

**Artifacts** work for both: local agents save files to `$COWORK_ARTIFACTS_DIR`
(the dispatcher collects them from disk); remote agents save to the same env dir
and the client **uploads** each file via `POST /api/artifacts/:taskId/:file`.
Either way they land in `artifacts/<task-id>/` and become downloadable from the
Inbox. **Outputs** (the task's final deliverables) go in the `outputs/` subfolder
(`$COWORK_OUTPUTS_DIR`; remote clients upload via `POST /api/outputs/:taskId/:file`)
and get their own OUTPUTS section on the card, after RESULT and before ARTIFACTS.

**Input files** flow the other way — a person attaches files for the agent to
**read**. Attach them on any Inbox card ("+ Attach
files"); they are stored under `inputs/<task-id>/` and mirrored onto
`context.inputFiles`. The dispatcher points a **local** agent at their absolute
paths in the prompt; a **remote** agent client downloads them (`GET /api/inputs/:taskId`)
before running and lists their local paths in the prompt. Inputs survive a re-run.

**Failed tasks & re-run.** When a task exhausts its whole fallback chain (every
agent failed verification) it finishes flagged `failed: true`. The Inbox groups
these under a red **"failed"** filter (kept out of the green "done" count) and
offers a confirm-gated **↻ Re-run** that resets the task to `pending` from the top
of its chain (`POST /api/inbox/:id/rerun`) — preserving its brief and attached inputs.

A connecting MCP client DECLARES the agents it can run via the `register_agent`
tool's `agents` field; the server auto-adds them to the registry (marked `dynamic`,
owned by that client). One machine can offer several models at once. Auto-registered
agents persist and are removed **only** by `deregister_agent` or the Agents view
(never on heartbeat timeout); removal cascades out of the default chain, every
tier list, and every special role. Manage all of this from the dashboard's
**Agents** and **Routing** views.

`deploy/remote-agent-client.mjs` is a zero-dependency (Node 18+) Cowork **MCP**
client that does exactly that loop. Its header comment documents every env var.

**Quickest — run it directly** (foreground; good for a first test). One agent:

git clone <cowork-repo-url> cowork # the git remote your hub was installed from COWORK_URL=http://<cowork-host>:6868 EXEC=claude MODEL=claude-sonnet-5-5
AGENT_ID=remote-<host>-cc-sonnet-5-5
node cowork/deploy/remote-agent-client.mjs


Several agents from one machine (declares them all; each becomes a targetable agent):

COWORK_URL=http://<cowork-host>:6868 EXEC=claude HOST=<host>
AGENTS='[{"id":"remote-<host>-cc-opus-5-5","model":"claude-opus-5-5"}, {"id":"remote-<host>-cc-sonnet-5-5","model":"claude-sonnet-5-5"}]'
node cowork/deploy/remote-agent-client.mjs


**As a boot service** (recommended for permanent machines):

mkdir -p ~/.config/cowork-remote-agent cp cowork/deploy/remote-agent-client.env.example ~/.config/cowork-remote-agent/aicodegen.env cp cowork/deploy/cowork-remote-agent@.service ~/.config/systemd/user/ systemctl --user daemon-reload && systemctl --user enable --now cowork-remote-agent@aicodegen


It connects to `COWORK_URL/mcp`, `register_agent` s (declaring its `AGENTS`, which
auto-register into the registry), then polls and claims only tasks whose
`context.agent` is one of its agent ids, runs the matching `EXEC`/` MODEL`, and
`complete_task` s with its result and uploaded artifacts — appearing in **Workers** like any other
client. **Flexible**: the same script serves any agent by changing env only.
**Scalable**: add machines/agents by adding `<name>.env` files; claims are atomic
(single-process compare-and-set) so even multiple clients on the *same* agent id
never double-run a task — first claim wins.

Every connected client shows up live in the **Workers** view — grouped by platform,
listing the agents it offers, its idle/busy state, and a per-session tally of how many
tasks each agent has run vs. submitted:

The always-on coordinator agent shown in **Workers** as `cowork/orchestrator`
polls the inbox, two-stage-routes unassigned tasks, reclaims orphans, and
dispatches; transient per-task workers appear as e.g.
`testing / Workflow Optimizer · local-hermes-qwen3.8-27b` or `video · local-comfy-ltx`
while running.

CEO flow: tell Hermes (e.g. via Discord) an idea → Hermes creates ONE task with
`context.persona: "orchestrator"` → the orchestrator decomposes it into subtasks via
`POST /api/inbox` → each subtask is two-stage-routed (persona + work role) and runs on
its tier/default chain → results and artifacts appear live on the dashboard.

An unassigned task (e.g. a free-text idea filed straight from Discord) no longer
stalls: the dispatcher runs the **two-stage LLM router** (default qwen3.8-27b
via Hermes, `orchestration.classifier` in config.json) that reads the task, picks
a division (plus a work role), then a persona, and the task dispatches normally on
its tier/default chain. Tag a task `manual` to skip both routing and dispatch.

`orchestration.staleClaimMs` (code default `0` = off; the template `config.json` sets 10 min) reclaims any in-progress task
whose claiming agent has disappeared — a crashed/exited live agent or a
dispatcher killed mid-run — back to `pending` so the work is retried. A task
still owned by a heartbeating agent is never touched.

Dispatcher status: `GET /api/dispatcher`. Tune `orchestration.maxConcurrent` /
`taskTimeoutMs` / `pollIntervalMs` / `classifier` / `staleClaimMs` in config.

Easiest — `deploy/install-server.sh` generates and enables the user service with this
machine's resolved node path and working dir:

deploy/install-server.sh # build + install & start the cowork-mcp user service


Manual equivalent (the tracked `deploy/cowork-mcp.service` has `/home/USER` placeholders
you must edit first — the script fills them in for you):

cp deploy/cowork-mcp.service ~/.config/systemd/user/ # then edit the USER/paths inside systemctl --user daemon-reload systemctl --user enable --now cowork-mcp


With `server.host: "0.0.0.0"` the dashboard is reachable from other machines at
`http://<host-ip>:6868/` (LAN or Tailscale IP — `0.0.0.0` itself is a bind
address, not a URL). A non-loopback host always requires a key: set `server.apiKey`
(or `COWORK_API_KEY`), or use the one generated into `api-key` beside the config on first
start. Log the dashboard in once with `http://<host-ip>:6868/?apiKey=<key>` (it sets a
same-site cookie). `server.allowUnauthenticated: true` opts out; anyone who can reach the port
can then create tasks for your agents.

**Moving a running open hub to a key without locking anyone out:** set the key *and* keep
`allowUnauthenticated: true`. That is soft (migration) mode: a valid key is accepted, a wrong key
gets 401, and keyless requests still pass but are listed at `GET /api/auth/keyless` (key required).
Give the key to every client (agent clients: `COWORK_API_KEY`; MCP configs: an
`Authorization: Bearer <key>` header, e.g. `deploy/install-skill.sh --key <key>`; dashboards:
`/?apiKey=<key>` once). Dispatched tasks get `COWORK_API_KEY` in their env automatically. When
`/api/auth/keyless` stays empty, drop `allowUnauthenticated` and restart to enforce.

By default the server listens over plain `http://`. That is fine over `localhost`,
but a browser only exposes the **microphone** (the New-task *Dictate* speech-to-text
button) in a **secure context** — `https://` or `http://localhost`. Reach the
dashboard over `http://<lan-or-tailscale-ip>:6868` and `navigator.mediaDevices` is
undefined, so Dictate can't start. Serving over HTTPS fixes it.

When `server.tls` points at valid cert/key files, Cowork runs a **multiplexed dual-protocol listener**
on port 6868 that accepts both HTTP and HTTPS simultaneously:

- **Localhost in Chrome (`http://localhost:6868/`)** : Chrome and modern browsers natively treat` http://localhost` as a W3C-standard**Secure Context** (`window.isSecureContext === true` ).
The New-task*Dictate* mic button and clipboard work out of the box with**zero certificate warnings** .
- **Remote / Mobile on Tailscale (`https://<machine>.tailXXXX.ts.net:6868/`)** : Remote connections
use the valid Let's Encrypt TLS certificate provisioned via Tailscale. Chrome shows a verified,
trusted green lock.
- **Automatic IP/Domain HTTP-to-HTTPS Redirection** : Requests arriving over plain HTTP for external
hostnames or IPs are automatically redirected (302) to the trusted HTTPS domain.
- **SNI Fallback** : Multi-domain TLS SNI callback routes domain traffic to the trusted certificate
and loopback TLS traffic to the local fallback certificate.

Migrate in this order:

1. 
**Obtain or Generate a TLS Certificate:**
  - **Tailscale Let's Encrypt (Recommended, Zero Warnings)** :
This fetches a genuine Let's Encrypt certificate via

deploy/update-tailscale-cert.sh


 `tailscale cert` and installs it to`~/.cowork/tls/cert.pem` and`~/.cowork/tls/key.pem` .
  - **Self-Signed (Alternative)** :

server/gen-tls-cert.sh ~/.cowork/tls cowork-host 100.64.0.1 cowork.tailXXXX.ts.net


2. 
**Point the config at the files.** Add a`tls` block under`server` in`~/.cowork/config.json` (the live config — the repo`config.json` is only the
default template and ships`"tls": null` ):

"server": { "host": "0.0.0.0", "port": 6868, "tls": { "certFile": "~/.cowork/tls/cert.pem", "keyFile": "~/.cowork/tls/key.pem" } }


 `~` and repo-relative paths are resolved for you.**This is the step most often
missed:** generating the cert alone does nothing — without this block the server
stays on`http://` .
3. 
**Restart the server so the new scheme takes effect.** A plain`systemctl --user restart cowork-mcp` run*from inside a task* SIGKILLs that task
(it is a child of the unit it's restarting), so use the idle-safe redeploy, which
waits until no task is in flight:

systemd-run --user --collect --unit=cowork-server-redeploy
deploy/redeploy-server-when-idle.sh


 Confirm it came up: 

journalctl --user -u cowork-mcp -n 10 | grep -E 'Trusted HTTPS|Local HTTP|API:' curl -s http://localhost:6868/api/status | jq .uptime


4. 
**Accessing the Dashboard in Chrome:**
  - **On the host machine** : Open`http://localhost:6868/` (Secure Context active, 0 SSL warnings).
  - **From remote devices or mobile** : Open`https://<tailscale-domain>:6868/` (Trusted Let's Encrypt SSL lock).
5. 
**Updating Agent Clients and Skills:** Run`deploy/install-skill.sh` , which automatically detects your Tailscale domain or localhost
configuration:

deploy/install-skill.sh --client all


**Server-side self-dispatch is loopback-optimized** — local subtask dispatch runs over
`http://localhost:6868` through the multiplexed listener, avoiding TLS handshake overhead
and certificate hostname mismatch errors entirely.

To roll back, set `server.tls` to `null` (or remove the block) and redeploy — the
server returns to `http://`.

Once connected, agents have access to these tools:

| Tool | Description | 
|---|---|
| `register_agent` | Register this client; optionally DECLARE the `agents` it can run | 
| `deregister_agent` | Remove this client and cascade-remove every agent it registered | 
| `heartbeat` | Update status and current task | 
| `get_roster` | Search ~285 personas across 19 divisions | 
| `create_task` | Create a task for another agent/platform | 
| `claim_task` | Claim a pending inbox task | 
| `complete_task` | Mark a task as done with results | 
| `list_inbox` | List inbox tasks with status/platform filters | 
| `get_dashboard` | Get full dashboard data (active workers, inbox stats, etc.) | 
| `create_goal` /`list_goals` /`get_goal` /`update_goal_progress` | Long-lived, phase-tracked Goals driven toward a yes/no success criterion | 
| `obsidian_info` /`obsidian_search` /`obsidian_read` /`obsidian_list` | Shared Obsidian knowledge base (when a vault is configured) | 
| `integrations` | Integrate a git / open-source repository (list, add, install, …) | 

You: "Review the auth code in saas-starter"

AGY Agent calls: create_task( title: "Review auth middleware", from_platform: "antigravity", from_agent: "self", to_platform: "claude", to_agent: "engineering-code-reviewer", priority: "normal" )

→ Task appears in dashboard inbox → Claude Code picks it up, runs review → Claude calls complete_task(...) with its result + artifacts → Result + artifacts appear on the task card


The declarative **Workflows** engine (`workflows/*.json`, `/api/workflows*`, the
Workflows tab) was removed on 2026-11-02. Its jobs are covered by:

- **Recipes** — a task saved from a good run, or written from scratch with**New recipe** ,
with`{{fields}}` you fill in per run. A recipe carries its agent, thinking level and
timebox; the Run sheet can override agent and thinking for one run
(`POST /api/recipes/:id/run` with`{values, agent, thinking, scheduledAt, recurrence}` ).
Recipes don't pin a persona: the router picks one per run.
- **Goals** — a long-lived objective with a yes/no success criterion and phases, driven by
the Achiever loop until it is met (`create_goal` ,`/api/goals` ). See[docs/goals-redesign.md](https://github.com/slashman413/cowork/blob/main/docs/goals-redesign.md) and[docs/goal-loop-idiom.md](https://github.com/slashman413/cowork/blob/main/docs/goal-loop-idiom.md) .
- **Orchestrator decomposition** — a task with`context.persona: "orchestrator"` splits
itself into subtasks through`POST /api/inbox` .

The Web UI uses these endpoints (also available for scripts/integrations):

| Method | Path | Description | 
|---|---|---|
| `GET` | `/api/status` | Dashboard overview data | 
| `GET` | `/api/workers` | All active workers (id → name) | 
| `GET` | `/api/connections` | Live MCP clients (heartbeat) + per-agent ran/submitted counters | 
| `GET` | `/api/inbox?status=pending` | Inbox tasks (filterable) | 
| `GET` | `/api/tasks` | Same list and filters as `/api/inbox` , wrapped as`{tasks}` | 
| `POST` | `/api/inbox` | Create a new task; optional `inputs: [{token,name}]` attaches uploaded files | 
| `PATCH` | `/api/inbox/:id` | Claim or complete a task | 
| `POST` | `/api/inbox/:id/rerun` | Re-queue a FAILED (chain-exhausted) task — reset to pending from the top of its chain, keep its inputs | 
| `GET` /`POST` | `/api/inbox/:id/inputs` | List / attach input files to an existing task ( `{inputs:[{token,name}]}` ) | 
| `POST` | `/api/uploads?name=<file>` | Stage an uploaded file (raw body) → `{token}` to pass as a task input | 
| `GET` | `/api/inputs/:taskId` ,`/:taskId/:file` | List / download a task's attached input files | 
| `POST` | `/api/inbox/:id/` ,`/resume` (or`/api/tasks/\|resume` with`{taskId}` ) |  a task (stops a running agent) / resume it with its context intact | 
| `GET` | `/api/tasks/d` | d tasks | 
| `GET` | `/api/roster?division=engineering` | Personas from the installed persona packs (filterable) | 
| `GET` | `/api/roster-divisions` | Personas grouped by division | 
| `GET` | `/api/dispatcher` | Special roles + agents + defaultChain + running | 
| `GET` /`PUT` | `/api/chains` ,`/chains/default` ,`/chains/tiers` ,`/chains/tier/:tier` | Read/edit the default chain and tier lists | 
| `GET` /`PUT` /`DELETE` | `/api/agents` ,`/api/agents/:id` | Agent registry (cascades on delete) | 
| `PATCH` | `/api/agents/:id/enable` ,`/disable` ,`/thinking` | Toggle an agent, set its default thinking level | 
| `GET` /`PUT` /`DELETE` | `/api/roles-config` /`/api/roles-config/:name` | Special-executor chains | 
| `GET` /`POST` | `/api/recipes` ,`/api/recipes/:id` (`PUT` /`DELETE` ),`/api/recipes/:id/run` ,`/api/recipes/draft-from-task/:taskId` | Saved recipes | 
| `GET` /`POST` | `/api/goals` ,`/api/goals/:id` (`PATCH` /`DELETE` ),`/goals/:id/activate\|\|block` ,`/goals/templates` | Goals | 
| `GET` | `/api/calendar` | Scheduled + recurring tasks for the Calendar view | 
| `GET` /`POST` | `/api/integrations` ,`/api/integrations/:id/:action` ,`/api/integrations/defaults` ,`/api/integrations/defaults/provision` | Integrations (skills, MCP, persona packs); verify / re-run first-run provisioning | 
| `GET` /`POST` | `/api/artifacts/:taskId` ,`/:taskId/:file` | List/download; POST (raw body) uploads a file from a remote agent | 
| `GET` /`POST` | `/api/outputs/:taskId` ,`/:taskId/:file` | Same, for the task's `outputs/` deliverables | 
| `GET` | `/api/config` | Current configuration | 
| `GET` | `/api/events` | SSE event stream | 

cowork/ ├── config.json # TEMPLATE only — real config at ~/.cowork/config.json ├── README.md # This file ├── PROTOCOL.md # Protocol specification ├── JOIN-AS-AN-AGENT.md # Onboarding for a remote agent client ├── CONVENTIONS.md # Operating rules injected into every dispatched task ├── layout.json / portal.json # Dashboard nav + widgets / Portal cards (live copies in ~/.cowork/) ├── methods/ # Playbook cards the sharpener attaches to a task ├── vendor/pstack/ # Vendored pstack skills ├── server/ # MCP Server + Web UI │ ├── src/ # TypeScript source │ └── public/ # Web UI (HTML/CSS/JS) ├── deploy/ # systemd units, remote-agent client, presets, skills ├── inbox/ # Task queue (JSON files, auto-managed) ├── artifacts/ # Per-task OUTPUT files (audio/video/md), downloadable ├── inputs/ # Per-task INPUT files a person attached for the agent (gitignored) ├── goals/ # Goal aggregates (runtime, gitignored) ├── decisions/ # Decision log └── .status/ # Runtime state (auto-managed)

~/.cowork/ ├── config.json # Real per-server config ├── integrations.json # Integration registry └── integrations/ # Cloned integrations, incl. the persona pack(s)


Test the MCP endpoint with the official inspector:

npx @modelcontextprotocol/inspector http://localhost:6868/mcp


This opens a web UI at [http://localhost:6274](http://localhost:6274) where you can browse and test all
MCP tools interactively.

curl http://localhost:6868/api/status | jq

curl http://localhost:6868/api/workers | jq

curl "http://localhost:6868/api/inbox?status=pending" | jq

curl "http://localhost:6868/api/roster?division=engineering" | jq

curl -N http://localhost:6868/api/events curl -X POST http://localhost:6868/mcp
-H "Content-Type: application/json"
-d '{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "register_agent", "arguments": { "platform": "antigravity", "agent_name": "test-agent", "current_task": "Testing MCP connection" } }, "id": 1 }'

curl -X POST http://localhost:6868/mcp
-H "Content-Type: application/json"
-d '{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_dashboard", "arguments": {} }, "id": 2 }' { "server": { "port": 6868, "host": "0.0.0.0", "name": "cowork-mcp", "version": "1.0.0", "apiKey": null }, "paths": { "inbox": "./inbox", "status": "./.status", "decisions": "./decisions", "artifacts": "./artifacts", "inputs": "./inputs" }, "platforms": { "claude": { "enabled": true, "agentsDir": "~/.claude/agents", "color": "#D97757" }, "hermes": { "enabled": true, "skillsDir": "../hermes-agent/skills", "color": "#7C3AED" }, "antigravity": { "enabled": true, "skillsDir": "~/.gemini/config/skills", "color": "#0EA5E9" } }, "inbox": { "autoArchiveDays": 30, "maxRetries": 3 } }


Portal launcher cards live in their own file, `portal.json` (live copy
`~/.cowork/portal.json`, hot-reloaded) — see [docs/dashboard.md#portal](https://github.com/slashman413/cowork/blob/main/docs/dashboard.md#portal).

The real config is read from `~/.cowork/config.json` by default. Point at a
different file with the `COWORK_CONFIG` env var (and override the port / API key
without editing any file):

COWORK_CONFIG=~/.cowork/config.staging.json COWORK_PORT=6900 npm start


The persona catalog is cached in memory and rescanned at most once per `COWORK_ROSTER_TTL_MS`
(default 30000). A newly installed or updated persona pack propagates within that window with no
restart;
set `0` to rescan on every query, or a larger value to reduce disk work.

GitHub stars tell us which agent integrations (Cursor MCP, Windsurf, Roo-Code, Goose) to build next.

- ⭐ **Star now** to bookmark this project and receive release alerts when new agent adapters drop.
- 🍴 **Fork** to run an isolated agent fleet for your team or homelab.

[Cowork Pro ($59)](https://slashmaster6.gumroad.com/l/cowork-pro?utm_source=cowork&utm_medium=readme) is the production
package: advanced workflows, priority support and lifetime updates. It is also part of the
[AI Developer Stack Bundle ($79)](https://slashmaster6.gumroad.com/l/ai-dev-stack?utm_source=cowork&utm_medium=readme).

[MIT](https://github.com/slashman413/cowork/blob/main/LICENSE) © 2026 Cowork. The core server, dashboard and remote-agent client are
free and open source; Cowork Pro (above) is the separate paid add-on.
── more in #ai-agents 4 stories · sorted by recency
── more on @cowork 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/cowork-self-hosted-t…] indexed:0 read:30min 2026-11-14 · —