OpenCode now ships as two different programs under the same opencode command, and most cheat sheets still document the old one. OpenCode 2 replaced the per-command server with a background service, moved export under opencode session, renamed the config keys and dropped web, attach and session sharing. The 1.x line still gets fixes through the opencode-ai npm package, so your install method decides which one you have.
This OpenCode CLI cheat sheet covers OpenCode 2 first (commands, headless run, sessions, the service, keybinds, permissions, Ollama and errors) and ends with a 1.x reference. If the October Console breach brought you here, the CLI wasn’t part of it, and the breach section says which keys you should rotate anyway. We re-checked the version, help, auth and debug output on Ubuntu 26.04 against OpenCode 2.1.4 and 1.19.4 in October 2026. The run, TUI and Ollama captures come from a 16 GB M4 Mac on 2.1.1 and 1.19.3 earlier that month. By November 2026 the 2.2 line had reached 2.2.5 and added TUI features such as /goal, so check opencode --help on your build against the command map.
Check which OpenCode you have
Check the version string to see which line you are on before you copy a single command from anywhere. OpenCode 2 prints a leading v and the program name, while 1.x prints the bare number:
opencode --version
On the two builds we re-checked, the outputs looked like this, 2.x first and 1.x second:
opencode v2.1.4
1.19.4
The install method decides which one you got, because the two lines live in different packages and the 1.x install script now refuses to install a 2.x build. Use this table when a command from a tutorial fails with “Unrecognized flag” or “Unexpected positional argument”, since that almost always means the tutorial was written for the other line:
| Install method | OpenCode 2 | OpenCode 1.x |
|---|---|---|
| npm | npm i -g @opencode/cli | npm i -g opencode-ai |
| Install script | curl -fsSL https://opencode.ai/v2/install | bash | curl -fsSL https://opencode.ai/install | bash |
| Homebrew | brew install anomalyco/tap/opencode-v2 (core: brew install homebrew/core/opencode) | brew install anomalyco/tap/opencode |
| Binary location (script) | ~/.opencode/bin | ~/.opencode/bin |
| Version output | opencode v2.x.y | 1.x.y |
| Docs | opencode.ai/v2/docs | opencode.ai/docs |
Both install scripts write to the same ~/.opencode/bin directory, and the 2.x npm package also ships an opencode2 alias for the same binary. Don’t keep both lines installed through different package managers, because whichever comes first on your PATH wins, and that’s the usual reason “I upgraded but nothing changed”. Run which -a opencode to see every copy on the path. Homebrew has its own version of the trap: with the anomalyco tap installed, brew info opencode on our Mac warned that opencode shadows homebrew/core/opencode and offered the tap’s 1.x build, so name the formula in full. The tap’s opencode-v2 formula also conflicts with its 1.x opencode formula, because both install an opencode binary, so run brew uninstall opencode before you install it.
Was OpenCode hacked? The Console breach and your keys
OpenCode Console was breached, but the OpenCode CLI was not. On October 17, 2026 OpenCode said it had “a security breach last night” that hit 1.4% of Console users, all of whom had an external provider key linked, and that every one of them got an email that morning. TechCrunch reported that the attackers broke into an older system, and quoted co-founder Dax Raad’s reply: “Note this is the OpenCode Console, not OpenCode itself, those keys never come to us.” A key you gave the CLI through opencode auth login or an environment variable stays on your machine, so it was never in the store that leaked.
This is what was public as of October 26, 2026:
- Provider keys saved in Console workspaces (OpenAI, Anthropic, Google and possibly others) may have been exposed
- The earliest possible exposure was 00:30 UTC on October 17, per OpenCode’s email
- The command-line tool and the open source app were not affected, per Raad via TechCrunch
- There is no CVE, since it was a hosted-service breach, and NVD had no OpenCode entry for October when we searched
OpenCode tied the incident to a “large scale distillation attack” without saying how the two connect, and some researchers questioned that label. The bills are not in dispute. BleepingComputer collected user reports of charges from several hundred dollars up to $1,700 overnight. Per TechCrunch, Anthropic and Google blocked the keys they believed were exposed, OpenAI said it would refund API customers, and OpenCode said refunds were underway.
Rotate a key that was in Console
If you got the email, deleting the key from Console isn’t enough, because a copied key keeps working until the provider revokes it. OpenCode told affected users to revoke and replace their keys with each provider, and The Hacker News spelled out the full sequence:
- Revoke the key in the OpenAI, Anthropic or Google dashboard and create a new one
- Update every script, CI secret and tool that used the old key
- Check usage and billing back to October 16 for requests you didn’t make
- Set a spend limit or alert, and give each tool its own key
The last step is the one that pays off next time. A key shared between Console, the CLI and a CI job can’t be traced to the tool that leaked it, and revoking it breaks all three at once.
Check where the CLI gets its keys
On your own machine, opencode auth list shows every credential the service can see and where it came from. We started the 2.1.4 service with a dummy key set as ANTHROPIC_API_KEY in its environment and nothing saved:
opencode auth list
The key shows up with its source:
Anthropic ANTHROPIC_API_KEY environment
Keys you save with auth login are kept locally instead. The database at ~/.local/share/opencode/opencode.db, the path opencode debug paths prints as db, has a credential table, which is where saved keys appear to live. We listed the table names only and didn’t save a real login to confirm it.
After a rotation, export the new key or run opencode auth login again, then drop the stale saved account with opencode auth logout <integration>, which opens an account picker when you leave out the credential name. Restart the service too, because it keeps the environment it started with (more on that below). You should also treat two outputs as secrets, opencode auth export, whose own help says it prints stored credentials “including secrets, as JSON”, and the heap dumps covered in the service section.
Upgrade from 1.x to OpenCode 2
Recent 1.x builds no longer update themselves and never offer a 2.x build as a regular update, so the move is always explicit. Run the upgrade from the CLI, or type /update in the 1.x TUI and pick the 2.0 offer. We didn’t run this command on the lab machine, because it replaces the global install; the migration results below come from starting 2.1.1 against a copy of a 1.x home instead:
opencode upgrade --major
The 1.x help describes --major as “upgrade to OpenCode 2 and remove OpenCode 1”, so it replaces the install rather than adding a second one. We tested what carries over by copying a working 1.x home directory and starting 2.1.1 against it. Sessions, the main config and the TUI settings all came across, but they came across in different ways, so it is worth knowing what happened to each file:
- Sessions: both 1.x sessions showed up in
opencode session liston first start opencode.json: read as-is and translated in memory, the file on disk is untouchedtui.json: rewritten into a new~/.config/opencode/cli.jsonthe first time the TUI opened- Share links: gone, since session sharing was discontinued on both lines
The cli.json it wrote from our 1.x tui.json shows the shape change, with the theme moved under name and the keybind ID renamed from session_new to the dotted session.new:
{
"$schema": "https://opencode.ai/v2/cli.json",
"theme": {
"name": "tokyonight"
},
"keybinds": {
"leader": "ctrl+x",
"session.new": "<leader>n"
}
}
Plugins are the part that won’t survive. OpenCode 2 has a new plugin API and the 2.1 release changed the plugin and SDK event contracts again, so any 1.x plugin needs a port before it loads. Popular bundles such as Oh My OpenAgent still had open “incompatible plugin API” issues against 2.x when we checked, so test a plugin-heavy setup on a spare machine before you run --major on your daily driver. The official OpenCode 2 migration guide also lists what 2.x no longer does, and two of those will surprise you: there are no LSP diagnostics, and it reads only AGENTS.md, with no CLAUDE.md fallback.
OpenCode 2 command map
Running opencode with no subcommand opens the TUI in the current directory, or in the directory you pass as the only argument. Everything else is a subcommand, and this is the full list from opencode --help on the 2.x build:
| Command | What it does |
|---|---|
opencode [dir] | Start the TUI (flags: -c, -s ID, --prompt, --auto, --standalone, --server URL) |
opencode run [msg] | One-shot headless run, the CI and scripting entry point |
opencode mini | Minimal interactive interface with history replay |
opencode session list|delete|export|import | Manage sessions (export and import moved here from top level) |
opencode auth list|login|logout|export|import|switch | Credentials and integrations; switch changes the active account |
opencode models | List every model your credentials can reach |
opencode mcp list|add|auth|logout | Manage MCP servers |
opencode plugin list|add|check|update|remove | Manage plugins (a real subcommand group now) |
opencode service start|stop|restart|status|get|set|unset | Control the background server every command talks to |
opencode serve | Run the v2 API and web server in the foreground |
opencode pair | Print one-time links that connect a browser or app |
opencode api <operation> | Call the running server’s API by OpenAPI operation ID |
opencode reload | Reload configuration without restarting |
opencode stats | Usage, cost and tool reliability, with --json |
opencode acp | Agent Client Protocol server for editors |
opencode debug agents|config|paths|heap-dump | Resolved agents, config sources, directories, and a heap snapshot of the running service (heap-dump is new in 2.1.4) |
opencode upgrade (alias update) | Upgrade, with --method curl|npm|pnpm|bun|yarn|vp|brew|winget |
opencode uninstall | Remove the install but keep sessions, config and state |
The global flags apply to every subcommand. --wizard walks you through a command’s options interactively, --completions zsh (or bash, fish, sh) prints a completion script, and --log-level plus --print-logs send logs to stderr. Server logs only print with --standalone, because without it the server is the background service and logs to its own file.
Run OpenCode headless with opencode run
opencode run sends one message, lets the agent work through its tools and exits, which makes it the piece you wire into scripts, cron jobs and CI. We planted a bug in a two-line calc.py (an add function that subtracts) and asked for a review:
opencode run -m anthropic/claude-haiku-5-5 "What does calc.py do? Is there a bug? Answer in two sentences, do not edit files."
In about four seconds of wall time the agent globbed for the file, read it and answered. Each line starting with > names the agent and model for that step, so you can tell when a subagent or a model switch took over:
> build · claude-haiku-5-5
✱ Glob "**/calc.py" 1 match
> build · claude-haiku-5-5
→ Read calc.py
> build · claude-haiku-5-5
`calc.py` defines a single function, `add(a, b)`, but it returns `a - b`, so it actually subtracts. Yes, there is a bug: the return statement should be `return a + b`.
To keep going in the same conversation, add -c to continue the last session. The follow-up below edited the file without asking, because the default build agent in OpenCode 2 allows every tool except reading .env files (more on that under permissions):
opencode run -c -m anthropic/claude-haiku-5-5 "Fix the bug in calc.py."
The edit prints as a unified diff. That’s also what you’ll see in CI logs, so you can review the change without opening the repo:
> build · claude-haiku-5-5
← Edit calc.py
Index: calc.py
===================================================================
--- calc.py
+++ calc.py
@@ -1,2 +1,2 @@
def add(a, b):
- return a - b
+ return a + b
> build · claude-haiku-5-5
I fixed `calc.py` by changing `return a - b` to `return a + b` in `add`, so the function now adds its arguments.
These are the run flags you will reach for most. The reasoning effort no longer has its own flag in OpenCode 2, because it now rides on the model string after a #, and -m anthropic/claude-haiku-5-5#high worked in our test where 1.x needed --variant high.
| Flag | Purpose |
|---|---|
-m provider/model#variant | Model, with an optional reasoning variant after # |
-c / -s ID | Continue the last session, or a session by ID (created if it doesn’t exist) |
--fork | Copy the session before continuing, so the original stays untouched |
-f FILE | Attach a file to the message |
--agent NAME | Run as build, plan or a custom agent |
--format json | Emit newline-delimited JSON events |
--title / --thinking | Set the session title, show thinking blocks |
--auto | Approve every permission that isn’t explicitly denied |
--standalone / --server URL | Use a private server, or a specific one, instead of the background service |
Attaching a file is the quickest way to hand a log to the agent without a copy-paste step, and it is what the “opencode include file” searches are after:
opencode run -m anthropic/claude-haiku-5-5 -f app.log "Summarise this log in one sentence."
Our two-line log came back as a one-sentence summary. You’ll get the same from a failed CI job’s log, which saves you pasting it into the TUI:
> build · claude-haiku-5-5
The log shows a database timeout error after 30 seconds on October 20, 2026, followed by a successful retry.
The file content goes into the session like any other message, so a -c follow-up can ask about the same log without attaching it again.
Parse the JSON event stream
With --format json every event is one JSON object per line carrying type, timestamp, sessionID and part. A run that read one file and then answered emitted five events in this order:
step_starttool_use(thereadcall)step_finishwith reasontool-calls, tokens and coststep_starttextwith the answer
There was no closing step_finish after the final text in 2.1.1, so a script that sums cost from step_finish events will undercount. Pull just the answer with jq:
opencode run --format json "Read calc.py and say in 5 words what it does." | jq -r 'select(.type=="text") | .part.text'
For cost, read the session afterwards with opencode stats --json or opencode session export <ID>, both of which carry the full token and cost totals. Cost and tokens sit inside part, so jq -c 'select(.type=="step_finish") | .part | {reason, cost, tokens}' returned this for the one step_finish we got:
{"reason":"tool-calls","cost":0.00015025,"tokens":{"input":4,"output":120,"reasoning":0,"cache":{"read":7860,"write":90}}}
In CI, add < /dev/null to the run line. One of our scripted runs with an inherited stdin produced no output at all, and the same command with stdin closed worked every time.
Manage sessions: fork, delete, export
Sessions are scoped to the project you run them in, so opencode session list shows only the top-level sessions for the current directory, newest first. The output is tab-separated (ID, title, last update), which makes it easy to script:
opencode session list
Our project had one session at this point, titled automatically from the first prompt. The outputs in this section come from separate test runs, so the session IDs differ between examples:
ses_ea4d19b42ffebmOtnf8jFLoK01 Database timeout error log review 10/21/2026, 12:18:50 AM
Forking is how you try a different direction without losing the original thread. Pass the session ID with --fork and the new message goes into a copy:
opencode run -s ses_ea4d19b42ffebmOtnf8jFLoK01 --fork "Now suggest one config change, one sentence."
Now the list shows the copy with a (fork #1) suffix and the original unchanged underneath it:
ses_ea4d1921bffe41ImCuhocOCT6t Database timeout error log review (fork #1) 10/21/2026, 12:18:53 AM
ses_ea4d19b42ffebmOtnf8jFLoK01 Database timeout error log review 10/21/2026, 12:18:50 AM
Inside the TUI, /fork does the same for the open session, and the mini help lists -c --fork to fork the last one in the minimal interface. There is no “delete all” subcommand, but because the list is tab-separated, one pipeline clears a project. Deleting a session also deletes its child sessions, so check the list first:
opencode session list | cut -f1 | xargs -n1 opencode session delete
Each deletion prints a confirmation, and a follow-up session list returned nothing:
Session ses_ea4f0d9c7ffe5r0psKiAbSCMx3 deleted
Session ses_ea4f118acffexZOOTrtqoJn36V deleted
Export writes the whole session as JSON to stdout, including metadata, cost, tokens and every message with its tool calls. According to its help, import accepts a file or a URL, which is how you move a session to another machine now that share links are gone:
opencode session export ses_ea4eace0bffebWnVM9g2sCr1Xf > session.json
opencode session import session.json
Read the export before you send it to anyone. Ours carried absolute file paths, the shell commands the agent ran with their output, and the provider’s opaque reasoning signatures, so treat it like a log file, not a summary.
Control the background service
OpenCode 2 runs one background server per user, and the TUI, run, models and most other commands connect to it instead of starting their own. It starts on demand and keeps running after the command exits. On our Mac it listened on 127.0.0.1:49374, and since 2.1.3 the changelog puts the Windows service on port 28894. That’s why the second command is faster than the first, and also why it shows up in lsof long after you closed the TUI.
| Command | Use it to |
|---|---|
opencode service status | Print the URL if it’s running, stopped if not |
opencode service start|stop|restart | Control it by hand |
opencode service get | Show its saved settings as JSON |
opencode service set port 49375 | Move it to another port |
opencode service set hostname 0.0.0.0 | Accept LAN connections (password-protected, firewall it) |
opencode reload | Re-read config without a restart |
--standalone on any command | Skip the service and use a private, short-lived server |
The service keeps the environment it started with, which bites you if you set a variable on a single command. We started the service, then ran OPENCODE_DISABLE_PROJECT_CONFIG=1 opencode debug config, and the project config still loaded. After OPENCODE_DISABLE_PROJECT_CONFIG=1 opencode service restart it was gone, and it stayed gone for later commands that did not set the variable, until a plain opencode service restart brought it back. So export the variable and restart the service, or add --standalone for a one-off. To make a variable survive later restarts, the v2 network docs use opencode service set env NAME value and then opencode service start, with service unset env NAME to remove it.
opencode pair replaces the old web and attach pair. It prints a one-time link for a browser or the desktop app, plus the SSH tunnel to use on a remote box:
opencode pair
Each link works once and expires after five minutes (the token is replaced here):
Open a link to connect. Links work once and expire in 5 minutes.
http://127.0.0.1:49375/auth/connect/<one-time-token>
Over SSH? Forward the port, then open the link on your machine:
ssh -L 49375:127.0.0.1:49375 <host>
If port 49375 is busy locally, forward another port and use it in the link.
To connect from other devices, run `opencode service set hostname 0.0.0.0`.
The SSH forward is the safe way to reach a server’s OpenCode from your laptop. Per its help text, opencode pair --remote goes further and exposes the server to the internet through OpenTunnel. Without an interactive terminal it also needs --yes, so keep it for machines where that is acceptable. For scripts, opencode api calls the running server by OpenAPI operation ID, for example opencode api session.list --param limit=2, and returns raw JSON.
Capture a heap dump of the service
When the service misbehaves, with memory creeping up or a hang you can’t explain, opencode debug heap-dump snapshots the running server’s heap without restarting it and prints the file path. Add --directory to write it somewhere other than the log directory, or --server URL to point it at another server:
opencode debug heap-dump
On our idle 2.1.4 service it took about three and a half seconds and wrote a 47 MB snapshot into the log directory, with mode 0600 so only your user can read it:
/home/josphat/.local/share/opencode/log/heap-1962-2026-10-26T075000215Z.heapsnapshot
That permission matters more than it looks. The service from the auth test above was still holding the dummy key in its environment, so we changed to the log directory, where that dump was the only snapshot, and searched it for the key:
cd ~/.local/share/opencode/log
grep -c canary-key-4003-not-real heap-*.heapsnapshot
grep found one match, so the key sits in the snapshot in plain text:
1
Never attach a heap dump to a GitHub issue or a support ticket as it is. Delete it when you are done, and if one has already left your machine, rotate every key the service had loaded. The same restart test showed the environment rule again: after a plain opencode service stop and start without the variable, opencode auth list went back to “No authenticated integrations”.
Learn the OpenCode 2 TUI keybinds
The leader key is still ctrl+x, so ctrl+x m means press ctrl+x, release, then m. We pulled this table from the command palette (ctrl+p) and status bar of a running 2.x TUI with an open session, so every binding is the shipped default:
| Action | Keys |
|---|---|
| Command palette | ctrl+p |
| Cycle agents (build, plan) | shift+tab |
| Switch model / agent | ctrl+x m / ctrl+x a |
| Cycle model variant (reasoning effort) | ctrl+t |
| New session / switch session | ctrl+x n / ctrl+x l |
| Open a session or project | ctrl+o |
| Rename session | ctrl+r |
| Close tab / reopen closed tab | ctrl+x w / ctrl+shift+t |
| Jump to a message (timeline) | ctrl+x g |
| Undo previous message | ctrl+x u |
| Compact session | ctrl+x c |
| Copy last reply / export transcript | ctrl+x y / ctrl+x x |
| Open external editor | ctrl+x e |
| Terminal pane / select terminal | ctrl+x t / ctrl+x ↓ |
| Queue a prompt behind the running one | ctrl+x enter |
| Hide sidebar / view status | ctrl+x b / ctrl+x s |
| Quit | ctrl+c, ctrl+d or ctrl+x q |
Two habits from 1.x break here. Tab no longer switches agents (it’s shift+tab), and per the v2 docs Enter while the agent is working now steers the active run instead of waiting, which is why ctrl+x enter exists for prompts you want queued. The v2 docs also list ctrl+b to send a blocking tool to the background and ctrl+alt+k for a which-key overlay. Rebind any of these in ~/.config/opencode/cli.json using the dotted IDs (session.new, model.list), because the 1.x underscore names are dropped on migration.
Use the slash commands
Type / in the prompt to get the list. Several commands only appear once a session is open (/compact, /undo, /fork, /timeline, /copy, /export, /rename, /terminal), so an empty project shows fewer. This is the full set from 2.1.1, with related commands grouped in one row:
| Command | What it does |
|---|---|
/new, /clear | Start a new session, or clear the current one |
/sessions, /resume, /continue | Switch session |
/open, /project, /projects | Open a session or project |
/fork / /rename | Fork or rename the open session |
/timeline / /undo | Jump to a message, undo the previous one |
/compact | Summarise history to free context |
/copy / /export | Copy the last reply, export the transcript |
/diff | Open the diff viewer |
/review | Review changes: commit, branch or pr, uncommitted by default |
/btw | One-shot side question that isn’t added to the conversation |
/models / /agents | Switch model or agent |
/effort, /variants, /thinking | Switch the model variant |
/connect | Connect a provider or integration |
/mcps, /skills, /plugins | Manage MCP servers, skills, plugins |
/init | Guided AGENTS.md setup for the repo |
/cd | Change working directory |
/terminal / /editor | New terminal pane, open the external editor |
/worktrees | Manage workspaces |
/settings, /themes, /status, /stats, /debug | Settings, theme, status, usage, debug info |
/pair, /web | Pair a browser or device |
/reload / /restart | Reload config, restart the service |
/update | Update OpenCode |
/help, /exit, /quit, /q | Help and quit |
/share is gone from both lines, and /btw is the one new command worth building a habit around. Ask “what does this flag do?” mid-task and the answer does not pollute the context the agent is working from. Inside the prompt, @ still opens the fuzzy file picker, so @src/auth.ts attaches a file the way -f does on the command line.
Translate a 1.x config to OpenCode 2
OpenCode 2 still reads opencode.json from ~/.config/opencode/ and from the project root, and it still accepts the 1.x shapes. You don’t have to rewrite a working file on day one. opencode debug config prints every config source together with the version 2 form it was translated into. That makes it the fastest way to learn the new keys from your own file:
opencode debug config
Our 1.x file had a permission object, a custom agent and a disabled MCP server. This is what 2.1.1 turned it into, with bash renamed to shell and enabled: false flipped to disabled: true. Note that the translation keeps the original order, so the git push* deny sits before the catch-all allow and never wins. Don’t copy it as a working rule set:
"permissions": [
{ "action": "shell", "resource": "git push*", "effect": "deny" },
{ "action": "shell", "resource": "*", "effect": "allow" },
{ "action": "edit", "resource": "*", "effect": "allow" }
],
"agents": {
"reviewer": {
"description": "Reviews diffs, never edits",
"mode": "subagent",
"permissions": [{ "action": "edit", "resource": "*", "effect": "deny" }]
}
},
"mcp": {
"servers": {
"fs": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "."],
"disabled": true
}
}
}
The rows marked “tested” came out of debug config on our own files, including a separate 1.x Ollama provider block and the cli.json the TUI wrote. The rest come from the migration guide, so confirm them against your file the same way:
| 1.x | OpenCode 2 | Source |
|---|---|---|
permission (object per tool) | permissions (ordered array of action, resource, effect) | tested |
permission bash | action shell | tested |
agent | agents | tested |
mcp.<name>, enabled: false | mcp.servers.<name>, disabled: true | tested |
provider, npm, options.baseURL | providers, package: "aisdk:...", settings.baseURL | tested |
tui.json, session_new | cli.json, session.new | tested |
permission task, write/patch | subagent, edit | docs |
agent prompt, maxSteps, disable | system, steps, disabled | docs |
command, plugin | commands, plugins | docs |
small_model | agents.title.model | docs |
autoupdate | update (disable, notify, auto) | docs |
Keep "$schema": "https://opencode.ai/config.json" at the top of opencode.json, because the obvious guess, https://opencode.ai/v2/config.json, returned a 404 when we tried it. The new CLI file is the only one with a v2 schema URL (https://opencode.ai/v2/cli.json). It is global only, with no project-level copy, and OPENCODE_CLI_CONFIG_CONTENT overrides it inline.
The 2.1 releases also added three experimental switches, all under experimental in opencode.json. The changelog is the source for what each one does. We only checked that OpenCode accepts and lists the first two, not that they change a long session:
| Key | Effect | Since |
|---|---|---|
background_compaction: true | Prepares the compaction summary in the background as a session nears its threshold; summaries that go unused still cost tokens | 2.1.4 |
todo: true | Gives models the todowrite tool and shows their task list in the session and sidebar | 2.1.0 |
location_idle_timeout: "4 hours" | When inactive location services unload, or false to keep them loaded | 2.1.2 |
With the first two set in a project file, opencode debug config listed it as a document source with both keys and no error:
{
"type": "document",
"path": "/home/josphat/proj/opencode.json",
"info": {
"$schema": "https://opencode.ai/config.json",
"experimental": {
"todo": true,
"background_compaction": true
}
}
}
Leave background_compaction off on pay-per-token providers until you have watched what it costs on your own sessions, because it spends tokens on summaries before it knows it needs them.
Set permissions for agents
Permissions in OpenCode 2 are an ordered list where the last matching rule wins, so put the broad rule first and your exceptions after it. The defaults are permissive. On a fresh install, opencode debug agents showed the build agent allowing everything (* on *) except an ask on *.env and *.env.* reads and a deny on the browser tool. The plan agent denies edit everywhere except its own plan directory. The migration guide also notes that 2.x allows external_directory access outside the project by default where 1.x asked, so add { "action": "external_directory", "resource": "*", "effect": "ask" } if you want that prompt back. If you run OpenCode on anything that matters, write your own rules:
vim opencode.json
We tested this exact file. It allows shell commands, asks before a push, blocks rm -rf and sudo, protects env files from edits and adds a read-only reviewer subagent:
{
"$schema": "https://opencode.ai/config.json",
"permissions": [
{ "action": "shell", "resource": "*", "effect": "allow" },
{ "action": "shell", "resource": "git push*", "effect": "ask" },
{ "action": "shell", "resource": "rm -rf*", "effect": "deny" },
{ "action": "shell", "resource": "sudo *", "effect": "deny" },
{ "action": "edit", "resource": "*.env*", "effect": "deny" }
],
"agents": {
"reviewer": {
"description": "Reviews diffs and never edits",
"mode": "subagent",
"permissions": [{ "action": "edit", "resource": "*", "effect": "deny" }]
}
}
}
We asked the agent to run sudo -n true with this file in place. The tool call failed with Error: Permission denied: shell, and the model reported the rejection instead of looking for a way around it. opencode run --agent reviewer started the custom agent from the project file. opencode debug agents only listed the built-in ones in our test, though, so start a project agent with --agent to confirm it loads. Remember that --auto approves every ask but still honours every deny, so deny rules are the ones that protect you in unattended runs.
Rules stop the agent doing damage, but they don’t make its output any better. That part is review discipline, and Beyond Vibe Coding (paid link) by Addy Osmani (O’Reilly, 252 pages) is the most tool-agnostic book on it. It covers setting constraints for the model and reading its diffs critically, and it is one of our best AI coding books picks. It works a level above flags, so it suits a multi-provider tool like OpenCode better than a single-vendor guide.
Connect a local Ollama model
Ollama works as an OpenAI-compatible provider. In OpenCode 2 the block lives under providers, and the SDK package name takes an aisdk: prefix. Add it to the project’s opencode.json (we ran Ollama on port 11435 to keep it isolated, the default is 11434):
vim opencode.json
Match the model key to the Ollama tag exactly, colon included:
{
"$schema": "https://opencode.ai/config.json",
"providers": {
"ollama": {
"name": "Ollama (local)",
"package": "aisdk:@ai-sdk/openai-compatible",
"settings": { "baseURL": "http://127.0.0.1:11435/v1" },
"models": { "qwen3.5:4b": { "name": "Qwen 3.5 4B" } }
}
}
}
Reload the config, check that the model is listed, then point a run at it with the ollama/ prefix:
opencode reload
opencode models | grep ollama
opencode run -m ollama/qwen3.5:4b "Fix the bug in calc.py. mul must multiply."
The 4B model found the bug and edited the file, the same result the hosted model gave (output trimmed to the edit):
> build · qwen3.5:4b
✱ Glob "**/calc.py" 1 match
> build · qwen3.5:4b
→ Read calc.py
> build · qwen3.5:4b
Found the bug! The `mul` function should be multiplying with `*`, not adding with `+`.
← Edit calc.py
@@ -1,2 +1,2 @@
def mul(a, b):
- return a + b
+ return a * b
It took 4 minutes 24 seconds on the 16 GB M4, and almost all of that was the first request. OpenCode sent two requests together at the start, and the Ollama log shows them taking 3 minutes 41 seconds and 4 minutes 3 seconds to get through the agent’s system prompt and tool definitions, about 5,400 tokens. The three calls after that took 4 to 9 seconds each. The first attempt never answered, because Ollama sized the context at 4,096 tokens for this machine’s 11.8 GiB of usable GPU memory, smaller than the prompt itself, and our 120-second timeout killed it. Start Ollama with OLLAMA_CONTEXT_LENGTH=16384 or higher for agent work, and pick models from the Ollama models cheat sheet by the memory you actually have. The Ollama commands cheat sheet covers the server side. Hermes Agent takes the same custom-endpoint route to Ollama; the Hermes Agent cheat sheet has the tested config keys and the OOM fix for a 16 GB box.
If you would rather not run models locally, opencode models also listed free hosted models under the opencode/ prefix. These three were in the list on our install:
opencode/big-pickleopencode/glm-5.3-flash-freeopencode/deepseek-v4.1-flash-free
Free models come and go, so check opencode models before you script against one. For paid models, the OpenAI-compatible API guide shows how to route OpenCode through a single key. To pool free-tier keys from OpenRouter, Gemini and Groq behind one provider entry instead, self-host FreeLLMAPI and point an OpenAI-compatible provider entry at its /v1 endpoint, using the 2.x providers form of the Ollama block above.
Set environment variables
The OpenCode 2 docs have no environment variable table, so we pulled the names compiled into each binary. The current 2.x binary references 48 OPENCODE_* names against 78 in the current 1.x build, and 49 of the 1.x names aren’t referenced by the 2.x binary at all, 21 of them experimental flags. The 2.x count hasn’t moved since 2.1.1 because one name came in as another went out: OPENCODE_DISABLE_FREE_MODELS was added and OPENCODE_RELEASE_HIGHLIGHTS dropped. These are the ones you’ll use. Rows marked tested were run on 2.1.1, and the unmarked ones are names compiled into the binary with the meaning the 1.x docs give them:
| Variable | Effect |
|---|---|
OPENCODE_CONFIG_CONTENT | Inline JSON config, added as an extra source (tested) |
OPENCODE_DISABLE_PROJECT_CONFIG=1 | Ignore the project’s opencode.json; OPENCODE_CONFIG_PROJECT_DISABLE=1 works too (tested, needs a service restart) |
OPENCODE_CLI_CONFIG_CONTENT | Inline cli.json (theme, keybinds), per the docs |
OPENCODE_DB | Move the SQLite database (tested with debug paths db) |
ANTHROPIC_API_KEY, XAI_API_KEY, HF_TOKEN | Provider keys, shown as environment in auth list (tested) |
OPENCODE_CONFIG, OPENCODE_CONFIG_DIR | Point at another config file or directory |
OPENCODE_LOG_LEVEL | Log level, replaces the old logLevel key |
OPENCODE_SERVER_PASSWORD | Basic auth for the server |
OPENCODE_DISABLE_AUTOUPDATE | Skip update checks |
OPENCODE_DISABLE_FREE_MODELS=1 | Remove zero-cost models from the catalogue, defaults and fallbacks (2.1.2 changelog; name present in the binary) |
OPENCODE_DISABLE_CLAUDE_CODE | Ignore .claude directories and skills |
HTTPS_PROXY, NO_PROXY, NODE_EXTRA_CA_CERTS | Corporate proxy and private CA |
These 1.x variables aren’t referenced by the 2.x binary, so a 1.x CI job that sets them silently does nothing after the upgrade:
OPENCODE_PERMISSIONOPENCODE_AUTO_SHAREOPENCODE_TUI_CONFIGOPENCODE_PUREOPENCODE_SERVER_USERNAME- every
OPENCODE_EXPERIMENTAL_*flag
Move permissions into the permissions array and the TUI settings into cli.json. Variables that change the server’s behaviour follow the service rule from earlier, so set them with opencode service set env or export them and restart the service. Behind a proxy, the v2 network docs say NO_PROXY must include localhost,127.0.0.1,::1, because the CLI reaches the service over loopback.
Directories haven’t moved. opencode debug paths prints them, and the ones you will touch are ~/.config/opencode (config, cli.json, service.json), ~/.local/share/opencode/opencode.db (sessions) and ~/.local/share/opencode/log. Clearing the cache means deleting ~/.cache/opencode. On our installs it held only models.json (the model catalogue) and a bin directory, and OpenCode rebuilds both on the next start, so it’s safe to remove while the service is stopped.
Fix common OpenCode errors
Every error below was reproduced in our lab, except the free-tier one, which comes from the project’s issue tracker and is marked as such.
Error: “Managed service port 49374 on 127.0.0.1 is already in use by another process”
Something else holds the service port, usually a second OpenCode install, another user’s service, or one left behind by a sudo opencode run. Find the owner first:
lsof -nP -iTCP:49374 -sTCP:LISTEN
If it’s a stale OpenCode, stop it with opencode service stop from the account that started it. If both need to run, move this one to another port and start it again, which is what fixed it for us:
opencode service set port 49375
opencode service start
The service came up on the new port and every later command used it without extra flags. The port is saved in ~/.config/opencode/service.json, so you only set it once per user.
Error: “Unrecognized flag: –dir in command opencode run”
You are running a 1.x command on OpenCode 2. --dir and --pure were removed from run, so cd into the project first, and debug plugins with opencode plugin list and remove instead of --pure. Tools that wrap OpenCode and still pass these flags fail the same way, which is the “could not start opencode: exit 1” message some editor integrations show.
Error: “ENOENT: no such file or directory, chdir … -> ‘web'”
OpenCode 2 has no web or top-level export command, so it treats the word as a directory to open and fails to chdir into it. opencode attach http://... fails similarly with “Unexpected positional argument”. Use opencode pair for the browser, opencode --server URL to attach to a server, and opencode session export for exports.
Error: “Permission denied: shell”
A deny rule matched the command. That’s correct behaviour if you wrote the rule, so read opencode debug config to find which one matched. Rules are evaluated in order with the last match winning, so a broad allow placed after a specific deny silently cancels the deny.
Ollama model never answers
No error appears in OpenCode. The request sits until your timeout, and the Ollama log shows POST "/v1/chat/completions" ending in a 500 when the client gives up. The cause is a context window smaller than the agent’s prompt plus a slow cold start, and the fix is the larger OLLAMA_CONTEXT_LENGTH from the Ollama section with a timeout of several minutes for the first call.
Uninstall leaves the opencode command behind
opencode uninstall stops the service and removes the cache, but it keeps your sessions, config and state on purpose. It also can’t always tell how the binary was installed. On our npm install it printed “Could not detect the installation method. Remove the installation manually after cleanup.” Run which -a opencode to find every copy, then remove each one with the tool that installed it:
- npm:
npm rm -g @opencode/cliornpm rm -g opencode-ai - Homebrew:
brew uninstall opencodeorbrew uninstall opencode-v2 - install script: delete
~/.opencode/bin
Run opencode uninstall --dry-run first if you want to see what it will touch.
Error: “OpenCode’s free tier can only be used from within OpenCode”
This one comes from GitHub issue #56324, one of the most-discussed 2.x issues, not from our lab. Maintainers suspected client causes such as a custom agent that denies shell or read, then concluded it was most likely network-dependent, and the issue was closed as not planned. Test on another network without a VPN before you change config. If you never meant to use a free model in the first place, set OPENCODE_DISABLE_FREE_MODELS=1 (added in 2.1.2) and restart the service, so no default or fallback can land on one.
Keep a 1.x quick reference
You’ll probably keep some machines on 1.x for a while, especially anywhere your plugins hold up the upgrade. These are the commands and keys that differ, checked against the 1.x help output and docs:
| Task | OpenCode 1.x | OpenCode 2 |
|---|---|---|
| Export / import a session | opencode export ID / opencode import file | opencode session export ID / session import |
| Browser UI | opencode web | opencode pair |
| Attach to a server | opencode attach URL | opencode --server URL |
| Run in another directory | opencode run --dir PATH | cd PATH first |
| Reasoning effort | --variant high | -m provider/model#high |
| Plugins | opencode plugin NAME, --pure | opencode plugin add|list|remove |
| Session list | Takes -n and --format json | Current project, tab-separated |
| Auth command | opencode providers (alias auth) | opencode auth |
| GitHub agent, PR checkout | opencode github, opencode pr N | Not in the CLI |
| Database shell | opencode db | opencode debug paths db for the path |
| Switch agent in the TUI | Tab | shift+tab |
| TUI settings file | tui.json (global or project) | cli.json (global only) |
| Upgrade within the line | opencode upgrade (stays on 1.x) | opencode upgrade |
The 1.x keybinds that carry over unchanged include the leader ctrl+x followed by n, l, m, a, c, u, e, x, y, b, s and q, plus ctrl+p for the palette. On 1.x, run also takes --auto and -i (split-footer interactive mode), and the base command takes --mini. The 1.x line still gets security updates, and 1.19.4 only moved the bundled Bun runtime to 1.4.4 for its TLS, HTTP and WebSocket fixes, the same bump 2.1.3 shipped, so update a 1.x machine even if 2 has to wait. When you are ready to move, opencode upgrade --major is the way across. For a broader comparison with the other agents, the Claude Code cheat sheet, Codex CLI cheat sheet and OpenCode vs Claude Code vs Cursor cover the same ground for each tool, and the OpenCode setup guide walks through a first install on Linux.