GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

No menu could be recovered. Follow the blue links in the page.

This view

  • Original page
  • New address

GeezerVison is showing 3code.capocasa.dev — 3code - the economical coding agent

☰

3code - the economical coding agent

Contents
  • Introduction
  • Quickstart
    • Get a provider
      • Free providers
      • Flat-rate coding plans
      • EU providers
      • Low-cost providers
      • Other models
    • Install
    • Manual install on Windows
    • Termux on Android arm64
    • Add a provider
    • Run your first prompt
  • Providers and authentication
    • API keys
    • xAI and SuperGrok
    • OpenAI and ChatGPT
    • Anthropic and Claude
    • Command Code
  • Provider catalog
    • Model labs
    • US inference clouds
    • EU providers
    • Routers and aggregators
  • Interactive use
    • Selecting text
    • Rebinding keys
  • The screen
    • Scrollback contents
    • The token bar
    • The thinking ticker
    • Receipts and conversation cost
  • Patient retry
  • When the model misbehaves
    • The flailing detector
    • The thinking loop guard
    • The empty response handler
  • Private mode
    • Picking a private provider
    • Walkthrough
  • Reasoning
  • Web research
  • Known-good models
  • System prompt override
  • Images
  • Computer use
  • Sessions
  • Notifications and auto-update
  • Context management
  • Chunked mode
  • Skills
  • Goal mode
    • Verifier models
  • No sub-agents
  • Sandbox
    • Policy format
    • Built-in default
    • Sandbox settings collection
    • Network rules
    • Editing the policy
    • Policy file protection
    • What is sandboxed
    • Platform behavior and fallback
  • Colors
  • Design decisions
    • No TUI
    • Kernel enforcement, no questions
    • One model, small tool set
  • Scripting
  • Library API
  • Agent Client Protocol
  • Build from source
  • Contributing
  • Prior art

Introduction

3code is the coding agent I wanted for my own work: capable, quiet, and not forever looking for new ways to spend tokens. It works with OpenAI-compatible providers, including free tiers, flat-rate coding plans, and local servers.

The design rule is simple: get a correct result with the least model work, token use, and computer time. Open models, especially GLM, are good enough for most of my programming work. 3code gives them a focused prompt, a small tool set, persistent sessions, and a terminal interface that stays out of the way.

The standard way to use 3code is also the simplest one: type what you want, like fix the flickering caret in the resize path or add tests for the parser edge cases, and press Enter. Current models read the code, do the work, and report back, without asking you anything in between. If a task is overly broad (improve this project), you will get questions or a plan first; narrow it down and it is back to hands-off. The interface exists to stay out of that loop: scrollback is plain terminal history, the footer is two rows, and everything else is your terminal's own scrolling, search, and selection.

This manual is organized how-to style: getting a provider, daily use, the screen, sessions, the sandbox, embedding. The dry inventory of every config key, command-line switch, and : command lives in the reference; the internal API, for embedding and contributing, in the developer docs.

Quickstart

First, give 3code somewhere to run inference: an API provider or a local OpenAI-compatible server. Local models can work (Ollama and friends speak the same API), though small ones still have a rough time with difficult coding tasks.

Get a provider

There are plenty of compatible providers. A free tier is a good place to start. Pay for one after you know which model actually works for your projects.

Free providers

NVIDIA Build and Novita often have useful free models. The lists and rate limits move around, as free things tend to do, so check before signing up.

Flat-rate coding plans

The z.ai coding plan is my main setup. GLM is capable, inexpensive, and happy to keep working through a long task.

3code can also log in with ChatGPT Plus/Pro and SuperGrok or X Premium+ subscriptions. These are separate from OpenAI and xAI API billing, despite the similar names.

EU providers

When data must stay under EU jurisdiction, I use GreenPT. Nebius and Eurouter are also in the provider catalog.

Low-cost providers

DeepInfra, OpenCode, nano-gpt, Novita, and similar aggregators serve quantized open models cheaply. Quality and availability vary, so a little comparison shopping pays off.

Other models

DeepSeek, MiniMax, Kimi, Tencent Hy, Qwen, Grok, and LongCat are all supported somewhere. The registry changes too often for a copied list to age gracefully, so ask 3code for the current one:

3code --good

OpenRouter and Eurouter are handy when you want to try many models through one account. Once you settle on one, a direct provider account removes a service from the request path and is often cheaper. The provider catalog below lists who hosts what, where, and which ones serve quantized weights.

Install

# macOS / Linux
curl -fsSL https://3code.capocasa.dev/install | sh

# Windows (PowerShell)
irm https://3code.capocasa.dev/install.ps1 | iex

The install script is the recommended way to install 3code. npm is there for those who prefer it:

npm install -g 3code

Both install the same release binaries.

Manual install on Windows

The PowerShell one-liner also downloads a private PortableGit tree (bash

  • unix tools) into %LOCALAPPDATA%\3code\git, so 3code always has a shell

of its own. If you would rather not, install Git for Windows and run 3code from a plain release folder:

  1. Download 3code-windows-amd64.zip from the latest release and unpack it, for example to C:\tools\3code. The zip carries 3code.exe and docs; OpenSSL and the CA bundle are compiled into the exe, so the folder is self-contained.
  2. Put the folder on your PATH, or call 3code.exe by full path.
  3. Run it. 3code resolves bash in this order: a bash_path setting, then the installer's own PortableGit tree (%LOCALAPPDATA%\3code\git), then the Git for Windows install, then a standalone MSYS2 (C:\msys64), then the legacy %LOCALAPPDATA%\3code\msys64 tree. With Git for Windows present, no MSYS2 download is needed. A bash setting overrides the automatic order with a full path to the shell (any OS); auto (the default) keeps the order above. Both keys are documented in settings in the reference.

The bash tool has its own timeout and diff computation built in, so it needs no GNU coreutils on the host; a bare PortableGit or MSYS2 bash is enough. If no bash can be found at all, 3code warns and drops the bash tool (file tools keep working) instead of failing to start.

The one-time 3code setup (sandbox user + network fence) is still required on Windows. It needs admin rights: run it from a console/RDP session and it asks for them via the Windows UAC prompt (from ssh it prints what to do instead; there the run must come from an admin account or elevated shell).

Termux on Android arm64

Inside Termux, the same one-liner installs the Android arm64 build:

curl -fsSL https://3code.capocasa.dev/install | sh

It detects Termux via $PREFIX and installs to $PREFIX/bin, which is already on PATH. If you prefer to build where it will run, Termux only needs:

pkg install nim git openssl
nimble install https://github.com/capocasa/3code

The binary uses Termux OpenSSL for TLS. Android has no OS sandbox or desktop notifications, but the in-process path checks still apply. Notifications do nothing. To update, install again or unpack a new archive.

Add a provider

Run 3code in your project directory. With no provider configured, it goes straight to the setup wizard. You can return to the wizard later with:

:provider add

The first field accepts:

  • a catalog name, such as nvidia, zai, xai, or openai
  • a provider URL when --experimental is enabled
  • an API key with a recognized prefix
  • supergrok for SuperGrok or X Premium+ login
  • chatgpt for ChatGPT Plus/Pro login
  • claudecode for Claude Pro/Max login

The wizard lists the provider's known-good models (a curated registry of combinations 3code has tested) and saves your selection as-is. With --experimental the wizard queries the provider's models endpoint and verifies each selection with a 1-token call; press Esc to stop verification.

The same steps work without the wizard, from the command line:

3code provider add zai --key $ZAI_KEY
3code provider add zai --key $ZAI_KEY --models "glm-5.3 glm-5.3-flash"
3code provider models zai glm-5.3
3code provider list
3code provider rm zai

add takes the same first field as the wizard (catalog name, URL under --experimental, API key, or a subscription login name) plus --key and --models; without --models it stores the provider's full known-good list. models replaces the list. No verification ping runs; a bad key surfaces on the first turn.

Run your first prompt

❯ Build a Hello World program in Nim

That is enough to get started.

The same prompt given as a command-line argument runs once and exits, for scripts and one-offs; -i/--interactive runs it and then drops you into the REPL. Chaining one-shot runs into a scripted session is covered in Scripting. All switches are in the reference.

Providers and authentication

:provider lists configured providers and marks the current one, along with the model a switch would land on. :model does the same for its models. Both commands have tab completion, so there is no need to memorize catalog names. Model choices are sticky per provider: switching back with :provider returns to the model you last used there, not the first one in its list. The current provider and model are also sticky per directory: each project starts where it last left off, while directories never touched by :provider / :model keep the global config default. All of this lives in the config file; see config file in the reference for every key (-c FILE points a single run at a different one).

:provider nvidia
:model gpt-oss-20b

When a provider ships new models, :provider update <name> re-runs just the models step of the wizard: the offered list refreshes (curated in regular mode, the provider's /models endpoint under --experimental) and Enter keeps the current selection. Name, URL, and key pass through untouched; :provider edit <name> is the full wizard when those need changing too.

To see what you are missing without entering the wizard, :provider new lists every configured provider's not-yet-configured models (:provider new <name> for one provider), and :provider pull brings them in, optionally scoped the same way or down to named models: :provider pull nvidia glm-5.4. New models append to the stored list; nothing is removed, and under --experimental each addition is verified with a one-token call first.

API keys

Most providers use an API key, and the wizard recognizes the common prefixes. For an unknown provider or a custom URL, start 3code with --experimental. The flag is deliberate: compatibility is likely, not promised.

xAI and SuperGrok

An xai- key creates a normal xai provider. Enter supergrok instead to sign in with a SuperGrok or X Premium+ subscription. They can live side by side, which is useful if you have both kinds of account.

The browser OAuth flow stores tokens in:

$XDG_DATA_HOME/3code/auth/xai.json

On POSIX systems the file is created with mode 0600, and tokens refresh automatically; a refresh token that dies ends the turn with a re-login hint, not a crash. Switch accounts with :provider xai and :provider supergrok.

OpenAI and ChatGPT

An OpenAI API key creates an openai provider. Enter chatgpt instead to use a ChatGPT Plus/Pro subscription. As with xAI and SuperGrok, both providers can exist together.

ChatGPT login opens the OpenAI browser OAuth flow and stores tokens in:

$XDG_DATA_HOME/3code/auth/openai.json

On POSIX systems the file is created with mode 0600, and tokens refresh automatically; a dead refresh token ends the turn with a re-login hint rather than a crash. ChatGPT model listings and requests go through the Codex backend at chatgpt.com/backend-api/codex.

OpenAI and ChatGPT use the Responses API. Other OpenAI-compatible providers use Chat Completions. The Codex backend is internal, unversioned, and intended for first-party clients. It works, but using it through 3code is your responsibility.

Switch accounts with:

:provider openai
:provider chatgpt

Anthropic and Claude

An sk-ant- key (or anthropic in the wizard) creates a normal API-key provider against api.anthropic.com. Claude runs on the native Anthropic Messages wire, not a compatibility shim: the system prompt is a top-level field, tool calls are typed content blocks. Pick a thinking level with :reasoning. Claude 5.x always thinks; models that reject off simply do not offer it. Thinking summaries stream to the reasoning ticker while you wait.

Thinking survives the long haul. Blocks replay across tool loops with their signatures, and a compacted or interrupted history degrades instead of failing.

A Claude Pro or Max subscription works like the ChatGPT login: enter claudecode in the provider wizard and finish the browser OAuth on claude.ai. Tokens live in:

$XDG_DATA_HOME/3code/auth/anthropic.json

On POSIX systems the file is created with mode 0600, and tokens refresh automatically. The subscription token only works against api.anthropic.com (the url is pinned), authenticates as a Bearer with Anthropic's OAuth beta opt-in, and the endpoint requires the request's system prompt to open with the Claude Code line — 3code sends that line first and its own Claude preamble second. An API-key anthropic provider can sit beside it.

The same caveat as the ChatGPT/Codex twin applies: the OAuth surface is scoped to Anthropic's own client and is neither versioned nor offered to third parties. It works today; using it through 3code is your responsibility, and it can stop at any time.

Command Code

A Command Code plan (GOAT, Pro, Max, or the Provider plan) gives API access through their provider gateway. Generate a key in Command Code Studio (the same key authenticates their cmd CLI) and add it with :provider add commandcode, or paste the key and pick commandcode when asked:

:provider add commandcode

The open-model lineup (DeepSeek, GLM, Kimi, MiniMax, Grok, MiMo) rides the gateway's OpenAI-compatible /provider/v1/chat/completions endpoint. Usage is metered against the plan's credits.

Provider catalog

Everything the wizard knows about, grouped by where inference actually runs. A ★ marks the providers on the home page rotation, which is the shortlist I recommend. Rows marked quantized serve fp8 or fp4 weights: cheaper per token, but a long agentic run hits the quality floor sooner than the same model at full precision, so treat them as bulk workhorse power, not the model you trust with a hard refactor. Hosting location is where your prompts run, not where the company is registered; for retention policies see Picking a private provider. 3code --good prints the validated provider/model pairs for whichever of these you configure.

Model labs

First-party APIs from the labs that trained the models. Three of the home page names are subscription logins rather than catalog entries: chatgpt is a ChatGPT Plus/Pro login that rides on openai, supergrok is a SuperGrok or X Premium+ login that rides on xai, and claudecode is a Claude Pro/Max login that rides on anthropic.

providerhostingnotes
★ zaicodeChina (Z.ai, intl endpoint)GLM coding plan, flat rate, my main setup
★ zaiChina (Z.ai, intl endpoint)GLM pay-as-you-go
★ deepseekChina (intl endpoint)DeepSeek V4 line
★ kimi / kimicodeChina (intl endpoints)Kimi API and Kimi coding plan subscription
moonshot / moonshot-cnChinasame models; moonshot-cn is the domestic endpoint
minimax / minimax-cnChinaminimax-cn is the domestic endpoint
qwen / qwen-cn / qwen-usChina (Alibaba)intl, domestic, and US-hosted DashScope endpoints
tencentChina (intl endpoint)Hunyuan on Tencent MaaS
volccodeChina (Beijing)Volcengine (ByteDance) Ark coding plan: GLM, Kimi, MiniMax, and Doubao under one subscription, 5-hour windows
alibabatoken / alibabatoken-cnChina (Singapore / Beijing regions)Alibaba Model Studio token plan: credits subscription carrying GLM and DeepSeek alongside Qwen
★ openaiUSAPI key, or log in as chatgpt on a Plus/Pro subscription
★ xaiUSAPI key, or log in as supergrok on SuperGrok/X Premium+
anthropicUSClaude on the Messages wire, or log in as claudecode on Pro/Max
googleUSGemini through the OpenAI-compatible surface
mistralFrance (EU)Mistral first-party, plus hosted GLM

US inference clouds

providerhostingnotes
basetenUS
cerebrasUSwafer-scale serving, fast GLM
deepinfraUSquantized (fp8 weights)
fireworksUSzero data retention by default
groqUSfast small models
hyperbolicUS
nvidiaUSfree credits to start
sambanovaUS
togetherUSno training on API data by default
veniceUSprivacy-leaning
arceeUS
friendliUSKorean-founded inference cloud
perplexityUS

EU providers

Where to point 3code when data must stay under EU jurisdiction.

providerhostingnotes
★ greenptEUquantized; my EU pick anyway
nebiusEU (Netherlands)
inceptronEU (Sweden)zero data retention, EU residency
ovhFranceno training, billing-only retention
scalewayFrance
hetznerGermanyfree experiment, quantized (fp8/fp4 ids)
together-euEUTogether's EU endpoint
★ platteGermanyEU sovereign host; url embeds your username, wizard asks for it
★ kolibriyour own hardwareAleph Alpha's Kolibri-1 (open weights, Apache 2.0, German/English): serve it yourself with vLLM + the aleph-alpha-inference plugin, then :provider add kolibri (url http://localhost:8000/v1)
★ tesseractedGermanyTesseracted Labs' hosted Kolibri-1 (same weights, their GPUs): :provider add tesseracted (url https://api.tesseracted.com/v1); still in preview, so model access is granted manually: email [email protected] describing your use case and they send a key (tl_live_...)
akiEU
lyceumEU
cortecsEU (gateway)routes to EU-sovereign upstreams (TensorX, Nebius, ...) under one API; GDPR posture, retention policy varies by route

Routers and aggregators

One account, many models. The model you get is the model some upstream provider served; quantization, price, and policy vary by route, so check the route before blaming the model.

providerhostingnotes
★ openrouterUS company, upstreams worldwidethe easy way to try many models
novitaSingaporefree tier; often quantized
nanogptvariescrypto payments; often quantized
opencode / opencodegogatewayOpenCode Zen (metered) and Go (subscription) gateways
vercelUS company, upstreams worldwideVercel AI Gateway; no markup on list prices, DeepSeek routes below list (vck_... keys)
aihubmixChinaaggregator speaking four wire protocols; a one-time $1 top-up unlocks permanent daily free GLM/Kimi/MiniMax coding quotas
engyvaries (GPU fleet)verified inference from a permissionless fleet; cheapest DS 4.1 flash around, GLM capped at 328K context
huggingfacevariesrouter to partner clouds
cheaperinferencevariesdiscount aggregator

The known-good registry also covers a few providers that are not in the wizard catalog: TensorX (EU, Dublin and Helsinki), poolside, kilo, LongCat, and Xiaomi MiMo. Add those by hand as custom providers when wanted.

Interactive use

At the interactive prompt, describe the result you want or enter a command beginning with :. Tab completes commands, provider names, model names, and paths where supported.

3code loads 3CODE.md and AGENTS.md from the project directory and its parents. These are good homes for project rules, build commands, and style requirements. To include a file directly in a prompt, prefix its path with @:

Review @src/parser.nim and add tests for malformed input.

To run a shell command yourself without sending anything to the model, prefix it with :!:

:! cargo test

Output streams into a live viewport while the command runs, then folds into your scrollback; the model never sees it. The command runs through the same sandbox policy and the same executor as model-run commands.

The daily keys:

keyaction
Entersubmit
Shift+Enter or Alt+Enterinsert a newline
Up / Downhistory
Ctrl+Arrowmove by word
Ctrl+R / Ctrl+Ssearch history as you type
Alt+Eedit the input in $VISUAL/$EDITOR
Ctrl+C or Escclear input; cancel the turn when it is empty
Ctrl+Dexit

If you forget one, :help prints the keys in effect. The full table, kill ring and all, lives in the reference.

Pasting behaves the way you want: a multi-line paste stays one draft instead of submitting on the first embedded newline, so you can paste a code block, edit it, and submit with Enter. :! and @file prefixes still work inside a pasted draft.

An empty prompt is a real gesture: press Enter with nothing typed and the ongoing session continues — a turn runs on the conversation exactly as it stands, no user message appended. It is the cheap way to say "keep going" after a turn you want more of. On a fresh session with no turns yet it does nothing. allow-empty-prompt = off in [settings] turns the gesture off (it is on by default).

The defaults are the same readline-style Ctrl keys everywhere, including macOS. If you want the Mac editing keys instead, bind the Cmd names in the config; they arrive through the kitty keyboard protocol (kitty, Ghostty, iTerm2, WezTerm, foot, Warp), which 3code enables automatically where available:

[shortcuts]
edit-in-editor   = AltE CmdE CtrlX>CtrlE
home             = Home CtrlA CmdLeft
end              = End CtrlE CmdRight
word-left        = AltB CtrlLeft AltLeft
word-right       = AltF CtrlRight AltRight
delete-to-bol    = CmdBackspace
delete-word-left = CtrlW AltBackspace

Terminals without the protocol ignore the handshake, and unbound Cmd keys simply do nothing. This is what makes the Command key usable on macOS: Option composes characters there (a dead accent on German layouts, so Alt+E and friends cannot arrive as keystrokes), while Cmd chords are reported as-is through the protocol.

Selecting text

The input buffer is a real text widget: hold Shift with the arrows (or Home/End, Ctrl+Arrow for words, Ctrl+Shift+Home/End for the whole buffer) and the range highlights in reverse video. Cmd+A selects the whole buffer. Then:

keyaction
Ctrl+Xcut the selection (without one, Ctrl+X is the emacs prefix)
Alt+Wcopy the selection
Ctrl+Vpaste from the system clipboard
Ctrl+Ypaste the last cut/copied text (kill ring)

Any edit (typing, backspace, delete) replaces the selection, and any plain cursor motion drops it, the way every desktop text field behaves. Copy and cut also publish to the system clipboard through OSC 52, which most modern terminals honor (xterm needs allowWindowOps); paste reads the clipboard through the platform helper (pbpaste, wl-paste, xclip, xsel, or PowerShell) and falls back to the kill ring when the platform has none.

Rebinding keys

Keys are commands, and commands are reassignable in the config file (~/.config/3code/config; see config file in the reference for every platform). Add a [shortcuts] section that maps a command name to a space-separated list of keys:

[shortcuts]
cancel = DoubleESC

Now a single Esc does nothing and two quick Esc presses cancel the current turn, handy if you keep brushing Esc by accident. Values are merged onto the defaults, so a command keeps its built-in keys until you override it. An empty value unbinds a command entirely:

[shortcuts]
suspend =

The commands: cancel, clear, quit-if-empty, home, end, left, right, word-left, word-right, up, down, history-previous, history-next, backspace, delete, insert, delete-word-left, delete-to-boundary-left, delete-word-right, delete-to-boundary-right, delete-to-eol, delete-to-bol, clear-screen, suspend, complete, reverse-complete, edit-in-editor, insert-newline, buffer-start, buffer-end, select-left, select-right, select-up, select-down, select-home, select-end, select-word-left, select-word-right, select-all, select-buffer-start, select-buffer-end, cut, copy, paste, yank, yank-pop, yank-last-arg, transpose-chars, transpose-words, upcase-word, downcase-word, capitalize-word, undo, revert-line, isearch-backward, isearch-forward, nonincremental-search-backward, nonincremental-search-forward, history-begin, history-end, set-mark, exchange-point-and-mark, quoted-insert, char-search, char-search-backward, delete-horizontal-space, tab-insert.

Keys joined by > form a chord: pressed in sequence, they run the command once. The default edit-in-editor binding is exactly that:

[shortcuts]
edit-in-editor = AltE CtrlX>CtrlE

Alt+E opens the editor directly, and so does Ctrl+X followed by Ctrl+E (the emacs edit-and-execute-command chord). While a chord is half pressed, the first key is reserved for the chord: its own single-key commands wait (Ctrl+X cuts only when a selection exists), a key that completes no chord is swallowed, and Esc abandons the chord without touching the line. A space still means alternatives, so Home End binds two keys, while CtrlX>CtrlE binds one chord.

Key names look like CtrlC, AltF, CmdE, ShiftTab, Home, End, Up, Down, Left, Right, Backspace, Delete, Insert, Tab, or ESC. CmdX (alias SuperX) is the macOS Command key, delivered by terminals with the kitty keyboard protocol. Modified navigation keys take a prefix: ShiftLeft, ShiftHome, CtrlLeft, CtrlRight, CtrlDelete, CtrlShiftLeft, CtrlShiftEnd, and so on across the arrows, Home, End, and Delete. Names are case-insensitive (ctrlc is CtrlC). Double requires two quick presses, as in DoubleESC or DoubleCtrlC.

:help always shows the bindings currently in effect.

The screen

3code is not a TUI. The screen is ordinary terminal history scrolling up (your terminal's own scrollback, search, and mouse selection all work), plus a small live footer at the bottom: a thinking ticker row and a token bar above the prompt. Everything committed below the footer stays in the scrollback forever and can be re-read or copied like any shell output.

Scrollback contents

Each conversation event gets a one-character marker or banner icon:

iconitem
❯your prompt, echoed as submitted
●assistant message, rendered as markdown (headings, code fences, tables fitted to the terminal width, [text](url) links as clickable OSC 8 hyperlinks with the target kept visible)
»a harness-autosend: a note 3code itself sent to the model (empty-reply steer, loop nudges), with the exact text shown
$bash command with its command line; the icon turns Ø when the exit code is nonzero
rfile read (path and line range)
wfile write (path; the diff follows)
ppatch, banner shows the first changed line
▸update_plan call, rendered as a checklist
⌕web search (query)
⇊web fetch (URL)
⟳context clear, followed by the handoff prompt for the fresh context
✉dmail (kimi family only): revert context to a checkpoint, keep the files
✕error, or a tool 3code does not know

Tool output below the banner is dimmed to keep the model's prose the loudest thing on screen. Long output is elided in the middle (... N lines omitted · T7.log ↗); :show N prints the whole capture, :log lists every tool call of the session. The whole elision row is a terminal hyperlink (OSC 8) over a dump of the full output in $TMPDIR/3code_out_<pid>/T<N>.log(.diff), so terminals that support links open it in the file: handler (e.g. $VISUAL) on a click; the visible file name keeps the row honest where links are unsupported, and the dumps live as long as the session does. Diffs are painted green/red, and a long diff is clipped the same linked way rather than flooding the screen. Failed tools mark their banner so a red Ø/✕ scan tells you where a turn went wrong without reading anything.

Grey · rows are 3code's own commentary: the context gauge warning before auto-summarization, the elapsed time when a turn ends without usage, the ● resumed banner after --resume. The » rows deserve a second look when something odd happened: they are the interventions described in when the model misbehaves, shown verbatim instead of hidden, so you can see exactly what was sent on your behalf.

The token bar

While a turn runs, the footer's bottom row is the token bar:

⠋ ◔25%  ↑4.2k  ↻18k  ↓1.1k  00:12

fieldmeaning
⠋braille spinner: a request is in flight; during a retry backoff it becomes an hourglass ⧗ while the notice row counts down
◔N%context window used; the circle fills (○ ◔ ◑ ◕ ●) as the window fills, one glance tells you how close you are to summarization
↑fresh input tokens this turn
↻cached input tokens: served from the provider's prompt cache, usually a tenth of the price
↓output tokens
00:12elapsed time for the live turn

The bar is an instrument panel, not a decoration: watch ↻ dominate ↑ to confirm the cache is hot (resuming a session re-sends byte-identical history precisely so it stays that way), watch the filling circle to decide between :summarize now and a clean :clear later, and treat a nonzero Ø together with a stuck spinner as your cue to look at the last » row. In private mode the whole bar repaints magenta so the mode is visible from across the room. Its color is configurable (token-bar in the reference).

The thinking ticker

The row above the token bar is a one-line ticker. While a reasoning model thinks, its planning text scrolls through it like a stock ticker, then the row goes quiet when the answer starts. It is an activity indicator and a quick-glance channel: enough to see the model is reasoning about the right files, not a panel to read. Nothing on it enters the transcript or the session; reasoning text is replayed to the provider only when the model's family needs it (the think-back parameter, see params in the reference). The same row doubles as the notice row for retry countdowns and live warnings.

Receipts and conversation cost

Each response ends with a compact token receipt spliced under the last scrollback item of the turn:

  ○12%  ↑4.2k  ↻18k  ↓1.1k  8s

fieldmeaning
○N%context window used at that turn
↑fresh input tokens
↻cached input tokens
↓output tokens
Xselapsed time

The receipts are a per-turn cost ledger. ↑ and ↻ are what the provider bills as input (cached usually at a steep discount), ↓ is the output you waited for, and the percentage tells you how much of the window the turn consumed. Adding the ↑, ↻, and ↓ columns down the scrollback gives the token cost of the whole conversation, which is usually more interesting than any single turn: it shows the cache working (a hot session is mostly ↻), the moment a task went sideways (a jump in ↑), and the true price of those just one more thing follow-ups. :tokens totals the same columns for the session so far. If a field is missing, the provider did not report it. 3code declines to invent accounting.

Patient retry

Patient retry is enabled by default. On 429 limits, 5xx responses, and network failures, 3code backs off exponentially rather than failing immediately. Delays are capped at 2048 seconds and retries can continue for about 36 hours, long enough to outwait a rolling limit or a bad connection.

After the short initial retry period, attempts become part of the transcript:

Usage limit reached for the past 5 hours. (code 429). retry 42/64 in 2048s

Use these commands to inspect or change the setting:

:retry list
:retry off
:retry on

:retry with no argument is retry now: it cuts a backoff wait short, keeping the backoff schedule as it was so later failures back off exactly as they would have, and it cancels and resends a request that has not shipped any output yet (still connecting, or headers only). Once model output is streaming, or at the idle prompt, it does nothing. Commands typed while a turn runs take effect at Enter, so this works mid-wait.

Even with patient retry off, 3code gives transient failures about a minute before returning to the interactive prompt. The setting is stored as patient_retry in [settings] (settings in the reference). Press Esc to cancel the wait. A stream that stalls network-quiet or arrives truncated mid-response is detected and retried on its own, regardless of the setting.

For a provider whose streaming is the problem rather than its availability, :streaming off switches requests to plain request/response mode: the reliable fallback when a provider's SSE implementation drops or truncates responses. The setting is per session, or permanent as streaming = off in [settings].

When the model misbehaves

Two failure modes are common enough across providers that 3code guards against them automatically. Both guards are deliberately small: a few hundred tokens of mechanism for a failure mode that would otherwise burn millions. Provider API errors along the way surface as formatted messages, never raw JSON bodies.

The flailing detector

Sometimes a model gets stuck repeating the same tool call: the same failing patch, the same unhelpful grep, a loop it cannot think its way out of. Every repeat burns another round trip and another batch of output tokens while making no progress. The flailing detector fingerprints each tool call and watches the recent window for repeats: identical calls back to back, A-B-A-B cycles, or a long uninterrupted streak of near-identical calls to the same tool whose results stopped saying anything new; a call about something else restarts the streak, and a streak that keeps delivering fresh output is never a loop no matter how similar the calls look, so a systematic scan (one fixed query walked over successive windows of a file) runs uninterrupted: every window is a new question, and "no match here" is its answer, not a repeat. A verification run retrying one command while reading other files in between is not a loop either. When a streak does flag, the recovery ladder climbs one rung per model response: a batch of parallel tool calls gets one nudge it can actually see, never three warnings and a sentence in a single breath.

When it sees one, it intervenes in the model's voice of authority: a tool result is replaced by a short SYSTEM note naming the loop and demanding a strategy change. Two more escalations follow if needed, the last one telling the model to stop and report what is blocking it. A fourth flagged repeat aborts the turn and hands control back to you. Every intervention appears in the scrollback as a » row with the exact text sent, so nothing happens invisibly. A genuinely new tool call resets the ladder; normal work never sees it.

The thinking loop guard

Reasoning models have their own version of getting stuck: the thinking stream wedges on one phrase and re-emits it until the model escapes, the output budget dies, or you interrupt. No tool call is ever emitted, so the flailing detector watches in vain. The stream itself carries the guard: while a reply is still pure thinking, the recent tail is scanned for one phrase repeating, and a wedged stream is cut mid-flight. What lands instead is an empty turn plus a » re-prompt naming what happened - "your reasoning kept repeating itself" - which is usually enough; two re-prompts that both re-wedge end the turn with a notice instead of burning retries. The tripwire is calibrated against 63,479 recorded reasoning passes: thinking that merely repeats a topic phrase, or quotes dense tables of output, sails under it; every recorded loop trips it.

The empty response handler

Some providers occasionally return a reply with no content and no tool calls: the model spent its budget thinking, or the gateway clipped the response. 3code refuses to accept that as an answer. Depending on what the response signaled, it either raises the output budget (the reply was cut off by length) or sends a short steering note demanding a real answer, then retries. Bare empties are retried up to twelve times with increasing backoff, and each attempt is visible as a grey or » scrollback row saying why and when the next attempt fires. If the provider stays empty, the turn ends with a notice instead of a silent nothing, and the scrollback shows exactly what was tried.

Private mode

Private mode is incognito mode for the agent: off by default, on for one session only, never persisted. While it is on, turns only run on providers you have marked as trusted with your data, and the live token bar repaints in the private color (magenta by default) so the mode is always visible. Scrollback receipts keep the normal color, so old receipts also show which turns ran private.

Turn it on either way:

3code -p            # from the start
:private on         # mid-session (turns the bar magenta)

A provider or model is allow-private when:

  1. you set it explicitly with :private allow, or in the config:

    [params]
    provider = "together"
    allow-private = "true"
    
    [params]
    provider = "zai"
    model = "glm-5.3"
    allow-private = "true"

    With model empty the whole provider is trusted; a model-scoped entry beats a provider-wide one, and allow-private = "false" revokes.

  2. or its known-good entry is curated for a provider whose published policy is zero data retention with no training on API data. Curated today: together, fireworks, ovh, novita. Everything else, including all first-party labs, must be allowed explicitly.

Everything else is refused at the gate with a pointer to the fix:

zai.glm-5.2 is not allow-private (trust it with :private allow zai, or
switch to a zero-training provider; :private lists)

The goal-mode verifier is fenced the same way: the satisfaction check ships the goal text and the working-tree diff, so while private mode is on a verifier that is not allow-private never sees them — the check quietly takes the session model instead (a dim note in scrollback says so). Trust a verifier with :private allow like any other model.

Picking a private provider

Pick a provider with a zero-training policy you are satisfied with (a strict zero data retention policy, or at least a no-training plus bounded retention policy). A shortlist by region:

regionproviderpolicy
EUTensorXzero data retention; GPUs in Dublin and Helsinki, Irish/EU jurisdiction (add it as a custom provider, it is not in the catalog)
EUOVHcloud AI Endpointsno training on your data, retention for billing only; hosted in France (catalog: ovh)
USATogether AIno storage of inputs/outputs by default; training is opt-in and off (catalog: together)
USAFireworks AIzero data retention by default; no logging of prompts or generations (catalog: fireworks)
AsiaNovitazero data retention by default, no training on your content, per their terms of service (catalog: novita)
AsiaZ.ai pay-as-you-gorouting guides report zero retention on the paid API; read the current DPA before trusting it (catalog: zai)

Or pick any provider you trust, and allow it explicitly. That is a judgment call, not a recommendation.

Walkthrough

❯ :provider myprovider
❯ :model glm-5.3
❯ :private allow myprovider glm-5.3   # persisted as [params] allow-private
❯ :private on                         # bar turns magenta, turns are gated

:private with no argument shows the mode, whether the current model is allowed, and which configured providers are trusted. :private deny removes trust. The bar color is configurable like the other colors (colors in the reference):

[colors]
private-bar = "\x1b[95m"
token-bar = "\x1b[36m"

Reasoning

:reasoning lists the levels the current model actually supports and marks the active one:

:reasoning
:reasoning high
:reasoning off

There is no common scale. Most models use low, medium, and high. Some start at off, some end at max, some cannot turn thinking off at all. 3code knows the scale of the model you picked and sends the right field for it. A level missing from the list is a level the model does not have.

When a provider streams reasoning text, 3code keeps it moving through a one-line ticker above the interactive prompt. It is not added to the transcript.

Web research

The model has two web tools: web_search (titles, URLs, snippets) and web_fetch (a URL rendered to readable text, boilerplate stripped). Ask for research in plain language and the tools load; the system prompt's web skill is only injected when the task needs it, so ordinary coding turns do not pay for it.

The search backend is configurable: Exa (default, runs keyless), Parallel (keyless), and Brave (needs a free API key). Pick one in the config:

[search]
engine = "brave"
brave-key = "..."

There is no automatic failover between engines; the chosen one is used as is. See search in the reference for all keys.

Known-good models

Provider APIs differ in small, consequential ways. 3code therefore keeps a registry of combinations tested with the right prompt, reasoning settings, token fields, and context size.

Print the current registry with:

3code --good

For combinations outside the registry, --experimental opens the gate.

System prompt override

Each model family's system prompt is compiled in, but a file override can replace it wholesale. Place a <family>.txt in one of these (first match wins):

  1. .3code/<family>.txt in the project
  2. .agents/<family>.txt in the project (the vendor-neutral name)
  3. <family>.txt in the user config root (~/.config/3code/ on Linux, ~/Library/Application Support/3code/ on macOS, %APPDATA%\3code on Windows)

The family name is the branch the prompt selection takes (glm, gpt, claude, and so on). 3code --good shows each combo's family, and the experimental family provider setting picks one for combos off the registry. When an override matches, startup announces it on stderr, and :prompt shows the exact prompt in effect.

Two placeholders are substituted into the file: {{credit}} (the provider credit line) and {{skills}} (the discovered skill catalog). A file without them runs without those sections.

An override takes effect in new sessions; a running session keeps its prompt until restart or profile change.

Images

Vision-capable models can see images. Two ways to get one in front of the model:

  • Attach: put @mockup.png in your prompt. The image rides the message as an image block (not 64 KB of inlined binary), the echo shows @mockup.png [image attached], and the model answers about what it sees.
  • Read: the model calls read on an image path — e.g. to inspect its own rendered output — and the image rides the next message. The tool result is a one-line text receipt (image logo.png 1920x1080 PNG -> delivered 1280x720 JPEG; attached below), the transcript shows · read img logo.png 1920x1080 -> 1280x720, and the follow-up message carries the pixels.

Which models can see: the ones --good marks as vision-capable. The mark is earned. Every curated combo was probed with a real image (grok looked at a button and said "Button."). Two exceptions stay off: nebius, whose proxy rejects image parts outright, and the claude line, whose wire path has no image translation yet.

You can overrule the mark with [params] vision = "off" or "on" (set via :provider edit). Nothing fails quietly either way: on a model without vision, @x.png and read x.png both fail with a hint naming a model that has it.

Delivery is budgeted, not original: images are re-encoded once to JPEG q85 inside a 1280 px box and a 190 KB base64 budget, with a quality-then-scale ladder shrinking further until it fits. The delivered bytes are written under the session's image dir and re-used verbatim, so a resumed session re-sends identical bytes and the provider's prompt cache stays hot. At most 3 images attach per message and 4 per turn; repeated reads of the same image count as flailing.

Session files store image <path> <sha1> records, never base64. On resume the image re-embeds from disk when file and checksum still match; otherwise it becomes an [image stale: name] note. Compaction collapses old image blocks to [image WxH] text notes — summaries never carry image blobs.

read on an image needs a resizer for the budget ladder: ImageMagick (magick or convert) or, on macOS, sips. Without one, small originals pass through unchanged and large ones error with an install hint. (Windows resizer support is pending the Windows sandbox work.)

Computer use

Vision models can also drive your desktop: :computer on (or --computer) arms the screenshot tool for the session. Off by default, never persisted — a resumed session starts with it off until you arm it again.

On activation the backend is probed (X11: one of import/maim/scrot plus xdotool; missing binaries are named in the error), a one-time note enters the conversation teaching the model the observe→act→verify loop, and the screenshot schema joins the offered tools (a one-time prompt-cache bust, like any mid-session tools change). Idle cost is one skills-catalog line (computer-use.md).

  • screenshot() captures the screen; the image rides the next message as an image block (same delivery path as @image and image reads), the tool result is a text receipt, the transcript shows · shot 1920x1080 -> 1280x720.
  • A shot script lands in the temp dir: shot click 640 360 (also doubleclick/rightclick/middleclick, drag X1 Y1 X2 Y2, type TEXT, key ctrl+s, scroll up 3, title). It takes delivered-raster coordinates and maps them to the native screen itself; a stale frame (no capture yet, or the resolution changed) errors loudly and tells the model to screenshot() again. The model reaches it through the normal bash tool — no new action surface.
  • Guardrails: captures are capped at 3 in a row without an intervening action, capture streaks feed the flail detector, and there is no sandbox between shot and your desktop — that is the point. Esc/Ctrl-C is the panic button.

macOS (screencapture + cliclick), Wayland (grim + ydotool) and Windows are not implemented; arming there fails with a clear error instead of half-working.

Sessions

List the 20 most recent sessions for the current directory:

3code --list
# or
3code -l

Resume the latest session:

3code --resume

Resume a session by ID (any value-taking option accepts the value attached, =/:-separated, or space-separated):

3code --resume=abc123
# or
3code -r abc123
3code -rabc123
3code -r:abc123

Or, without restarting, from inside a running 3code: :resume lists this directory's sessions, :resume abc123 swaps the running REPL onto that session (state, locks, and profile move with it; the scrollback is not replayed, scroll up or :show for old turns).

Sessions are listed and searched per working directory by default, so unrelated projects stay out of each other's history; -a opts out of the scoping for both. Provider prompt caches may expire before a saved session does. The session will still resume, but its old context may no longer get the cache discount.

A resumed session replays the full prior conversation into your scrollback (the same ❯/●/tool banners, receipts included) so you land back in context, and the history is re-sent byte-identically to the provider, which keeps the prompt cache hot: a resume usually costs cached-input prices on everything before your new prompt.

Session files are ordinary JSON under 3code's data directory and are created readable only by your user (mode 0600 on POSIX). Also note that 3code refuses to run as root: it executes the shell commands the model proposes, and root blast radius is unacceptable. Run it as your normal user (THREECODE_ALLOW_ROOT=1 overrides, for containers).

Notifications and auto-update

When a turn ends, 3code can fire a desktop notification, useful when you switched to a browser while a long turn grinds. It is on by default where supported; toggle it with :notify on|off or the notify setting. On Termux there is no notification service, so the setting does nothing.

The terminal tab title also reports state: a 3 brand plus a spinner glyph while a turn runs, then a checkmark (or an ✗ when you cancelled) when it ends. The project directory rides along for wide tabs. Toggle it with :tab-title on|off or the tab-title setting.

Prebuilt binaries self-update in the background: a detached worker checks GitHub releases at most every four hours, downloads the matching tarball, and atomically swaps the binary for the next launch. One dim line on the first launch after a swap is the whole visible footprint. Source builds do not self-update by default (what you built should not quietly replace itself); the auto-update setting overrides the default either way. Both settings are covered in settings in the reference.

Context management

:clear starts a new conversation with the same provider and model:

:clear

:summarize replaces older turns with a generated recap. It is useful when the task must continue but the context has become expensive:

:summarize

If the old work is no longer relevant, clear it. A fresh context is safer than a summary of things the model no longer needs.

Chunked mode

For a large mechanical task, ask the model to split the work into files. Each chunk ends by calling the clear tool with instructions for the next one. The model carries the plan forward and leaves the spent context behind.

Example:

Divide this task into 4-6 chunks in impl-N.md files.
End each file by calling clear with instructions to read the next file.
Then execute chunk 1.

Task: add test coverage to the parser module.

Chunked mode works best when little human input is needed, such as adding basic tests across many modules. If the model forgets to load the next chunk, the task was probably too difficult or the plan too vague.

Skills

A skill is a markdown file with instructions for a class of task: how to run a systematic debug, how to write release notes, how to behave as a thinking partner. Skills are cheap by construction: the system prompt carries only the file list (one bullet per skill), and the model reads a skill file with the normal read tool only when the task calls for it. Nothing is preloaded, so a skill you never use costs nothing.

Skill files are found in these directories, first match wins on a name collision:

  1. .3code/skills/ in the project
  2. .agents/skills/ in the project (the vendor-neutral name)
  3. ~/.config/3code/skills/ (your overrides)
  4. ~/.local/share/3code/skills/ (built-ins, extracted by 3code itself)

The built-ins shipped today are the role skills (conversational, sysadmin, thinking partner, writing) and task-debug-systematic. To override one, put a file with the same name in a directory higher up the list. To add your own, drop a .md file in any of those directories with an instructive name: the filename is the trigger, so release-notes.md loads when the task smells like release notes. For heavier machinery than a skill there is a native plugin tier (C-ABI hooks and model-facing tools, running host-side); see plugins. A skill file plus command-line tools still covers most of the same ground at a fraction of the context cost.

Goal mode

Goal mode is the sturdier option for a long coding task. Drop a goal.md into the project root stating the goal: what must be true when the work is done, how to verify it. Anyone can write the file — you directly, or the model when you ask for multi-part work.

While goal.md exists, every turn end runs a satisfaction check: a fresh side conversation with the session's own model, given the goal text, a summary of what the turn actually did (files, patches, commands — never the agent's own claims), and the working-tree diff, judges from those inputs alone and answers with a verdict. Not satisfied, the agent is sent back in with the verifier's feedback. Satisfied, the harness renames the file to goal-confirmed.md (the record of the run) and idles. The check has no tools and no rounds: orientation is the harness's job, so the check is one model call. And it shows its work: the verifier's reply lands in scrollback as an item of its own, led by a ⌕ glyph and rendered exactly like any other response — same markdown, same wrapping — with the verdict line stripped and replaced by an icon row under the reply: a green ✓ when the goal is satisfied, a red ✗ when it is not. Nothing the harness runs is ever silent.

Verifier models

The check can also run on a dedicated verification-class model:

:verifier                 show the verifier setting
:verifier list            list the verification-class models you can use
:verifier jev-latest      run every goal check on that model
:verifier off             back to the session model (the default)

Verification-class models are decision models — TypeSafe's Jev (served through OpenRouter, so an existing OpenRouter key covers it) is the curated one. A decision model does not write prose: it answers typed questions (yes/no with a calibrated probability, pick-one-of-N) in a single forward pass, which makes a check per turn end cheap enough to run always. The goal check asks it two questions — is the goal satisfied, and what is the most important gap — and turns the answers into the same verdict/feedback the text verifier produces: p(satisfied) at or above 0.75 confirms the goal, anything else sends the agent back in with the gap as its direction. The decision lands in scrollback as a ⌕ item with the model's own numbers and the same verdict icon row as a text check.

These models never appear in ordinary model lists (:model, completion, the provider wizard): they cannot run a turn, they only judge one, so :model jev-latest refuses and points here. Any ordinary model works as a verifier too (:verifier glm-5.3-flash on the current provider) — that check is the regular text side call, just on the named model instead of the session's. verifier off is exactly the behavior from before verifier models existed: the session model checks itself. The selection persists in the config ([settings] verifier_model = "openrouter.typesafe/jev-latest", plus verifier = "off" only while disabled), and the welcome block grows a verifier line naming the active verifier so the state is visible at a glance. A verifier that stops resolving (provider removed, key gone) is skipped with a visible note — the check falls back to the session model rather than going silent.

The file is the whole interface. Edit goal.md mid-run to steer (:goal edit opens it in $VISUAL/$EDITOR; the next check reads it fresh), delete it to end goal mode silently, interrupt to pause. Arming starts work immediately: :goal <text> writes the file and sends the agent off in the same breath, no extra prompt needed — and a goal.md that is already on disk when 3code starts arms identically: the session loads it and the kickoff turn runs at once, before the first prompt. The arm itself is quiet (the echoed command is the whole announcement); a session resumed from file replays the kickoff prompt under its ◎ marker. :goal cancel (or :goal!) cancels the active goal, retiring the file to goal-cancelled.md (:goal new <text> is the balking synonym that refuses a live goal, :goal! <text> replaces it, bare :goal shows it). A failing verifier call is retried at the next turn end; the harness does not give up on its own, but if it ever does the record lands in goal-given-up.md. Retired files accumulate harmlessly; delete or archive them whenever.

No sub-agents

3code does not provide sub-agents. They use plenty of tokens, and I have not seen enough benefit to justify the little management consultancy they form. For parallel or long-running work, use worktrees with goal mode.

Sandbox

The model is useful, but it does not need the run of your entire computer. 3code applies a filesystem policy to every tool call. Bash commands also enter an OS sandbox, and host rules can fence their network access.

The sandbox is questionless: it never interrupts the turn to ask whether a command may run. That is a deliberate trade. An agent that asks is an agent that costs a round trip and a context refresh every time it touches a new directory, which in practice means either an agent that nags or an agent whose guardrails get switched off. Instead the boundary is enforced by the kernel (Landlock, Seatbelt, Windows restricted tokens), decided by a policy you wrote before the turn started, and a denial is simply a failed command the model can read and route around. The tradeoff is that the policy needs setting up first: a couple of minutes once per project, and after that it does not stop on trivialities.

The one exception is the ask verb. An ask rule denies exactly like deny, but when a sandboxed bash command is blocked on an ask-protected path, 3code puts one question in the tool output view:

sandbox ask: ~/vault/notes/x.md is ask-protected.  [f] ~/vault  [d] ~  [n]o  (60s)

One key answers it: f allows the ask-protected path (the ask rule's own target), d the directory around it (the grant is written into the active policy file, so it sticks for the session and beyond), n keeps the denial. No answer within 60 seconds counts as n, and the failed command is never retried without an explicit grant. Use ask for the gray zone: paths you would probably allow if you were watching, like a browser profile or a notes vault, where deny would cost a manual policy edit and allow feels too broad unattended.

In practice:

  • The project directory is writable out of the box (allow ./ is in the built-in default), plus the temp directories. A fresh clone needs no setup at all.
  • The default covers command-line builds of the common languages. Git, Go, Nim, Python, Rust, Zig, and C build out of the box. On Windows, git gets a seeded copy of your global config, so it works there too. Files that carry tokens stay fenced: ~/.ssh, ~/.git-credentials, ~/.npmrc, ~/.cargo/credentials. Every rule is listed in the built-in default below, and stacks beyond it get recipes in sandbox settings.
  • Stacks not covered by the default add their home with one rule: maven wants :sandbox allow ~/.m2/repository, gradle :sandbox allow ~/.gradle/caches, an npm prefix like ~/.node_modules becomes :sandbox readonly ~/.node_modules (plus :sandbox allow ~/.npm for its cache).
  • Other directories need a rule. When a task will wander (a monorepo sibling, ~/src, the directory where you keep checkouts), consider adding them broadly (:sandbox allow ~/src) rather than playing whack-a-mole one path at a time; the model reads the policy and works within it without asking.
  • Keep backups of what the sandbox protects. The fence is there so a confused model cannot rewrite ~/.ssh or dotfiles; it is not a substitute for version control, and git inside a writable project remains your real undo.
  • Use deny for sensitive data you know the model should never see or send: deny ./secrets, deny ~/.ssh, credentials directories. Denial is cheap, silent, and total (read, write, and network), so err on the side of denying.

Exactly one policy source is active:

  1. .sandbox in the project directory, if it exists
  2. ~/.config/3code/sandbox, if you created it
  3. the built-in default, held in memory

Policies do not cascade or merge. One file wins, so what is allowed remains inspectable. A project file replaces the user file. 3code never creates the user file; create it yourself to set defaults for projects without .sandbox.

The first :sandbox allow, :sandbox readonly, :sandbox deny, or :sandbox edit command in a project copies the effective policy to .sandbox before changing it. You get a complete project policy, not a mysterious patch on top of another file.

Policy format

A policy has one rule per line:

WordMeaning
denyno read, write, or network connection
readonlyread and execute, for paths only
allowread and write a path, or connect to a host

The target type is determined by its first character:

StartTarget
/absolute path
X:Windows drive path
~path under the home directory
.path relative to the project directory
letter/digithostname or IP address, with an optional port

Paths are intentionally explicit. Use ./foo for a project-relative path. An access word on its own means the project directory itself:

deny /
allow
readonly /var
deny ./secrets

Rules are read from top to bottom and the last matching path rule wins. In this example the project is writable, /var is read-only, and ./secrets is denied. Everything else is denied because unmatched paths are denied by default.

Blank lines and lines beginning with # are ignored. An access word only has special meaning when followed by whitespace or the end of the line, so a host such as deny.example.com remains a hostname.

Built-in default

The default is a plain text table in the source, one literal block per OS, so it is as inspectable as any policy file: src/threecode/defaults.nim. It changes only through ordinary commits.

On macOS, Linux, and other POSIX systems, the built-in policy is:

deny /
allow /tmp
allow /var/tmp
allow ./
allow ~/go
allow ~/.cache
allow ~/.npm
allow ~/.cargo/registry
allow ~/.cargo/git
readonly ~/.cargo/bin
readonly ~/.cargo/config.toml
readonly ~/.rustup
allow ~/.nimble
readonly ~/.choosenim
readonly ~/.local/share/uv
readonly ~/.gitconfig
readonly ~/.config/git
readonly ~/.hgrc
readonly ~/.config/hg
allow *

On macOS the same policy plus allow ~/Library/Caches applies. This lets the model write in the project and temporary directories and the development caches, not wander through other user files. System programs, libraries, configuration, and device files get a read-only baseline so commands can still run, and the version-control config lines exist because git aborts every command when ~/.gitconfig exists but is unreadable. allow * leaves the network open.

On Windows, the built-in policy is:

deny /
allow ~/AppData/Local/Temp
allow ~/AppData/Local/npm-cache
allow ~/AppData/Local/pip/cache
allow ~/go
allow ~/.npm
allow ~/.cargo/registry
allow ~/.cargo/git
readonly ~/.cargo/bin
readonly ~/.cargo/config.toml
readonly ~/.rustup
allow ~/.nimble
readonly ~/.choosenim
allow ~/AppData/Local/zig
allow ./

Windows grants network capability when no host rules are present.

When no policy file exists, the built-in default is materialized into a per-run temporary file for the Bash sandbox; nothing is written into the project or the config directory. The default grants the project directory itself (allow ./), so a fresh project is writable out of the box.

Sandbox settings collection

Recipes for stacks past the basics. The default policy already covers git, mercurial, C, Zig, Go, Nim, Python (pip, uv, venv), Rust, and the npm/pip caches; on macOS javac itself runs too (the JDK homes are read-only by default, the JVM's system-level lookups ride the sandbox profile) - nothing below is needed for those. Copy the lines you need into .sandbox (via :sandbox allow / :sandbox readonly), or into ~/.config/3code/sandbox to make them your baseline. Each entry is the tool's own documented home, narrowed wherever the tool keeps credentials next to its data; the Windows line is given when the paths move.

maven (Java)::

allow ~/.m2/repository

`~/.m2/settings.xml` holds server passwords, so never allow
`~/.m2` whole. The rule is inert until the directory exists:
run the first build unfenced once, or `mkdir -p ~/.m2/repository`.
Inside a fenced network, JVM tools ignore `http_proxy`; pass
`-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=$WALL_PROXY_PORT`.

gradle::

allow ~/.gradle/caches
allow ~/.gradle/wrapper
allow ~/.gradle/jdks

Not `~/.gradle` whole: `gradle.properties` often holds
credentials. Same JVM proxy note as maven.

dotnet (.NET / MSBuild)::

allow ~/.nuget/packages
readonly ~/.nuget/NuGet

NuGet dies on an unreadable config (`~/.nuget/NuGet/NuGet.Config`
can hold feed tokens, so it is readable, not writable - the same
treatment `~/.gitconfig` gets), and the package cache is the only
writable dot it needs: `~/.dotnet` writes are optional. MSBuild's
worker nodes and the dotnet diagnostic server bind unix sockets
in the temp dir, which is writable by default; the socket rights
ride the writable grant (sandwall 0.5.12). Inside a fenced
network, dotnet honors `http_proxy`/`https_proxy`.

ruby (RubyGems + Bundler)::

allow ~/.gem/ruby
allow ~/.local/share/gem
allow ~/.bundle/cache

`gem install --user-install` writes the first two (macOS uses
~/.gem/ruby, Linux's RubyGems prefers the XDG dir). Bundler needs
~/.bundle/cache for `bundle install`; point installs at the
project (`bundle config set --local path vendor/bundle`) since the
system gem dir is read-only. Fenced on purpose:
~/.gem/credentials (the rubygems push token) and ~/.bundle/config
(can hold API tokens).

pnpm::

allow ~/.local/share/pnpm     (Windows: allow ~/AppData/Local/pnpm)

yarn (berry)::

allow ~/.yarn
readonly ~/.yarnrc            (can hold npm tokens)

bun::

allow ~/.bun

deno:: cache already rides ~/.cache / ~/Library/Caches; add readonly ~/.deno only for the old standalone layout.

nvm / volta (node version managers)::

readonly ~/.nvm
readonly ~/.volta

pyenv / rbenv::

readonly ~/.pyenv             (add allow ~/.pyenv/versions to install)
readonly ~/.rbenv             (add allow ~/.rbenv/versions to install)

conda::

readonly ~/miniconda3

Writable envs belong in projects (`conda create -p ./env`).

asdf / mise::

readonly ~/.asdf
allow ~/.local/share/mise     (mise's installs; Windows:
                               allow ~/AppData/Local/mise)

zig toolchain:: the build caches are already covered (~/.cache/zig, project .zig-cache, Windows ~/AppData/Local/zig). The compiler itself is wherever you unpacked it - typically readonly ~/zig or /opt/zig.

flutter / dart::

allow ~/.pub-cache
readonly ~/flutter            (wherever the SDK is cloned)

android::

readonly ~/Android/Sdk
allow ~/.android/avd          (emulator state, only if you run sims)

julia::

allow ~/.julia                (depots, artifacts, registries)

R packages::

allow ~/R                     (Windows: allow ~/Documents/R)

composer (PHP):: cache rides ~/.cache/composer; ~/.config/composer/auth.json is a token file - keep it out and pass credentials via env.

terraform::

allow ~/.terraform.d/plugin-cache

xcode::

allow ~/Library/Developer/Xcode/DerivedData
allow ~/Library/Developer/CoreSimulator
readonly ~/Library/MobileDevice

Prefer `-derivedDataPath <project>/build` so DerivedData stays in
the project. Executing Xcode needs no rule: `/Applications` is a
read-only baseline dir. CoreSimulator is only needed to run
`xcodebuild test`. Not yet verified against a full Xcode install.

Deliberately not defaultable, because handing them over is handing over the account: ~/.kube, ~/.aws, ~/.config/gcloud, ~/.gnupg, ~/.pgpass, ~/.my.cnf, ~/.netrc, and the docker socket (/var/run/docker.sock is root-equivalent). If a task needs one, allow it for that project, for that session, and revoke it.

Network rules

With no host rules, every host is allowed. allow * says the same thing explicitly. To close the network completely, use:

deny *

Host targets may be domains or IP addresses. A single allow rule turns the policy into an allowlist: that host works and all others do not.

allow api.example.com

A single deny rule does the inverse: that host is blocked and the rest of the network stays open.

deny telemetry.example.com

IP addresses work the same way, for example allow 192.0.2.10 or deny 192.0.2.10. The spelling matters: bare foo is a hostname, while ./foo is a file or directory named foo in the working directory.

A host without a port matches every port. Add one when it matters, as in allow github.com:443. Wildcards work too: allow *.example.org allows that domain pattern.

As with paths, the last matching rule wins. Any allow host rule makes unmatched hosts denied. With only deny host rules, unmatched hosts remain allowed. This is the small detail that decides whether you have made an allowlist or a blocklist.

On Linux and macOS, fenced commands reach the network through a local CONNECT and SOCKS5 allowlist proxy. Linux puts the command in a network namespace; macOS confines it to loopback with Seatbelt. 3code sets the standard proxy variables and a Git SSH proxy command for the child process. Denied HTTP connections return 403.

Note: Inside the fence, only clients that speak the proxy variables get out: curl, wget, git over HTTPS, package managers. ping (ICMP) and host/dig (their own DNS queries) ignore the proxy and fail: the fenced namespace has no route off the machine, and name resolution happens at the proxy. REST over HTTP(S) only, for now.

On Windows, host rules require one admin setup command:

3code setup

It asks for admin rights through the UAC prompt when run unelevated from a console or RDP session; over ssh (no screen for the prompt) it says so and exits - run it from an account with admin instead.

Without that setup, 3code warns and runs the command without a network fence. That is safer than pretending the fence exists. Set sandbox_wall_warn = off in [settings] if you no longer need the warning. 3code unsetup removes the firewall filters; it is safe to run repeatedly.

Editing the policy

Use an editor or the sandbox commands:

:sandbox
:sandbox show
:sandbox edit
:sandbox allow /opt
:sandbox readonly /var
:sandbox deny ./secrets
:sandbox on
:sandbox off

:sandbox edit tries $VISUAL, then $EDITOR, then the platform default. Changes take effect when the editor exits. Rule commands store project paths in portable ./foo form and reload immediately.

:sandbox off disables filesystem and network enforcement for the session. The setting is stored as sandbox = off in [settings]. Use :sandbox on to restore enforcement. The one-shot equivalent is the --no-sandbox flag, and 3code sandbox --policy FILE restrict -- CMD runs a single command under a policy without starting the REPL.

To allow the full filesystem, replace the policy with allow /. Without host rules, the network is unrestricted too. This is yolo mode. 3code makes you ask for it explicitly and will not have the bright idea on its own.

Policy file protection

A policy is not much use if the model can edit it. The file tools therefore cannot change .sandbox or ~/.config/3code/sandbox. After loading the policy, 3code adds hidden read-only guards for both files. They do not appear in :sandbox show, and later rules cannot override them.

macOS and Windows enforce these guards for Bash and in-process tools. Landlock cannot subtract a read-only file from an already writable root. On Linux, the in-process tools still enforce the guard, but a Bash command under allow / can write the policy file. Avoid allow / if policy-file protection matters.

A denied Bash command usually reports Permission denied. 3code includes the active policy path in denial messages, saving you from debugging the wrong layer.

What is sandboxed

The read, write, and patch tools check paths inside 3code itself. Bash needs a stronger boundary, so commands are re-executed through:

3code sandbox --policy FILE restrict -- COMMAND

3code sb is the short alias. The command and all its children inherit the OS restriction. The sandbox process reads the policy afresh, so an edit applies to the next Bash launch.

Platform behavior and fallback

The Bash backend uses Landlock on Linux, Seatbelt on macOS, and restricted tokens plus ACLs on Windows. 3code probes it at startup rather than assuming it works. Old Linux kernels and containers whose seccomp policy blocks Landlock can fail the probe.

If the OS backend is unavailable, Bash runs without filesystem confinement. The in-process read, write, and patch checks remain active. This keeps 3code usable, but the distinction matters: an arbitrary Bash command is then outside the filesystem fence.

Colors

3code detects whether your terminal has a dark or light background by querying it directly (OSC 11) and picks the matching palette, so a light theme just works without configuration. Terminals that do not answer are assumed dark. To pin a palette regardless of the terminal:

[settings]
tone = "light"

The white family (prompt text, help, banners) adapts between tones; the functional colors (cyan receipts, green/red diffs, magenta private bar) are the same in both. Every white-family color plus the two bar colors can be overridden with quoted ANSI escapes under [colors]; see colors in the reference.

Design decisions

Why 3code looks and behaves the way it does. The through-line is economy: the least tokens, the least computer time, the least of your attention.

No TUI

3code renders plain scrolling text, not a full-screen terminal interface. Committing to the shell/REPL paradigm buys a lot for free: your terminal's own scrollback, search, copy-paste, and font rendering keep working; the transcript is history in the literal sense, so yesterday's session is 3code -r and a scroll up; logging and terminal multiplexers capture it unchanged. A TUI owns the screen, reimplements all of that worse, and adds a rendering surface that can itself be buggy (every terminal-rendering bug 3code ever had lived in the footer, never in the history). The live surface is deliberately two rows.

Kernel enforcement, no questions

The sandbox asks nothing and enforces at the kernel (Landlock, Seatbelt, Windows restricted tokens). The alternative architectures were worse. A judge model re-reading every command is unacceptable: it burns tokens on guesswork, adds latency to every tool call, and is security by vibes, a second model approving what the first one wrote. Approval prompts are security by interruption, and in practice train users to mash yes. A static kernel policy uses zero tokens, adds microseconds, and fails closed; the cost is the one-time setup described in the sandbox section. And the honest calibration: most coding agent sessions run effectively unfenced anyway (yolo mode, --dangerously- skip-permissions and friends), so a fence that is cheap to set up and never interrupts is a real improvement over both the nagging gate and the absent one, even before it stops anything.

One model, small tool set

No sub-agents, no trees of delegated prompts. A single conversation with bash, file tools, plan, and web access, plus skills loaded on demand, keeps the token budget on the work instead of on orchestration. When a task outgrows one context, chunked mode and goal files split it along worktrees instead of spawning agents.

Scripting

The REPL is optional. A prompt given as a command-line argument runs one turn and exits (oneshot mode), and -r continues the directory's latest session in another oneshot, so a shell script or cron job can hold an ongoing conversation:

3code "list the failing tests in this repo"   # starts a session
3code -r "fix the first one and rerun"        # continues it
3code -r "commit what changed"                # and so on

Each -r call resumes the latest session saved for the working directory with the full prior context, byte-identically, so the prompt cache stays warm and the model sees the whole conversation. Run from the project directory; sessions are per-directory, same as --list.

Oneshot output is the regular transcript rendering on stdout (the reply, tool banners, receipts, exactly what the interactive screen commits); diagnostics go to stderr. The limitation: for now that regular output is all a script gets. The exit code separates a completed turn (0) from usage, config, and crash failures (nonzero), but anything in between, including provider errors mid-turn, has to be parsed out of the text. A parseable-output flag is planned; until then, prompt for the shape you need (answer with just the file names, one per line). For embedding without parsing anything, the library API returns events as data.

Library API

A Nim program can embed the same agent loop without bringing along the terminal interface:

import threecode

let session = initAgentSession(
  AgentOptions(model: "deepinfra.deepseek-v3.2"))

# blocking: run a full turn (model calls + tool calls) and get the reply
let reply = session.prompt("what does this project do?")

# streaming: same call, events as they happen
session.onEvent = proc(event: AgentEvent) =
  case event.kind
  of aevDelta: stdout.write event.text        # assistant text chunks
  of aevRetry, aevTool: stderr.writeLine event.text  # tool results, notices
  of aevDone: echo event.usage                # per-call token usage
  else: discard
discard session.prompt("Run the tests and fix the failure.")

# colon commands work too
echo session.command(":tokens")
session.close()

AgentOptions mirrors the CLI flags: model, cwd, resume/resumeId, sessionPath, experimental, debug. prompt blocks until the model finishes calling tools and returns its reply. Meanwhile, onEvent receives text, reasoning, tool output, retry notices, and usage. promptAsync puts the turn on a library-managed thread and reports failures with aevError; interrupt cancels it. Sessions use the same provider config, filesystem policy, locks, and saved format as the CLI.

Only one AgentSession may be active in a process, and calls on it are not thread-safe. In an asynchronous server, keep the session on one worker thread and pass prompts and events through queues. This avoids sharing session state across threads.

example/webserve.nim is the complete example: a web frontend that serves a chat page, keeps the session on a worker thread, and streams replies to the browser over server-sent events. nim c -r example/webserve.nim and open http://localhost:8501.

Agent Client Protocol

3code --acp turns the binary into an ACP v1 agent: newline-delimited JSON-RPC on stdin/stdout, the protocol Zed, JetBrains, Neovim plugins, and a growing pile of custom clients speak. Your editor launches 3code like any other ACP agent:

# Zed: agent settings, "Add custom agent"
Command: 3code
Arguments: --acp

One session per process. The session id is the .3log id, so session/load resumes any past 3code conversation and session/resume reconnects without replaying it. Prompt turns stream as session/update notifications (message and reasoning chunks, tool activity, token usage when the context window is known); session/cancel is Ctrl-C over the wire and answers the prompt with cancelled. Model and provider come from the normal config (-m picks, :model philosophy otherwise); the sandbox and loop guard run exactly as in the terminal.

Not advertised (because 3code does not do them over ACP yet): MCP servers, client-side filesystem and terminal execution, permission requests, elicitation, modes, and authentication.

Diagnostics go to stderr; stdout is protocol records only, so the stream stays parseable.

Build from source

nimble install https://github.com/capocasa/3code

Requires Nim >= 2.0 and curl on PATH.

Contributing

Patches welcome at github.com/capocasa/3code.

  • Known-good provider/model pairs with test results
  • Bug fixes with a clear reproduction case

Bug reports welcome, but make sure you give enough specific information to reproduce the issue. Terminal rendering bugs need a visual PTY test that shows the bad frame before the fix. A test that cannot show the bug has not reproduced it yet.

Developer API documentation lives in the developer docs: module-by-module, generated from the source doc comments.

Prior art

3code is influenced by Claude Code, Goose, Pi, and Codex. It chooses different tradeoffs, but the family resemblance is intentional. As a European, using Claude Code can feel like driving a turbocharged Ford F-150 to buy a pack of gum. It is still a fine truck.

3code omits features that do not help it write software reliably with fewer resources. Economy is the feature, not a billing surprise.


Made with Nim. Generated: 2026-10-27 14:28:45 UTC
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.