# How to compare OpenCode, Claude Code, and Codex CLI

> Compare OpenCode, Claude Code, and Codex CLI with one reproducible bug fix, independent tests, setup checks, and an honest look at cost and intervention.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-19 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/opencode-claude-codex/

I want a coding agent to make a correct change that I can review. A fluent explanation and a passing test suite are useful, but I also want to know what happened between the prompt and the result.

This guide gives you one small JavaScript project to compare [OpenCode](https://flaviocopes.com/opencode/), [Claude Code](https://code.claude.com/docs/en/overview), and [Codex CLI](https://flaviocopes.com/codex-cli/) on your own setup. It contains a search bug involving archived bookmarks and pagination.

I tried the fixture with the three CLIs while preparing it. OpenCode and Codex completed the repair; Claude Code stopped at authentication. The completed runs used different models and each tool's existing configuration. Their results appear below as examples of what to record, with those limits made explicit.

We'll prepare identical starting files, run each agent independently, and check the changes against acceptance cases we control. You can then compare the configurations you would use for real work.

## The bug we're asking them to fix

Our bookmark search returns a page of matching records. Ordinary search should exclude archived bookmarks. An explicit option includes them.

The broken implementation slices the matching records before removing archived ones. If the first match is archived, asking for one result returns an empty page even though a visible match exists.

The total is wrong too. It counts every matching record, including records ordinary search should exclude.

This is a small task, but it checks something useful. An agent can make the archived record disappear while leaving pagination broken. The acceptance criteria need to catch that incomplete repair.

The example runs with Node's built-in test runner. It doesn't need a framework, database, or package install. That keeps dependency downloads and environment setup out of the coding task.

## Create the starting project

Make a folder called `bookmark-trial` and save this as `search.mjs`:

```javascript
export const searchBookmarks = (records, {
  query = '', offset = 0, limit = 2, includeArchived = false
} = {}) => {
  const matching = records.filter(record =>
    record.title.toLowerCase().includes(query.toLowerCase())
  )
  const page = matching.slice(offset, offset + limit)
  return {
    items: page.filter(record => includeArchived || !record.archived),
    total: matching.length
  }
}
```

Save the initial failing tests in `search.test.mjs`:

```javascript
import test from 'node:test'
import assert from 'node:assert/strict'
import { searchBookmarks } from './search.mjs'

const records = [
  { id: 'a', title: 'CSS layout', archived: true },
  { id: 'b', title: 'CSS grid', archived: false },
  { id: 'c', title: 'CSS selectors', archived: false },
  { id: 'd', title: 'JavaScript arrays', archived: false }
]

test('fills the first page with visible matches', () => {
  assert.deepEqual(
    searchBookmarks(records, { query: 'CSS', limit: 1 }),
    { items: [records[1]], total: 2 }
  )
})

test('paginates the visible set', () => {
  assert.deepEqual(
    searchBookmarks(records, { query: 'CSS', limit: 1, offset: 1 }),
    { items: [records[2]], total: 2 }
  )
})

test('allows archived records when requested', () => {
  assert.deepEqual(
    searchBookmarks(records, {
      query: 'CSS', limit: 1, includeArchived: true
    }),
    { items: [records[0]], total: 3 }
  )
})
```

Run `node --test` before asking any agent to change the code. The first two tests should fail. The archive opt-in case should pass.

Make three independent copies of this starting folder. Don't give the second agent the first agent's repair. Keep your evaluation notes outside those folders so they don't become accidental hints.

On macOS, Node's installer or your usual package manager can provide Node. On Omarchy, use `omarchy pkg add nodejs npm` if needed. The source and tests work the same way on both.

## Give each agent the same prompt

This was the task:

```text
Fix searchBookmarks so filtering archived records happens before pagination
and totals describe the eligible matching set. Preserve input order and do
not mutate input records or options. By default exclude archived records;
includeArchived:true includes both archived and visible matching records.
Keep case-insensitive title substring search and the current exported API.
Do not add dependencies. Add useful tests, run node --test, and report what
you changed. Work only in this folder. Do not commit, push, access external
services, or read files outside this project.
```

The prompt states what the existing behavior means, including the archive option. It also limits the change to this small folder.

For a real comparison, I would keep the prompt unchanged after the first run. If I discover an ambiguous requirement, I would correct the task and restart all participants from the same initial files.

Changing the wording only for the agent that struggled makes the result difficult to interpret.

## Check setup before measuring coding work

Install and authenticate each CLI before starting a timed run. The [OpenCode tutorial](https://flaviocopes.com/opencode/) covers its current installation, and the other linked tutorials cover Claude Code and Codex CLI.

OpenCode's model choice comes from a provider and model combination. Claude Code has its own account and model selection. Codex CLI also depends on the model available through the signed-in account.

Record those settings. A local model through Ollama and a hosted model through a subscription are different configurations, even when the task prompt is identical.

Check the executable too. On this machine, `opencode` reached an older installation while `opencode2` reached the current release. A command name alone wasn't enough to establish which program ran.

I would keep installation and authentication time separate from repair time. Setup still matters when choosing a tool, but a failed login isn't evidence about its ability to fix JavaScript.

## Run the agents independently

You can run this interactively by opening each tool in its own copy and pasting the prompt. That is the best way to assess the interface you expect to use daily.

For the recorded trial, I used noninteractive commands and saved their event output. OpenCode's [CLI documentation](https://opencode.ai/v2/docs/) describes `run`. With the current v2 executable selected, the command is:

```bash
opencode2 run --standalone --format json "$(cat ../prompt.txt)"
```

Save the prompt next to the three folders as `prompt.txt` before using these commands. The standalone option uses a private server for the run. It doesn't isolate the agent from the operating system.

Claude Code supports [print mode](https://code.claude.com/docs/en/headless):

```bash
claude -p "$(cat ../prompt.txt)" --output-format json \
  --permission-mode acceptEdits \
  --allowedTools 'Read,Edit,Write,Bash(node --test)'
```

The allowed tools are deliberate for this trusted fixture. Review the permissions for your own project. Shell patterns and tool names are specific to the agent; don't assume one product's settings carry over to another.

Codex supports [`exec`](https://learn.chatgpt.com/docs/non-interactive-mode):

```bash
codex exec --skip-git-repo-check --sandbox workspace-write \
  --ephemeral --json "$(cat ../prompt.txt)"
```

`--skip-git-repo-check` is useful here because our disposable fixture needn't be a Git repository. Leave that override out when working in your normal repository.

Choose an available model explicitly if the account's default doesn't work, and record that change. In this trial, the configured Codex model was rejected before editing began. I retried with a model listed as available to the CLI account.

These shell examples use command substitution in macOS's default shell. In fish, paste the prompt directly or use fish's supported substitution syntax. The code under test is shell-independent.

## Example observations from the fixture

These observations are from October 15, 2026. Each tool received the same task and initial files.

| Tool and configuration | Observed outcome | Independent acceptance cases |
| --- | --- | --- |
| OpenCode v2 with `qwen3.5:35b-a3b-coding-nvfp4` through Ollama | Repaired the function and finished with 18 passing tests. | 8 of 8 passed. |
| Claude Code | Stopped with an expired OAuth session before changing code. | No completed implementation to evaluate. |
| Codex CLI with `gpt-5.6-sol` | After replacing a rejected default model, repaired the function and finished with 6 passing tests. | 8 of 8 passed. |

The completed OpenCode process took about 149 seconds. The successful Codex retry took about 23 seconds. Those are single observed process durations, including startup, rather than repeated model-speed measurements. The failed setup attempts are excluded from those two durations.

The hardware and model-serving conditions weren't held constant. I wouldn't use these times to claim one CLI is faster. A repeat comparison needs the provider, model, and runtime conditions recorded alongside several runs.

Neither successful repair needed a follow-up prompt. I did intervene in Codex setup by selecting an available model. Claude would need authentication before its coding attempt could begin.

The two implementations moved archive filtering before pagination and calculated the total from that eligible set. That is the behavior the task required.

## Run acceptance checks outside the agent's tests

An agent can add many tests that repeat the same assumption as its code. The number of tests it writes doesn't establish how much behavior it covered.

I checked the completed implementations against these cases independently:

- The first page contains the first visible match.
- The second page contains the next visible match.
- Archive opt-in includes both archived and visible matches.
- Search remains case-insensitive.
- An empty query keeps the documented default page size.
- A query with no matches returns an empty result and zero total.
- A zero-sized page still reports the correct total.
- Frozen records and options can be used without mutation errors.

The prompt disclosed the requirements but didn't provide this acceptance file to either agent. Both completed repairs passed it afterward.

I would also inspect the diff. Does it preserve the API? Did it add unrelated validation or change the meaning of a missing option? Are the new tests readable enough to keep?

For this small task, both diffs were straightforward to review. A passing fixture says little about handling a large repository, browser state, or an unfamiliar database. Those need separate tasks. For a larger change, I would write those acceptance cases before starting the agent.

## Compare cost without inventing a price

OpenCode reported zero cost in its event records for the Ollama-backed run. That isn't a measurement of hardware, electricity, or the cost of operating its model server.

Codex returned token usage for the completed retry. That doesn't give a per-task invoice when access comes through a subscription. Cache accounting and provider pricing also make raw token totals unsuitable for a direct comparison across these configurations.

Claude made no successful inference call in this trial. Its failed request doesn't provide a useful cost comparison either.

For a pay-as-you-go evaluation, I would use provider billing records and record cache hits and retries. For a subscription, I would track how much useful work fits into the actual account limits. Keep those observations separate from the monthly headline price.

The time I spend reviewing and correcting the result belongs in the comparison too. A cheap first attempt that needs repeated repair can consume more of my day than an expensive call that produces a small, correct diff.

## Run your own comparison

Check authentication before starting, then give each agent a fresh copy of the fixture and the unchanged prompt. Record a setup failure separately from a coding result. Repeat completed runs before interpreting their timing.

After this bug, I would choose a task with more repository context: change a route, preserve an existing data format, and exercise a browser flow. Keep its acceptance criteria explicit and record each intervention.

I would also try my preferred interactive workflow. A print-mode run can show whether an agent completes a bounded task. It doesn't show whether its permission prompts and diff review fit the way I like to work.

Keep the starting files and acceptance checks beside your notes. When a model or tool changes, you can repeat the same task and compare its behavior with the configuration you already use.
