ethanhq/team
Spawn long-lived provider LLM teammates in tmux panes that you message via the native agent-team tools — multi-turn, collaborative, watchable. Use for sustained parallel build/work ("spawn workers", N teammates on N files), or when you need a collaborator you message across turns. NOT a fire-and-forget one-shot or a flat batch of independent prompts — that is /cc-fleet:subagent. NOT a scripted multi-phase run — that is /cc-fleet:workflow.
npx skills add https://github.com/ethanhq/cc-fleet --skill team
Run a third-party provider model as a real Claude Code teammate in a tmux pane: same tool stack and team coordination as a native teammate, LLM backend swapped to the provider. Your main session's own auth stays untouched.
Wrong lane? A fire-and-forget one-shot or flat batch → /cc-fleet:subagent; a scripted multi-phase run → /cc-fleet:workflow; full arbitration in cc-fleet-shared/routing.md.
When this skill cites cc-fleet-shared/<file>.md, OPEN it with the Read tool at ../cc-fleet-shared/<file>.md relative to this SKILL.md — the cited content is load-bearing, not optional background.
> Execution environment — check before running anything. Confirm your shell tool executes on the host where cc-fleet is installed. In sandboxed or remote agent sessions, a tool named Bash may run on an isolated machine with a different filesystem, PATH, processes, and tmux server — command not found, a healthy-looking doctor whose leaves can't reach your files, or a wrong working directory should prompt you to verify whether you are in a sandbox shell, not conclude that cc-fleet is broken. If so, route commands through a host-executing bridge tool (for example, desktop-commander) and pass host paths for any files you reference; do not retry the same Bash call expecting different results. If no host-executing tool is available, stop and explain that cc-fleet must run on its installation host.
Precondition — agent-teams must be ON: check your own tool list for SendMessage / TeamCreate; absent → do NOT spawn (an unmessageable pane bills the provider with no work) — enablement + fallback in cc-fleet-shared/routing.md.
Steps 1, 3, 6 are native tools; steps 2, 5, 6a are cc-fleet via Bash with --json.
1. TeamCreate({team_name: "<team>"})
← native, FIRST — the main session becomes the lead
2. cc-fleet spawn [<provider>] --as <name> --team <team> [--model <slot|id>] --json
← Bash; check ok:true, grab .pane_id / .agent_id
The provider arg is OPTIONAL — omitted, the default provider applies
(see "The provider ask ladder"). Full flag table: cc-fleet-shared/cli-reference.md.
3. SendMessage({to: "<name>", message: "<task>. When done, send your result
back with SendMessage."})
← native; always tell it to report — see "Getting a result back"
4. (optional) repeat 2+3 to fan out more workers in parallel
5. wait for idle notifications — WITH a timeout. A teammate on a failed
provider API (429 / out-of-balance / 401) wedges in a retry loop and never
goes idle, so "just wait" blocks forever. Poll cc-fleet ps --json --check
and dispatch on error_class — see "Watching for stuck teammates".
6. report to the user, then ASK before tearing down. On confirm, BOTH, in order:
a. cc-fleet teardown <team> --json ← Bash FIRST: kills provider panes + reaps procs
b. TeamDelete() ← native SECOND: removes the team/tasks dirs
On a spawn failure (ok:false), dispatch on error_code — table + self-heal flow in cc-fleet-shared/troubleshooting.md.
Teardown order — two hard rules (apply to wedged and probe teams too):
cc-fleet teardown FIRST, TeamDelete() SECOND — teardown reads the team's config.json (the swarm socket lives there), which TeamDelete() deletes; and TeamDelete() alone never touches tmux, so a provider pane/process would be orphaned and keep billing.TeamCreate({team_name: "refactor-api"}) # native
cc-fleet spawn --as worker-1 --team refactor-api --model strong --json # default provider
# → {"ok":true,"agent_id":"worker-1@refactor-api","name":"worker-1","pane_id":"%42", ...}
SendMessage({to: "worker-1", message: "Refactor src/api/handlers.go: split each handler into its own file under src/api/handlers/. Keep tests passing. Report your result via SendMessage when done."})
# … wait with timeout + ps --check …
# report; tear down only after the user confirms:
cc-fleet teardown refactor-api --json # Bash, FIRST
TeamDelete() # native
TeamCreate({team_name: "translate-docs"})
cc-fleet spawn kimi --as zh-1 --team translate-docs --json # leaf: omit --model
cc-fleet spawn kimi --as zh-2 --team translate-docs --json
cc-fleet spawn deepseek --as polish --team translate-docs --model strong --json # synthesis: strong
SendMessage({to: "zh-1", message: "Translate docs/intro.md to zh-CN beside it. SendMessage me when done."})
SendMessage({to: "zh-2", message: "Translate docs/api.md to zh-CN beside it. SendMessage me when done."})
SendMessage({to: "polish", message: "When zh-1 and zh-2 finish, copy-edit their outputs for tone consistency. SendMessage me the result."})
# … notifications per worker; report first, tear down on confirm:
cc-fleet teardown translate-docs --json # FIRST: kills all three panes + procs
TeamDelete()
cc-fleet default --json: if it returns a provider (source "configured" or "auto"), use it and STATE it in your kickoff line (e.g. "using glm (default)").cc-fleet list --json (name + default_model + the one-line note in cc-fleet-shared/providers.md). After they pick, run cc-fleet default <chosen> so you never ask again. (cc-fleet default <p> is user-layer; only run it to FILL a blank default, never with --force.)Model tier within a provider: fan-out / leaf work → omit --model (or --model fast); judge / synthesis / sustained work → --model strong. The provider's roster decides the actual model — see cc-fleet-shared/providers.md.
A teammate reports by calling SendMessage to the lead; the harness delivers it to you. Mode-independent — split pane or swarm pane, the pane is where it *runs*, never how you talk to it. Two provider-specific notes:
SendMessage: *"You appear done — reply with your result via SendMessage."* If the second ask still yields nothing, read the pane directly — don't bother the user: cc-fleet ps --json # → the teammate's tmux_socket + pane_id
tmux -L <tmux_socket> capture-pane -t <pane_id> -p | tail -40
Safe: the provider API key is never in the pane (resolved via apiKeyHelper, never printed). tmux_socket is empty for in-tmux teammates (plain tmux capture-pane) and cc-fleet-swarm-<team> for swarm teammates.
The one runtime difference from a native teammate. A provider teammate's brain *is* the provider API: on 429 / out-of-balance / 401 its claude process retries in a loop and never goes idle, never messages you — either would need the very LLM that's down. The error shows only in its tmux pane, never in your inbox. So you must poll.
cc-fleet ps --json --check. --check scans each pane and adds status (ok | error | unknown) plus error_class + detail on error — only the class, never raw pane text. Note the key: this runtime-wedge detection dispatches on error_class (lower_snake), a DIFFERENT key from the error_code (UPPER_SNAKE) that up-front spawn/subagent failure envelopes carry — don't switch on the wrong one. Dispatch:ok — keep waiting (within your ceiling).unknown — pane couldn't be captured (teammate exited / tmux down). Confirm with cc-fleet ps --json; treat a vanished teammate as failed.error — act now, per error_class:| error_class | Meaning | What you do |
|---|---|---|
| insufficient_balance | Provider out of balance / quota. | Retrying can't help. Tear down; STOP, tell the user, propose the next provider, wait for confirm (provider ask ladder, step 4). |
| auth | Provider rejected the key (401/403). | Tear down. Tell the user to rotate the key — file backend: cc-fleet edit <provider> --api-key-stdin <<<"$NEW_KEY" (or --api-key-file <path>); other backends via the secret manager. Don't re-spawn the same provider. Never the raw key in argv. |
| rate_limit | Provider 429. | Tear down; wait a bit and re-spawn, or propose a switch (confirm first). Never keep a wedged teammate looping. |
| api_error | Generic provider failure (5xx, overloaded, rejected). | Tear down; retry once, or propose a switch (confirm first). |
unknown or not specific enough → capture-pane and read it yourself (same command + key-safety note as above). ps --check is the first probe; the raw pane is a fine fallback.Tear down just the wedged worker (siblings keep running) by pane id: cc-fleet teardown <pane_id> --json, or the whole team with cc-fleet teardown <team> --json (then TeamDelete() if done). Then surface it and propose the fallback — another provider (re-spawn + re-SendMessage after the user confirms) or native Agent({subagent_type: "general-purpose", model: "sonnet", prompt: "<task>"}). Never leave a teammate wedged and keep waiting.
Either way you drive it with native SendMessage and it reports via SendMessage.
cc-fleet-swarm-<team> tmux server and runs the teammate there — silent unless you tmux -L cc-fleet-swarm-<team> attach. (A reusable, SendMessage-able teammate is an interactive process polling its inbox — needs a TTY; the truly-headless path is /cc-fleet:subagent.)Declutter the layout without killing the process:
cc-fleet hide <target> --json # pane → detached "claude-hidden" session; keeps running
cc-fleet show <target> --json # pane → back to its origin window, re-tiled
<target> = pane id %42 · team/member · name@team · bare team (every member with a pane). Origin is recorded at hide time.
SendMessage still work, teardown still cleans it.error_code: "SWARM_UNSUPPORTED" is a terminal no-op, not a tmux failure; attach to view instead.error_code, not prose: SWARM_UNSUPPORTED / TEAM_NOT_FOUND / MEMBER_NOT_FOUND / PANE_NOT_FOUND / NOT_HIDDEN / NO_ORIGIN / TMUX_FAILED / BAD_ARGS (malformed target) / CONFIG_WRITE_FAILED (the pane moved but the config write failed — tmux and config are now DIVERGENT; follow the envelope's suggestion to reconcile, never blindly re-run) / INTERNAL (lock/parse failure). Hiding an already-hidden pane is idempotent ok.Agents Board (human-facing): bare cc-fleet → Tab to a live board of every teammate across all teams (ps --check health, HIDDEN column, h/s hide/show). You use cc-fleet ps --json --check programmatically, not the TUI.
SendMessage — task delivery is always SendMessage. (Reading a pane for a result is fine.)TeamCreate before spawn → NO_LEAD_SESSION / TEAM_NOT_FOUND. Native TeamCreate first.ps --check.rm -rf ~/.claude/teams/... to tear down — skips pane/proc cleanup. cc-fleet teardown FIRST, then TeamDelete().apiKeyHelper; keys never enter env / ps aux / history..error_code — every --json failure carries a code (cc-fleet-shared/troubleshooting.md).Take ethanhq/team from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.