CodingOpen SourceFreeActiveMachine-verified· intermediate · ~10 min setup

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.

by Shilpa Mitra· verified today· v1.0.0

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.x

Side 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. 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. 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 today
  • headless-safecontainstimeout 30s · max $0

    Expected: config OK: headless run uses --agent plan --format json, plan is read-only (edit/bash deny)

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 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).