Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog

[0.31.0] — 2026-11-04

Added

  • The SDK in Python. pip install megacode-agent-sdk gives you the megacode_agent_sdk module: the Rust crate’s shapes in asyncio — query, Client.start, Client.resume, your own tools as async functions marked @tool, approvals, testing on a mock file with no key. It needs Python 3.10 or newer and uses the standard library alone. A python.org Python on macOS needs its Install Certificates.command run once before the binary’s download works.
  • The SDK in TypeScript. npm install megacode-agent-sdk gives you the same shapes for Node 20 or newer, as an ES module with no runtime dependencies and its own type declarations. Always await client.close() (or declare the client with await using, Node 20.4 or newer): a client left open keeps Node running. The download follows HTTPS_PROXY and NO_PROXY only on Node 22.21 or newer, or 24.5 or newer; on older Node behind a proxy, name the binary option.
  • Examples you can run with no key. sdk/python/examples/ticket_fix.py (a ticket-to-fix agent with a tool of its own and harness’s file tools) and sdk/typescript/examples/minimal.ts (one prompt, one tool), each with a mock file beside it.

Both packages fetch and verify the harness binary of their own version as the Rust crate does, and every session they start is recorded like any other, so harness session, replay and export work on it. Their differences from the Rust crate, such as tool arguments handed to your handler unconverted, are described in docs/sdk.md; ADR-0097 lists them all.

[0.30.0] — 2026-11-03

Added

  • An SDK: build your own agent on harness from Rust. The megacode-agent-sdk crate starts a harness session from your program, gives the model tools you wrote, and streams back what the session does. Use it for an agent that is not a coding agent: a text-message assistant, a support bot, a nightly job. Each session is one harness process, and every session is recorded like any other, so harness session, replay and export work on it. On its first run on a machine the SDK downloads the harness release of its own version, checks it against digests built into the crate, and keeps it in a cache it checks again at every start; a program can name its own binary instead. Linux on x86-64 and macOS are supported. Python and TypeScript packages, with the same shapes, come in the next release. See docs/sdk.md.
  • --options <file> for a conversation. harness run --input-format stream-json --options <file> takes the session’s setup from one JSON file: model, mode, sandbox, which built-in tools are offered (a ceiling for the whole session, mode switches included), your own tools, the system prompt (replaced or appended to), the approval timeout, and which personal setup is read. With --options, nothing personal is read unless the file asks for it: no config.toml, no AGENTS.md, no saved model or account, no skills, agent definitions or hooks. A resume, on any surface, keeps the setup the session was started with. The file has the authority of the command line; never build it from a model’s output (docs/headless.md).
  • Client tools. A conversation can offer the model tools that run in your program: harness writes client_tool_call, your program answers client_tool_result with text and at most one image, and a call left unanswered ends at its timeout (5 minutes by default) with client_tool_cancel. Each call is recorded, so a replay needs neither the model nor your program. A tool marked ask asks through the usual approval_request (kind client_tool); client tools are never judged, no earlier approval covers them, and they run outside harness’s sandbox. A session resumed in the panel shows them, and each call fails with “no program is attached”.
  • --mock in a conversation. harness run --input-format stream-json --mock <file> runs a live conversation whose model answers from a mock file, with no key and no network, so a program’s own agent can be tested against the real binary. Sub-agents are refused in a mock conversation.
  • The conversation protocol is written down. docs/protocol/stream-json.schema.json describes every line in both directions, checked in CI against transcripts recorded from the real binary (sdk/conformance/). The protocol stays at v: 1; everything new is additive.
  • The idle compact command says what it compacted. Its done answer now carries data.compacted (trigger, pre_tokens) when that attempt wrote a compaction.

Changed

  • The update notice is amber. A new release’s note on the notice line, in /notices, and in the plain prompt’s start line is painted in the warning colour, so it stands out from the other notes. The status-bar item is unchanged.
  • A spinner while sub-agents work. When the status bar reads N sub-agents running · esc stops them, it now leads with the spinner (or the static ⏳ with animations off), so a session waiting on its sub-agents no longer looks stuck.
  • abort acts at once. In a conversation, an abort interrupts at once, ahead of any command sent after it; with a prompt_id it takes a still-queued prompt back (dequeued) or stops the turn that prompt opened. The SDK’s abort() takes back its own queued prompt and ends that prompt’s stream, so the next send runs.

Fixed

  • Turning away off during an away wait takes effect. A /away off that arrived at one moment of the wait was held until something else was sent, so the turn kept waiting.
  • A prompt sent while a queued compaction runs waits for it. In a conversation, a compact queued during a turn runs once the turn ends; a prompt sent in that window used to skip ahead of it, and an abort of that prompt stopped the compaction instead. The prompt now waits, and its abort stops the prompt.
  • An MCP server’s earlier processes are checked on a busy machine. If any process on the machine was exiting at the moment harness checked a left-over MCP server’s processes, the check failed with “its earlier processes could not be checked: /proc/<pid>/stat: not understood” and the server’s record stayed. Exiting processes are now skipped.

[0.29.2] — 2026-11-02

Fixed

  • An answer typed the moment an approval question appears is no longer lost. The plain prompt drops what was typed before a question (an approval, the auto question after a sign-in), but it did so just after printing the question, so a key pressed right then could be dropped too and the question waited. It now drops the type-ahead before printing.

[0.29.1] — 2026-11-02

Fixed

  • The transcript stays in scrollback on xterm.js terminals (fix branch fix-scrollback-xtermjs, note). In xterm.js-based terminals such as Orca, every transcript row that scrolled off the top of the screen was deleted instead of kept, so scrolling up after a long answer showed only the last screenful. Rows now leave the screen by line feed, which xterm.js archives. Checked against Orca’s xterm.js build and tmux; VS Code uses the same engine but was not run.

[0.29.0] — 2026-11-01

Added

  • A quiet start, and /notices. A start prints the splash, the hints and the signed-in line, plus any question it must ask; everything else it used to print (a key not set, the home folder, a hook, the sandbox off, a broken config) is now a note. In the panel, one line below the splash types the notes one at a time until you send a message. A fact you have already seen in this project is not typed again for 90 days; a warning (⚠) is typed at every start while it holds. /notices lists them all in four sections: needs your attention, about this start, privacy, status. After your first message the status bar counts ⚠ N while a warning holds and M notes until you open /notices. The plain prompt prints one line, the first note and (+N more · /notices); -p and stream-json print what they printed before. What you have seen is kept per project in $STATE/notices-seen.json (docs/config.md).
  • /update and /restart. A newer release shows as one status-bar item, 0.29.1 available · /update, typed once on the notice line; nothing goes into the transcript. /update installs it without leaving the session: the same signed, checked install as harness update, each step shown; Esc cancels before the swap, and a failure changes nothing. The item then reads 0.29.1 installed · /restart. /restart waits for the session to be idle, closes it as /quit does and starts the installed binary on the same session, so the conversation, mode and approvals carry over; the new process says Now on harness 0.29.1 (was 0.29.0). A session learns of an install from the receipt harness update and /update write ($CACHE/install-receipts.json), never by running a binary (docs/config.md).
  • exit, quit and harness update typed at the prompt do what was meant. Typed alone at an idle prompt, exit or quit (any case) quits as /quit does, and harness update offers to run /update instead [Y/n]. A leading space, more words, or a running turn send the text to the model as before ( exit is the escape). /exit is now the same as /quit (docs/shortcuts.md).
  • Skills used. /session gains a Skills row: each skill the session used, how often, and how many times by you, sub-agents’ after. /skills gains a THIS SESSION column with the same counts, and marks a skill added on disk since the start new on disk: the model sees it next session.

Changed

  • A new spinner. The working indicator is three braille cells turning in red and blue, ramped to the theme’s background; a turn that ends normally closes with a short green pulse. [ui] animations = false, NO_COLOR, TERM=dumb or a pipe show the static ⏳, and the plain prompt draws the same frames without colour.
  • The usage-counts disclosure moves to /notices. Signing in prints a one-line form, harness sends daily usage counts (never prompts, files or commands) · /notices; /notices lists the full sentence, with its switch (telemetry = false or DO_NOT_TRACK), at every start. A start that finds you already signed in no longer prints it. A judging mode’s disclosure is listed there too; choosing a judging mode in a session still prints it once.
  • The signed-in line no longer names the model; the status bar does. With no key and no sign-in, the status bar says no model instead of naming a model that cannot answer.
  • [ui] quiet_startup hides what it hid before. For an update it hides only the note on the notice line; the status-bar item and /notices stay.

Fixed

  • Plan mode at start. harness run --mode plan (with or without -p) told the model to write its plan to the literal path {plan_file}, a file that never existed. It now gets a real plan file, as /mode plan does, and resuming such a session fixes it. A plan file’s number no longer counts resumes, and a number used before is never reused.
  • The approved retry of a credential read checks the file itself. After you approve reading a protected file such as .env, the read now takes exactly the file you were asked about, by its identity, whichever of its names leads there. On macOS a file with several hard links was now and then refused right after you approved it; that is gone, and a file swapped in between the question and the read is refused.
  • A resumed conversation’s result names the model. harness resume <id> --input-format stream-json printed "model": null on every turn’s result, since a resume never sees the session’s start record; it now names the session’s model, as init does.
  • A conversation that fails to start prints no init. When the driver failed before it was ready (a log it could not append to, say), stdout could still carry init before the process exited 1 — a resume’s {"type":"init",…,"resumed":true} always, a new session’s whenever the failure came after its start record — so a client read a session that never started. Now stdout stays empty and stderr carries the driver’s own reason, exit 1.

[0.28.0] — 2026-10-30

Added

  • A program can hold a session. harness run --input-format stream-json (and harness resume <id> --input-format stream-json) keeps the session open: stdin takes one JSON command per line — prompt, steer, abort, approval, compact, get_state, away, quit — each answered by one response line; stdout prints the stream-json lines -p prints, every gate as an approval_request the program answers with the panel’s choices (allow_once, allow_for_turn, deny with a reason the model reads, deny_and_stop), a sub-agent’s gates included, and one result per turn with the prompt’s own id. An unanswered request times out at [away] timeout with the panel’s consequences. A full escape is never asked of a program: a conversation runs under escape = "network_only" as --approve-all does, unless --allow-escapes says a person answers; a crash resume keeps the rule its log recorded, and refuses the flag or its absence when that rule disagrees. The log of such a session is a panel session’s: harness replay reproduces it. See docs/headless.md, “A conversation”.
  • Closing stdin means “no more input”, not “stop”. printf '<prompt line>' | harness run --input-format stream-json runs the prompt to its answer: the running turn finishes, the prompts still queued run in order, then the session ends, exit 0. A gate opened once stdin is closed is refused at once with -p’s reason, since nobody can answer it. quit and Ctrl-C stop.
  • SIGTERM is Ctrl-C for a conversation: the turn’s result prints, exit 0; a second signal exits 1. A supervisor’s kill whose grace period outlasts the effect in flight ends the session cleanly instead of leaving a crash close for the next resume to report; a SIGKILL inside the grace period still does.
  • result.cost on a conversation’s result: the model calls since the last result (the turn’s, and a compaction between turns in the next one’s) priced as harness session prices a session (usd, priced, unpriced, free, bound, basis).
  • abort {"prompt_id": …} takes a queued prompt back (dequeued) or interrupts the turn it opened.
  • compacted carries trigger (automatic/manual) and pre_tokens; get_state.data.context reports last_prompt_tokens, window, percent and auto_compact_at_percent. A resume starts both from the log.
  • A client example, and the conversation on a real model. examples/stream-json-client/driver.py holds a session over stdin (standard library only) and drives eight scenarios — prompts and steers, approvals, abort and compact, the away timeout, EOF, quit, SIGTERM, resume — against a real provider, checking what the protocol promises. The five whose turns all completed are committed as live cassettes, crates/agent/tests/fixtures/sessions/anthropic-conversation-*.jsonl, replayed offline by fixture_replay; scripts/rerecord-cassettes.sh conversation re-cuts them. A real terminal on stdin is refused before anything starts, on run and resume alike (the pty suite).

Changed

  • -p ends on SIGTERM as on Ctrl-C. The turn ends interrupted (or model_failed with error: "interrupted: " when the signal lands on the model call), its result prints, exit 1; before, the process died and the next resume reported a crash close.
  • -p --output-format stream-json lines gain fields: user.kind (message or steer), result.turn_id and result.prompt_id. Additive; a reader that ignores unknown keys is unaffected.
  • Protocol lines escape U+2028 and U+2029 (\u2028, \u2029) in every JSON output — a conversation’s lines, -p --output-format stream-json and json — so a reader that splits on Unicode line separators (Python’s splitlines(), Node’s readline) sees one line per object. The text format’s answer is unchanged.

[0.27.1] — 2026-10-30

Fixed

  • A notice sent once stays in its place. The away and back notices, the turn-budget notice, the empty-reply nudge and the continue notice were folded into one request’s last message and derived again on the next, so the next request no longer carried them. Anthropic binds a thinking block’s signature to the content that preceded it, so the first request after such a notice left — the turn after an away task ended, the call after a budget notice — was refused: 400 … Invalid signature in thinking block … Content that preceded this block when it was created is missing from this request. Every request — a turn’s, and the summarize call a compaction makes — now begins with the messages of the last turn request the model answered, as they were sent (read back from the log’s recorded pieces, driver::with_sent_notices), notices included, and the new notices follow; a compaction or a model-family switch, which change the start, restore nothing, and a request whose call failed is not copied. One case remains: the empty-reply nudge after a reply that was all thinking is not kept, so the request after that can still be refused. Each segment’s start or resume record now states that it keeps sent notices (keeps_sent_notices, ADR-0090), and the restore follows the segment’s own record: a log recorded by 0.27.0 or earlier replays in the shape it was recorded in, so every committed fixture replays byte-identical; a session started on 0.27.0 or earlier gets the fix from its first resume here; a segment an older harness resumed replays without it. Found on claude-haiku-5-5 with thinking on, after /away; reported and fixed by Jingtao Xu (PR #100).

[0.27.0] — 2026-10-27

Added

  • Intel Macs, and every Mac from macOS 15.4. Releases now include x86_64-apple-darwin, and the Mac floor drops from macOS 26 to 15.4 on both chips, the sandbox included. harness update keeps an Intel Mac current. The one-line installer offers the Intel build once the site’s installer is updated, after this release is live; it refuses a Mac below 15.4 with needs macOS 15.4 or later (this Mac runs <version>), and a HARNESS_VERSION pin the Mac cannot run: below 0.27.0 there is no Intel build, and on Apple Silicon a release before 0.27.0 needs macOS 26 (docs/sandbox.md).
  • Windows through WSL2. Run the harness inside a WSL2 distribution, as a Linux install (docs/sandbox.md). There the sandbox hides each Windows drive’s Users folder (/mnt/c/Users: every account’s .ssh, AppData and Documents) under every spelling that names a folder, since the drive ignores letter case; the rest of the drive stays readable, read-only. A project, or its git folder, inside a Users folder makes the sandbox unavailable: move it to the Linux file system (~/code) or elsewhere on the drive. A project elsewhere on a Windows drive keeps the sandbox, and its names compare without case, as on a Mac’s default disk, so write_file claude.md cannot replace a protected CLAUDE.md. Windows programs (cmd.exe) do not start from a sandboxed command. WSL1 and Windows on ARM are not supported, and this is untested on real Windows.

Changed

  • The sandbox’s network warning covers every macOS below 27. Apple fixed CVE-2026-84551 (“an app may be able to bypass network restrictions”) in macOS 27 only, so below 27 doubt the network rules. An Apple Silicon Mac clears it by updating to 27; an Intel Mac cannot, since 27 does not run on Intel (docs/sandbox.md).
  • A macOS missing one of the sandbox’s private system functions makes the sandbox unavailable instead of stopping the harness at start, replay included. The start names it: this macOS lacks `<name>`, a system function the sandbox needs; [sandbox] mode = "external" or "off" runs commands without it. The start-up self-test also fails when its process query fails, where it passed before.

[0.26.0] — 2026-10-26

Changed

  • A hook on the host runs from /. A hook with sandbox = false, or any hook in a session with no sandbox, now starts in /, not the project, so nothing it or its interpreter loads by a relative name comes from the project. Its input’s cwd still names the project: cd "$(jq -r .cwd)" first if it needs it. In a sandboxed session its PATH drops the folders the agent can write. The start check now also checks the path in a --flag=path argument (docs/hooks.md).
  • The file tools read only regular files, up to 16 MiB. read_file, edit_file and write_file refuse a FIFO, a device, a socket or a folder at once, and any file in /proc or /sys; read_file and edit_file refuse a file over 16 MiB, while write_file still replaces one. search skips files over 16 MiB and says how many (docs/sandbox.md).
  • Provider, judge and catalog clients follow no redirect, and a plain-http:// endpoint never goes through a proxy. A gateway that answers with a redirect now fails visibly.

Fixed

  • A command that writes without end holds only the first 1 MiB of its output in memory, the part the model always saw; search no longer holds every match.
  • harness export -o with .. in the path goes through the same link checks as any other path in the project.
  • The sign-in link must be https, and the sign-in lines escape every invisible formatting character the server sends.

[0.25.0] — 2026-10-25

Added

  • A reply cut at the output cap continues. When a reply stops at the output cap without a tool call, the harness asks the model to pick up where it stopped, up to 3 tries in all, and shows ↻ The reply hit the output limit. Continuing (2/3).; when the second try was all thinking, the third runs one thinking level lower. Esc stops it; after the third try the turn ends truncated, as before. Each try is a recorded model call and counts against max_iterations. In -p and harness eval the answer joins the tries, so a case that used to end truncated may now finish.
  • Update checks at start and every hour. An interactive session asks the install site for the newest release at start and every hour while it is open, sending nothing about you, and says harness X is available. Run harness update, then restart. between turns (docs/config.md).

Changed

  • mega:full sends 65,536 output tokens: a mega: model’s cap is the server’s figure. A cap you set still wins.
  • The output-cap warning keeps its measurement; its advice moved to the truncated turn’s footer.
  • Older versions refuse a config.toml that has telemetry; remove it before downgrading.

Fixed

  • The resume hint prints harness resume <id> when harness on your PATH is this binary, not its full path.

[0.24.0] — 2026-10-24

Added

  • Hooks. Your own commands, listed as [[hooks]] in config.toml, can run before a tool call (PreToolUse) and block it, with your reason going to the model, or run when you send a prompt (UserPromptSubmit) and add context beside it or block it. The input and answers are Claude Code’s shapes, so a script ports with small changes (tool names here are lower case: bash, edit_file). Only your own config can define hooks, never a repository. A hook runs in the session’s sandbox unless it sets sandbox = false, and a failing hook blocks unless it sets on_failure = "continue". The session refuses to start if a hook’s files lie where the agent could change them. The banner lists the hooks, every block is recorded and replays, and --no-hooks runs a session without them. Hooks are a guardrail, not a security boundary: a hook sees one call at a time and can be routed around; the sandbox is the boundary. See docs/hooks.md.
  • harness update downloads the newest release through the install site, checks its SHA-256, tries the new binary, and only then replaces the old one; a failed step installs nothing. It sends nothing to our server. Sessions already running keep the binary they started with; restart them.
  • The update notice. After the daily check finds a newer release, the next interactive start shows harness X is available. Run `harness update` under the banner, and hides it with the banner (quiet_startup).
  • A thinking level per sub-agent. The agent can give each sub-agent its own thinking level, lower for a quick lookup and higher for hard reasoning. A definition’s own effort: still wins, and the sub-agent’s header and its approval show the level.
  • Sub-agent rows say what each one is doing. Under the status line, each running sub-agent’s row shows its prompt, its status (waiting for you in amber when it needs your approval), its seconds, its tokens and, when it differs from the main agent’s, its model, in aligned columns. The side reply’s row looks the same.
  • --no-mcp, on run and resume, starts no MCP servers for that session and records it, so a harness -p run from inside an MCP server does not start that server again.
  • When a hook blocks the prompt, harness -p exits 1 with outcome prompt_blocked; harness sessions --json and the /session card report it, and an eval case can expect it.

Changed

  • Auto loads skills without asking. Loading a skill adds its text and its list of files and grants nothing; manual and plan still ask. A session resumed from before 0.24.0 keeps asking. To keep the question in new sessions, add skill to [modes.auto] ask (docs/config.md).
  • Older versions refuse a config.toml that has [[hooks]] or check_for_updates; remove them before downgrading.

Fixed

  • A skill whose name differs from its folder was skipped without a word; a start-up warning now names it.
  • A sub-agent definition whose effort: is not a level was ignored without a word; a start-up warning now names it.
  • Resuming a session from another folder prints a cd … && harness resume … line to paste; it dropped --sandbox, and now keeps it.

[0.23.1] — 2026-10-23

Fixed

  • Each model gets its own output cap. With no --max-tokens and no max_tokens in config.toml, a reply’s cap is now its model’s own output limit, not 32,768 for every model: Claude Opus 5 and Sonnet 5 get 128,000, and a model with a lower limit (GPT-4o, 16,384) is no longer asked for more than it allows. On those models, a long high-thinking reply that used to spend the whole 32,768 on thinking now has room to answer. Gemini 4 Argon’s own limit is 32,768, so on it (and on mega:full, which serves it) nothing changes: lower the thinking level. The limit comes from the provider’s own model list, else the catalog, else 32,768. It follows the model through a /model switch, resume, sub-agents and side replies. A cap you set yourself still applies to every model.
  • The warning for an all-thinking reply names your settings: “Ask more narrowly, lower the thinking level, or raise the cap (–max-tokens, or max_tokens in config.toml)”. It used to name a source-code constant.

[0.23.0] — 2026-10-22

Added

  • Ask while a long tool call runs. In the panel, once a call has run 2 s, Enter sends your message to a side reply that answers at once — same model and thinking, reading files only, never asking — in a muted block under the call. The message still joins after the call, so the agent sees it without answering again. Esc stops the side reply first, then the turn. Transcripts, harness resume and exports show both where they joined, and harness replay <side id> replays a side session beside its main log.
  • The docs as a website, at /docs/ on the install site: getting started, modes and the judge, the sandbox, checkpoints and /undo, shortcuts, config.toml, harness -p and remote MCP servers, with search.

Changed

  • Every block is set apart by a blank row — each tool call, text going into a tool call or thinking, whatever follows a footer — in the panel and the line client. A streamed answer’s line no longer moves when it commits. harness -p output gains the same blank rows and drops an answer’s leading blank lines.

Fixed

  • The model picker’s provider page says ←/Esc back; check-invariants.sh checks a source folder named target.
  • A resize that arrived with a keystroke could leave the panel laid out for the old size, losing or doubling rows.
  • rustls 0.23.46, which fixes four advisories (RUSTSEC-2026-0346 to 0349); none applied to this build’s use of it.

[0.22.0] — 2026-10-21

Added

  • A resumed session shows what happened in it. Resuming draws the session’s recent turns — tool calls, thinking, approvals, answers, footers — into the terminal’s scrollback, so you can scroll up through them, then a dim ── resumed · <time> ── line before the prompt. Long sessions show their newest turns that fit 2,000 rows, with one line saying how many earlier turns are left out and how to see them (harness session <id> --transcript). [resume] replay_rows sets the budget; 0 turns the replay off (docs/config.md).
  • harness session <id> --transcript shows a background sub-agent’s ↳ <name> finished row.

Changed

  • Turns are easier to tell apart. Your message has a blank row above and below it, and a blank row follows each thinking block and tool block, in the panel and the line client alike. The turn’s footer stays right under its answer. In the line REPL, one blank row separates turns.

Fixed

  • Two sub-agents started together showed the same short id. The id now runs 14 characters, which reaches the random part, and still opens the child’s session as a prefix.
  • harness judge fit crashed on score files from older question sets (u1/u2); it now counts them under “scored on another question set”.

[0.21.0] — 2026-10-21

Added

  • In the panel, sub-agents run in the background and you can keep talking to the agent. An agent call returns as soon as its children start, and their rows stay under the transcript. Each result comes back on its own: an idle agent picks it up and answers; a busy one takes it after the tool calls already running. [agents] background = false keeps the old behaviour (docs/config.md).
  • Stopping and leaving with children running. Esc while the agent is idle stops them. Quitting stops them and waits up to 10 s for their results; Ctrl-C after the first half second leaves at once. Work done while you are away waits for its sub-agents, and /away also reaches the ones already running.
  • /model thinking <level> changes only the thinking level. In the plain prompt, /model <id> asks for the level before it switches.

Changed

  • /iterations <n> reaches the running task before its next model call, not from the next task on.
  • A resumed session follows config.toml’s max_iterations unless you chose a cap with --max-iterations or /iterations; a cap you chose still carries over.
  • /thinking replaces /reasoning: it shows, previews or hides the model’s thinking, and thinking levels are picked in /model. /reasoning prints a pointer to /thinking throughout 0.21.x. The config key is [ui] thinking; the old [ui] reasoning still loads.
  • /help groups the commands under six headings, in coloured columns.
  • /model to a model whose key is not set warns at once, and a turn that fails there ends with an amber footer.
  • Signed out, the start-up line names /login for the free models.
  • Messages print the config lines to add instead of pointing at a doc in this repository.
  • Auto mode’s judge is now typesafe:jev-1.14.0, with a cutoff of 0.20 (was 0.30). Auto mode asks somewhat more often and lets fewer risky commands through: on the 300-command test set, false alarms go from 5 to 11 of 186 safe commands and misses from 63 to 49 of 114 risky ones. The away cutoff, 0.20, now equals the ordinary one. Every session records the judge it ran under, so an older session replays under its own.

Fixed

  • On OpenAI-compatible (Chat Completions) providers, a sub-agent’s result is separated from the next message by a blank line, so the model does not read your words as the end of the result.
  • Esc pressed just as the model answers now stops the turn. Before, the Esc could be lost: the turn went on and could run a command from that answer that needed no approval, or wait at an approval nobody was asked for. The same holds during an automatic compaction. Such a turn ends interrupted, like any other Esc.

Security

  • The harness no longer trusts the emSign ECC Root CA - G3 certificate authority. Upgrade from 0.20.x or earlier. In October 2026 the private keys of eight eMudhra intermediates under that root turned out to be recoverable, so anyone in your network path could pose as a model provider and read your API key and conversations. 0.21.0 drops the root and refuses the eight intermediates even when they are cross-signed by another root (RUSTSEC-2026-0381, RUSTSEC-2026-0382).

For operators

  • Rebuild and redeploy mega-server from 0.21.0 as well: it carries the same certificate fix.
  • mega-server accepts both jev-1.13.0 and jev-1.14.0 for the judge; redeploy it before clients move to 0.21.0.

[0.20.0] — 2026-10-18

Added

  • Two free models after a sign-in, with no key of your own. Type /login in a session, or run harness login: the harness prints a link and a code, you open the link on any device (over SSH too) and sign in with Google, and the harness notices on its own. You then have mega:flash and mega:full, listed in /model right after the current model, with how fast each uses your allowance. Signed in, with no saved pick and no config model, a session starts on mega:full and sub-agents use mega:flash (docs/config.md).
  • A daily allowance, shown as a percentage. On a mega model the status bar, and the line REPL’s per-turn line, end N% of today; a notice comes at 80%, and at 100% a turn stops with when the allowance resets (00:00 UTC) and how to keep going on your own key. No dollar figure is shown: /session says free for these calls and names the model that answered, as in mega:full (gemini-4-argon).
  • /logout and harness logout sign out and end the sign-in on the server. A sign-in also ends after 7 days without use; /login again starts a new one.
  • [agents] model and [agents] thinking set the model and thinking level a sub-agent gets when its definition and the spawn call name none, before the parent’s. The session log records them, so replay rebuilds the choice.
  • Auto mode’s judge works signed in, without a TypeSafe key: judged commands reach TypeSafe through the mega server, and a one-time notice says so.

Changed

  • A start with no usable model says how to get one: No model yet. Type /login for free models, or set your own key. With -p: No model yet. Run harness login for free models, or set your own key., and exit 1.
  • Your own keys work as before. A model you picked or configured still wins at start whenever it can answer, and the harness never moves you onto your own key by itself.

Security

  • The sign-in token lives in $CONFIG/.secrets/accounts.json (0600, in a 0700 folder). The file tools refuse that folder outright, --approve-all included; the sandbox hides it; a .secrets folder, account file or lock that is a link is refused; and bundle and export refuse a log that holds a token.

For operators

  • The account server, mega-server, is in server/, with its setup wizard (server/setup-gcp.sh) and deploy script (server/deploy.sh).

[0.19.0] — 2026-10-17

Added

  • read_file now returns PNG, JPEG, GIF, and WebP images visually on vision-capable models. When the active model accepts image input (resolved from models.dev’s modalities.input and provider /v1/models lists, and recorded on the session log so replay stays byte-identical), read_file advertises image support in its tool description and returns valid image/png, image/jpeg, image/gif, and image/webp files up to 3.75 MiB raw (5 MiB base64) and 8,000 × 8,000 pixels as native visual content across Anthropic, OpenAI Responses, Gemini (PNG, JPEG, and WebP), and OpenAI Chat Completions / OpenRouter.
  • Image validation, request budget, and model-switch protection. Oversized (> 3.75 MiB), > 8,000 px, or header/trailer-corrupt images return descriptive text notes ([image too large: …], [image dimensions too large: …], [invalid image: …]) instead of poisoning the session with a provider 400. Requests keep up to the newest 16 images and 15 MiB of base64 image data, and switching /model to a text-only model (or to Gemini after reading a GIF) omits earlier image blocks with an explanatory note. Inside script, tools.read_file on a supported image returns a short note instructing the model to call read_file directly.
  • Transcript, export, and secret-scan support for image reads. The panel and transcript show image · <mime> · <N> bytes, image too large · <mime> · <N> bytes, or invalid image · <mime> · <N> bytes; harness export --format md|html elides the base64 payload while --format jsonl and -p --output-format json|stream-json preserve the full record; and export / bundle skip verified base64 image payloads at the two read_file locations so random key-prefix collisions (AIza, AKIA, ASIA) in image bytes do not trigger false refusals.

[0.18.1] — 2026-10-17

Fixed

  • File tools (read_file, write_file, edit_file, glob, search) now accept an absolute path inside the project root as the project-relative path; paths outside the project, .. traversal, and symlinks leading outside stay refused. Prefix stripping always matches the folder the session started in, even after resume continues it elsewhere: there, absolute paths naming the new folder stay refused and paths naming the original folder resolve in the new one. (Sessions recorded before 0.18.1 that captured a refused in-project absolute path will also replay differently under 0.18.1.)
  • read_file on a recognised binary file (PNG, JPEG, GIF, WebP, BMP, TIFF, ICO, PDF, ZIP/JAR/Office/EPUB, gzip, zstd, xz, bzip2, 7z, tar, ELF, Mach-O, Java .class, WebAssembly, SQLite 3) now returns [binary file: <mime>, <N> bytes — read_file returns text only] instead of raw bytes, and the transcript result row shows binary · <mime> · <N> bytes (with <N> thousands-grouped). (Sessions recorded before 0.18.1 whose read_file returned raw bytes for a recognised binary file will also replay differently under 0.18.1.)

Changed

  • The busy status spinner is now the D5 3-cell red and blue taegeuk swirl. The three spinner cells use D5’s fixed red and blue swatches; [ui.colors] warning continues to style the thinking… text.

[0.18.0] — 2026-10-16

Added

  • A provider you add under [providers.<name>] is described by models.dev. The table is matched to the models.dev provider of the same name, unless models.dev places that provider on another host than your base_url; models_dev = "<id>" names one outright ([providers.fireworks] models_dev = "fireworks-ai"), and models_dev = false turns matching off (docs/config.md). Its models then get names, context windows, prices and featured rows, in /model, in the window a session uses and in the price /session shows. Where the provider’s own model list states a window, output limit, date or price, the list wins over models.dev. A new or changed table is picked up by the next interactive start’s refresh, even when the catalog was refreshed earlier that day. [models.context] still wins, and eval still reads no cache.
  • Featured rows for your own providers. A gateway whose model ids look like lab/model, such as Vercel’s AI Gateway, features the same models as the direct providers, as OpenRouter does. Any other provider, and a gateway that carries none of those models, features its newest model of each line.
  • /model <word> says how to add a provider you have not set up. When the word names a models.dev provider that neither a built-in nor one of your tables reaches (/model fireworks), a hint above the search’s rows names the table to add. For the six keyed providers docs/config.md documents (Fireworks, Groq, Cerebras, Moonshot, Hugging Face and, new, Vercel’s AI Gateway) it is a block ready to paste. The hint prints only text the binary carries, never a URL from models.dev, and works offline.

Changed

  • An OpenAI-compatible provider’s own model list now supplies names, windows, output limits, release dates and prices where it states them, as Vercel’s does. /model <text> finds a model by the list’s name; a model the catalog does not know yet shows the list’s window; each model’s release date, not one date for the whole list, decides its new tag and its place; and an id the list types as embedding, image, video, speech or another non-chat kind sinks to the bottom as (not a chat model). A price from the list shows only on a model models.dev also describes, so /model and /session show the same price.

Known limits

  • A gateway may advertise a window your account is not served. Vercel’s list gives Claude Sonnet 4.5 a 1,000,000-token window, a tier not every account has. Such a session compacts late, and a turn past the real limit fails with the provider’s “context too long” error, which the harness does not retry. Set [models.context] for that model (docs/config.md).

[0.17.0] — 2026-10-15

Added

  • The model catalog refreshes itself from models.dev. Once a day, an interactive session refreshes it in the background from models.dev, so /model lists a new model, with its name, window and price, the day models.dev does. Nothing waits for it and nothing is printed. It runs only when a provider with a key is set up off this machine, never under -p, --mock, eval or the other one-shot commands, and the download is compressed. Turn it off with [models] refresh_catalog = false; HARNESS_MODELS_DEV_URL reads a mirror instead (docs/config.md). The /model footer says when the catalog was last refreshed.
  • Before models.dev lists it, a new model still shows its provider’s name and window, as the first row under its provider after the chat models, marked (not in the catalog yet).
  • Recent models say new: anything released in the last 21 days.

Changed

  • Featured models are computed: the newest of each model line, at most four per provider, among the models the provider’s list serves. Nobody keeps a hand list, so a flagship is featured the day the catalog learns of it. OpenRouter features the same models as the direct providers, and orders the rest of its list by weekly use.
  • Inside a provider, chat models come first, newest first. Ids the catalog does not know come next. TTS, image and embedding ids the catalog knows sink to the bottom with a reason, (not a chat model) or (deprecated); a brand-new one it does not know yet sits with the other unrecognised ids, below the chat models.
  • The Gemini list is read in full, not only its first 50 ids.
  • A new model’s context window comes from the refreshed catalog, or from its provider’s list before that, instead of falling to 200,000. A model this binary already knows keeps the window the binary carries, and [models.context] still wins over both. eval reads no cache, so an eval’s window is the same on every machine; its report prints the window it used.
  • /session and harness session --json can price a model from the refreshed catalog, the same price /model shows, so two machines may now report a different cost for the same session.
  • Rows from a provider the catalog never covers (a local server, your own [providers.*] table) no longer say (new since the catalog); they carry no suffix.
  • The direct claude-sonnet-4-5 ids lost their hand-entered window: the catalog now says 200k itself. anthropic/claude-sonnet-4.5 on OpenRouter keeps its cap.

[0.16.3] — 2026-10-14

Fixed

  • harness -p prints the answer when a tool call failed on the way. A single failed call (a read_file on a missing file, a refused command) used to throw away the model’s whole final answer and exit 1. Now the answer is printed and stderr names the failed calls after it: harness: 2 tool calls failed in this turn: read_file, bash.
  • An empty reply no longer ends a turn silently. When the model answers with no text and no tool call (or with whitespace only), the harness asks it once more to continue. A second empty reply ends the turn as a model failure, “the model returned an empty response twice (no text and no tool call)”, instead of a blank answer.
  • A slow model is no longer cut off. A provider’s connection may now stay silent for 600 s (was 120 s) before the call fails, so a model writing a large tool call without sending a byte (seen with Gemini) finishes. A stream that does break says how far it got: gemini stream interrupted after 92 bytes, 600.0 s silent: ….
  • The status row stays visible while you type during a turn. The running tool, the thinking timer or compacting… keeps its place, and the hint enter steers · tab after turn · esc stops follows it. On a row too narrow for both, the hint is dropped whole. With nothing running, the hint still replaces ready.
  • An MCP server that lists only older protocol revisions connects (ADR-0064). A server that answered the 2026-07-28 probe with a list of older revisions the harness speaks (2025-11-25, 2025-06-18, 2025-03-26, and 2024-11-05 over stdio), either as its discovery result or in an “unsupported protocol version” error, used to be refused. The official Go SDK’s default HTTP mode and Datadog’s hosted server answer this way. Now the harness starts the server at the newest revision both sides name. A list holding only 2024-11-05 over HTTP is still refused, and now says why: that revision uses the HTTP+SSE transport, which the harness does not support.

Added

  • /session shows the last model failure: a row under Turns, model failures N · last: <error>, absent when there were none. The Turns row already counted model_failed turns; what is new is the error itself, on one line. A turn you stopped with Esc is not counted there. harness session --json’s turns.outcomes.model_failed is unchanged and still counts stopped turns. The same row shows in harness session <id>.

Changed — for scripts

These change what a script driving the harness sees.

  • -p exits 0 and JSON’s is_error is false when the turn answered, even after failed calls (was exit 1 and is_error true). outcome still reads tool_failed. A call refused under --away is treated the same way: the answer after it is printed, exit 0. Exit 1 now means the turn ended without an answer; see docs/headless.md.
  • The failed-calls line goes to stderr in every output format, json and stream-json included, so a script reading stderr in a JSON format sees one more line.
  • A response head that never arrives within 600 s is not retried. The call fails at once with <provider> timed out waiting for the response, instead of waiting up to five times. A connect timeout, a refused connection and a timeout the operating system reports still retry.
  • Logs recorded before 0.16.3 whose final response was empty or whitespace-only no longer replay: this build asks the model once more at that point, where the log holds no second call. Replay fixtures are version-scoped (ADR-0010); no committed recording has such a response.

Known limits

  • A reply of thinking only, followed by the harness’s request to continue, is unverified against Anthropic’s API. The request then carries an assistant message holding only thinking blocks before the nudge. If Anthropic refuses that shape, the turn fails with its error, visible only in a live run. Chat-completions servers get no empty assistant message: one left with no text, no tool call and no reasoning_details is left out of the request.

[0.16.2] — 2026-10-14

Fixed

  • The startup splash no longer disappears after a resize, a tmux session switch or a return from the external editor. A redraw used to clear the art and keep only the version line, with blank rows above it. Now the splash is redrawn as it was drawn, in its colours. In a pane narrower than the splash, the art is cut at the pane’s edge (never wrapped), and text that no longer fits beside the art is stacked under it.
  • A start in a narrow pane wraps the splash’s text the same way a redraw does, so a redraw at the same width draws the same rows.

Known limits

  • A resize can still leave copies of the screen in tmux scrollback, now including the splash. This is unchanged from 0.16.1: tmux moves rows into scrollback as the pane resizes, before the harness redraws them. The note explains why, and the alternative that was not taken.

[0.16.1] — 2026-10-14

Fixed

  • A box table no longer breaks apart in scrollback after the pane is widened and narrowed again (in tmux: a split closed and reopened, or a switch to a session of a different size). The harness redrew the visible reply at the wide width, and tmux re-wrapped those wide rows into scrollback when the pane narrowed, leaving a second, shattered copy of the table. Now a repaint never draws a reply wider than it was first drawn. Two limits remain: tmux can still leave a few whole rows duplicated in scrollback, and a reply no longer spreads out when the pane widens.

Changed

  • A turn whose only failed calls were ones you denied ends (1 call denied) or (N calls denied) instead of (tool failed), in the panel, the line REPL and playback. A turn with any other failure still ends (tool failed).

[0.16.0] — 2026-10-13

Added

  • [skills] extra_roots folders are searched (docs/config.md). Until now they were recorded and ignored. Their skills reach /skills, the slash menu and the model’s catalog, ranked below every default skills folder, so a name clash goes to the project’s, ~/.agents/skills or the harness’s own.
    • Each entry must be ~/… or absolute, with no .., at most 16; anything else fails the config load. At start, an entry that repeats another, names a default skills folder or sits in the project is dropped with a notice; a missing folder is kept with one.
    • A session keeps the roots it started with: a resume uses them, not today’s config, and says so when the config has changed. The log records them as ~/…, never with your home folder’s path.
  • /skills marks an extra root’s skills “extra folder N”, with a line naming the folder, and gains a Not loaded section listing every skill that did not load and why. A session start with any says ⚠ 1 skill not loaded — /skills says why.
  • Markdown tables are drawn as tables in the panel, boxed and fitted to the pane, re-fitted when it narrows; too narrow for columns, each row is drawn as a record. While one streams the live row reads table · N rows. The line REPL, -p, export and the session log keep the raw text.
  • /session is coloured: the session id in the accent blue, the cache hit rate green, allowed counts green, denied and flagged red.
  • Gemini 4 Argon (gemini:gemini-4-argon) is known: its 1M-token context window is used without a [models.context] entry.

Changed

  • A skills folder is read only inside itself, for every skills root (~/.agents/skills, the harness’s own and extra roots). The folder itself may be a link (a dotfiles setup works), but a SKILL.md or file that links out of its skills folder, or any absolute link inside it, now stops loading, and /skills says what links where and what to do (copy the file in, link the whole folder, or recreate the link with ln -sr).
  • A skills folder that overlaps a protected place is not used: one inside, or containing, a folder the command sandbox hides (~/.ssh, ~/.config/gh and the like), the harness’s session store, or (for an extra root) the project, so ~, ~/.config or / is refused. ~/.claude/skills itself is allowed. The message names the folder and why. If such a place cannot be checked (a permissions problem, say), no skills folder is used until it can, and the message names the folder that could not be checked.

Fixed

  • A reply keeps its colours and its code indentation after a resize or a tmux session switch. A repaint used to redraw it plain and squeeze code blocks; a long line was squeezed even when first printed.
  • /agents’s undefined-nickname row keeps its amber on a repaint.
  • A ~~~ fence is drawn as code; a code fence closes only on a bare marker of its own kind (one followed by a language name no longer closes it); and an interrupted answer no longer leaves the next one drawn as code.
  • Folder and skill names holding control characters are shown escaped in /skills, harness paths and start notices.

Notes

  • Do not resume a 0.16 session with 0.15.x. Nothing stops it, but its segment does not search the extra roots, and 0.16 can no longer replay the log (AF-R8). 0.16 reads, replays and resumes every older log; a log written before 0.16 keeps its extra roots unsearched.

[0.15.1] — 2026-10-12

Fixed

  • The harness now works behind a proxy that inspects HTTPS. A corporate gateway or a sandbox’s egress re-signs each connection with its own certificate authority, and every model call failed with transport error: error sending request, because the harness trusted only the authorities compiled into it. Set SSL_CERT_FILE to a PEM file holding the proxy’s certificate (your system’s bundle is fine): it is trusted as well as the compiled-in ones, for every provider, the judge and remote MCP servers (docs/config.md). Unset, nothing changes. A file the harness cannot use (unreadable, not a regular file, over 16 MiB, or holding no usable certificate) stops run, -p, eval, resume and judge score before they start, naming the file.

[0.15.0] — 2026-10-12

Added

  • MCP servers, configured in config.toml under [mcp.servers.<name>] (docs/config.md). A session with servers offers the model their tools through two tools, mcp and mcp_schema, and a catalog in its system prompt; a session without servers sends exactly what it sent before. A server that fails to start does not stop the session: one line says why, and its tools are absent.
    • Local servers (command) are started by the session and supervised: a harness that dies, even by kill -9, leaves no server running. Each runs in its own sandbox box, read-only on the project, with no home folder, a stripped environment plus the variables env names, and no network unless network = true. read binds a folder read-only into the box, for a program under your home folder (node under ~/.nvm, say). sandbox = false runs a server outside the box (a Docker-run server needs it); that is recorded and shown as sandbox off.
    • Remote servers (url, Streamable HTTP) take a token from the variable token_env names, sent only to that server’s own address; redirects are never followed. No OAuth.
    • Both protocol eras: the stateless 2026-07-28 revision and the initialize revisions before it.
  • Every MCP call asks, once (y allow once · n deny · 4 tell the agent), in manual and in auto (the judge is not consulted), unless the server’s allow names the tool, or the session runs with --approve-all. No turn or session grant covers an MCP call. Plan mode does not offer MCP; under /away an MCP call that would ask is refused. The gate shows the server’s own read-only hint, which changes nothing.
  • /mcp lists the servers this session’s start listed: transport, command or address, sandbox, protocol version, tool count, an estimate of the catalog’s tokens, the allow list, and the calls and failures since. The /session card gains an MCP row, and harness session --json an mcp object.
  • Every MCP call is recorded in the session log, with credentials redacted. The values of env variables and the token never enter the log, and any of them a server echoes back is replaced by [redacted]; a value shorter than 8 characters is refused at start, since it could not be redacted safely. Secrets go in env or token_env, never in command or url, which are recorded as written; a url holding a user name or password is a load error. harness replay replays an MCP session with no server, no config and no credential.
  • Resume reads the catalog from the log and lists nothing; a server starts at its first call. A server whose config differs from the session’s start is refused (configured differently; start a new session to use it).
  • Results are text, capped at 64 KiB; an image or audio block reads as a placeholder ([image: image/png, 1234 bytes]).

Changed

  • Typing /tokens no longer points to /session. The one-release hint (0.14) is over; /tokens is refused like any unknown command.
  • Every sandboxed session hides $STATE/mcp (the servers’ private homes) from its commands, MCP or not, so a new session’s record names one more hidden folder than under 0.14. Recorded sessions replay unchanged.
  • A config found through HARNESS_HOME or XDG_CONFIG_HOME inside the project cannot define servers (a load error): a cloned repository must not be able to start a process.

Notes

  • A session that used MCP is read by 0.14.x as holding unknown records: it lists and shows its card, and resume and replay refuse it, naming the effect. 0.13.x and older cannot read it. An older harness also refuses a config.toml that holds [mcp.servers] (it rejects unknown keys).

[0.14.0] — 2026-10-12

Added

  • /session shows what this session has done, in the panel and the line client (/status works too). One card, read from the session log when you run it: the id, its resume command and log file; where the time went (in turns, the model, tools, waiting on you); turns and model calls; tool calls that succeeded, failed, never ran or are still pending; approvals; the judge’s verdicts; files changed; tokens and the cache; an estimated cost; how full the context is; and the model, thinking, mode, sandbox and checkpoints in force now. Child agents’ figures follow their parent’s on the same rows (+ 2 children: …).
  • harness session <id> prints the same card for any session, finished, crashed or still running, from a full id or a unique prefix as resume takes them. --json prints the figures as one JSON object ("v": 1; later versions only add keys). --transcript prints the session’s scrollback as the line client showed it, paged on a terminal like git ($PAGER, else less); --no-pager prints it straight out.
  • A summary line above the resume line when you quit: 5 turns · 43m in turns · 2.49M in / 21.6k out · ≤ $1.95. It is left out after a forced quit (Ctrl-C while quitting), when the log may still be growing.

Changed

  • /tokens is gone. Its figures are on /session’s Tokens and Cache rows; typing /tokens says use /session.
  • harness sessions and the resume picker list a session ended by a store failure as failed. Before, a session stopped because its log could not be written or read could list as completed.
  • The cost is an estimate at list price. It is marked ≤, an upper bound, only when every model call is priced and none sent a prompt over 200K input tokens, where some providers charge long-context rates the catalog does not hold (the card then says long-context rates not priced). When some calls have no price it reads $X for N of M calls. A cost the provider reports (OpenRouter) is taken as is; judge calls are not priced.

Fixed

  • A session log whose last line was cut inside a multibyte character (a crash mid-write) no longer reads as corrupt: it no longer stops the next write to the log, or hides the session from harness sessions and the picker.
  • A log holding an effect this version does not know (0.15.0’s MCP calls, for one) now lists and shows its card instead of reading as unreadable; resume and replay refuse it, naming the effect.

[0.13.1] — 2026-10-11

Fixed

  • A decimal number in a tool call no longer ends the session. A model calling a tool with, say, limit: 100.0 ended the session at its next model call (session failed: store append failed: … floating point is not permitted in a digested payload), and resuming failed the same way. Such a session now carries on, resumes and replays. A session 0.13.0 ended this way can now be resumed; harness replay still cannot replay it. read_file still reads limit: 100.0 as no limit (whole numbers only, as before).
  • Cases and bundles can be made from OpenRouter sessions, whose recorded usage holds a decimal cost.

0.13.0 and older cannot resume or replay a session that holds a decimal: they stop where they stopped before.

[0.13.0] — 2026-10-11

Added

  • A message typed during a turn now reaches the agent while it works. Press Enter while the agent is busy and your message is sent at once: it waits in a ↳ row above the box and the model reads it before its next call, right after the tool calls it has already asked for have run. It then appears in the transcript where it landed. If the turn ends before that point (the agent answered without another tool call), it is sent as your next message. The status row says enter steers · tab after turn · esc stops while you type, and an approval that opens while a message waits says when it goes in. Each delivered message is recorded in the session log as a new record, user.steered.v1, and replays exactly.
  • Tab keeps the old queue. Tab instead of Enter during a turn holds the message until the turn ends, as Enter did in 0.12 (⧗ … · after the turn · ↑ edit; ↑ takes it back).
  • /skill:<name> <request> sends the skill’s instructions and your request as one message (/skill:show-me make a diagram of the store). /skill:<name> alone works as before.
  • Auto by default, asked once. If TYPESAFE_API_KEY is set and config.toml has no mode, the first interactive start asks Start in auto by default? … [y/N] and saves your answer as mode = "auto" or mode = "manual". It never asks again. --mode still overrides it for one run, and Shift+Tab still switches within a session.
  • A top-level mode key in config.toml names the mode new sessions start in (manual, plan, auto, or a [modes.<name>] table). harness eval always starts in manual.

Changed

  • Enter during a turn steers instead of queueing. Use Tab for the old behaviour. Esc still stops the turn and puts everything that had not gone in back in the box. Quitting while a message waits stops the turn first; Ctrl-C during that leaves at once.
  • Headings follow your accent colour. With [ui.colors] accent set and no md_heading, markdown headings use the accent, bold. The dark theme’s default accent is now a pale blue, #a6d3fd (256-colour 153). --help keeps the previous teal, which reads on light and dark backgrounds alike.
  • A stop that arrives just before the agent’s next model call now ends the turn as interrupted before the call is made, rather than cutting the call short.

Notes

  • An older harness refuses a config.toml that holds the new mode key (it rejects unknown keys), and refuses to resume or replay a session that holds a steered message (it names the record it does not know).

[0.12.1] — 2026-10-11

Fixed

  • export --format md no longer ends each reasoning block with empty > lines. Gemini’s recorded reasoning ends with blank lines, and every one was quoted. Blank lines before and after each reasoning text are now left out, in md and html alike; blank lines between paragraphs stay.

[0.12.0] — 2026-10-10

Added

  • harness -p "question" runs one turn and exits, for scripts and other agents. harness run -p is the same, --prompt and --print are other names for it, and the root takes every run flag with it (harness -p "q" --model openai:gpt-6). Piped input joins the prompt, data first and your question last: cat log | harness -p "why does this fail?"; git diff | harness -p takes the whole prompt from stdin. In a terminal you see the whole turn live, as run --prompt showed it before. When stdout is a pipe or a file it gets only the final answer, and only when the turn succeeded: exit 0 means stdout is the answer, 1 means it failed and says why on stderr, 2 means the arguments or stdin were refused. --verbose puts the live transcript on stderr. The contract is docs/headless.md.
  • --output-format json and stream-json. json prints one result object when the run ends (result, is_error, session_id, usage where Claude Code’s callers look for them, plus outcome and the calls that were denied). stream-json prints one line per finished step (the prompt, reasoning, answer text, each tool call, its approval and its result), then that result. Both carry "v": 1; fields and types are only ever added.
  • When nobody can answer an approval, the call is refused and the turn goes on. Under -p with piped input, or with no terminal to ask on, a call that needs approval does not run; the model is told nobody was there to approve it and finishes with what it can, and stderr names the refused call. A turn whose only failures are these refusals still succeeds. In a terminal you are asked, as before.
  • Reasoning from every provider, in the transcript. OpenAI (Responses) and Gemini now send their reasoning summaries too. The panel writes reasoning into the transcript as it streams, under a ✻ Thinking header with the seconds it took, instead of one flickering line on the status row, and keeps it in the history. [ui] reasoning = "full" | "preview" | "off" chooses all of it, the first four lines, or the timer only; /reasoning switches it for the session. The plain line interface shows reasoning only on a terminal, so piped output is unchanged.
  • A message typed while the agent works is queued, not sent. It shows as a ⧗ row above the message box and is sent when the turn ends, one turn per message, in order. ↑ on an empty box takes the newest one back to edit. Esc stops the turn and puts every queued message back in the box, unsent. Quitting sends none of them; they are in your input history.
  • export --format md and html include the model’s reasoning before each reply, whatever [ui] reasoning says. In html it is a closed “Thinking” section.

Changed

  • The approval menu no longer takes keys meant for the message box. A gate that opens within a second of a key you typed, or of sending a message, waits for ↑ or ↓ before it answers; until then your keys keep going to the box, and the menu says you were typing — ↑/↓ to answer. Esc still denies and stops at once.
  • run --prompt into a pipe or a file prints the answer, not the transcript. A script that read the transcript from stdout now adds --verbose and reads stderr.
  • Piped input no longer answers approvals for run --prompt: it joins the prompt. To answer approvals from a pipe, pipe the conversation into plain harness run.
  • OpenAI’s reasoning summaries need a verified organisation. Where OpenAI refuses them, the harness retries the call once without them and stops asking for the rest of the session; that session shows no OpenAI reasoning, and says nothing about why.

Known limits

  • -p cannot continue a session (resume -p), takes no slash commands, and stream-json carries no live text deltas.
  • A stdout that fails to be written fails a -p run only when it is a pipe or a file; a terminal that goes away mid-run is not reported that way.
  • Reasoning already on screen re-wraps raggedly, and loses its bold, when the panel is resized.
  • A call refused because nobody could answer shows in the transcript as ✗ denied by you: ….

[0.11.0] — 2026-10-09

Changed

  • New session logs are about ten times smaller on long sessions. A log used to copy the whole conversation into every model call’s record, so it grew with the square of the session’s length. Now each message, the system prompt and the tool list are stored once, and each call lists them by hash. On the three largest logs measured, a 125 MiB log comes to about 8 MiB. resume, replay, check, export, flag and bundle read correspondingly less.
  • A file the agent reads or writes is stored as its text when it is valid UTF-8, instead of one number per byte.
  • Old logs read and replay exactly as before. Nothing rewrites a stored log. Resuming an old session cleanly switches it to the new form from that point; what it already holds stays the size it was. A session resumed after a crash keeps the form it was writing.
  • flag pins with fingerprint scheme 2 ("fingerprint_scheme": 2 in the case file): the fingerprint leaves the stored text out. check verifies each stored piece a pinned turn relies on against its hash, whatever the case’s scheme, naming any that is missing or altered. A case pinned before 0.11.0 over an old log checks as it did, and the turn it pins is still found when flagged again. A version pinned by a newer harness’s scheme is an error naming the scheme.
  • bundle ships the stored pieces a pinned turn lists from earlier in the session, once each, and its manifest names them (added_pieces, bundle_format: 2). A bundle of a case pinned before 0.11.0 over an old log is unchanged, byte for byte.
  • export and bundle now see file contents. Their key scan could not match a file stored as numbers. A new session that read a file holding key material now refuses to export or bundle without --allow-keys, where 0.10 passed it.
  • replay refuses a session whose model call was cut between storing its text and starting the call, as it refuses any step a crash cut short. resume continues such a session.
  • run --mock prints the stored pieces among its records.

Known limits

  • 0.10 and a 0.11 log: 0.10 lists the session with its new records counted as unknown and exports them, but refuses to resume or replay it once the session has made a model call. A session 0.10 crash-resumes before its first model call cannot then be replayed by 0.11.
  • 0.10 and a 0.11 case: 0.10 ignores the scheme. Its check and bundle report a fingerprint mismatch for a turn whose log stores pieces, and agree over an old log. Its flag --case drops the scheme from every version it rewrites, which breaks them for 0.11: do not re-pin a 0.11 case with 0.10. Its export --format jsonl of a new log drops the format field, so the copy reads but does not replay under 0.11.
  • Logs written before 0.11.0 keep their size, and the key scan still cannot see the file contents they hold.

[0.10.1] — 2026-10-09

Fixed

  • Ctrl-C is no longer lost on the plain line interface (run and resume without the panel, piped input, and run --prompt). A press that landed while the harness was printing could be dropped, so a turn kept running, or an approval kept waiting, until a second press. Now it is kept and handled at the next chance, with the same meaning as before: it interrupts the turn, denies and stops at an approval, and quits at the idle prompt. Presses that arrive before the harness reads them count as one, and a second press that lands after an interrupt has ended the turn quits. An approval still queued when Ctrl-C interrupts the turn no longer opens a prompt you would have to answer or cancel again: the call shows as denied. The panel was not affected.
  • File-change checks in evals see edits made through the shell. The committed review case, the code-mode adoption case and the check scripts/adlc-demo.sh adds now count file.mutated.v1 or turn.files.v1, so a run that edits with sed or a script no longer fails them, while checkpoints are on. A turn-end list with no changed, new or deleted file in it (only a note, or only paths it could not check) does not count as a change.

Added

  • An eval check can count any of several event kinds. In a case’s expect.event_shape, kind may be a list: ["file.mutated.v1", "turn.files.v1"] counts events of either kind. A case written with a single kind reads and writes back unchanged.

[0.10.0] — 2026-10-08

Added

  • A new session starts on a model you have a key for. The order: --model; then the model you saved in /model or config.toml’s model, whichever you changed last, if its key is set; then the other of the two; then the first provider with a key, in /model’s order (anthropic, openai, gemini, openrouter, together, deepseek, zai, xai), on its default model. When the harness had to choose, a line under the banner says why (stderr for --prompt). resume keeps the log’s model, and harness eval uses --model and config.toml only, as before.
  • With no key at all, the panel opens with a warning naming the variables to set; run --prompt and piped input print it and exit 1.
  • /model remembers your pick. Enter switches and saves the model and its thinking level as your default for new sessions; Tab switches for this session only. A typed /model <id> or /thinking <level> saves too, and the /thinking picker takes the same two keys. The pick is kept in the harness’s state folder, never in config.toml; edit config.toml’s model or thinking after a save and config.toml wins.
  • When the sandbox cannot start on Linux, the message says what the harness seems to be running inside (Docker, Podman, gVisor, a Kubernetes pod, WSL, …), with the evidence, and offers mode = "external" first. It is a guess: it changes nothing and is not recorded.
  • Default models for openrouter, together, deepseek, zai and xai.

Changed

  • The gatekeeper is now the judge. Config takes [judge] and a mode’s judge = […]; [gatekeeper] and gatekeep are still read, and a file using both spellings is refused. harness gatekeeper score|fit is now harness judge score|fit, with no alias. Every message says “judge”, and the first-run notice shows once more. A session log recorded before 0.10.0 whose judge gave a reason no longer replays.
  • /keys is now /shortcuts, with no alias; ? on an empty input still opens the table, and its page is docs/shortcuts.md. Side by side the table needs 83 columns; narrower panes page it.
  • Switching models brings that model’s thinking level (its saved one, else config.toml’s thinking, else medium), where a typed /model used to keep the session’s.
  • The sandbox hides its secret folders inside HARNESS_HOME too (.secrets, .ssh, .aws and the rest of its list), as it does in your home folder.
  • Any file inside a .secrets folder of your project is a credential file: reading or editing it asks, search skips it, checkpoints leave it out, and commands allowed the network cannot see the folder.
  • docs/sandbox.md now says that a credential read asks in every mode except under --approve-all and in run --mock, where nothing can ask. The behaviour is unchanged.

Fixed

  • /model search finds a provider by name: “open” lists every openai: and openrouter: model, where it used to miss openai: ones. /model openai still lists openai’s own models.

Known limits

  • On macOS, a file matching .cargo/credentials* that is created after a session starts stays visible to commands until the next session; on Linux, every hidden name created after the start does.

[0.9.1] — 2026-10-07

Fixed

  • A session killed again after its recovery notice, before the turn closed, now shows at the next resume what that resume’s leftover check found, or that the check was cut short, instead of only repeating the earlier notice.
  • A notice shown again says “this resume checked again” only when the resume actually checks leftovers.
  • 0.9.0’s notes said two things too broadly, and are corrected in place: the model gets the notice only when the killed step was a tool call, and the stop for a session resumed from another folder needs a killed sandboxed command.

[0.9.0] — 2026-10-07

Added

  • A killed session can be resumed. If the harness is killed while a command, a file write, a picture, an /undo restore, a model request or a child agent runs, harness resume now picks it up instead of refusing.
    • The killed step is never run again. Its outcome is unknown. You get a notice saying what was killed and what changed in the project since the picture before it, with the /undo for each loss; when the killed step was a tool call, the model’s result for it carries the same notice. (Corrected in 0.9.1.)
    • The killed turn gets its turn-end list, as an interrupted turn does.
    • The notice is shown again at each later resume until you send a message, marked as from an earlier resume.
  • Leftover commands are checked at that resume. While a command runs, the harness keeps a small owner-only record of it under its state folder, and removes it once the command has settled.
    • On macOS, a sandboxed command still running from before the kill is stopped. On Linux, the sandbox already ends with the harness.
    • Processes left by a command run outside the sandbox are reported, never stopped, since one may be a server you want.
    • The protected-file check runs again against what was recorded before the kill, and moves aside a planted file.
    • If cleanup cannot be confirmed, sandboxed commands are held, and the notice says why and what to do. A record that cannot be read holds them until you remove it; the notice gives the command.
    • A session folder of records that cannot be listed, or a check that fails, stops the resume with what to do; resuming again checks again.
    • A session whose killed command ran in the sandbox, resumed from another project’s folder, stops and prints the cd … && … resume … line to run there; other killed steps resumed from the wrong folder are not caught (see Known limits). (Corrected in 0.9.1.)
  • docs/checkpoints.md “Resuming a killed session” and docs/sandbox.md “A killed command’s leftovers” have the details.

Changed

  • A sandboxed command is refused if its record cannot be written (for example, the state folder is not writable). The refusal names the folder and how to fix it.
  • A turn closed by a crash now gets its turn-end list, and a resumed line session no longer shows each turn one input late after a crash.
  • harness replay refuses a session that holds an abandoned step, naming it; harness export shows the abandoned step and the recovery notice.
  • The refusal to resume a sandbox-off session without the sandbox names --sandbox off as well as the config setting.

Known limits

  • A session held at resume, then closed while idle, is neither held nor checked at its next resume.
  • A killed command’s change to the git files the gatekeeper watches is not re-checked at resume.
  • A refusal or other tool failure longer than the terminal is cut on screen; harness export shows it whole.
  • A killed file write, or a command run outside the sandbox, resumed from another project’s folder is compared against that folder, so its change list may be wrong. (Added in 0.9.1.)

[0.8.0] — 2026-10-06

Added

  • Checkpoints. Before every bash, exec and agent call, and before a turn’s first file write, the harness takes a picture of the project: each covered file’s type, size, mode and times, with a copy of its content in the harness’s own store. Later writes in the turn picture only the path they touch, and a picture copies only what changed since the last one. It never touches your .git and never reads git to decide what to copy.

    • Not copied: what .gitignore excludes (in a project without git, a built-in list: node_modules/, .venv/, target/ and similar), your [checkpoints] skip list, and the harness’s own folders. Credential files (.env, *.pem, *.key, …) and files over the size cap are recorded but never copied. A folder whose .gitignore the harness cannot read exactly as git does is shown as “not checked” rather than copied past its rules.
    • The store is owner-only, under the data folder (checkpoints/<project key>/), and shares unchanged copies between pictures. Its limits are in [checkpoints]: 100 MiB per file, 5 GiB and 14 days per project, and per picture 10,000 files, 500 MiB or 10 seconds.
    • When a picture stops early, it says so and names what it skipped. The tool call always runs, unless you interrupted it.
    • No pictures are taken when the project is your home folder, /, or inside the harness’s own folders. A notice at start says so.

    docs/checkpoints.md has the whole story.

  • The turn-end list. After a turn that changed files, a short list shows files: N changed, N new, N deleted, up to ten lines, with a folder whose files were all deleted shown as one line.

    • Losses are flagged, each with the /undo that brings it back: not in git, not checked against git (when the harness cannot tell), 20+ deleted and protected.
    • What the harness could not check is stated, not hidden.
  • /undo and /undo <path> put files back from a picture.

    • The flow: pick a picture, see a preview of what will be written back, recreated, left alone and kept, then confirm. The result lists each path the same way.
    • /undo never deletes. Files created since the picture are listed as kept, for you to remove.
    • It takes a picture of what it overwrites first, so an /undo can itself be undone.
    • The model is told what was restored, and that stays true after /compact.
    • In the plain line client you pick by number and confirm with y.
  • /checkpoints on|off and [checkpoints] enabled (on by default). /undo still works on pictures taken before you turned them off.

  • /model asks for a thinking level for the model you pick, with the level in force highlighted. Enter switches both; Esc goes back to the list.

  • Opus 5.5 is in the bundled model list, and replaces Opus 5 on the shortlist (Opus 5 is still reachable by browsing).

Changed

  • Sandboxed commands can no longer read the harness’s session logs or its checkpoint store. If the project holds either folder (a harness home inside the project, or the project is your home folder), the agent’s file tools refuse them too. As a result, harness export and other commands that read session logs do not work from inside a sandboxed command.
  • A failed write to the session’s journal now ends the session, as a failed log append already did, instead of carrying on with an incomplete record.
  • The Markdown export shows thinking-level, iteration-cap and /checkpoints changes, not only model changes.

Known limits

  • /undo covers project files only. It cannot undo what a command did outside the project or sent over the network.
  • Commands approved to run outside the sandbox can still reach the store and the session logs.
  • Resuming a session that was killed while a command ran is part 2 (0.9.0).

[0.7.2] — 2026-10-06

Fixed

  • The static Linux binary builds again. The x86_64-unknown-linux-musl release build stopped linking at 0.4.0 (renameat2 undefined), so no static Linux binary could be built since then. It links again, and behaves the same.

[0.7.1] — 2026-10-04

Fixed

  • @ path completion after editing back into a path. Once the cursor had touched the @ (Home, or ← past it), the menu closed and nothing reopened it, so Tab did nothing. Tab now completes the path the cursor is in.
  • /mode without the sandbox (off, external, or unavailable) no longer says it asks before a command uses the internet or runs outside the sandbox: those questions are only asked while the sandbox is on. The two rows read “no extra question: commands have it” and “no extra question: commands run outside”.

Changed

  • The resume line’s command (Resume this session with: harness resume …) is amber, the colour a command has in the model’s answers, instead of teal.

[0.7.0] — 2026-10-03

Added

  • The sandbox runs on macOS. On macOS 26 or later every bash and exec command runs under Seatbelt, Apple’s kernel sandbox, through /usr/bin/sandbox-exec, enforcing the policy bubblewrap enforces on Linux. It is on by default, so auto, --approve-all, run --mode auto, harness eval and away work on a Mac with the sandbox on; 0.6.0’s “no sandbox for commands on macOS yet” is gone. docs/sandbox.md, “On macOS”, sets the two systems side by side.
  • What a command can do on a Mac: write the project, $TMPDIR (a private folder for the session), a linked worktree’s git folder, your [sandbox] writable entries, and your user temp folder except Apple’s com.apple.* folders in it. A plain command has no network at all, 127.0.0.1 included, so a test against a local fake server needs network: true, which gives the whole network and still hides the project’s credential files. The machine’s /tmp is out of reach (/tmp/x fails; use $TMPDIR); other processes are invisible and cannot be signalled; disks, the console and your other terminals are refused, and so are open, Apple Events to other apps, launchd jobs, the keychain and the pasteboard. The hidden list gains the keychains, cookies, accounts, mail, messages and app data under ~/Library, the browsers’ profiles and ~/.zsh_sessions.
  • A command’s processes end with it, normally, on a timeout or on Ctrl-C. If the harness cannot confirm they all have, the command’s result is withheld with an error naming them (at most eight, then how many more) and how to end them, and the session runs no command and no file tool, a child session’s included, until a later command checks clean.
  • The sandbox is checked at every session start, under its full rules, in about 0.2 s. It is unavailable, with the cause named, when a check fails, when /usr/bin/sandbox-exec is missing, or when the harness runs inside another sandbox; that last notice suggests mode = "external" when the outer sandbox is the boundary.

Changed

  • On a Mac the model is told that temp files go in $TMPDIR, that /tmp is not writable, and that a command that listens on or connects to any port, local ones included, needs network. Linux’s texts are unchanged.
  • Costs on a Mac: a sandboxed command takes about 30 ms longer than an unsandboxed one, and a session start about 0.2 s longer. A command’s profile is capped at 1,000 path filters, so a very long [sandbox] hide list slows every command and, past the cap, is refused; the command line with its environment must fit in 1 MiB; and a project path of more than about 1,000 bytes cannot run sandboxed commands.
  • What you carry on a Mac: a command cannot rename or remove a folder that holds a hidden or read-only path, nor the project itself, and cloning a whole folder in one call is refused (copy tools fall back to copying file by file). It cannot create the first missing folder above a hidden path or a read-only git path that does not exist yet: while .git/hooks links to tools/githooks, a command cannot create tools/. A command is refused, naming the folder and the fix, when a folder on the way to a hidden or read-only path, inside the project or another writable folder, cannot be searched. The keychain is refused inside, so keychain-backed credentials do not work there; for git’s osxkeychain and gh that is expected, not tested. Tools that sandbox themselves fail inside (swift build --disable-sandbox turns SwiftPM’s off). Your user temp folder is shared with your other programs.
  • A Mac session’s record names the backend (seatbelt macOS 27.0 (26A428)), the network as none, the temp folder as $TMPDIR and $USER_TEMP among the writable folders. A log a Mac session records with the sandbox on cannot be replayed by a 0.6.x binary: replay it with 0.7.0 or later. Linux records are unchanged.

[0.6.0] — 2026-09-30

Added

  • The harness builds and runs on Apple Silicon Macs with macOS 26 (Tahoe) or later. An older macOS refuses to start the binary, replay included, and Intel Macs are not supported. The README has the Mac setup. The file tools keep their confinement there: a link out of the project is refused, a credential-file read asks, a protected-file write asks. Commands keep their hygiene: stdin from /dev/null, a new session, no inherited descriptor past stderr.
  • macOS has no sandbox for commands yet (the Seatbelt backend is the next slice). A Mac starts with the sandbox unavailable and says so at every start: manual works, and auto, --approve-all, run --mode auto, run --away and harness eval need [sandbox] mode = "off" (all but away, unconfined) or mode = "external" (all of them).
  • [sandbox] mode = "on" | "off" | "external", and --sandbox <mode> on run, resume and eval for one session. The flag beats the config, and cannot be combined with eval --target. enabled = false still means off.
  • External mode, for a dedicated VM, a container or a CI runner that is itself the boundary, on Linux and macOS. The unattended modes work, away included; --approve-all refuses only the hard-deny list; the gatekeeper, the local asks and the file tools’ asks apply as they do without a sandbox. Commands see the machine as your user does: no hidden folders, no read-only protected files, no private /tmp, no network rule. Only you can set it, in your own config or with the flag; there is no environment variable. The session record says external, which switch set it, and where the config came from. docs/sandbox.md, “External mode”, says what the machine must do instead: keep keys and long-lived tokens off it, limit its network, and snapshot or discard it after a run.
  • Under external, credential variables stay out of commands’ environment — the credential-shaped names, a URL with a password, and your env_hide — except exact names you list in [sandbox] env_pass. Provider keys are always removed, and naming one in env_pass is a config error in every mode. The record lists the names removed and passed, never values, and the model is told which variables are missing. This is not a credential boundary: a command running as your user can still read the harness’s own environment.
  • harness paths names where the config came from: default, XDG_CONFIG_HOME or HARNESS_HOME.
  • On macOS the model’s prompt says the machine runs macOS with the BSD command-line tools (sed -i '', stat -f).

Changed

  • On a disk that ignores letter case (a Mac’s default), protected files, credential names, .git (the hard-deny rule too), path grants, the plan file and git’s index ignore ASCII case: write_file claude.md asks as a write to CLAUDE.md, and read_file .ENV asks as a credential read. The disk’s answer is recorded when a session starts, and a session whose disk cannot be asked refuses to start.
  • On macOS a credential-file read through a link to a file with several hard links asks, since its other names are unknown.
  • A [sandbox] table in a config found through HARNESS_HOME or XDG_CONFIG_HOME is refused when the file lies inside the project, by any spelling of the project’s path, or when its real path cannot be established; the rest of that config still loads. Your default ~/.config/harness, a HARNESS_HOME outside the project, and an XDG_CONFIG_HOME that is ~/.config spelled out are never refused.
  • enabled and mode that disagree, either way, are a config error.
  • Start-up notices name the switch that set the mode (set by --sandbox, set by $HARNESS_HOME/config/config.toml). The unavailable notice’s fix names mode = "off" rather than enabled = false; with enabled = false the notice is unchanged.
  • A crash resume under external continues only while external is both what was recorded and what is set now, and the recorded environment names, platform and case fact still hold; otherwise it exits 2, naming the first difference. Moving a session to or from external takes /quit and a clean resume. Other recorded states resume as before.
  • Git’s system config is also read where macOS keeps it: under the active developer directory, as xcode-select finds it, the Command Line Tools and Homebrew.
  • A log that records external cannot be read by a 0.5.x binary: replay or resume it with 0.6.0 or later.

Fixed

  • cargo build -p cli --release works on macOS 27, whose Command Line Tools’ strip left the build-time proc macros unloadable; the shipped binary is still stripped.

[0.5.1] — 2026-09-30

Changed

  • On a prompt for a delete git cannot undo, decoded text being run, a protected-file write or a credential read, the panel’s highlight now starts on deny, so an Enter pressed out of habit refuses; press y (or move up) to allow. Other prompts are unchanged, and the plain REPL already treated a bare Enter as no.

[0.5.0] — 2026-09-29

Added

  • A bash or exec command that empties or deletes a file git cannot bring back asks first, in manual and in auto, whatever the gatekeeper would say: truncate, an empty redirect (: > f, > f, true > f, cat /dev/null > f, on any descriptor), cp /dev/null f, rm f, git rm -f f, or git checkout -- f / git restore f, when the file is untracked and not ignored, changed since its last commit, or in no git repository. The prompt reads Asked because it empties or deletes notes.md, which git cannot bring back (untracked). A committed, unchanged file and an ignored file go on to the gatekeeper as before. The git state is read from git’s own index, never by running git.
  • A command that decodes hidden text and runs it (… | base64 -d | sh, eval "$(… base64 -d)", xxd -r -p … | bash, python3 -c "exec(…b64decode…)") asks first: Asked because it decodes hidden text and runs it. Decoding into a file without running it does not ask.
  • A command that empties or deletes files past what the harness reads — more than 1,024 of them, or behind more directory changes or nesting than it follows — asks too: Asked because the harness cannot tell which files it empties or deletes.
  • None of these asks can be granted for the turn or the session; all are refused while you’re away, and --approve-all skips them. They see only the files a command names: a glob, a variable, find -delete, xargs rm or a script still goes to the gatekeeper. docs/modes.md, “Local asks”, lists the forms.
  • harness gatekeeper score and fit apply these asks before the gatekeeper: a row they catch counts as ask at every cutoff, and fit prints a line naming every catch.

Changed

  • Protected files that git doesn’t track, at their fixed places, are read-only inside the sandbox: a local .envrc, .claude/settings.local.json, an untracked file in .vscode/, .claude/ or .github/workflows/, and your own [protected] add entries. No command can change one, however it spells the write; the file tools still ask. A protected folder holding nothing git tracks is read-only whole.
  • A protected path a sandboxed command creates, such as a new .envrc or a new file in .claude/, is moved aside to <name>.harness-quarantined-<16 hex digits>, never deleted, and the result says where. That holds even when the command stages it or a checkout brings it in, and for a tracked one turned into a directory, a link or a FIFO; putting it back takes one approval. One that can’t be moved stops every later sandboxed command, naming it, until it moves or you move it away.
  • Tracked protected files (a committed CLAUDE.md, a workflow) and every build.rs stay writable to commands, so a sandboxed git checkout or git switch completes. Review what a task changed against the commit it started from, with the steps in docs/sandbox.md: note the commit and the ignored files before it starts, then diff and list from a fresh index built from that commit (GIT_INDEX_FILE=<tmp> git --no-replace-objects read-tree <commit>), the ignored list included. A command can write .git — the index, a replace ref, an ignore rule — to hide a change from git diff <commit> and git status; the steps resist the tricks known today and are not a proof. Code planted in a build file runs the next time you build or test on the host, so review first.
  • A protected path that is a link to the project or a folder holding it (.claude -> .), or to a target so deep that binding it needs more mounts than bubblewrap allows, stops every sandboxed command, naming the link, until you replace it.
  • Under harness run --approve-all, a command asking to run outside the sandbox (outside_sandbox: true) is refused before it runs, and the agent is told to carry on with the work that needs no approval and name the step in its final message; network: true still runs unasked. harness eval, run --mock and child sessions follow the same rule. To leave the sandbox, run with a person present.
  • A session recorded before 0.5.0 gains these asks at its next clean resume. A session started with --yes before 0.5.0 that crashed, and is crash-resumed, still lets a full escape run unasked until you resume it cleanly.
  • A log written by 0.5.0 under --approve-all, eval or run --mock records the network_only escape rule, which a 0.4.x binary cannot read: replay or resume it with 0.5.0 or later.
  • On a prompt whose ask no turn grant covers (a protected file write, a credential read, and the local asks), the “this turn” choice now says except this kind: another call of the same kind asks again.

Fixed

  • A git config with a very long section header and many keys no longer takes memory far out of proportion to its size when a session starts.

Removed

  • harness run --yes is gone: use --approve-all (the rename shipped in 0.4.1). A script still passing --yes now fails at argument parsing, before anything runs.

[0.4.1] — 2026-09-28

Changed

  • harness run --approve-all replaces harness run --yes and does what it did: a tool call that would ask you goes ahead instead. --yes still works in this release, but is no longer listed in --help and prints a one-line note; a later release removes it. Messages and docs that named --yes now say --approve-all.

[0.4.0] — 2026-09-27

Added

  • Every bash and exec command runs in a sandbox, in every mode, --yes included. It can write only in the project and a private /tmp, cannot read your key folders (~/.ssh, ~/.aws and the rest of the hidden list) or credential variables, has no network but localhost, cannot reach local sockets or see other processes, and cannot change what git runs later: hooks, git config, and what they name. Linux only, through bubblewrap; docs/sandbox.md has the requirements and what the sandbox does not stop.
  • Two ways out, each with the model’s reason shown to you: network: true adds the internet and keeps the rest of the sandbox, hiding the project’s own credential files; outside_sandbox: true runs the command as you and always asks.
  • A command the sandbox blocked says so in its result, and how to ask for more.
  • /away: while you’re away, anything that would ask you is refused instead, and the agent carries on with the work that needs no approval. A breaker goes back to asking after 3 refusals in a row or 20 in all. harness run --away --prompt … does the same for a scripted run.
  • A question left unanswered for [away] timeout (default 15 minutes) is refused, and the rest of the task runs as if you had typed /away. This applies only where /away works, such as auto with the sandbox on: in manual, a question waits for you as long as it takes, as in 0.3.0, with no countdown.
  • harness run --mode <name>.
  • /iterations <n> raises the per-task limit on model calls from inside a session.
  • [sandbox] and [away] config tables, and [gatekeeper] away_cutoff (default 0.2, stricter than the ordinary 0.3).
  • /mode shows where commands run, and whether a mode may use the internet or leave the sandbox.

Changed

  • Where the sandbox cannot run, auto is not available: choosing it prints the cause and the fix, a session resumed or configured in auto starts in manual, and --yes, run --mode auto, run --away, run --mock and harness eval exit 2. In manual a command you approve still runs unconfined there, as in 0.3.0. [sandbox] enabled = false runs without the sandbox on purpose, for example inside a container that is already your sandbox.
  • The file tools follow no link out of the project: a CLAUDE.md, AGENTS.md or .agents/ entry linked outside it no longer loads, an absolute link is refused even when it points back inside, and harness export -o through a linked folder is refused.
  • A local ask says why it asked (Asked because …), instead of saying the gatekeeper flagged it.
  • Resuming a session keeps the model-call limit its log last stated, over config.toml.
  • A --yes or --mock session does not resume where the sandbox cannot run; a session from before 0.4.0 resumes unconfined, in manual.
  • Every session start runs a quick check that the sandbox holds.

Fixed

  • A process a command leaves running in the background no longer holds the call open: the call returns about two seconds after the command exits. Outside the sandbox, a process that detached itself keeps running, and the result ends with a line saying its output was cut.
  • A command no longer inherits the harness’s stdin, terminal session or open files, including on kernels where close_range fails.
  • Model text, child rows, the panel’s repaint and the echo of your message draw control characters escaped, never raw.

Security

  • Reading a credential file (.env, *.pem, *.key, *.p12, *.pfx) with the file tools asks, in every mode, and is refused while you’re away; search skips such files and says how many.
  • rm with recursive and force flags on anything with a .git path component is refused in every mode.
  • A command cannot point git elsewhere: a planted .git/commondir, or a changed link to git’s config or hooks, is moved aside with a note. A hooks folder, an include or another file git would run or read, created where none existed at session start, is flagged in the result.
  • Git’s hooks and config stay read-only inside the sandbox, your global and system git config included, even when tracked. A sandboxed git command that would change one, such as a git checkout of a branch with different hooks, stops half-done, and the agent is told so: which files are left and, after a checkout, the git checkout -- <files> that finishes it outside the sandbox, which asks you first.

[0.3.0] — 2026-09-25

Added

  • Auto mode now asks only when a command looks risky, instead of asking before every bash/exec call: a second model, the gatekeeper, reads the command beside your request and decides run or ask.
  • /mode shows a table of what each mode does, and marks the mode actually in force.
  • A [gatekeeper] config section, including its ask/run cutoff.
  • Readline-style editing keys in the message composer: Ctrl-A / Ctrl-E to the start / end of the line; Alt-← / Alt-→ (also Ctrl-←/→, Alt-b / Alt-f) a word left / right; Ctrl-W deletes the word before the cursor; Ctrl-U / Ctrl-K delete to the start / end of the line. Terminals that send Cmd-←/→ as Ctrl-A/E (Ghostty and iTerm2 on macOS) get those too. docs/keys.md lists every key.
  • ? on an empty input, or /keys, shows the keys; docs/keys.md lists them with terminal notes.

Changed

  • The composer keeps its last column free, so the cursor always has a cell of its own at the end of a row. Its text is one column narrower.

Fixed

  • The composer drew a run of spaces as one space, so a space typed between two words seemed to do nothing and ←/→ sometimes seemed not to move. Every space now draws where it was typed, and every ←/→ press moves the cursor, including at the edge of a wrapped row.
  • A tab or another whitespace control pasted into the composer now draws as one space instead of shifting the line.

Security

  • A control character pasted into the composer, such as ESC, is drawn in caret notation (^[) and never reaches the terminal raw.
  • Protected files always ask for approval, in every mode.
  • A short list of commands is refused outright, in every mode, gatekeeper or not.
  • Provider API keys never reach the environment of a command the harness runs.

Earlier

0.1.0 (slices A–N)

  • Slice A — the walking skeleton: a single-owner agent loop over an append-only event log, replayed byte-identical.
  • Slice B — a real, minimal harness: Anthropic and Gemini providers, five tools, streaming, an interactive REPL.
  • Slice C — hardened for real work: log compaction, prompt caching, a search tool, timeouts, cancellation, an approval gate.
  • Slice D — CI in front of the tree, an exec tool beside bash, honest exit statuses, diff previews in the approval gate.
  • Slice E — the daily driver: output-cap recovery, a live status line, a config file, an iteration cap, OpenAI Responses and Chat Completions as two more providers.
  • Slice F — the surface: color, a spinner, a startup identity line, context-% in the footer, streaming markdown, bounded approval previews.
  • Slice G — the protocol spine and the panel: a typed stream, correlated approvals, Build/Plan/Auto modes, the ratatui bottom panel.
  • Slice I — the daily driver, round two: @ file references, bracketed paste, layered AGENTS.md/CLAUDE.md context with /init, the resume picker, the Ctrl-G external editor.
  • Slice J — skills: SKILL.md discovery and invocation through the approval gate, plus picker row expansion, session export, composer vertical navigation.
  • Slice K — subagents as replayable child sessions, /model mid-session, the startup splash, the build mode renamed manual.
  • Slice L — the first ADLC loop end to end: flag, check, bundle, eval, cases as repo citizens; /model becomes a picker with a per-spawn child model.
  • Slice M — the first target that isn’t this harness: harness import over OpenTelemetry GenAI traces, eval --target against a real external agent.
  • Slice N — the fix slice: a log holding a record of a kind this build doesn’t know is carried, not corrupt; key-material scanning on export; tightened file permissions on the harness’s own data.

0.2.0 (slices O–T; the bump is cd6900d, 2026-09-10)

  • Slice O — code mode: the script tool runs in an embedded QuickJS-NG isolate, every inner call journalled and replayed byte-identical.
  • Slice P — code-mode adoption: a steering block in the system prompt when a mode offers script, and the adoption matrix that measured what moved it.
  • Slice Q — a resumed session rebuilds its status line, history and context-% from its own log; every provider’s token counts are whole; a project is named by its folder.
  • Slice R — a thinking level you can change mid-session, a deny that can steer instead of only stopping, OpenRouter as a provider, color on the splash.
  • Slice S — a slash-command menu with typo refusal, and a tool call rendered once, legibly, with its output.
  • Slice T — any OpenAI-compatible provider through provider:model ids, a built-in list of compatible providers plus three lines of config for any other, nicknames for helper agents, and file tools that report their own truncation honestly.