Skip to content

v5.17.0

Choose a tag to compare

@Kanevry Kanevry released this 01 Nov 21:58
· 2 commits to main since this release

Added

  • Private vulnerability reports in the security pulse (#1902): the detached refresher checks up to three GitHub fetch remotes in parallel after OSV, paginates all triage reports with timestamp-only output, and caches counts and timestamps. Readable remotes remain visible when another remote fails; the cache and banner reasons record the unreadable count. Each remote has a 10-second timeout. Session-start reports open submissions and warns about lost access only after a previous successful measurement.

  • Error pages in the nextjs-minimal archetype (#1885): both generators (templates/nextjs-minimal/ and the inline steps in skills/bootstrap/standard-template.md) now create app/error.tsx with retry(), app/global-error.tsx with its own <html>/<body>, and app/not-found.tsx, per projects-baseline frontend.md § Error Pages. No pages/500.tsx. retry needs Next ≥ 16.3, so the archetype moves to Next ^16.4.2. In templates/nextjs-minimal, next lint becomes eslint . with the native flat config of eslint-config-next 16.

  • German command reference /de/commands (#1766): the same 31 cards as /commands, with an EN/DE switch on /commands, /de/commands and /guide, hreflang, sitemap and SITEMAP_PAGES. /guide has no German twin; its DE link goes to /de and says so. The German home page now links the German reference. The site tests hold both pages to the same roster, order, hreflang and language-switch rules.

  • Tool switcher on /commands (#1776): Claude Code, Codex, Cursor or Pi. Each card then shows only that tool's form, and command mentions elsewhere on the page follow the choice (the Codex form is read from each command's own card). Without JavaScript all forms stay visible. The choice is kept in localStorage (so-tool) and can be set with ?tool=. The /plan card gains a worked example and the PRD path.

  • A Full Gate receipt can stand in for a repeated run (#1897): the Quality wave and session-end both ran the Full Gate, mostly on the same tree. run-quality-gate.mjs --variant full-gate --receipt keys a receipt on the content tree (tracked plus untracked, non-ignored files) and the gate definition (the three commands, Node and plugin version). A green run writes one only when the tree did not change while it ran. A receipt younger than 12 h is published instead of running the suite again, marked reused_from in the envelope and in the orchestrator.quality_gate.passed event. The flag is opt-in: the Quality wave and session-end pass it, pre-push, CI and other callers are unchanged. No receipt is given below the repository top level, for index entries marked assume-unchanged or skip-worktree, in a sparse checkout, or with an inherited GIT_DIR/GIT_WORK_TREE. Receipts live in <git common dir>/so-gate-receipts.jsonl, shared by all worktrees.

Changed

  • The destructive guard reads words the shell still expands, and zsh's precommand modifiers (#1892). Before, each of these let the whole command through, rule included:

    • An option word with an expansion counts as a possible -r and -f and passes no path exception, because it may split off a path (rm -rf${x} /data, rm "-rf$x" /data, rm -rf${x} node_modules).
    • An rm word that reads rm once empty expansions drop counts as rm (r${x}m, rm$(true), rm${${x:-}:+}).
    • builtin, noglob, nocorrect and zsh's - are unwrapped like command, so builtin eval "…", - bash -c '…' and noglob git clean -fdx are judged by their payload or subcommand.
    • Named limit: a bare variable (rm -f "$T") could carry -rf too, but 437 real rm commands in 7 days carry one, so it stays a plain operand. So does an expansion that is not empty ($R holding rm).
  • The destructive guard reads an empty expansion inside a word (#1893): with x unset, bash runs git res${x}et --hard as git reset --hard, and every git rule (reset, push --force, clean -f) allowed it. Each command is now also judged as it reads with empty expansions: $name, ${…}, $((…)), $( … ) and backticks read as empty, ${x:-w} as w, and a match in either reading counts. An unknown ref before -- stays unjudged as before (git checkout $REF -- f). A default word nested deeper than 8 levels counts as a match (fail closed). Measured on 145,960 real Bash commands from 7 days against 13 rules: 0 verdict changes, mean cost per command 0.36 → 0.50 ms.

  • $"…" strings and $"…"/$'…' here-doc delimiters are read as bash runs them (#1895): bash without a message catalog drops the $ of $"…", so git reset $"--hard" and git push $"--force" ran while the guard read $--hard and allowed them. The pattern rules now take the bash reading; path resolution keeps zsh's, so > $"PWD"/f stays unresolved. A here-doc delimited by <<$"EOF" or <<$'E\x4fF' (escapes decoded) now ends where bash ends it, so the lines after it are judged as commands instead of body text.

  • so-hud reads a data command's quoted pattern as text (#1891): grep -E "denied|git push|deny" … asked for a push, because the | inside the quoted pattern split off git push as a command of its own. A segment cut out of a quoted word of a data command (grep, rg, echo, …) now counts as text, unless eval parses the word again or the output is piped into a command that runs it (| sh).

  • Project cleanup exceptions cannot traverse escaping symlinks (#1886). rm -rf now checks the real filesystem location under node_modules, .orchestrator/tmp and the state-directory filescopes, including a trailing / or /..

    • Removing a bare symlink stays allowed. A resolution error blocks the exception.
    • A target the shell still resolves no longer passes any exception, /tmp included. That covers a .. segment (rm follows it through a link), $, a backtick, a zsh (, and a glob or brace in a directory segment (node_modules/*/).
    • A glob in the final segment (node_modules/*) stays allowed.
    • A relative target is judged where the shell runs it: from the hook payload's cwd and every literal cd on the line. The cd target is added as a candidate rather than replacing the old directory, because a failed cd leaves the shell in place. A cd with .., a subshell, a substitution, pushd/popd, eval or source make the directory unknown.
    • A cd behind builtin or a zsh precommand modifier (noglob, nocorrect, -) counts, and so does zsh chdir. A bare cd or cd ~ is unknown when the line may set HOME. A relative cd is unknown when the line or the environment may set CDPATH.
    • $TMPDIR is expanded only in the first command of the line, only when TMPDIR is set, and only when no other word of that command expands something: zsh expands words left to right, so rm -rf -v${${TMPDIR::=/usr}:+} $TMPDIR/lib deleted /usr/lib. There is no os.tmpdir() fallback any more. A trailing / on a /tmp target follows the link, as in the project branch.
    • Replay of 801 real recursive-rm commands from 7 days: 797 verdicts unchanged. Of the 4 new denials, 3 cost a real command: twice $TMPDIR after another command, once cd "$P". The fourth was a review probe. The third review's fixes changed none of 785 replayed verdicts.
  • rm -rf "$D" on an mktemp variable stays blocked, now with a hint (#1883): the deny reason says why and what works instead (skip the cleanup, since the OS clears $TMPDIR, or remove the literal temp path). A text-level grant was built and rejected: on the 94 real denials it proved only 1 cleanup, and an adversarial review found 6 bypasses (rebinding through zsh builtins, $E holding eval, a PATH-hijacked mktemp, macOS mktemp -t printing two paths).

  • Templates-first respects Session Config (#1882): under the strict hook profile, enabled: false bypasses the guard and hosts limits enforcement. Config is read only for matched create calls; read/parse failures warn and keep enforcing.

  • Hooks block less by default; the hook profile is a host setting (#1881). Over 7 days on the maintainer's host the hooks denied 792 of 185,316 tool calls, most of them false alarms.

    • New default profile standard: pre-bash-templates-first and pre-tool-state-md-path-guard are off, and worktree-remove-unsafe warns when it cannot measure the target (110 of its 136 denials). A measured hit and --force still block. strict (the old full, which stays an alias) keeps everything as before.
    • owner.yaml takes hooks.profile (strict, full, standard) and hooks.disabled, so the profile no longer has to be exported per process. owner.yaml can relax but never disarm: minimal, off and disabling a safety hook stay env-only, because an agent can append to the file. SO_HOOK_PROFILE / SO_DISABLED_HOOKS still win. An unknown SO_HOOK_PROFILE is now ignored instead of meaning full.
    • When one command removes several worktrees, a measured hit now wins over an earlier target the guard could not measure. A removal behind an unknown directory (bash -c '…', $(…), git -C $X) is measured from an absolute target itself or from the payload cwd, so wrapping a denied removal no longer turns the deny into a warning. Only a call whose every word is literal and that names one target may warn: a --force hidden in $F, $(…), a glob or past the payload depth cap still blocks.