Claude Code and ZCode Both Leaked Security Problems, So I Read Through Open Source opencode's Source Code

19 min read

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.

TermPlain-language explanationAn analogy
LLMA “super word-completion machine” that has read a massive amount of text and can continue writing and make judgmentsA consultant who is very smart but only talks, never acts
AI Assistant / AgentLLM + a pair of “hands” (can read/write files, run commands) + a set of rulesA consultant with an assistant by their side who can do the work for you
PromptThe words you send to the AIThe 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
TokenThe smallest unit of text an AI processes, roughly “a fragment of a character/word”Billing by the character count on ruled paper
ToolA concrete action the AI can invoke, like “read file”, “edit file”, “run command”The screwdriver, hammer, or scanner in the assistant’s hands
SessionOne 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.

LayerWhat goes in itPlain language
System contextWorking 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 epochA “baseline note” + a “change record”“Finalize the background material into a note, and keep recording what changes afterward”
Session historyWhat 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.

NameWhat it isCan the AI see itDoes it change
Baseline note (baseline)The complete background material collected the first time, frozen and no longer modified once finalizedYes, 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 comparisonUpdated every round
Supplemental noteWhen some item changes, a sentence is appended separately to notify the AIYes, inserted into the conversationOnly 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.

  1. It opens the change record; the date entry was March 1 last time.
  2. Now it sees March 2—changed.
  3. It doesn’t reprint the handbook; it appends a supplemental note to the conversation: “today is March 2”.
  4. The AI sees the handbook plus supplemental note and knows to go by the new date.
  5. 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.

  1. Summarize the earlier conversation into a paragraph.
  2. Keep the most recent small stretch verbatim.
  3. The summary replaces the old records, freeing up space.
  4. 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.

MechanismNumberLocation
When compaction triggersWhen the estimated character count exceeds budgetpackages/core/src/session/compaction.ts:232-243
How much recent text is keptAbout 8000 tokenscompaction.ts:137-158
Maximum summary lengthAbout 4096 tokenscompaction.ts:15
How the count is estimatedRoughly “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.

RuleValueLocation
Save separately above how many lines/bytes2000 lines or 50KBpackages/core/src/tool-output-store.ts:13-14
How long temporary files are kept7 daystool-output-store.ts:15
How they’re cleaned upScan once per hourtool-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.

ActionWhat it means in the UIWhat it doesCan 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 happenedNo need
commit“Confirmed, that’s it”The file rollback stays, and all conversation records after this message are deletedNo

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.

MemoryWhat it storesHow long it livesAnalogy
Working memory (current context)What’s actually sent to the model this roundOne request; “turned over” once sentDocuments spread on the desk
Conversation memoryWhat you talked about in this sessionKept long-term within the sessionThe minutes book
Persistent ledgerAll raw events, in orderPermanent, until you delete the session or revertThe event log
Project knowledgeAGENTS.md, skill manuals, etc.Permanent, in your filesystemThe 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

ArchiveWhat it storesPlain language
Event logAll events in order (each session has its own sequence)The event log
Session messagesThe organized result of the current conversationMeeting minutes
Input inboxThings you sent that haven’t been processed yetThe todo tray
Context epochThat “baseline note” and change recordThe note on the desk
SnapshotWhat the project looked like at a moment (only the ID is recorded; content is in the hidden git repo)Game save
Permission archiveRules you approved as “allow from now on”Pass
CredentialsYour API keysKeychain
TodosThe TODO list the AI keepsSticky 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.

  1. Summarize the earlier conversation into a paragraph.
  2. Keep the most recent small stretch verbatim.
  3. The summary replaces the old records, freeing up space.
  4. 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 articleTechnical term you might hearOne-line explanation
Baseline noteBaseline System Context / Context EpochA context base draft set before work and rarely changed afterward
Session historySession HistoryYour conversation record with the AI
Supplemental noteMid-Conversation System MessageA system message appended when context changes
CompactionCompactionSummarizing old conversation to free space
Admitted / promotedAdmitted / PromotedInput is persisted first, then enters model history
SnapshotSnapshotA save of the project at a moment, used for revert
Event logEvent Log / Event SourcingRecording all events in order as the single source of truth
ProjectionProjectionQueryable data computed from the event log
ArchiveSQLite DatabaseThe local database file
Real-time broadcastSSEA 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.

TopicWhere to start
Overall architecture and dual enginesCONTEXT.md, AGENTS.md, packages/core/src/session.ts
Context sources and assemblypackages/core/src/system-context/index.ts
Project description file readingpackages/core/src/instruction-context.ts
Context epoch and compactionpackages/core/src/session/context-epoch.ts, session/compaction.ts
Permissions and security boundariespackages/core/src/permission.ts, filesystem.ts, location-mutation.ts
Tool definitions and invocationpackages/core/src/tool/tool.ts, tool/registry.ts
Saving large outputs separatelypackages/core/src/tool-output-store.ts
Crash protection and retriespackages/core/src/session/runner/llm.ts, packages/llm/src/route/executor.ts
User interaction and event streamspackages/protocol/src/groups/session.ts, packages/app/src/context/server-sdk.tsx
Database and migrationspackages/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.

  1. 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.
  2. The model’s desk is finite—the context window is limited. All must do context selection, compaction, and on-demand loading.
  3. 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.

DimensionCommon approachDivergence (varies by project)
ContextSystem prompt + history + compactionSome rely on tool search (grep / glob, OpenCode, Claude Code, etc.), others on code vector index / RAG (Cursor, Windsurf, etc. pre-building indexes)
Project descriptionAgree on a description fileAGENTS.md (OpenCode, etc.) / CLAUDE.md / .cursor/rules / .github/copilot-instructions.md
Tool protocolFunction calling / JSON SchemaConverging on MCP (think of it as “the USB interface for tools”); most mainstream tools already support it
Permission securityAsk + allow listThe 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
MemoryShort-term via conversation historyMost have no true cross-session memory and rely on description files; a few have a “memory” feature or an external vector store
Crash recoveryPersist as much as possibleOpenCode’s event sourcing + durable inbox is fairly aggressive; many tools are an in-memory loop + saving a conversation JSON
RevertCheckpoint / snapshotCursor checkpoints, Aider relying on git commits, OpenCode using git snapshots + revert
Multi-agentMain agent dispatches subtasksClaude 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