Run scoped tasks on a local opencode runtime as background subagents.
A plugin for Claude Code and Codex CLI that wraps a locally installed opencode runtime as a background subagent. Send a self-contained task brief, get a handle back immediately, and collect a compact structured result later. Multi-turn continuation and parallel fan-out included.
The point is context, not convenience: a supervisor follows a compact, resumable live stream of opencode's completed text/tool/error events, while the raw NDJSON and full result JSON stay out of the conversation. The caller sees a verdict line and an answer.
Everything lives in the CLI, and every host is a thin adapter over it — which is what lets one implementation serve two harnesses with different capabilities.
Install
/plugin marketplace add tuomashatakka/delegate-local-plugin
/plugin install delegate-local@tuomashatakka-tools
If the install summary says Run /reload-plugins to activate., do that.
For local development, add the working copy as a marketplace:
/plugin marketplace add /path/to/delegate-local
Installed plugins are copied into a version-pinned cache
(~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/), and
/plugin marketplace update does not refresh a copy that is already there. To pick up local
edits, either bump version in plugin.json or reinstall:
claude plugin uninstall delegate-local
claude plugin install delegate-local@tuomashatakka-tools
Requires opencode and python3 on PATH — nothing else, no pip install. Run
/delegate-local:delegations (or delegate-local doctor) to check the runtime.
Permissions
The runner works in the background, where a permission prompt stalls the whole run. Allowlist
the shim once, in ~/.claude/settings.json or the project's .claude/settings.json:
{ "permissions": { "allow": ["Bash(delegate-local:*)"] } }
Plugin subagents cannot set permissionMode themselves — this rule is the supported way to
grant it, and the bin/ shim exists so that one stable pattern covers every subcommand.
Codex CLI
The same core runs under Codex. Register the MCP server once:
codex mcp add delegate-local -- /path/to/delegate-local/bin/delegate-local mcp
Then copy the skill and the runner where Codex looks for them, and allow the command:
mkdir -p ~/.codex/skills ~/.codex/agents
ln -s /path/to/delegate-local/skills/delegate-local ~/.codex/skills/delegate-local
cp /path/to/delegate-local/codex/agents/runner.toml ~/.codex/agents/
cat /path/to/delegate-local/codex/rules.delegate-local.rules >> ~/.codex/rules/default.rules
codex/ holds the Codex-side assets and a README explaining each one. The skill itself is
shared verbatim — the judgment about when delegating is worth it does not depend on the host.
Codex has no equivalent of a plugin monitor, so to be told when a delegation finishes, symlink
the completion hook and merge one SessionStart hook (details in codex/readme.md):
STORE="${DELEGATE_LOCAL_HOME:-$HOME/.local/state/opencode-delegate}"
ln -sf /path/to/delegate-local/codex/notify-codex.sh "$STORE/notify"
It queues one line into the live session via codex queue. Any host can use the same hook —
$STORE/notify runs on every terminal transition with the run's facts in the environment.
What differs between hosts
Three Codex limits shape the design, and they are why all the intelligence sits in the CLI:
| Claude Code | Codex CLI | |
|---|---|---|
| Skill | skills/<n>/SKILL.md |
same file, from ~/.codex/skills/ |
| Subagent | agents/runner.md |
~/.codex/agents/runner.toml |
| Push into a live session | Monitor |
none — but codex queue --thread <id> injects |
| MCP tool timeout | generous | 60 s by default |
| MCP progress notifications | consumed | received then dropped (codex#28003) |
| MCP resources | readable | never surfaced to the agent (codex#4956) |
| Command allowlist | permissions.allow |
~/.codex/rules/default.rules |
Consequences: no MCP tool blocks (spawn returns a handle, you poll), progress
notifications are never a mechanism, and raw logs come from the explicit delegate_logs tool
rather than a resource.
What it ships
| Component | Name | Role |
|---|---|---|
| Agent | delegate-local:runner |
Executes one brief: spawn, stream, report. Background, haiku, Bash only. |
| Skill | delegate-local |
When delegating is worth it, how to write the brief, how to read the result. |
| Command | /delegate-local:delegate |
Compose a brief for a task and hand it to the runner. |
| Command | /delegate-local:delegations |
In-flight runs and runtime health. |
| MCP server | delegate-local mcp |
The protocol as MCP tools — the surface both hosts share. |
| Monitor | delegate-local watch |
One line per run transition, for the whole store, from one process. |
| Binary | delegate-local |
The protocol CLI, on the Bash tool's PATH. |
The runner is deliberately a dumb pipe — it does not author briefs and does not retry on failure. Both of those need context it doesn't have, so they stay with the caller.
Usage
/delegate-local:delegate --dir src/api add docstrings to every exported function
Or drive the CLI directly:
delegate-local list # recent runs, status, lineage
delegate-local report <id> # verdict line + the answer, nothing else
delegate-local stream <id> --cursor me --deadline 540 --heartbeat 60
delegate-local watch --all --level milestones # one line per transition
delegate-local logs --stderr <id> # warnings that never reach the result
delegate-local cancel <id>
delegate-local prune --keep 50 # the run store grows forever otherwise
delegate-local doctor # live round-trip against the configured default
Subcommands: spawn, status, stream, watch, result, report, wait, send, list,
cancel, logs, serve, prune, doctor, mcp.
The warm server
By default each delegation runs opencode run, which boots its own embedded server on a
random port for that one run — a full config, provider and MCP startup every time. Starting
one shared server instead and using --runtime server measured 5.5 s against 28 s for the
same brief on the same model:
delegate-local serve # start or reuse one; idempotent
delegate-local spawn --runtime server --dir src/api "…"
delegate-local serve --stop
It is opt-in rather than the default because it moves where the delegate's project scope
lives, and because the subprocess path is the one opencode itself exercises. Note that
opencode run --attach is deliberately not how this works: combining --attach with
--dir silently emits a single step_start and exits 0, so it would turn a warm server into
a delegation that confidently does nothing. The HTTP path scopes work with ?directory=,
which is supported.
See skills/delegate-local/SKILL.md for the full protocol
and skills/delegate-local/references/opencode-cli.md
for the reverse-engineered CLI flags, event-stream contract and HTTP API.
Layout
.claude-plugin/
plugin.json manifest
marketplace.json the repo is its own marketplace
.mcp.json registers the MCP server with Claude Code
CHANGELOG.md what changed, and which claims were retracted
agents/runner.md → delegate-local:runner
commands/ → /delegate-local:delegate, /delegate-local:delegations
monitors/monitors.json one store-wide run-transition monitor
codex/
readme.md setup, and the three Codex limits that shaped the design
agents/runner.toml the runner, as a Codex subagent
rules.delegate-local.rules execpolicy allowlist for the CLI
hooks.snippet.json SessionStart hook to merge (do not copy over yours)
record-thread.sh records the session id for the completion hook
notify-codex.sh queues a line back when a delegation finishes
bin/delegate-local shim onto the Bash tool's PATH
skills/delegate-local/
SKILL.md
scripts/delegate.sh stable entry point; dispatches to _cli.py
scripts/_cli.py every subcommand, one process per invocation
scripts/_events.py one event reducer + the resumable stream follower
scripts/_runtime.py subprocess and warm-server adapters
scripts/_store.py run store: ids, status, atomic writes, retention
scripts/_mcp.py stdio MCP server, stdlib only
references/opencode-cli.md
Development
claude plugin validate . --strict
bash -n skills/delegate-local/scripts/delegate.sh bin/delegate-local
python3 -m py_compile skills/delegate-local/scripts/*.py
python3 -m unittest discover -s tests -v
CI runs the same checks on every push. There is no release artifact — plugins install straight from git.
The public page
https://tuomashatakka.github.io/delegate-local-plugin/ is generated from this file.
scripts/build_page.py turns the # heading and the prose above the first ## into a
hero, each ## section into a page section, and reads the title, version and blurb from
plugin.json so the manifest stays the single source of truth. Output is deterministic,
which is what lets CI commit it back only when it genuinely changed.
python3 -m venv .venv && .venv/bin/pip install -r scripts/requirements.txt
.venv/bin/python scripts/build_page.py # write public/index.html
.venv/bin/python scripts/build_page.py --check # exit 1 if stale
Editing public/index.html by hand is pointless — the next push to main overwrites it.
Change readme.md instead.