OpenCode: run it headless in scripts and CI, with JSON output
Use opencode run non-interactively with --format json and a read-only agent so a pipeline gets machine-readable events and can never edit or execute.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Anyone running OpenCode in scripts or CI. CI validates that the headless invocation uses a named agent (plan) and --format json, and that the opencode.json it runs under denies edit and bash to that agent. No key, no model call. The actual run is fenced.
Not for
- Running a headless build agent in CI, run honors your permission config so it could edit and execute; keep CI on a read-only agent
- Expecting CI to execute the run, only that the invocation and config are safe and well-formed
The Stack
Tested Against
opencode@1.17.4opencode.ai/docs (2026-06)node@20.xSide effects & data flow
- Network
- none, local only
- Writes
- ./opencode.json, ./ci-review.sh
- Credentials
- none required
Prerequisites
- OpenCode installed
- A provider logged in to actually run it
Steps
- 1
Write a read-only headless invocation and validate it
opencode run executes a prompt non-interactively; --format json gives machine-readable events. Pin the CI run to the read-only plan agent. For PRs, `opencode github install` wires a GitHub Actions workflow. CI checks the invocation uses --agent plan --format json and that plan denies edit/bash.
cat > opencode.json <<'JSON' { "$schema": "https://opencode.ai/config.json", "agent": { "plan": { "mode": "primary", "model": "provider/your-model", "permission": { "edit": "deny", "bash": "deny" } } } } JSON cat > ci-review.sh <<'SH' #!/usr/bin/env bash opencode run --agent plan --format json "Review the uncommitted changes for bugs and security issues" SH node -e 'const fs=require("fs");const c=JSON.parse(fs.readFileSync("opencode.json","utf8"));const p=c.agent.plan.permission;const safe=p.edit==="deny"&&p.bash==="deny";const cmd=fs.readFileSync("ci-review.sh","utf8");const usesPlan=cmd.indexOf("--agent plan")>=0;const usesJson=cmd.indexOf("--format json")>=0;if(safe&&usesPlan&&usesJson){console.log("config OK: headless run uses --agent plan --format json, plan is read-only (edit/bash deny)")}else{console.log("BAD");process.exit(1)}' - 2
Wire it into the pipeline (the model step, not checked by CI)
Run the script in CI, or attach fast runs to a warm server: `opencode serve &` then `opencode run --attach http://localhost:4096 -m provider/your-model "..."`. The actual run executes the model, so CI never claims it.
Eval, 2 fixtures
Last passed: verified todayheadless-safecontainstimeout 30s · max $0Expected:
config OK: headless run uses --agent plan --format json, plan is read-only (edit/bash deny)clean-exitexit_codetimeout 30s · max $0Expected:
0
Results
The same agent you use interactively, now in a pipeline. Keep CI runs on a read-only agent and treat --dangerously-skip-permissions as the loaded gun it is named after.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).