Changelog
[0.31.0] — 2026-11-04
Added
- The SDK in Python.
pip install megacode-agent-sdkgives you themegacode_agent_sdkmodule: the Rust crate’s shapes in asyncio —query,Client.start,Client.resume, your own tools asasyncfunctions 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 itsInstall Certificates.commandrun once before the binary’s download works. - The SDK in TypeScript.
npm install megacode-agent-sdkgives you the same shapes for Node 20 or newer, as an ES module with no runtime dependencies and its own type declarations. Alwaysawait client.close()(or declare the client withawait using, Node 20.4 or newer): a client left open keeps Node running. The download followsHTTPS_PROXYandNO_PROXYonly on Node 22.21 or newer, or 24.5 or newer; on older Node behind a proxy, name thebinaryoption. - 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) andsdk/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-sdkcrate 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, soharness session,replayandexportwork 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: noconfig.toml, noAGENTS.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 answersclient_tool_resultwith text and at most one image, and a call left unanswered ends at its timeout (5 minutes by default) withclient_tool_cancel. Each call is recorded, so a replay needs neither the model nor your program. A tool markedaskasks through the usualapproval_request(kindclient_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”. --mockin 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.jsondescribes every line in both directions, checked in CI against transcripts recorded from the real binary (sdk/conformance/). The protocol stays atv: 1; everything new is additive. - The idle
compactcommand says what it compacted. Itsdoneanswer now carriesdata.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. abortacts at once. In a conversation, anabortinterrupts at once, ahead of any command sent after it; with aprompt_idit takes a still-queued prompt back (dequeued) or stops the turn that prompt opened. The SDK’sabort()takes back its own queued prompt and ends that prompt’s stream, so the nextsendruns.
Fixed
- Turning away off during an away wait takes effect. A
/awayoff 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
compactqueued during a turn runs once the turn ends; a prompt sent in that window used to skip ahead of it, and anabortof that prompt stopped the compaction instead. The prompt now waits, and itsabortstops 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./noticeslists them all in four sections: needs your attention, about this start, privacy, status. After your first message the status bar counts⚠ Nwhile a warning holds andM notesuntil you open/notices. The plain prompt prints one line, the first note and(+N more · /notices);-pand stream-json print what they printed before. What you have seen is kept per project in$STATE/notices-seen.json(docs/config.md). /updateand/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./updateinstalls it without leaving the session: the same signed, checked install asharness update, each step shown; Esc cancels before the swap, and a failure changes nothing. The item then reads0.29.1 installed · /restart./restartwaits for the session to be idle, closes it as/quitdoes and starts the installed binary on the same session, so the conversation, mode and approvals carry over; the new process saysNow on harness 0.29.1 (was 0.29.0). A session learns of an install from the receiptharness updateand/updatewrite ($CACHE/install-receipts.json), never by running a binary (docs/config.md).exit,quitandharness updatetyped at the prompt do what was meant. Typed alone at an idle prompt,exitorquit(any case) quits as/quitdoes, andharness updateoffers to run/updateinstead[Y/n]. A leading space, more words, or a running turn send the text to the model as before (exitis the escape)./exitis now the same as/quit(docs/shortcuts.md).- Skills used.
/sessiongains a Skills row: each skill the session used, how often, and how many times by you, sub-agents’ after./skillsgains aTHIS SESSIONcolumn with the same counts, and marks a skill added on disk since the startnew 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=dumbor 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;/noticeslists the full sentence, with its switch (telemetry = falseorDO_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 modelinstead of naming a model that cannot answer. [ui] quiet_startuphides what it hid before. For an update it hides only the note on the notice line; the status-bar item and/noticesstay.
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 plandoes, 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
resultnames the model.harness resume <id> --input-format stream-jsonprinted"model": nullon every turn’sresult, since a resume never sees the session’s start record; it now names the session’s model, asinitdoes. - 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 carryinitbefore 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(andharness 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 oneresponseline; stdout prints thestream-jsonlines-pprints, every gate as anapproval_requestthe program answers with the panel’s choices (allow_once,allow_for_turn,denywith a reason the model reads,deny_and_stop), a sub-agent’s gates included, and oneresultper turn with the prompt’s own id. An unanswered request times out at[away] timeoutwith the panel’s consequences. A full escape is never asked of a program: a conversation runs underescape = "network_only"as--approve-alldoes, unless--allow-escapessays 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 replayreproduces it. See docs/headless.md, “A conversation”. - Closing stdin means “no more input”, not “stop”.
printf '<prompt line>' | harness run --input-format stream-jsonruns 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.quitand Ctrl-C stop. - SIGTERM is Ctrl-C for a conversation: the turn’s
resultprints, exit 0; a second signal exits 1. A supervisor’skillwhose grace period outlasts the effect in flight ends the session cleanly instead of leaving a crash close for the nextresumeto report; a SIGKILL inside the grace period still does. result.coston a conversation’sresult: the model calls since the lastresult(the turn’s, and a compaction between turns in the next one’s) priced asharness sessionprices a session (usd,priced,unpriced,free,bound,basis).abort {"prompt_id": …}takes a queued prompt back (dequeued) or interrupts the turn it opened.compactedcarriestrigger(automatic/manual) andpre_tokens;get_state.data.contextreportslast_prompt_tokens,window,percentandauto_compact_at_percent. Aresumestarts both from the log.- A client example, and the conversation on a real model.
examples/stream-json-client/driver.pyholds 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 byfixture_replay;scripts/rerecord-cassettes.sh conversationre-cuts them. A real terminal on stdin is refused before anything starts, onrunandresumealike (the pty suite).
Changed
-pends on SIGTERM as on Ctrl-C. The turn endsinterrupted(ormodel_failedwitherror: "interrupted: "when the signal lands on the model call), itsresultprints, exit 1; before, the process died and the nextresumereported a crash close.-p --output-format stream-jsonlines gain fields:user.kind(messageorsteer),result.turn_idandresult.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-jsonandjson— so a reader that splits on Unicode line separators (Python’ssplitlines(), Node’sreadline) 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
thinkingblock’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 onclaude-haiku-5-5with 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 updatekeeps 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 withneeds macOS 15.4 or later (this Mac runs <version>), and aHARNESS_VERSIONpin 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
Usersfolder (/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 aUsersfolder 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, sowrite_file claude.mdcannot replace a protectedCLAUDE.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,
replayincluded. 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 withsandbox = 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’scwdstill names the project:cd "$(jq -r .cwd)"first if it needs it. In a sandboxed session itsPATHdrops the folders the agent can write. The start check now also checks the path in a--flag=pathargument (docs/hooks.md). - The file tools read only regular files, up to 16 MiB.
read_file,edit_fileandwrite_filerefuse a FIFO, a device, a socket or a folder at once, and any file in/procor/sys;read_fileandedit_filerefuse a file over 16 MiB, whilewrite_filestill replaces one.searchskips 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;
searchno longer holds every match. harness export -owith..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 endstruncated, as before. Each try is a recorded model call and counts againstmax_iterations. In-pandharness evalthe answer joins the tries, so a case that used to endtruncatedmay 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:fullsends 65,536 output tokens: amega: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.tomlthat hastelemetry; remove it before downgrading.
Fixed
- The resume hint prints
harness resume <id>whenharnesson yourPATHis this binary, not its full path.
[0.24.0] — 2026-10-24
Added
- Hooks. Your own commands, listed as
[[hooks]]inconfig.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 setssandbox = false, and a failing hook blocks unless it setson_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-hooksruns 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 updatedownloads 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 youin 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, onrunandresume, starts no MCP servers for that session and records it, so aharness -prun from inside an MCP server does not start that server again.- When a hook blocks the prompt,
harness -pexits 1 withoutcomeprompt_blocked;harness sessions --jsonand the/sessioncard 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
skillto[modes.auto] ask(docs/config.md). - Older versions refuse a
config.tomlthat has[[hooks]]orcheck_for_updates; remove them before downgrading.
Fixed
- A skill whose
namediffers 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-tokensand nomax_tokensinconfig.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 onmega: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/modelswitch,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 resumeand exports show both where they joined, andharness 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 -pand 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 -poutput 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.shchecks a source folder namedtarget. - 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_rowssets the budget;0turns the replay off (docs/config.md). harness session <id> --transcriptshows a background sub-agent’s↳ <name> finishedrow.
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 fitcrashed 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
agentcall 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 = falsekeeps 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
/awayalso 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’smax_iterationsunless you chose a cap with--max-iterationsor/iterations; a cap you chose still carries over. /thinkingreplaces/reasoning: it shows, previews or hides the model’s thinking, and thinking levels are picked in/model./reasoningprints a pointer to/thinkingthroughout 0.21.x. The config key is[ui] thinking; the old[ui] reasoningstill loads./helpgroups the commands under six headings, in coloured columns./modelto 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
/loginfor 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-serverfrom 0.21.0 as well: it carries the same certificate fix. mega-serveraccepts bothjev-1.13.0andjev-1.14.0for 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
/loginin a session, or runharness 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 havemega:flashandmega:full, listed in/modelright after the current model, with how fast each uses your allowance. Signed in, with no saved pick and no configmodel, a session starts onmega:fulland sub-agents usemega:flash(docs/config.md). - A daily allowance, shown as a percentage. On a
megamodel the status bar, and the line REPL’s per-turn line, endN% 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:/sessionsaysfreefor these calls and names the model that answered, as inmega:full (gemini-4-argon). /logoutandharness logoutsign out and end the sign-in on the server. A sign-in also ends after 7 days without use;/loginagain starts a new one.[agents] modeland[agents] thinkingset 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-allincluded; the sandbox hides it; a.secretsfolder, account file or lock that is a link is refused; andbundleandexportrefuse a log that holds a token.
For operators
- The account server,
mega-server, is inserver/, with its setup wizard (server/setup-gcp.sh) and deploy script (server/deploy.sh).
[0.19.0] — 2026-10-17
Added
read_filenow returns PNG, JPEG, GIF, and WebP images visually on vision-capable models. When the active model accepts image input (resolved frommodels.dev’smodalities.inputand provider/v1/modelslists, and recorded on the session log so replay stays byte-identical),read_fileadvertises image support in its tool description and returns validimage/png,image/jpeg,image/gif, andimage/webpfiles 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/modelto a text-only model (or to Gemini after reading a GIF) omits earlier image blocks with an explanatory note. Insidescript,tools.read_fileon a supported image returns a short note instructing the model to callread_filedirectly. - Transcript, export, and secret-scan support for image reads. The panel and transcript show
image · <mime> · <N> bytes,image too large · <mime> · <N> bytes, orinvalid image · <mime> · <N> bytes;harness export --format md|htmlelides the base64 payload while--format jsonland-p --output-format json|stream-jsonpreserve the full record; andexport/bundleskip verified base64 image payloads at the tworead_filelocations 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 afterresumecontinues 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_fileon 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 showsbinary · <mime> · <N> bytes(with<N>thousands-grouped). (Sessions recorded before 0.18.1 whoseread_filereturned 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] warningcontinues to style thethinking…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 yourbase_url;models_dev = "<id>"names one outright ([providers.fireworks] models_dev = "fireworks-ai"), andmodels_dev = falseturns 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/sessionshows. 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, andevalstill 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 providersdocs/config.mddocuments (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 itsnewtag 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/modeland/sessionshow 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
/modellists 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,evalor the other one-shot commands, and the download is compressed. Turn it off with[models] refresh_catalog = false;HARNESS_MODELS_DEV_URLreads a mirror instead (docs/config.md). The/modelfooter 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.evalreads no cache, so an eval’s window is the same on every machine; its report prints the window it used. /sessionandharness session --jsoncan price a model from the refreshed catalog, the same price/modelshows, 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-5ids lost their hand-entered window: the catalog now says 200k itself.anthropic/claude-sonnet-4.5on OpenRouter keeps its cap.
[0.16.3] — 2026-10-14
Fixed
harness -pprints the answer when a tool call failed on the way. A single failed call (aread_fileon 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 hintenter steers · tab after turn · esc stopsfollows it. On a row too narrow for both, the hint is dropped whole. With nothing running, the hint still replacesready. - 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
/sessionshows the last model failure: a row under Turns,model failures N · last: <error>, absent when there were none. The Turns row already countedmodel_failedturns; what is new is the error itself, on one line. A turn you stopped with Esc is not counted there.harness session --json’sturns.outcomes.model_failedis unchanged and still counts stopped turns. The same row shows inharness session <id>.
Changed — for scripts
These change what a script driving the harness sees.
-pexits 0 and JSON’sis_erroris false when the turn answered, even after failed calls (was exit 1 andis_errortrue).outcomestill readstool_failed. A call refused under--awayis 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,
jsonandstream-jsonincluded, 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_detailsis 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_rootsfolders 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/skillsor 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.
- Each entry must be
/skillsmarks 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. /sessionis 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 aSKILL.mdor file that links out of its skills folder, or any absolute link inside it, now stops loading, and/skillssays what links where and what to do (copy the file in, link the whole folder, or recreate the link withln -sr). - A skills folder that overlaps a protected place is not used: one inside, or containing, a folder the command
sandbox hides (
~/.ssh,~/.config/ghand the like), the harness’s session store, or (for an extra root) the project, so~,~/.configor/is refused.~/.claude/skillsitself 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 pathsand 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. SetSSL_CERT_FILEto 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) stopsrun,-p,eval,resumeandjudge scorebefore they start, naming the file.
[0.15.0] — 2026-10-12
Added
- MCP servers, configured in
config.tomlunder[mcp.servers.<name>](docs/config.md). A session with servers offers the model their tools through two tools,mcpandmcp_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 bykill -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 variablesenvnames, and no network unlessnetwork = true.readbinds a folder read-only into the box, for a program under your home folder (node under~/.nvm, say).sandbox = falseruns a server outside the box (a Docker-run server needs it); that is recorded and shown assandbox off. - Remote servers (
url, Streamable HTTP) take a token from the variabletoken_envnames, 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
initializerevisions before it.
- Local servers (
- Every MCP call asks, once (
yallow once ·ndeny ·4tell the agent), in manual and in auto (the judge is not consulted), unless the server’sallownames 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/awayan MCP call that would ask is refused. The gate shows the server’s own read-only hint, which changes nothing. /mcplists the servers this session’s start listed: transport, command or address, sandbox, protocol version, tool count, an estimate of the catalog’s tokens, theallowlist, and the calls and failures since. The/sessioncard gains an MCP row, andharness session --jsonanmcpobject.- Every MCP call is recorded in the session log, with credentials redacted. The values of
envvariables 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 inenvortoken_env, never incommandorurl, which are recorded as written; aurlholding a user name or password is a load error.harness replayreplays 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
/tokensno longer points to/session. The one-release hint (0.14) is over;/tokensis 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_HOMEorXDG_CONFIG_HOMEinside 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
resumeandreplayrefuse it, naming the effect. 0.13.x and older cannot read it. An older harness also refuses aconfig.tomlthat holds[mcp.servers](it rejects unknown keys).
[0.14.0] — 2026-10-12
Added
/sessionshows what this session has done, in the panel and the line client (/statusworks 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 asresumetakes them.--jsonprints the figures as one JSON object ("v": 1; later versions only add keys).--transcriptprints the session’s scrollback as the line client showed it, paged on a terminal like git ($PAGER, elseless);--no-pagerprints 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
/tokensis gone. Its figures are on/session’s Tokens and Cache rows; typing/tokenssaysuse /session.harness sessionsand the resume picker list a session ended by a store failure asfailed. Before, a session stopped because its log could not be written or read could list ascompleted.- 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 sayslong-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 sessionsand 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;
resumeandreplayrefuse 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.0ended 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 replaystill cannot replay it.read_filestill readslimit: 100.0as 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 saysenter steers · tab after turn · esc stopswhile 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_KEYis set andconfig.tomlhas nomode, the first interactive start asksStart in auto by default? … [y/N]and saves your answer asmode = "auto"ormode = "manual". It never asks again.--modestill overrides it for one run, and Shift+Tab still switches within a session. - A top-level
modekey inconfig.tomlnames the mode new sessions start in (manual,plan,auto, or a[modes.<name>]table).harness evalalways 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] accentset and nomd_heading, markdown headings use the accent, bold. The dark theme’s default accent is now a pale blue,#a6d3fd(256-colour 153).--helpkeeps 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.tomlthat holds the newmodekey (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 mdno 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 -pis the same,--promptand--printare other names for it, and the root takes everyrunflag 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 -ptakes the whole prompt from stdin. In a terminal you see the whole turn live, asrun --promptshowed 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.--verboseputs the live transcript on stderr. The contract is docs/headless.md.--output-format jsonandstream-json.jsonprints one result object when the run ends (result,is_error,session_id,usagewhere Claude Code’s callers look for them, plusoutcomeand the calls that weredenied).stream-jsonprints 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
-pwith 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
✻ Thinkingheader 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;/reasoningswitches 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 mdandhtmlinclude the model’s reasoning before each reply, whatever[ui] reasoningsays. 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 --promptinto a pipe or a file prints the answer, not the transcript. A script that read the transcript from stdout now adds--verboseand 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 plainharness 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
-pcannot continue a session (resume -p), takes no slash commands, andstream-jsoncarries no live text deltas.- A stdout that fails to be written fails a
-prun 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,flagandbundleread 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.
flagpins with fingerprint scheme 2 ("fingerprint_scheme": 2in the case file): the fingerprint leaves the stored text out.checkverifies 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.bundleships 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.exportandbundlenow 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.replayrefuses a session whose model call was cut between storing its text and starting the call, as it refuses any step a crash cut short.resumecontinues such a session.run --mockprints 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
checkandbundlereport a fingerprint mismatch for a turn whose log stores pieces, and agree over an old log. Itsflag --casedrops the scheme from every version it rewrites, which breaks them for 0.11: do not re-pin a 0.11 case with 0.10. Itsexport --format jsonlof 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 (
runandresumewithout the panel, piped input, andrun --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.shadds now countfile.mutated.v1orturn.files.v1, so a run that edits withsedor 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,kindmay 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/modelor config.toml’smodel, 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).resumekeeps the log’s model, andharness evaluses--modeland config.toml only, as before. - With no key at all, the panel opens with a warning naming the variables to set;
run --promptand piped input print it and exit 1. /modelremembers 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/thinkingpicker takes the same two keys. The pick is kept in the harness’s state folder, never in config.toml; edit config.toml’smodelorthinkingafter 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’sjudge = […];[gatekeeper]andgatekeepare still read, and a file using both spellings is refused.harness gatekeeper score|fitis nowharness 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. /keysis 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/modelused to keep the session’s. - The sandbox hides its secret folders inside
HARNESS_HOMEtoo (.secrets,.ssh,.awsand the rest of its list), as it does in your home folder. - Any file inside a
.secretsfolder 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-alland inrun --mock, where nothing can ask. The behaviour is unchanged.
Fixed
/modelsearch finds a provider by name: “open” lists everyopenai:andopenrouter:model, where it used to missopenai:ones./model openaistill 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
/undorestore, a model request or a child agent runs,harness resumenow 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
/undofor 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.
- 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
- 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 replayrefuses a session that holds an abandoned step, naming it;harness exportshows the abandoned step and the recovery notice.- The refusal to resume a sandbox-off session without the sandbox names
--sandbox offas 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 exportshows 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,execandagentcall, 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.gitand never reads git to decide what to copy.- Not copied: what
.gitignoreexcludes (in a project without git, a built-in list:node_modules/,.venv/,target/and similar), your[checkpoints] skiplist, 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.gitignorethe 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.
- Not copied: what
-
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
/undothat brings it back:not in git,not checked against git(when the harness cannot tell),20+ deletedandprotected. - What the harness could not check is stated, not hidden.
- Losses are flagged, each with the
-
/undoand/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.
/undonever 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
/undocan 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|offand[checkpoints] enabled(on by default)./undostill works on pictures taken before you turned them off. -
/modelasks 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 exportand 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
/checkpointschanges, not only model changes.
Known limits
/undocovers 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-muslrelease build stopped linking at 0.4.0 (renameat2undefined), 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./modewithout 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
bashandexeccommand 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 evaland 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] writableentries, and your user temp folder except Apple’scom.apple.*folders in it. A plain command has no network at all,127.0.0.1included, so a test against a local fake server needsnetwork: true, which gives the whole network and still hides the project’s credential files. The machine’s/tmpis out of reach (/tmp/xfails; use$TMPDIR); other processes are invisible and cannot be signalled; disks, the console and your other terminals are refused, and so areopen, 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-execis missing, or when the harness runs inside another sandbox; that last notice suggestsmode = "external"when the outer sandbox is the boundary.
Changed
- On a Mac the model is told that temp files go in
$TMPDIR, that/tmpis not writable, and that a command that listens on or connects to any port, local ones included, needsnetwork. 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] hidelist 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/hookslinks totools/githooks, a command cannot createtools/. 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; forgit’sosxkeychainandghthat is expected, not tested. Tools that sandbox themselves fail inside (swift build --disable-sandboxturns 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 asnone, the temp folder as$TMPDIRand$USER_TEMPamong 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,
replayincluded, 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 --awayandharness evalneed[sandbox] mode = "off"(all but away, unconfined) ormode = "external"(all of them). [sandbox] mode = "on" | "off" | "external", and--sandbox <mode>onrun,resumeandevalfor one session. The flag beats the config, and cannot be combined witheval --target.enabled = falsestill meansoff.- 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-allrefuses 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 saysexternal, 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 yourenv_hide— except exact names you list in[sandbox] env_pass. Provider keys are always removed, and naming one inenv_passis 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 pathsnames where the config came from:default,XDG_CONFIG_HOMEorHARNESS_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.mdasks as a write toCLAUDE.md, andread_file .ENVasks 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 throughHARNESS_HOMEorXDG_CONFIG_HOMEis 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, aHARNESS_HOMEoutside the project, and anXDG_CONFIG_HOMEthat is~/.configspelled out are never refused. enabledandmodethat 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 namesmode = "off"rather thanenabled = false; withenabled = falsethe notice is unchanged. - A crash resume under
externalcontinues only whileexternalis 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 fromexternaltakes/quitand a cleanresume. Other recorded states resume as before. - Git’s system config is also read where macOS keeps it: under the active developer directory, as
xcode-selectfinds it, the Command Line Tools and Homebrew. - A log that records
externalcannot be read by a 0.5.x binary: replay or resume it with 0.6.0 or later.
Fixed
cargo build -p cli --releaseworks on macOS 27, whose Command Line Tools’stripleft 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
Enterpressed out of habit refuses; pressy(or move up) to allow. Other prompts are unchanged, and the plain REPL already treated a bareEnteras no.
[0.5.0] — 2026-09-29
Added
- A
bashorexeccommand 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, orgit 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 readsAsked 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-allskips them. They see only the files a command names: a glob, a variable,find -delete,xargs rmor a script still goes to the gatekeeper. docs/modes.md, “Local asks”, lists the forms. harness gatekeeper scoreandfitapply these asks before the gatekeeper: a row they catch counts asaskat every cutoff, andfitprints 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] addentries. 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
.envrcor 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 everybuild.rsstay writable to commands, so a sandboxedgit checkoutorgit switchcompletes. 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 fromgit diff <commit>andgit 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: truestill runs unasked.harness eval,run --mockand 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
--yesbefore 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,evalorrun --mockrecords thenetwork_onlyescape 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 --yesis gone: use--approve-all(the rename shipped in 0.4.1). A script still passing--yesnow fails at argument parsing, before anything runs.
[0.4.1] — 2026-09-28
Changed
harness run --approve-allreplacesharness run --yesand does what it did: a tool call that would ask you goes ahead instead.--yesstill works in this release, but is no longer listed in--helpand prints a one-line note; a later release removes it. Messages and docs that named--yesnow say--approve-all.
[0.4.0] — 2026-09-27
Added
- Every
bashandexeccommand runs in a sandbox, in every mode,--yesincluded. It can write only in the project and a private/tmp, cannot read your key folders (~/.ssh,~/.awsand 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: trueadds the internet and keeps the rest of the sandbox, hiding the project’s own credential files;outside_sandbox: trueruns 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/awayworks, 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)./modeshows 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 --mockandharness evalexit 2. In manual a command you approve still runs unconfined there, as in 0.3.0.[sandbox] enabled = falseruns 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.mdor.agents/entry linked outside it no longer loads, an absolute link is refused even when it points back inside, andharness export -othrough 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
--yesor--mocksession 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_rangefails. - 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;searchskips such files and says how many. rmwith recursive and force flags on anything with a.gitpath 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 checkoutof a branch with different hooks, stops half-done, and the agent is told so: which files are left and, after a checkout, thegit 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/execcall: a second model, the gatekeeper, reads the command beside your request and decides run or ask. /modeshows 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
searchtool, timeouts, cancellation, an approval gate. - Slice D — CI in front of the tree, an
exectool besidebash, 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, layeredAGENTS.md/CLAUDE.mdcontext with/init, the resume picker, the Ctrl-G external editor. - Slice J — skills:
SKILL.mddiscovery and invocation through the approval gate, plus picker row expansion, session export, composer vertical navigation. - Slice K — subagents as replayable child sessions,
/modelmid-session, the startup splash, thebuildmode renamedmanual. - Slice L — the first ADLC loop end to end:
flag,check,bundle,eval, cases as repo citizens;/modelbecomes a picker with a per-spawn child model. - Slice M — the first target that isn’t this harness:
harness importover OpenTelemetry GenAI traces,eval --targetagainst 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
scripttool 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:modelids, 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.