Headless workers and adapters
contained_run is the one-shot reviewer surface in ContextRelay.
headless_runThe tool used to be called headless_run. It collided with
Headless, a separate product that ships an MCP
tool of the same name, so an agent told to "use headless" would reach for the wrong one. The old
name is still accepted for one version and still works in CONTEXTRELAY_MCP_TOOLS allowlists, but
it is no longer advertised.
The feature is still called headless workers — only the tool identifier changed.
It does not start a paired Claude + Codex session. It runs a single read-only terminal-coder job, gives it a prompt plus optional context refs, records a headless_result artifact, and returns the worker output to the caller.
Use it when you want an independent pass:
Review this diff for release blockers. Findings first. Cite file:line evidence.
Use a normal handoff or deliberation when you want the live peer agent to own follow-up work.
How to invoke it
From Codex or Claude, call the MCP tool:
{
"agent": "codex",
"prompt": "Review the current diff for correctness regressions.",
"context_refs": ["git diff", "src/backup/runner.ts"],
"label": "runner-review",
"timeoutMs": 180000
}
From a shell, use the daemon-free CLI wrapper:
ctxrelay headless run \
--target codex \
--prompt "Review this diff for release blockers. Findings first."
For file-backed prompts:
ctxrelay headless run \
--target claude \
--prompt-file .contextrelay/review-prompt.txt \
--json
The CLI is for one job. If you want several reviewers at once, call contained_run multiple times from an agent workflow; the daemon runs them through a bounded pool.
Built-in adapters
contained_run.agent is not hard-coded to two choices. It is populated from registered, contained HeadlessAdapters.
Current built-ins:
| Adapter | Runtime | Prompt delivery | Containment |
|---|---|---|---|
codex | codex exec --json --sandbox read-only ... - | stdin | Native Codex read-only sandbox |
claude | claude -p --output-format stream-json --allowedTools Read,Grep,Glob,LS | stdin | Restricted Claude Code tool surface |
opencode | opencode run --format json --pure --dir <repo> | argv | Darwin sandbox-exec write-denial profile plus opencode tool denies |
opencode is conditional. It is registered only when:
- the host is Darwin/macOS,
opencodeis onPATH, and- the write-denial sandbox probe passes.
If you want to run ctxrelay headless run --target opencode, install and authenticate the opencode CLI first, then start ContextRelay from an environment where opencode is on PATH. If any condition is missing, the adapter is not registered and the target is rejected instead of falling back to an uncontained run.
To hide only the opencode adapter on a machine that otherwise supports it, set CONTEXTRELAY_OPENCODE_ENABLED=0 before starting the daemon or running the daemon-free CLI command. To disable the whole headless surface, use CONTEXTRELAY_HEADLESS_ENABLED=0.
That is why some machines expose only codex and claude, while supported macOS setups expose codex, claude, and opencode.
Adapter extensibility model
Adapters are not external runtime plugins today. There is no config setting, package hook, or local drop-in directory that lets an installed npm user add an adapter without changing ContextRelay source.
The supported extension path is a PR to this repository:
- Implement a
HeadlessAdapterinsrc/backup. - Register it in the in-process adapter registry.
- Prove read-only containment (
containment.enforced === true) or keep it unavailable until a probe proves containment. - Add tests for command construction, env allowlisting, output parsing, containment failure, and tool-schema exposure.
- Document install/auth prerequisites and any kill switches.
After review and release, the adapter ships through the normal npm package. This is intentional for now: a headless adapter launches subprocesses, filters credentials, delivers prompts, parses model output, and asserts read-only containment, so unreviewed runtime loading would need a separate trust model.
What happens when a reviewed adapter lands
The public tool does not need to change.
Once a new adapter is registered and its containment is enforced, it appears in the contained_run.agent enum:
containedHeadlessAdapterIds()
The dispatcher then validates the requested agent with:
isContainedHeadlessTarget(agent)
So a new contained adapter is invoked the same way as the built-ins:
{
"agent": "my-adapter",
"prompt": "Review this change for missing tests."
}
If an MCP server or daemon process is already running after upgrading to a release that includes a new adapter, restart it so the registered adapter list and generated tool schema are refreshed.
Adapter contract
Adapters implement HeadlessAdapter in src/backup/adapters.ts:
export interface HeadlessAdapter {
id: HeadlessTarget;
promptDelivery: "stdin" | "arg";
buildCommand: (request: BackupRunRequest) => { command: string; args: string[] };
buildEnv: (sourceEnv: NodeJS.ProcessEnv, mode: BackupRunMode) => NodeJS.ProcessEnv;
prepareRun?: (
request: BackupRunRequest,
sourceEnv: NodeJS.ProcessEnv,
mode: BackupRunMode,
) => {
command: string;
args: string[];
cwd: string;
env: NodeJS.ProcessEnv;
cleanup?: () => void;
};
parseOutput: (stdout: string) => {
output: string;
cost: number | null;
tokens: number | null;
};
credentialPrefixes: string[];
containment: Containment;
defaultMode: "read-only";
groundingPrompt?: string;
}
The important fields:
id: thecontained_run.agentvalue.promptDelivery: whether the prompt is sent over stdin or appended as the final argv value.buildCommand: the default command shape.buildEnv: the environment allowlist. Do not forward broad provider credentials unless the adapter needs them.prepareRun: optional per-run setup for sandboxes, temporary homes, auth copying, profiles, and cleanup.parseOutput: converts runtime stdout into user output, USD cost, and token counts.containment: the proof that this adapter is safe for read-only dispatch.groundingPrompt: optional preamble prepended in read-only mode before the user prompt.
contained_run rejects adapters whose containment.enforced is not true.
Minimal registration pattern
Create the adapter, then register it:
import { registerHeadlessAdapter, type HeadlessAdapter } from "./adapters";
const myAdapter: HeadlessAdapter = {
id: "my-adapter",
promptDelivery: "stdin",
credentialPrefixes: ["MY_ADAPTER_"],
containment: { kind: "native-readonly", enforced: true },
defaultMode: "read-only",
buildCommand: () => ({
command: "my-adapter",
args: ["run", "--json", "--read-only", "-"],
}),
buildEnv: (sourceEnv) => ({
PATH: sourceEnv.PATH,
HOME: sourceEnv.HOME,
CONTEXTRELAY_WORKER: "1",
}),
parseOutput: (stdout) => ({
output: stdout.trim(),
cost: null,
tokens: null,
}),
};
registerHeadlessAdapter(myAdapter);
That pattern is enough for a runtime that already has its own read-only containment.
If the runtime does not have native read-only containment, use prepareRun to wrap it in an OS-level sandbox and keep containment.enforced false until the probe proves the sandbox works. The opencode adapter is the reference implementation for that pattern.
Grounding prompts
groundingPrompt exists for runtimes that do not already self-ground their role and tools.
Only opencode currently sets it. opencode run --pure is intentionally minimal, so ContextRelay prepends a read-only preamble that names the real tools (read, glob, grep, list), denies edits/shell/web/subagents, and requires file:line evidence.
Codex and Claude do not set groundingPrompt; their native headless runtimes already receive their own mode/tool constraints.
Budgets and output shaping
The daemon path is pooled and optionally budgeted:
CONTEXTRELAY_HEADLESS_MAX_CONCURRENCYCONTEXTRELAY_HEADLESS_MAX_QUEUECONTEXTRELAY_HEADLESS_TIMEOUT_MSCONTEXTRELAY_HEADLESS_BUDGET_USD
Under usage-control lean or strict, ContextRelay budgets the worker prompt and caps the returned output with head/tail preservation so the final verdict is less likely to be truncated away.
Related pages
- MCP tools reference - the exact
contained_runtool shape. - CLI command reference - the
ctxrelay headless runcommand. - Autonomy, idle scanner, and safe automation - how headless differs from backup-agent autonomy and idle workers.