OpenCode vs Codex CLI on one bug fix, and where Claude Code stopped

By

Updated

Tagged: AI

I ran OpenCode and Codex CLI on the same bug fix and checked both with my own tests. Claude Code stopped at sign-in. Here's the fixture to repeat it.

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 post gives you one small JavaScript project for comparing AI coding tools on your own setup. It contains a search bug involving archived bookmarks and pagination.

I ran it with three CLIs: OpenCode, Codex CLI and Claude Code. OpenCode and Codex fixed the bug, and the results below come from those two runs. Claude Code stopped at sign-in before changing any code, so there’s no Claude Code result here, only the command to run it yourself.

The two completed runs also used different models and each tool’s existing configuration, so read them as examples of what to record, not as a ranking.

We’ll prepare identical starting files, run each agent independently, and check the changes against acceptance cases we control.

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.

Filter before paginating bookmark resultsOrdinary bookmark search must remove archived matches before counting and slicing, so an archived first match does not hide visible results.Matching bookmarksExclude archivedunless opted inEligible matchesCount totalSlice requested pageReturn page andtotal

It’s a small task, and an agent can make the archived record disappear while leaving pagination broken. The acceptance criteria need to catch that incomplete repair. I wrote about that habit of stating the expected behavior in What teaching can tell us about prompting.

The example runs with Node’s built-in test runner, with no framework, database, or package install, so dependency downloads and environment setup stay out of the coding task.

Create the starting project

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

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:

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.

Give each agent the same prompt

This was the task:

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, and limits the change to this folder.

Keep the prompt unchanged after the first run. If you discover an ambiguous requirement, correct the task and restart every agent from the same initial files. Rewording it only for the agent that struggled makes the result hard to interpret.

Check setup before measuring coding work

Install and authenticate each CLI before starting a timed run. The OpenCode tutorial 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 with the same prompt.

Check the executable too. If an older OpenCode is still installed, opencode can run that one, while the v2 release runs as opencode2. Run which or check the version before you start.

Keep installation and authentication time separate from repair time. Setup matters when choosing a tool, but a failed login says nothing about how well it fixes JavaScript.

Fig 1 · 1/3Compare the same acceptance cases
Each agent starts with the same small project and prompt.

Run the agents independently

You can run this interactively by opening each tool in its own copy and pasting the prompt. That’s the best way to judge the interface you’d use every day.

For my runs I used noninteractive commands and saved their event output. OpenCode’s CLI documentation describes run. With the current v2 executable selected, the command is:

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:

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

These allowed tools fit this trusted fixture. Review the permissions for your own project, and don’t assume one product’s shell patterns and tool names carry over to another.

Codex supports exec:

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.

If Codex rejects the configured default model before editing begins, pick a model listed as available to the CLI account, and record the change.

Example observations from the fixture

These are my runs 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 OpenCode process took about 149 seconds and the successful Codex retry about 23 seconds. Each is a single run, including startup, and the failed setup attempts aren’t counted. One model ran locally through Ollama and the other was hosted, so the times say nothing about which CLI is faster. For that you’d need several runs with the provider, model and hardware recorded.

Neither repair needed a follow-up prompt. My only intervention was in Codex setup, picking an available model. Claude Code needed a fresh sign-in before its coding attempt could begin.

Both implementations moved archive filtering before pagination and calculated the total from the eligible set, which is what the task asked for.

Run acceptance checks outside the agent’s tests

An agent can add many tests that repeat the same assumption as its code, so 18 tests versus 6 tells you little.

I checked both implementations against my own cases:

The prompt described the requirements, but neither agent saw this acceptance file. Both 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 easy to review. A large repository, browser state or an unfamiliar database need their own tasks, and for a larger change I would write the acceptance cases before starting the agent.

Compare cost without inventing a price

For a wider look at API billing versus local hardware, see how to use a coding agent without another subscription.

OpenCode reported zero cost in its event records for the Ollama run, which leaves out the hardware, the electricity and running the model server.

Codex returned token usage for the completed retry. With access through a subscription, that’s not a per-task invoice, and cache accounting and provider pricing make raw token totals hard to compare across these configurations. Claude Code made no successful call, so it has no cost to compare.

For a pay-as-you-go evaluation, I would use the provider’s billing records and note cache hits and retries. For a subscription, I would track how much useful work fits into the account’s actual limits, which is a different number from the monthly price.

Review and correction time belongs in the comparison too. A cheap first attempt that needs repeated repair can take 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 the interactive workflow I’d use every day. A print-mode run shows whether an agent completes a bounded task, but not whether its permission prompts and diff review fit the way I 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.

Want me to talk about your product? You can sponsor this site.

Related posts about ai: