Days 213–219. If you run --yes with a --deny list, take this release. In 0.1.19, yoyo --yes --deny "echo BLOCKED_ZZ" still ran echo BLOCKED_ZZ. The minor bump is for two new flags (--save-session, --continue-strict). The rest is mostly exit codes and stdout that tell the truth: more shell subcommands exit nonzero when they fail, and a pipe gets one answer instead of a retried attempt spliced into it.
Security
- A user
denypattern (--deny,.yoyo.toml[permissions] deny) now blocks bash on every approval path. Before,yoyo --yes --deny "echo BLOCKED_ZZ"ranecho BLOCKED_ZZ(measured:Exit code: 0). Answeringa(always) once also skipped every later deny check. And a command the safety analyzer flags (e.g.git push --force) was matched against the decorated warning text instead of the command, sodeny = ["git push --force*"]prompted instead of refusing. Deny is now checked once, on the raw command, inside the bash tool, before any prompt or auto-approval.allowand--yesbehave as before for commands no deny pattern matches. File-path deny forwrite_file/edit_fileis unchanged. - Sub-agent and explore-agent bash now follows the same rules as the parent's: the user's
permissions.deny, the hard deny list, and a refusal for anything the parent would have asked the user to confirm. Under a worktree-pinned parent, the child's bash also runs inside the pin and refuses git redirection escapes (git -C,--git-dir,--work-tree) (#977). - The hard deny list matches commands instead of substrings. A recursive delete of a build directory under
/tmp, and prose that merely mentions a pattern (a grep, an echo, a commit message), are no longer refused. Recursive deletes of the root or home directory are refused in every flag spelling (-fr,-r -f,-Rf,--no-preserve-root); before, several of those spellings got through. The safety analyzer's rm check also catches a tab afterrm, a backslash-escaped\rm, and a quoted'rm'. /cdnow applies the new directory's[permissions] denypatterns: they are added to the session's deny list and enforced on bash, in the agent and in sub-agents, for the rest of the session. The list only grows (/cd-ing back removes nothing). Deny applies to bash only; file tools never read it, and/run,!and background jobs do not check it. Everything else in the new directory's config (allow,[directories], hooks, MCP servers, skills) is still not reloaded, and/cdprints a yellow⚠ warning:(plain:warning:) naming what it skipped, never hidden by quiet mode (#869 stays open).
Added
- New
--save-session <path>flag: after a-por piped run, yoyo writes the session to<path>(the same JSON/savewrites, loadable with/load), including when the turn failed. It never creates missing directories. An unwritable path printserror: --save-session: could not write <path>: <reason>and exits non-zero, keeping the turn's own failure code if it already had one (#978). Without the flag nothing changes. - New
--continue-strictflag: like--continue, but a missing or unparsable session file exits 1 before any model call, instead of warning and silently starting over with exit 0 (#979). Plain--continueis unchanged. --output-format stream-jsonwrites{"type":"sessionRestored","messages":N}right afteragentStartwhen a session was restored (#979).
Fixed
- A pipe gets one answer. When stdout is not a terminal and a turn dies after text has already streamed, yoyo no longer retries and re-streams the partial answer (before: the same partial text up to six times). It exits nonzero with the original error and one stderr line saying why; rerun the command. A terminal, and a turn that dies before any text, retry as before (#976). The same rule now covers yoagent's own internal provider retry on the plain
-ptext path, which had spliced a dead attempt's text into stdout (PARTIAL_\n\nPONG\n, exit 0) (#989).--output-format stream-jsonis unchanged on purpose: its NDJSON closes the dead attempt in band, so the reader can tell the attempts apart. - The terminal bell no longer writes
0x07bytes onto stdout when stdout is not a terminal. The bell goes to stdout only when stdout is a tty and not reserved for the payload; otherwise to stderr when stderr is a terminal, and it is dropped when neither is (#976). - Plain
yoyo -p "..."and piped stdin without--printno longer wrap the answer in blank lines. Measured before:yoyo -p "Reply with exactly: PONG"printed\n\n\nPONG\n\n. It now printsPONG\n. Leading whitespace-only lines are dropped (the--printrule, indentation kept), the answer ends with exactly one newline, and blank lines inside the answer are kept byte-for-byte. The REPL,--printand--output-format json/stream-jsonare unchanged. - Models whose provider preset carries no price now fall back to yoyo's own price table instead of being costed at $0 in
/cost. - Shell subcommands that print a failure now exit nonzero, so a script or CI step can tell (#982 slices):
yoyo testandyoyo runreturn the child's real status (a failingcargo testgives 101;yoyo run falsegives 1; nothing ran gives 1).yoyo diff,yoyo commit,yoyo blame,yoyo changelog,yoyo evolutionandyoyo treeexit 1 outside a git repository.yoyo lintandyoyo healthexit 1 when no project is found.yoyo model <unknown>exits 2;yoyo skill show <missing>exits 1.yoyo docs <missing>exits 1 (also when docs.rs is unreachable), and it no longer prints a green ✓ for a crate or item docs.rs 404s: it decides on the HTTP status, not the page text.yoyo config get <typo>says the key is not a settable config key, suggests the nearest one (did you mean model?) and exits 2. A real key that is merely unset still prints "using default" and exits 0.- #982 stays open: about 30 other shell arms can still print a failure and exit 0.
yoyo checkpoint save,yoyo git status,yoyo pr list,yoyo stash pop,yoyo mcp listand the other shell forms of ten REPL-only commands (checkpoint,fork,bg,revisit,spawn,history,stash,pr,git,mcp) are refused for free when the second word is one of that command's own subcommands, instead of starting a billed model turn. Prose still reaches the model (yoyo git how do I rebase), and the refusal names theyoyo -p "..."hatch.plan,refactor,copyandwebare deliberately not gated because their subcommand words also start ordinary prompts (#936).
Changed
- yoagent 0.18.1 → 0.24.1 (#987, #991 step 1).
ThinkingLevelgainedXHigh/Maxupstream; yoyo still sends at mostHigh. MiniMax now uses yoagent'sapi.minimax.iodefault. Bedrock should work per yoagent 0.22, unverified here. Not in this release: wiring yoagent 0.24.1's retry-safe event filter (#991 step 2 was attempted and reverted, because on pipes it dropped the partial answer of a turn that fails for good).
Price table audit
price_drift_audit(DeepSeek rows): passed, 3 rows compared.- General sweep:
price audit: SUMMARY compared 36, matched 15, drifted 9, cache_read_only 12, unpriced 137 of 173 catalogue rows (rel_tol 1%). The drifted rows are UNRECONCILED in this release, as in 0.1.19: the table was not edited, because the skill requires reading each vendor's pricing page first and that did not happen in this session. models.dev now listsdeepseek-v4-proat 0.66/1.98 per MTok against yoyo's 0.27/1.1./costfigures for the drifted models may be wrong. The 12cache_read_onlyrows are the known unmodelled-cache gap.