Claude Code and ZCode Both Leaked Security Problems, So I Read Through Open Source opencode's Source Code
Recently, Claude Code and ZCode have both been hit by security problems one after another. These tools need to read your code and run your commands, so when something goes wrong the cost is high—and with closed-source products, you basically can't see what they actually do. Open source agent assistants are a bit more reassuring, because at least you can read the source. Today we'll look at how opencode edits code for you.
Letting AI directly touch your codebase makes most people uneasy at first. Will it edit the wrong file, will it casually delete something, what happens if it dies halfway through.
Lately this worry has become more concrete. Claude Code and ZCode were hit by security problems one after another. They need to read your code and run your commands, they’re granted broad permissions, the cost of something going wrong is high—and with closed-source products, you basically can’t see what actually happened. By contrast, open source agent assistants are more reassuring: at least you can pull up the source and see exactly how they act, and what rules they impose on themselves.
Today we’ll dig into how opencode edits code for you. I read through its source, trying to see exactly what it does in these areas.
Its operation is really just one loop. Figure out what to do next, act, look at the result, then figure out the next step. All the rest of the design exists to keep that loop running steadily.
First, a Few Terms
No need to memorize these—just get familiar with them.
| Term | Plain-language explanation | An analogy |
|---|---|---|
| LLM | A “super word-completion machine” that has read a massive amount of text and can continue writing and make judgments | A consultant who is very smart but only talks, never acts |
| AI Assistant / Agent | LLM + a pair of “hands” (can read/write files, run commands) + a set of rules | A consultant with an assistant by their side who can do the work for you |
| Prompt | The words you send to the AI | The instructions you give the assistant |
| Context | “All the material the AI can see in front of it right now” | The documents spread out on the assistant’s desk |
| Token | The smallest unit of text an AI processes, roughly “a fragment of a character/word” | Billing by the character count on ruled paper |
| Tool | A concrete action the AI can invoke, like “read file”, “edit file”, “run command” | The screwdriver, hammer, or scanner in the assistant’s hands |
| Session | One continuous conversation between you and the AI (a project can have many) | One long talk with the same assistant, with session history |
One premise needs to be stated clearly first. The LLM itself can’t touch your computer; it only “speaks”. What reads files, edits code, and executes commands is the outer layer of software, which is opencode. So the answer to “will the AI randomly change my code” depends on how many rules the outer layer sets.
1. What OpenCode Is
It’s an AI coding assistant that runs on your computer, with a terminal UI, a web UI, and a desktop app. You can say to it
- “Help me figure out what’s going on with this error”
- “Add remember-password to the login feature”
- “This function is too messy, split it up”
- “Run the tests and fix the failures”
Which files to look at first, which line to change next, what command to run afterward to verify—it arranges all of that itself. You’re mainly responsible for stating requirements and doing the final review.
The biggest difference from a web-based chatbot is this: it can directly act on the project on your computer. What we’ll cover below are the six layers of safeguards behind its willingness to act.
2. What It Looks Like Overall
First think of it as a company with clear divisions of labor.
graph TD
You["You<br/>state requirements / review results / approve"] --> UI["Frontend / UI<br/>terminal, web, desktop app"]
UI --> Server["Reception desk<br/>receive requests, queue, broadcast progress"]
Server --> Brain["Brain / core engine<br/>decide what to do next"]
Brain --> LLM["External advisor (LLM)<br/>only gives advice, doesn't act"]
Brain --> Tools["Toolbox<br/>read files, edit files, search code, run commands, go online"]
Tools --> Disk["Your project files"]
Brain --> Memory["Archive<br/>records the whole conversation and every change"]
Take one sentence—“help me add a button”—and walk it through.
sequenceDiagram
participant You as You
participant App as UI
participant Core as Core engine
participant DB as Archive (database)
participant LLM as LLM
participant Tools as Toolbox
You->>App: "help me add a button"
App->>Core: send this sentence in
Core->>DB: first "register" your words
DB-->>Core: registered
Core->>Core: gather desk material (project description, history, available tools)
loop think + act repeatedly
Core->>LLM: here's the situation, what's next?
LLM-->>Core: I want to look at a certain file first
Core->>Tools: go read that file
Tools-->>Core: file contents
Core->>LLM: here are the file contents
LLM-->>Core: I want to change this line
Core->>Tools: go edit the file (may ask for your approval first)
Tools-->>Core: done
end
Core->>DB: record this stretch of activity
Core-->>App: push progress to you in real time
App-->>You: you watch the text pop out character by character
Think, act, look at the result, think again. This loop looks simple; the hard part is that every turn can go wrong. The following sections break things down by where the trouble can occur.
3. How It Understands Your Project
Before the LLM says anything each time, you have to lay the material it needs in front of it. Project structure, your requirements, what was discussed before, which tools exist. Together these are called context.
There are three tricky spots. The AI’s desk is finite—there’s an upper limit on how many characters it can see. The material also keeps changing: you edit files, time passes, tasks change. Putting too much in front of it is slow, expensive, and inaccurate; putting too little and it guesses wildly. Context management therefore becomes the most essential craft of this kind of product.
opencode’s approach is like an assistant who knows how to organize a desk: it doesn’t dump every file on the consultant, but sorts them into categories. One folder of long-term, unchanging material holds the project description, today’s date, and the list of available skills. A stack of constantly updated session history records what you said and what the AI did. Specialized documents are fetched on demand, such as the detailed description of a particular skill.
It divides context into three layers.
| Layer | What goes in it | Plain language |
|---|---|---|
| System context | Working directory, operating system, the project’s AGENTS.md description file, list of available skills, date | “The background material the assistant checks before starting work each time” |
| Context epoch | A “baseline note” + a “change record” | “Finalize the background material into a note, and keep recording what changes afterward” |
| Session history | What you said, what the AI replied, what the tools did | “The minutes book” |
The easiest of these three layers to get confused about is the baseline note, so let’s cover it separately.
What the Baseline Note Is
Before it works each time, it automatically collects a batch of background material. Which folder you’re in, what operating system, what the project’s AGENTS.md says, which skills are available, what today’s date is.
The trouble is that some of this material changes—the date changes every day, you might edit AGENTS.md at any time—and some doesn’t. Re-sending the whole set every round has two problems. First, it’s slow and expensive; the material can be thousands of characters. Second, the AI gets confused: if the opening briefing differs every time, it’s hard for it to stay consistent.
So it splits the material into three things, each minding its own business.
| Name | What it is | Can the AI see it | Does it change |
|---|---|---|---|
| Baseline note (baseline) | The complete background material collected the first time, frozen and no longer modified once finalized | Yes, sent to the AI as the “opening briefing” | No |
| Change record (snapshot) | A little ledger recording “what value each item had the last time it was handed to the AI” | No, purely for its own comparison | Updated every round |
| Supplemental note | When some item changes, a sentence is appended separately to notify the AI | Yes, inserted into the conversation | Only added, never modified |
In everyday terms: the baseline note is the employee handbook handed out on your first day of work—once given, it’s final. The supplemental note is the individual notices added later, posted next to the handbook. The change record is the comparison table HR keeps, noting which entries have already been notified as updated.
When the Baseline Note Is Generated
This is the easiest point to misunderstand. Creating a session doesn’t generate it, and neither does your just sending a message.
The moment it picks is right before the first time it’s about to send content to the LLM. At the instant the AI has decided to get to work and is about to open its mouth to ask the LLM, it goes and gathers the background material and finalizes it into a note. Collecting the material itself has a cost—looking up directories, reading files, pulling the skill list. If you create a session but never send a message, there’s no need to do the work in advance. This use-it-when-needed approach is called lazy generation.
Before each round of calling the LLM, it first checks whether this session has a baseline note. If not, it collects one now and stores it in the archive. If it already has one, it skips and reuses it.
There’s also a small security detail here. During collection, if an important piece of material can’t be read at the moment—say AGENTS.md happens to be locked—it won’t force out a crippled note. Instead it pauses that round, leaving your message in a pending state so it can be retried later. Better to be a little late than to give the AI an incomplete opening briefing.
Besides the first time, a fresh baseline note is also reprinted after compaction. By design, it’s also reprinted after a session is “moved” to another location, but that capability isn’t enabled yet.
An Example with the Date
On the first day of work, the baseline note says “today is March 1”. The next day you come back to it.
- It opens the change record; the date entry was March 1 last time.
- Now it sees March 2—changed.
- It doesn’t reprint the handbook; it appends a supplemental note to the conversation: “today is March 2”.
- The AI sees the handbook plus supplemental note and knows to go by the new date.
- It also updates the date in the change record to March 2, so it won’t notify again next time.
When AGENTS.md is edited, the same logic applies: the supplemental note is written as a full new version of the description, replacing all previously loaded project instructions.
Why Not Resend the Whole Thing Every Round
First, it saves money and time—only notifying what changed, instead of hauling thousands of characters every round. Second, LLM providers cache long, fixed content at the start, so reusing it is faster and cheaper; the more stable the opening briefing, the better the deal. Third, having a stable base draft makes it easier for the AI to stay consistent. Fourth, attention can be spent where it matters, letting the AI focus on what just changed rather than re-reading what it already knows.
Here’s the generation timing strung together.
flowchart TD
A["Create session"] --> B{"Have you sent a message?"}
B -->|"No, never chatted"| C["Don't generate<br/>save the cost of collecting material"]
B -->|"Yes, AI is about to start"| D["Before the first LLM call"]
D --> E{"Does this session<br/>already have a baseline note?"}
E -->|No| F["collect material now<br/>generate baseline note"]
E -->|Yes| G["reuse directly, don't regenerate"]
F --> H["afterward only append supplemental notes on change"]
G --> H
H --> I{"Compaction happening?"}
I -->|Yes| J["reprint a brand-new baseline note"]
I -->|No| K["keep using the old note + supplemental notes"]
sequenceDiagram
participant Core as Core engine
participant Registry as Material collector
participant Epoch as Baseline note + change record
participant History as Minutes book
participant LLM as LLM
Core->>Registry: look through all the background material
Registry-->>Core: directory / system / instruction file / skill list / date
Core->>Epoch: is there a baseline note?
alt First time
Core->>Epoch: generate baseline note, and record the change record
else Already exists
Core->>Epoch: compare item by item using the change record
opt some items changed
Core->>Epoch: append a "supplemental note", and update the change record
end
end
Core->>History: fetch this conversation's record
Core->>LLM: baseline note + supplemental notes + session history + tool list
The Project Description File AGENTS.md
You put a file named AGENTS.md in your project root—it’s like an employee handbook for the AI, and opencode reads it automatically and passes it to the AI. It searches upward level by level from the current folder, and also reads an additional one from your computer’s global config. When a file can’t be read at the moment, it keeps using the old content and won’t assume the file was deleted. This design is called stale but retained, and it’s quite thoughtful in practice.
This is mainly how it remembers your project’s rules across conversations. There’s no mysterious memory store—it re-reads the description files you wrote at the start of every work session.
How Skills Are Provided
A skill is a professional manual. It doesn’t stuff all the manuals’ full text at the AI—too much space—and only provides a table of contents, containing the skill name and a one-line introduction. Only when the AI thinks it’s needed does it use a permission-controlled “fetch manual” action to open a particular manual.
What If the Desk Gets Too Full
When the material is nearly too much to fit, it performs a compaction.
- Summarize the earlier conversation into a paragraph.
- Keep the most recent small stretch verbatim.
- The summary replaces the old records, freeing up space.
- Reprint a brand-new baseline note, because the earlier conversation has been folded and it has to start fresh.
When the desk fills up, it condenses the earlier discussion into session minutes, keeps only the last few pages verbatim, and keeps talking.
| Mechanism | Number | Location |
|---|---|---|
| When compaction triggers | When the estimated character count exceeds budget | packages/core/src/session/compaction.ts:232-243 |
| How much recent text is kept | About 8000 tokens | compaction.ts:137-158 |
| Maximum summary length | About 4096 tokens | compaction.ts:15 |
| How the count is estimated | Roughly “character count ÷ 4” | packages/core/src/util/token.ts:3-5 |
Its character estimate is quite rough—not an exact calculation. That’s one of its current small weaknesses.
Where the Note Is Stored, and What Gets Sent to the Model Each Time
First, storage. The baseline note is stored in the local database’s session_context_epoch table, with three fields. baseline is the finalized opening briefing text, snapshot is the change record the model can’t see, and baseline_seq records from which event sequence number it takes effect. The database file lives in the system data directory; on your machine it’s usually ~/.local/share/opencode/opencode.db. This table has at most one row per session, and it’s reused directly after a process restart—no need to regenerate.
Now let’s look again at what’s actually packed into a request. Whenever it asks the LLM, the request is assembled from three blocks.
The first block is system, in two segments. One segment is the system prompt for the current mode, defining the role and basic rules. The other segment is exactly the baseline note. The note contains the working directory, operating system, the project’s AGENTS.md instructions, list of available skills, citation notes, today’s date, and sources registered by plugins.
The second block is messages—the messages projected from this session history. It includes your messages and attachments, the AI’s text and reasoning, the tools it invoked and tool results, mid-conversation supplemental notes, and shell commands and output. For a compacted session, the history has an extra summary wrapped in <conversation-checkpoint> tags, sent out as a user message. Mode and model switch records are not sent to the model; they’re only kept for the record.
The third block is tools—the definitions of tools available in the current mode. At the last step before hitting the step limit, tools are taken away and replaced with a sentence reminding it to wrap up.
Traveling with the request are also model parameters, cache keys, and a few session identifier headers. Credentials are injected at this layer and never appear as text in the context.
What the model can’t see is also worth stating clearly. The raw event log isn’t sent, the change record’s snapshot isn’t sent, the old verbatim conversation folded away by compaction isn’t sent, and the complete file for large tool outputs isn’t sent—only a preview of the beginning and end plus a file path. The git content inside snapshots is for revert, unrelated to the model.
So what the model sees each time is a freshly assembled copy. The baseline note stays fixed, supplemental notes are appended, and history is trimmed at the compaction boundary. It has no continuously accumulating archive it can flip through.
4. How It Keeps From Causing Trouble
The core attitude is just one sentence: you can’t fully trust what the AI says. So by default it doesn’t trust the AI; it trusts only three things. The folder boundary your project sits in, the rules you configure, and your clicks in the dialog.
Think of it as home renovation. The AI is a highly capable contractor you don’t completely trust. You can let him into the house to work, but only within the specified house—he can’t climb into the neighbor’s. Some dangerous actions, like knocking down a load-bearing wall, must be asked about every time. What you’ve approved once can be recorded for automatic permission later.
Folder Boundary
It treats “your project folder” as a wall. When the AI wants to read or write a file, the system first resolves the path into a full path, checks whether it’s inside the project, then checks whether this path, after going through shortcuts—symbolic links—is still inside the project. Only when both checks fail is it blocked, keeping the AI from peeking outside the project with ../ or shortcuts.
There’s a known small hole at present. Search tools don’t apply the same strict check to their search starting point, so in theory the AI could search out of bounds. This is one of the things to fix.
The Three Outcomes of Permission
Each time the AI wants to do a sensitive action, the system arrives at one of three outcomes.
flowchart TD
A["AI wants to do something<br/>e.g. edit a file, run a command"] --> B{"Have you configured a rule?"}
B -->|"configured deny"| DENY["deny outright"]
B -->|"configured allow"| ALLOW["allow outright"]
B -->|"not configured / configured to ask"| ASK["ask in a dialog"]
ASK --> C{"What do you choose?"}
C -->|"allow just this once"| ONCE["allow this time"]
C -->|"allow from now on"| SAVE["record in the archive, auto-allow afterward"]
C -->|"deny"| DENY
DENY --> D["AI receives 'denied', tries another way or stops"]
ONCE --> E["keep working"]
SAVE --> E
ALLOW --> E
Three details are worth noting.
A saved allow never overrides an explicit deny you configured. Security first.
When the system can’t tell which mode is current, it denies everything by default. Better not to act than to act wildly.
If you switch modes mid-way—say from casual questions to read-only—the action already in progress still follows the old rules. That way there’s no moment during the switch when a dangerous operation slips through.
Commands and the Terminal
When the AI runs a shell command—installing dependencies, running tests—the permission is asked per whole command. A reminder here: once a command is allowed, it has the full permissions of your computer’s user. opencode has no sandbox. So agreeing to run a command deserves care.
The terminal also has a dedicated one-time ticket mechanism, to prevent bad actors on the web from quietly taking over your terminal.
Network and Keys
When fetching web pages online, only http/https is allowed, with size and timeout limits, and scripts in the page are scrubbed. The shortfall is that it doesn’t specifically block internal network addresses, so in theory there’s a risk of being used to probe the intranet.
Your API keys and other credentials are stored in the local database, exposed externally only by name, and never appear in the AI’s conversation records. They’re stripped from requests when sent to the LLM provider.
5. How It Actually Edits Code
Tools Are the Action List
The AI can’t edit files directly; it can only say “I want to invoke the edit tool with these parameters”. When opencode receives that, it does four things. First it checks whether the parameter format is correct, to prevent the AI from talking nonsense. Then it checks permissions. Then it actually executes. Finally it shapes the result into a form the AI can understand and sends it back.
Its built-in tools are roughly these. Read file, write file, edit file, apply patch, search by filename, search by content, run commands, open a terminal, fetch web pages, search the web, load skills, update todos, ask you questions.
The Complete Process of One Tool Call
sequenceDiagram
participant LLM as LLM
participant Core as Core engine
participant Registry as Tool manager
participant Tool as Specific tool
participant Store as Large file store
LLM-->>Core: I want to invoke the edit tool
Core->>Registry: check whether this tool exists and the parameters are correct
Registry->>Tool: execute
Tool-->>Registry: result (possibly a very long stretch)
Registry->>Store: result too long, trim it
Store-->>Registry: keep only a beginning/end preview + save the full file separately
Registry-->>Core: the shaped result
Core->>LLM: here's the result, please continue
What If the Result Is Too Long
The AI’s desk is finite. When some command outputs a huge amount—tens of thousands of lines of test logs—opencode writes the full content to a temporary file and hands the AI only the beginning and end fragments plus a sentence saying “the full content is in such-and-such file”. These temporary files are kept for 7 days, then cleaned up automatically.
| Rule | Value | Location |
|---|---|---|
| Save separately above how many lines/bytes | 2000 lines or 50KB | packages/core/src/tool-output-store.ts:13-14 |
| How long temporary files are kept | 7 days | tool-output-store.ts:15 |
| How they’re cleaned up | Scan once per hour | tool-output-store.ts:176-205 |
Do Tools Expire
If you switch modes mid-way from development to read-only, the AI may still be holding an old tool list. If it uses the old list to call a new tool, the system notices the IDs don’t match and returns “this tool has expired” instead of executing incorrectly.
6. Why It Doesn’t Easily Crash
The goal is modest. A single tool failing, network flakiness, your hitting stop suddenly, a power outage—none of these should take down the whole program or corrupt the data.
A good restaurant doesn’t close because one dish was burned; it throws the dish out, tells the guest, and keeps cooking the next one.
Errors Treated as Normal Results
A failed tool execution—say a file doesn’t exist—becomes an error message the AI can read, letting the AI decide the next step, without crashing the program. Event listeners—the little workers responsible for pushing progress to the UI—only log an entry on error and don’t affect the main flow.
Network Requests Are Retried
When accessing the LLM service, if it hits temporary problems like too many requests (429) or a busy server (5xx), it automatically waits a bit and retries, up to 2 retries. The wait grows exponentially with random jitter, avoiding all requests piling on at once.
flowchart TD
A["Send request to the LLM"] --> B{"Succeeded?"}
B -->|Yes| OK["continue"]
B -->|"429 / 500 / 503 / 504"| RETRY{"Any retries left?"}
RETRY -->|Yes| WAIT["wait a bit (longer and longer + random jitter)"] --> A
RETRY -->|No| FAIL["tell upstream: failed"]
B -->|"401 / 403 no permission"| FAIL
B -->|"400 and content too long"| COMPACT["compact the conversation first, then try once more"] --> A
Content Too Long Is Special
When the LLM says “what you sent is too long”, opencode doesn’t foolishly wait for an error; it triggers the compaction from the previous section, frees up space, and sends again. This remedy is allowed only once, to avoid an infinite loop.
What Happens When You Hit Stop
This is where skill is tested most. A good interruption must do this: record clearly what’s done, mark clearly what isn’t, and leave no half-finished state.
Its approach has four points. The “stop” request itself can’t be cancelled midway, or it would get stuck in the middle. A running tool is interrupted and explicitly marked as execution interrupted. If the AI has said half a sentence, that half-sentence is also marked complete or failed. Tool results that already succeeded but haven’t been recorded yet are protected and recorded first—this process can’t be interrupted.
When emergency-stopping an assembly line, it lets the current part finish and get labeled, marks the undone ones as cancelled, and only then stops. The whole process doesn’t just yank the power cord.
What Happens on Power Loss or Restart
All important matters are recorded in the local database, so most state can be recovered after a restart. But there’s one explicit rule.
Tool calls that didn’t finish during a power loss are all marked failed after restart, letting the AI see the failure and decide for itself whether to retry. It won’t secretly re-execute.
This choice is very conservative, but very safe. The AI may have already edited half a file, and quietly re-running could cause duplicate changes.
A few things are non-persistent and lost on restart: in-progress LLM requests, background tasks, and the intermediate state of a half-done tool side effect. This is a limitation it currently publicly acknowledges.
Running Commands Leaves No Zombie Processes
If a command the AI runs hangs or isn’t cleaned up, it can become a resource-hogging zombie. opencode first politely asks it to exit—SIGTERM. If it won’t exit, it’s forcibly killed—SIGKILL. It also cleans up the entire process group it started, avoiding orphan processes.
7. How You Normally Use It
Your Message Actually Goes in Two Steps
It looks like your message goes out the moment you press Enter, but internally it’s two steps. First registered (admitted): your words are safely written to the database, so this request won’t be lost. Then promoted: at a safe moment, it’s actually placed into the history the AI can see.
The benefit is that if the program crashes between the two steps, your words aren’t lost and can continue after restart. Some products show “queued” in the UI first—that’s the same idea.
Why a Message Can Pop Out Character by Character
The AI’s answer isn’t generated all at once; it spits out word by word. opencode uses a one-way broadcast technology called SSE to push each word to your UI in real time.
sequenceDiagram
participant You as Your UI
participant Stream as Real-time broadcast
participant Core as Core engine
You->>Stream: subscribe to "notify me on progress"
Stream-->>You: connected
loop AI is replying
Core->>Stream: just emitted "l"
Stream-->>You: show "l"
Core->>Stream: just emitted "o"
Stream-->>You: show "lo"
end
Note over Stream,You: network disconnected
You->>Core: re-fetch the complete history
You->>Stream: subscribe again
There’s a design I find quite clever. After the UI disconnects and reconnects, it doesn’t ask the server to resend the messages from the disconnect period, because the real-time stream doesn’t guarantee nothing is lost. It simply asks the server for a complete history afresh. Simple to implement, and not afraid of gaps.
The UI also merges multiple pushes of the same character before refreshing, once every 16 milliseconds, avoiding jank from frequent refreshes.
When It Needs You to Decide
The permission dialog pops up when the AI does a sensitive operation, showing what it wants to do—say which lines of which file to change—and gives you three options: allow just this once, allow from now on, deny. When denying, you can also write a reason, which the AI will see.
A question card is the AI asking you a question in return, like “which style do you want”. The UI pops up options, supporting single choice, multiple choice, and custom input.
The Stop Button
In the terminal UI, you press Esc twice in a row to actually stop; the first press prompts “press again to interrupt”, preventing accidental taps. Stop requests can be sent repeatedly, and sending when nothing is running is fine too—no error.
Switching Mode or Model Mid-way
You can switch the AI’s mode at any time—more cautious or more aggressive—and change the model too. The switch takes effect on the next step and doesn’t interrupt what’s currently being done. That’s more stable.
The Regret Pill, Revert
The AI may change many files at once; if it gets something wrong, you need to roll back with one click. opencode’s revert has two parts: snapshots and a three-step confirmation.
Step One, Take a Snapshot
Before and after each AI action, it takes a snapshot of the entire project. Before the action, one snapshot records the starting point; after, one records the result.
A few key points define the limits of its capability.
- Where it’s stored. It doesn’t copy a pile of files; it stores them in a hidden git repository, keeping only one copy of identical content—very space-efficient. The database records only an archive number.
- When it exists. It takes effect only for git projects. A non-git project has no snapshots, and therefore no revert.
- What’s captured. Files in the project managed by git. Untracked files over 2MB are skipped.
- Can it be turned off. You can disable snapshots in the config by writing
snapshots: false.
When you play a single-player game, it auto-saves at key checkpoints. A save isn’t a copy of the whole game world; it records the fingerprint of the current state.
Step Two, Why Revert Takes Three Steps
Many people are confused the first time: revert isn’t a one-click immediate delete—it’s split into preview, back out, and confirm.
| Action | What it means in the UI | What it does | Can you back out |
|---|---|---|---|
| stage | “I want to revert to before this message” | Immediately restores the files to that state and generates a diff list of “what changed” | Yes |
| clear | “Never mind, no revert” | Restores the files to how they were before the revert, as if nothing happened | No need |
| commit | “Confirmed, that’s it” | The file rollback stays, and all conversation records after this message are deleted | No |
You can still back out after stage because it only changed files and hasn’t deleted the conversation yet. You can look in the UI at what things would look like after rollback, and clear it if it seems wrong. This is far safer than a one-click irreversible delete.
The reason commit deletes the conversation is to keep the files consistent with the AI’s memory. The files went back to the past, but the AI’s memory still holds that reverted work; next time you chat it would be schizophrenic—clearly it edited before in memory, yet the files don’t reflect it. So when confirming a revert, opencode deletes all conversation messages after that message along with your subsequent input.
Think of it as document editing. You want to delete the last three paragraphs. stage shows you what it looks like after deletion; clear means keep them after all; commit really deletes, and erases those three paragraphs from the history record too.
The Three States of Revert
stateDiagram-v2
[*] --> Normal
Normal --> Staged: stage preview rollback
Staged --> Normal: clear abandon revert
Staged --> Committed: commit confirm
Committed --> Normal: file rollback + delete later conversation
How to Use It in the UI
In the terminal, /undo first stops the running task, finds the last message from you, performs the revert, and restores what you said then into the input box, so you can tweak it and resend. /redo recovers after backing out.
On the web and desktop, the buttons are called Revert and Redo, and underneath they’re the same stage and clear interface.
Notes
It only rolls back file changes the AI recorded. Files you manually edited elsewhere are outside the protection scope. The granularity of a revert is one message, not a line. The snapshot object store currently has no automatic cleanup and is retained forever, because it stores by content and duplicate content takes no extra space.
A Common Question: Is Revert Based on Git
Half true. More precisely, it borrows git as a storage and rollback engine, but uses a separate, hidden auxiliary repository and won’t alter your project’s git history.
It must first be a git project—only a git repository has snapshots, and a non-git project has no revert; you can also actively disable it in the config. The auxiliary repository lives in a unified cache directory, not the same place as your project’s .git. It borrows your project’s object store through git’s object-sharing mechanism and copies the index, rather than duplicating the whole project. When taking a snapshot, it uses git tree objects to record the current working tree, recording only the current working directory’s scope, respecting ignore rules, and skipping untracked large files. When rolling back, it selectively restores the specified files to their version in the snapshot.
It won’t commit to your project, doesn’t touch your branches, staging area, or git stash, and won’t rewrite your commit history. So it uses git as an efficient archiving system, and doesn’t call git’s built-in revert commands. This also explains why the granularity of revert is snapshots and messages rather than line by line: it records what the entire working tree looked like at a moment.
8. How It Remembers Things
This section covers two things: what opencode actually records, and why it’s designed that way.
It Has Four Kinds of Memory
Don’t think of it as one brain; it’s more like four layers of memory with different jobs.
| Memory | What it stores | How long it lives | Analogy |
|---|---|---|---|
| Working memory (current context) | What’s actually sent to the model this round | One request; “turned over” once sent | Documents spread on the desk |
| Conversation memory | What you talked about in this session | Kept long-term within the session | The minutes book |
| Persistent ledger | All raw events, in order | Permanent, until you delete the session or revert | The event log |
| Project knowledge | AGENTS.md, skill manuals, etc. | Permanent, in your filesystem | The employee handbook |
Three sentences are enough: working memory is temporary, the ledger is permanent, and conversation memory is computed from the ledger.
Everything Defers to the Log
opencode’s design philosophy is to first record what happened; all other information can be recomputed from the log.
Accounting is a good analogy. The original vouchers—each transaction—matter most. The various reports—balances, summaries—are all computed from the transactions. If a report is wrong, recompute it from the transactions.
Here the original vouchers are the event log, recording who did what and when. The reports are what the current conversation looks like, which todos exist, and what changed in the files.
There are three benefits. A broken report can be recomputed from the transactions. After a restart, reading through the transactions restores the situation. Every step has an ID (seq), so the ordering is crystal clear.
The cost is that you must carefully keep the ledger and reports consistent, so they’re written in the same atomic transaction—either both succeed or both fail. There’s no mess where the log recorded something but the report didn’t update.
What’s in the Database
| Archive | What it stores | Plain language |
|---|---|---|
| Event log | All events in order (each session has its own sequence) | The event log |
| Session messages | The organized result of the current conversation | Meeting minutes |
| Input inbox | Things you sent that haven’t been processed yet | The todo tray |
| Context epoch | That “baseline note” and change record | The note on the desk |
| Snapshot | What the project looked like at a moment (only the ID is recorded; content is in the hidden git repo) | Game save |
| Permission archive | Rules you approved as “allow from now on” | Pass |
| Credentials | Your API keys | Keychain |
| Todos | The TODO list the AI keeps | Sticky notes |
One easily misunderstood point: there’s no cross-conversation memory table here. opencode doesn’t do the kind of long-term memory that remembers what you talked about last time. Cross-conversation memory is just re-reading the AGENTS.md and other project description files you wrote each time. If you want it to remember something across sessions, you have to write it in a file—don’t expect it to remember on its own.
Tool call state also has no separate table; it’s stuffed inside the content of session messages. Things like login attempts and background tasks live only in memory and aren’t persisted.
A Message’s Complete Journey Through Memory
Let’s string the tables above together and follow one message’s life.
sequenceDiagram
participant You as You
participant Core as Core engine
participant Inbox as Input inbox
participant Log as Event log
participant Report as Session history
participant LLM as LLM
You->>Core: send "help me add a button"
Core->>Inbox: registered (admitted), model can't see it yet
Core->>Log: record "input received"
Note over Core: wait for a safe moment
Core->>Inbox: take out and promote (promoted)
Core->>Log: record "input promoted"
Core->>Report: project into a user message
Core->>LLM: baseline note + supplemental notes + session history + tools
LLM-->>Core: reply + tool calls
Core->>Log: log every word and every tool result
Core->>Report: update session history in sync
Note over Log,Report: power outage
Core->>Log: rebuild from the event log after restart
Core->>Report: unfinished tools marked failed, pending input continues
Two words are worth remembering separately. Registered, i.e. admitted: your words are safely persisted first, so a program crash afterward won’t lose them. Promoted: at a safe moment, it’s actually placed into the conversation the model can see.
The reason to register first without promoting directly is that letting the model see something has prerequisites—say the baseline note must be ready first. If the conditions aren’t met, the words stay waiting in the inbox and aren’t forced in. That way messages aren’t lost, and the model doesn’t see a half-finished product.
How Compaction Rewrites Memory
When the conversation gets too long and the model can barely fit it, it performs a compaction.
- Summarize the earlier conversation into a paragraph.
- Keep the most recent small stretch verbatim.
- The summary replaces the old records, freeing up space.
- Reprint a brand-new baseline note, so the new round starts from a clean state.
When the desk fills up, it condenses the earlier discussion into session minutes, keeps only the last few pages verbatim, and keeps talking.
Note that compaction rewrites how memory is presented. The original verbatim conversation is gone, turned into a paragraph. But the raw records in the ledger are still there—they’re just no longer sent to the model one by one.
How to Recover Key Memory Lost in a Summary
Compaction doesn’t delete any raw records; it only changes what’s sent to the model, not what’s stored. Most key memory can still be recovered.
There’s no delete operation anywhere in the compaction code; it just folds the old conversation into the summary. The original conversation still lies in two places: the session messages table and the event log. The summary itself is also a persistent record—at the end of compaction it saves the actually-generated full summary text, plus the retained recent verbatim text.
flowchart TD
A["The summary seems to have lost key information"] --> B{"Want the model to see it again?"}
B -->|Yes| C["Write it into AGENTS.md<br/>or feed it again with a new message"]
B -->|"Just want to verify"| D["Read the session history API (durable event log)"]
B -->|"Want to dig into raw data"| E["Query the local database's<br/>session messages table / event table directly"]
C --> G["The model sees it again next round"]
D --> F["Get the raw events from before compaction<br/>text, tool calls, errors, etc."]
E --> F
To view or export raw content, use the session’s history API—it reads the durable event log and can fetch raw events from before compaction, such as text and tool results. The other “current context” API returns a filtered view after compaction and can’t see the folded old messages; to dig up old accounts you need the history API.
To make the AI know something again immediately, write the key information into AGENTS.md—that’s the most stable and takes effect across sessions. Or feed it again with a new message; new messages fall within the retained recent range and won’t be folded a second time right away. To dig through data thoroughly, open the local database’s session messages table and event table directly.
But there’s no one-click undo for compaction. There’s currently no feature to cancel compaction or restore from a summary, and the manual compaction API isn’t open yet—calling it returns feature unavailable. So there’s no automatic recovery; you can only rely on the manual methods above. Summaries are generated by the LLM and inherently lossy, and the product has no mechanism to automatically detect whether this summary lost key information.
To lose as little key information as possible, compaction itself does two things. One, retain recent verbatim text: the most recent conversation, about 8000 tokens by default, doesn’t participate in the summary and is kept as is. Two, rolling summaries: on the second compaction, it hands the previous summary, the previously retained verbatim text, and the new content to be compacted to the model together for re-summarizing. The summary template also explicitly requires preserving concrete file paths, function names, commands, original error text, and links, with the latest taking precedence when old and new conflict, and completed work, constraints, and user instructions all carried over.
This is equivalent to carrying the old memory along each time it reorganizes, rather than simply overwriting the old with a new summary.
To truly be reliable, long-term memory has to live in files, especially AGENTS.md. Conversation summaries are just a space-saving device, not reliable storage.
How One Operation Is Safely Booked
sequenceDiagram
participant Core as Core engine
participant DB as Database
participant Log as Event log
participant Report as Projection
Core->>DB: begin a "transaction" (all succeed or all fail)
Core->>Log: write a ledger entry (assign an ID)
Core->>Report: update the projection in sync
opt notes that need updating together
Core->>DB: update "the baseline note's change record"
end
Core->>DB: commit the transaction
DB-->>Core: success
The word “transaction” sounds fancy, but it’s just a bundle of operations: either all done or none done. That way there’s no intermediate state where the ledger recorded something but the projection didn’t update.
What It Won’t Remember
Setting expectations clearly matters.
It doesn’t remember your chat content across sessions unless you write it into AGENTS.md. Background tasks aren’t persistent—close the program and they’re gone, and in-progress work is interrupted. In-progress network requests aren’t persistent—a power loss means starting over. The intermediate state of a half-done tool isn’t guaranteed safe, so unfinished tools are marked failed after restart and won’t secretly resume. Login attempts are memory-only and not kept long-term.
Temporary Files and Expiry
Temporary files for large tool outputs are kept for 7 days, then cleaned up automatically. Beyond that, conversation messages don’t expire automatically and stay forever. Sessions can be archived, but there’s currently no automatic deletion mechanism. The snapshot object store has no automatic cleanup either. Background tasks aren’t persistent—close the program and they’re gone.
9. What Shortcomings It Still Has
No software is perfect. Below are places it publicly acknowledges or explicitly marks as todo in the code, to set reasonable expectations.
Security. When the search tool finds by filename or content, the path doesn’t get the same strict boundary check as read/write, so in theory it can search out of bounds. Web fetching doesn’t specifically block internal network addresses, so there’s a risk of being used to probe the intranet. The terminal’s working directory isn’t strictly confined to the project. Keys are stored in plaintext in the local database, with no system keychain integration.
Stability. Network request retries only cover the request-sending phase; once streaming reception begins, an error won’t be retried automatically. There’s no circuit-breaker mechanism for a tool that keeps failing repeatedly. After a process restart, unfinished tools aren’t re-run automatically—this is deliberate design, but it means possible interruption. Distributed capability for multiple nodes working together isn’t implemented yet.
Experience. The terminal UI still runs on the old engine, while web and desktop run on the new engine; the two coexist and their features aren’t fully consistent. Some APIs of the new engine are still experimental and can change at any time. The character estimate is a rough character count divided by 4, not an exact calculation.
10. Terminology Mapping and Source Index
| Term in this article | Technical term you might hear | One-line explanation |
|---|---|---|
| Baseline note | Baseline System Context / Context Epoch | A context base draft set before work and rarely changed afterward |
| Session history | Session History | Your conversation record with the AI |
| Supplemental note | Mid-Conversation System Message | A system message appended when context changes |
| Compaction | Compaction | Summarizing old conversation to free space |
| Admitted / promoted | Admitted / Promoted | Input is persisted first, then enters model history |
| Snapshot | Snapshot | A save of the project at a moment, used for revert |
| Event log | Event Log / Event Sourcing | Recording all events in order as the single source of truth |
| Projection | Projection | Queryable data computed from the event log |
| Archive | SQLite Database | The local database file |
| Real-time broadcast | SSE | A technology for the server to push messages one-way to the UI |
If you want to look at the code too, here are the entry points I noted while reading.
| Topic | Where to start |
|---|---|
| Overall architecture and dual engines | CONTEXT.md, AGENTS.md, packages/core/src/session.ts |
| Context sources and assembly | packages/core/src/system-context/index.ts |
| Project description file reading | packages/core/src/instruction-context.ts |
| Context epoch and compaction | packages/core/src/session/context-epoch.ts, session/compaction.ts |
| Permissions and security boundaries | packages/core/src/permission.ts, filesystem.ts, location-mutation.ts |
| Tool definitions and invocation | packages/core/src/tool/tool.ts, tool/registry.ts |
| Saving large outputs separately | packages/core/src/tool-output-store.ts |
| Crash protection and retries | packages/core/src/session/runner/llm.ts, packages/llm/src/route/executor.ts |
| User interaction and event streams | packages/protocol/src/groups/session.ts, packages/app/src/context/server-sdk.tsx |
| Database and migrations | packages/core/src/database/database.ts, session/sql.ts |
11. Compared with Other AI Coding Tools
This section is an extended comparison. opencode isn’t a special case; it belongs to the same category as Claude Code, Cursor, Cline, Aider, Codex CLI, and OpenHands—only the trade-offs differ. The descriptions below are each project’s rough approach; details change with versions, for reference only.
Same origin in principle, difference in engineering trade-offs. Almost all AI coding tools look similar, because they all face the same few hard constraints.
- The LLM can only speak, not act. Any agent must prepare its own set of tools to translate the model’s words into real actions.
- The model’s desk is finite—the context window is limited. All must do context selection, compaction, and on-demand loading.
- Model output can’t be fully trusted. All must have a permission, approval, or sandbox layer as a backstop.
So you’ll find that almost all agent tools can be broken down into the same loop.
flowchart LR
A["User requirement"] --> B["Assemble context<br/>system prompt + history + available tools"]
B --> C["Ask the LLM: what's next"]
C --> D{"Does it want to call a tool?"}
D -->|Yes| E["Execute tool<br/>read / write / search / run command"]
E --> F["Stuff the result back into context"]
F --> C
D -->|No| G["Output the answer / end"]
C -.->|error| H["Retry or degrade"]
H --> C
Context, tools, the loop, permissions, persistence, and streaming interaction—these six are the common skeleton of this kind of product. opencode is just one implementation.
Their shared origins and divergences across dimensions look roughly like this.
| Dimension | Common approach | Divergence (varies by project) |
|---|---|---|
| Context | System prompt + history + compaction | Some rely on tool search (grep / glob, OpenCode, Claude Code, etc.), others on code vector index / RAG (Cursor, Windsurf, etc. pre-building indexes) |
| Project description | Agree on a description file | AGENTS.md (OpenCode, etc.) / CLAUDE.md / .cursor/rules / .github/copilot-instructions.md |
| Tool protocol | Function calling / JSON Schema | Converging on MCP (think of it as “the USB interface for tools”); most mainstream tools already support it |
| Permission security | Ask + allow list | The biggest divergence: some use OS-level sandboxes (Codex CLI’s seatbelt / landlock, Claude Code’s sandbox, OpenHands using Docker); OpenCode currently relies mainly on permission approval, with no system-level sandbox |
| Memory | Short-term via conversation history | Most have no true cross-session memory and rely on description files; a few have a “memory” feature or an external vector store |
| Crash recovery | Persist as much as possible | OpenCode’s event sourcing + durable inbox is fairly aggressive; many tools are an in-memory loop + saving a conversation JSON |
| Revert | Checkpoint / snapshot | Cursor checkpoints, Aider relying on git commits, OpenCode using git snapshots + revert |
| Multi-agent | Main agent dispatches subtasks | Claude Code subagents, OpenCode’s task tool, various subagents |
The places that really create separation, I think, are these trade-offs.
How to find code. Rely entirely on retrieval, i.e. RAG, or let the model use search tools itself. The former is fast, the latter more flexible; opencode leans toward the latter.
What security relies on. OS sandboxes are harder, application-layer approval is more flexible but more dependent on user restraint. This is the biggest security difference between products.
How memory is stored. Files, vector databases, or a dedicated memory system. At present, most still use files plus conversation history.
How deep recovery goes. Whether to seamlessly resume after a power loss. Going deep is complex; most tools choose to mark unfinished work as failed after restart.
Once you understand opencode’s setup, picking up Claude Code, Cursor, Cline, or Aider will be much faster. They’re all solving the same problem: letting a model that can only speak get work done on a real codebase safely, stably, and reversibly.
Comments