Using OpenCode
Run a model profile's soldiers on OpenCode instead of Claude Code, on OpenCode 1.x or 2.x, including its free Zen models.
A model profile can name a harness, the agent CLI its soldiers run on. With "harness": "opencode" the soldiers of that profile run on OpenCode, which can use many providers and models, including free ones. The commander stays in Claude Code unless you choose otherwise (see OpenCode as the commander), and a task keeps its harness through vx redispatch, vx prompt, vx decide, vx land and vx strike.
OpenCode has two generations that share the opencode command, and vexillum drives both:
- 2.x (checked against 2.0.32). Its TUI has no
--modelflag, so vexillum creates the soldier's session on that model through OpenCode's background service and opens it. - 1.x, from 1.18.29. Its TUI takes
--model.
vx dispatch runs opencode --version and records the major series on the task, and vx redispatch, vx prompt and vx decide use the same one. A task dispatched on 2.x is not relaunched on 1.x: install the series it was dispatched on, or strike it and dispatch again. Nothing about the version is cached between commands.
Only some free models keep your code private
A soldier sends whatever it reads to the model provider: your prompts, the files it opens, command output and anything in your environment that a command prints. OpenCode's own docs (opencode.ai/docs/zen, read on 2026-10-19) name four of Zen's free models as zero-retention: glm-5.3-flash-free, deepseek-v4.1-flash-free, minimax-m3.1-flash-free and longcat-2.5-preview-free. Several others are documented as collecting data to improve the model or as retaining it for 30 days, and a free model the page does not mention, such as space-bunny-free, is not documented as zero-retention, so treat it as logging everything. A free model also gives no guarantees as a commander. Terms change, so read them before you point a profile at a free model, and do not use one for private or proprietary code, or on a machine whose files or environment hold secrets. OpenCode, like Claude Code in vexillum, has full host access under your user.
Only OpenCode 2.x was verified live
The 2.x launch, the blocked-screen handling and the one-shot used by vx ship were run on a real OpenCode 2.0.32. The 1.x paths come from the 1.18.29 and 1.19.3 sources and are covered by tests against a fake opencode that reports 1.x, not by a real 1.x. docs/verification/opencode-soldier-checklist.md in the repository lists every step still to confirm.
Set it up
-
Install OpenCode, then run
opencodeonce and connect a provider. The free Zen models need no key.opencode modelslists what you can use. -
Install herdr's integration for OpenCode, so herdr reads a soldier's state (working, idle, blocked) from OpenCode itself instead of from the screen:
herdr integration install opencode -
Run the OpenCode TUI once after each OpenCode upgrade, before you dispatch. A new version can show a "what's new" dialog on its first start. Nobody is at a soldier's pane to dismiss it, so the soldier would sit on it.
-
Run
vx doctor. When a profile selectsopencode, or anopencodebinary is on yourPATH, it reports the binary, its version (1.x from 1.18.29, or 2.x), whether an older 1.x install shadows a 2.x one earlier inPATH, and the herdr integration. They only warn: a missing piece never makes the environment not ready, and a setup without OpenCode sees none of these lines.
Add a profile
Add a profile with "harness": "opencode" to .vexillum/models.json (or the global ~/.vexillum/models.json). The same profile works on 1.x and 2.x:
{
"profiles": {
"opencode-scout": {
"model": "opencode/glm-5.3-flash-free",
"effort": "low",
"when": "Scout whose question reduces to verifiable facts, run on a free zero-retention model.",
"harness": "opencode"
},
"opencode-mission": {
"model": "anthropic/claude-sonnet-5#high",
"effort": "medium",
"when": "Mission on OpenCode with Sonnet at its high variant.",
"harness": "opencode"
}
}
}vx dispatch "which flags does the build script accept?" --kind scout --profile opencode-scoutOn an OpenCode profile the two fields mean:
| Field | Value | What happens |
|---|---|---|
model | provider/model, with an optional #variant, such as opencode/glm-5.3-flash-free or anthropic/claude-sonnet-5#high | the soldier starts on that model, and on that variant when there is one |
effort | any plain label, such as low or medium | recorded on the task and shown in vx status, never passed to OpenCode |
vexillum checks the shape when it reads the profile. On 2.x, vx dispatch also checks the provider, model and variant against what OpenCode lists for the camp (see Which model a soldier gets); on 1.x OpenCode reports a model it does not have when the soldier starts. Effort is not mapped to a variant because variant names differ per model: one model's high is another model's error. A variant is used only when the profile writes one after the #.
A variant the model does not have is an error. On 2.x vx dispatch refuses it and names the variants the model has. A one-shot (vx ship's reviewer and fixer, which use opencode run) fails with Variant unavailable for <model>: <variant>. The footer of the soldier's pane shows the variant, such as · high, when it applies; check it the first time you use a variant.
Which model a soldier gets
On 2.x the TUI cannot be told a model on its command line, and the configuration that decides the model is read by OpenCode's shared background service, not by the TUI. A file written into the camp, or an environment variable on the pane, would either clash with a project's own OpenCode configuration or be ignored. So vx dispatch gives each soldier a session of its own:
- The service. vexillum makes sure OpenCode's background service is running. If it is not, vexillum starts it itself with
opencode service start, from an environment withOPENCODE_CONFIG,OPENCODE_CONFIG_DIRandOPENCODE_CONFIG_CONTENTremoved. The service reads those variables once, when it starts, and keeps them for every OpenCode window that connects to it afterwards.opencode apiwould start a stopped service with whatever environment its caller has, and so would the soldier's own TUI, so vexillum starts the service first and never leaves it to them. A service that is already running is used as it is. - The check. vexillum asks the service which models it can run for the camp and checks the profile's provider, model and variant against that list. When one is not there, the dispatch fails before any pane exists and names what does exist, for example
OpenCode's provider "anthropic" has no model "claude-sonnet-9"; its models: .... The task is leftfailed, andvx strikeremoves it. - The session. vexillum creates a session located in the camp, with the profile's model and variant, titled
vexillum <task-id>. It sets no agent and no permission rules. - The start. The soldier starts as
opencode --auto --session <id>, withOPENCODE_DISABLE_AUTOUPDATE=1in its pane.
A session's model ranks above the project's own model and above its default agent's own model, so a project that sets its own default agent or model does not override the profile. Only the soldier's own agent runs on the session's model: OpenCode's built-in subagents keep the models they are configured with, and the soldier can still switch model in its TUI.
Nothing is written into the camp or the project, no environment variable carries configuration, and no line is added to info/exclude. A project's own opencode.json or opencode.jsonc, its agents (even one named vx-soldier), its default agent and its permission denies apply exactly as they do when you run OpenCode there. A repository that tracks OpenCode configuration dispatches like any other, and a mission can create opencode.json or .opencode/ without git add -f. A task with no model starts a plain OpenCode on the project's defaults, with no session.
The session id is recorded on the task (harness_session in vx status --json). vx redispatch opens the same session again when the service still has it and it is located in the camp the task gets back, so the conversation continues. If the service lost it (it was deleted, or the service's data was reset) or the camp is another one, vexillum creates a new session. vx strike does not delete the session: it stays in OpenCode's session list, titled vexillum <task-id>, until you delete it there.
On 1.x nothing is created or written: the model goes with --model, and a variant with the OPENCODE_CONFIG_CONTENT environment variable of the soldier's pane. Both generations set OPENCODE_DISABLE_AUTOUPDATE=1, so a running soldier is not updated under you.
Older versions of vexillum wrote .opencode/agents/vx-soldier.md and opencode.json into the camp of a 2.x soldier and added /.opencode/ and /opencode.json to the repository's info/exclude. A reused camp that still holds those files has them removed before the next launch, but only when they are byte-identical to what vexillum wrote, so an edit of yours is never deleted. The lines in info/exclude are left alone, because a soldier started by an older version may still depend on them. They do nothing now, apart from hiding an untracked opencode.json in your main checkout from git status; delete them from .git/info/exclude when no older soldier is running.
Permissions
A soldier starts with --auto, which approves every permission that is not explicitly denied. vexillum sets no permission rules of its own, in the session or anywhere else. In 2.x the last matching rule wins, so a wildcard rule of vexillum's would override the denies in your own OpenCode config. Without a rule of its own, your denies keep applying inside a soldier. As with every harness, landing is the control: nothing reaches your history until you approve vx land.
What differs from a Claude Code soldier
| Topic | Claude Code soldier | OpenCode soldier |
|---|---|---|
| Permissions | --dangerously-skip-permissions | --auto; your own deny rules still apply. See above. |
| Model | --model and --effort | --model on 1.x; a session created for the soldier on 2.x. Effort is recorded, not passed. |
| First prompt | typed into the pane | delivered through herdr agent prompt once the pane is ready. vx dispatch waits for an agent_not_ready start error to clear before it treats the start as failed. |
| Rules | CLAUDE.md, which imports AGENTS.md | OpenCode reads AGENTS.md and, only when there is none, CLAUDE.md. It does not expand @AGENTS.md or any other @file reference inside them. The vexillum block lives in AGENTS.md (or in CLAUDE.md when the project has no AGENTS.md), and OpenCode reads whichever holds it. |
| Plugins | n/a | V1 plugins do not run in V2. A project that relies on one for its workflow behaves differently under a 2.x soldier. |
| Skills | the project's .claude/skills | none. Soldiers do not use skills in this release. |
| Questions | Claude's question selector | OpenCode's own question form and permission prompt. See below. |
vx ship | the tribunal's reviewer and fixer run claude -p | they run opencode run --auto --format json on the mission's model and variant, with no input on stdin |
Blocked soldiers
With --auto a permission prompt is rare, and the usual way an OpenCode soldier blocks is its question form. vx decide answers what it can recognize, on 2.x only, and reads the pane again right before it types so it never types into a screen that has moved on:
| Screen | vx decide <task-id> <answer> |
|---|---|
| Permission prompt | allow once presses Enter. reject presses Esc, and the soldier sees "The user declined this tool call". always allow is refused: it may save a rule for good, and a one-line answer cannot carry that decision. |
| Question form | an option's number (2) or its exact text selects it with arrow-down presses and Enter. Any other text is typed into the form's "Type your own answer" field, one key press per character, so it is limited to printable ASCII. A bare number that is not an option is refused instead of typed. |
| A screen vexillum does not know, or any 1.x screen | refused, with nothing typed. Attach to the pane with herdr and answer there. |
No blocked screen (the soldier ended its turn with a needs-decision: line) | the answer is delivered as plain text, like every other harness |
herdr itself rejects herdr agent prompt with agent_blocked while a pane is blocked, which is why a typed answer goes in through key presses. See Blocked and interrupted.
Not in this release
- Mapping
effortto a variant. - Structured answers for a question form of several questions.
- Skills for OpenCode soldiers.
For a checklist to verify an OpenCode setup, 1.x in particular, on a real machine, see docs/verification/opencode-soldier-checklist.md in the repository.