delegate-local · v2.0.1

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.