Repository navigation
v7.200.10
Preserve interrupted coder settlement work
Coder settlement recovery now retains surviving recorded source commits even
when a lane checkout and branch are missing. Merge recovery can resume on a
compatible advanced primary branch while retaining conflict, dirty-overlap and
source-identity checks. Pending settlements protect their artifacts from
cleanup through shared dispatch/deletion admission. That protection covers
dispatch and lane cleanup performed by the plugin; it does not cover an
explicit /swarm close, which archives the session and then removes the
.swarm/coder-settlements/ directory.
Mixed foreground/background completion now hands off settlement ownership
before removing the lane. Interrupted handoff and leftover cleanup can replay
from saved provenance without merging twice; changed lanes remain preserved
for manual recovery.
Recovery also detects the specific historical missing-worktree self-heal that
may already have marked a settlement aborted. It reports the exact recoverable
commit and retained backup ref without reviving an old task generation. That
RECOVERABLE_ORPHAN_COMMIT report is advisory: task status, re-dispatch and
close are not blocked by it. Committed recovery releases its backup; unstaged
work and historical work that has not landed by ancestry remain retained for
explicit operator review. See
coder settlement recovery for discovery
and safe cleanup commands.
No settlement schema or configuration migration is required.
Fix: macOS sandbox profile embeds kernel-resolved system paths
What changed
The macOS sandbox-exec profile builder now writes the kernel-resolved form of a path when it starts with one of the three macOS system links: /tmp, /var and /etc become /private/tmp, /private/var and /private/etc. The rewrite applies to scope paths and to the temp dir, in both the production and the probe profile, and only when this host's realpath confirms the link.
The rewrite is a prefix swap on the input string. No other symlink is ever followed, so a link created inside a project can neither change the result nor widen the profile to its target. A path that is already resolved is used as it is. A path under any other symlink (for example ~/projects -> /Volumes/data), a host without the link, or an error leaves the path exactly as written (the previous behaviour): that path stays unmatched by Seatbelt and writes to it are denied.
Seatbelt matches subpath against the kernel-resolved path. Before this change a scoped (allow file-write* (subpath "/var/…")) line never matched, so the allow was inert while the profile looked correct. Verified with raw sandbox-exec: the /tmp/… form exits 1 (Operation not permitted), the /private/tmp/… form exits 0.
The wrapped command now passes the profile inline (sandbox-exec -p '<profile>' bash -c …) instead of staging it in a file under the temp dir. The temp dir is inside the write grant (see below), so a staged profile there could be rewritten by a still-running sandboxed process before a later call's sandbox-exec read it; with no file there is nothing to race, and no per-call profile files accumulate in the temp dir any more.
Behaviour changes you may notice (macOS, guardrails.sandbox_macos_enabled: true)
- The temp-dir write grant is now effective. Every wrapped command has always been given write access to
os.tmpdir(). On macOS that is the per-user$TMPDIR(/var/folders/<xx>/<yyyy>/T), which sits under the/varlink, so the grant never matched and temp writes were denied. It now matches: wrapped commands can write anywhere under$TMPDIR, including files other processes of the same user keep there. The grant is a whole-temp-tree allowance, not one scratch directory per command, and it is wider than the per-run temp of the Linux executor (--tmpfs) and of the Windows native runner (a per-runtemp_root); the Windows PowerShell wrapper has no OS-enforced write containment. - The capability probe reports
filesystem: real. The probe's own in-scope write used to be denied, so it reportedweakon macOS. It now reportsreal. guardrails.sandbox.mode: requiredcan now enforce. Withguardrails.sandbox.require_filesystem: truethe probe'sweakmade required mode block every shell command on macOS. Withrealit can proceed, but only whenguardrails.sandbox.network_mode: onis also set andguardrails.sandbox.network_allowlistis empty: macOS reportsnetwork_mode: offas unsupported, so the defaultoffis still blocked.guardrails.sandbox.require_networkandguardrails.sandbox.require_processstay blocked because macOS reports no real containment for them. Advisory mode andrequire_filesystem: falseare unchanged.- Scope paths. Paths that exist are already resolved by the scope resolver, and a not-yet-existing path under a symlinked root is rejected as a traversal. In normal tool use the rewrite therefore mostly affects the temp dir and the probe; it matters directly for callers that build a profile from a
/tmp,/varor/etcpath.
Why
Closes #3462. Sibling of #2590 / PR #2630 (invalid setenv in the same profile).
Known caveats
- The sandbox stays opt-in (
sandbox_macos_enableddefaults tofalse). - Windows and Linux are unaffected.
- Other symlinked roots are not rewritten by the profile builder; a path under one stays unresolved and Seatbelt does not match it (writes denied).
Fix: config-doctor no longer reports top-level agents as deprecated
What changed
config-doctor warned The "agents" field is deprecated. Use "swarms" instead and proposed removing the key. The key is not deprecated: it is documented as the per-agent override for the default swarm (docs/configuration.md), it is merged into every swarm at runtime (src/agents/index.ts), and the v7.11.0 notes name agents.<role>.model as where to set a model. Following the suggestion would delete working configuration.
An object-valued agents now produces no finding. A non-object value (for example a string, number or array) is still rejected: config loading discards it and config-doctor reports invalid-config-value (error). The invalid-agents-type check in config-doctor is a defense-in-depth guard that keeps the key's validation case, which the schema-introspection test requires for every top-level key.
Separately, when config-doctor validates a config that has not been through the loader (direct runConfigDoctor callers and the re-run after --fix, whose result is written to the doctor artifact), an array given to an object-typed key such as agents, swarms, pipeline or guardrails now reads got array instead of the contradictory got object. In a config file such a value is dropped by the loader and reported as invalid-config-value, as before.
Why
Closes #3463.
PR-review verdict-row pipeline integrity
What changed
- Quarantine escape closed (#3357, completing #3275 RC2): a verdict row that ARRIVED over its marker's legal arity was tail-merged down to exactly the legal count, so the #3310 arity-keyed quarantine never saw it — the Stage-B/echoed-format row entered the exact-set count with an unassigned id and voided every valid sibling row. Rows are now quarantined on their RAW arrival arity when they required tail-merge AND name an unassigned id (
rawFieldCountis surfaced by the parser;indexVerdictRowstakes the assigned-id set). The #3310 under-arity arm also feeds the same quarantine surface. The spoofing floor is unchanged and exact: a row that arrived at EXACTLY the legal arity naming an unassigned id still voids the lane — that is the forged-row class; over-arrival is by construction non-format-conformant and cannot authenticate. - Quarantine is now visible on the durable receipt (#3357 AC2): a verdict lane whose artifact contained quarantined strays settles with
legacy-verdict-row-quarantinerecovery entries (bounded id + raw arrival arity in the reason; itemization capped at 16 with an+N moreoverflow note on the last entry) on the same durable surface aslegacy-verdict-row-recovery, in BOTH the TypeScript union and the persistedWorkflowLaneRecoverySchema. Previously a quarantine-influenced settle produced a receipt byte-identical to a clean settle. - Host-aborted verdict lanes are never typed (#3397 RC-03): a verdict lane the host aborted after producing partial text previously flowed to validation failure and was stamped
workflowLaneFailureClass: 'verdict_contract', making a host cancellation admissible typed-dead evidence once the retry budget expired — contradicting the shipped "aborted terminals never typed" contract. Both stamp sites (the ordinary!okleg and the transport-validation throw catch) now skip the stamp whenterminalError.kind === 'aborted'. Such lanes settle as classlesserrorterminals and BLOCK typed-dead admission by design (classless owners are rejected before the budget arm inverdict-settlement.ts), so the budget arm cannot admit them. Two operator escapes exist: a re-dispatched lane resolves the item on success, and the disclosed-INCOMPLETE completion path names the blocked item (also stated in the settlement BLOCKED remedy text). - Embedded credentials are redacted (#3382): the fixed-length credential patterns (bare AWS access key id, Google API key, the labeled AWS-secret value arm, the context arm's AKIA anchor) and the token prefixes (GitHub
ghp_/gho_/ghu_/ghs_/ghr_/github_pat_, GitLabglpat-/glptt-, Slackxox[abprs]-) match credentials embedded inside longer charset-clean tokens. The github short-prefix bodies are tightened to underscore-free alnum runs (real classic tokens are base62), which keeps benign snake_case identifiers such asload_ghs_metadata_from_cache_storeunredacted while embedded real tokens still match;github_pat_keeps its underscore-tolerant body (fine-grained tokens contain one). Anchors are deliberately KEPT where the short alnum-only prefix collides with benign text (OpenAIsk-matches inside "task-"/"disk-", Stripesk_/rk_inside "risk_live_…", JWTeyJinside base64 blobs). The whole-token bearer arm now runs FIRST, so a credential embedded inside a bearer token is redacted as one span instead of being split. Detected embedded shapes are redacted instead of passing byte-identical throughredactSecrets/containsSecretinto verdict-row diagnostics, telemetry, receipts, and analysis output (the verdict-row id sentinel path yields(redacted)). - Sequential-replacement masking fixed (pre-existing, found by this work):
redactSecretsapplies patterns sequentially, and the bare AWS access-key arm ran BEFORE the AWS-secret context arm, replacing theAKIA…anchor and leaving the 40-char context secret un-redacted even in the exact-40 shape (the existing test asserted detection only, not redaction). The AWS-secret arms now run first; the family count is unchanged.
Why
A single Stage-B grammar row with pipes in its summary could void an entire reviewer lane (the #3275 deadlock shape), a host abort could launder into typed-dead settlement evidence, and an embedded credential could ride a verdict-row id past redaction into operator-visible surfaces. All four are the same defect class: a decision keyed on a weaker discriminator than the grammar requires.
Migration / operator actions
REDACTION_POLICY_SALT2→3 andFINGERPRINT_ALGORITHM_VERSION2→3: detection semantics changed for unchanged config, so memory cohort members must re-run/swarm memory linkafter upgrading (legacy cohort files surface as a re-link instruction, not a config-mismatch error).- Memory durable-secret rejection is tighter for embedded credential shapes: content embedding fixed-length credential shapes or real token shapes is now rejected/redacted where it previously passed.
- Downgrade: do not roll back to a pre-upgrade build after lanes have settled with
legacy-verdict-row-quarantinerecovery entries — the older strict schema does not know that kind and a pre-upgrade reader nulls the whole coordination read. Drain running PR-review workflows (or complete them) before downgrading.
Residual disclosures
- Over-redaction trade-offs introduced by the wider matching, accepted deliberately: the now-live
aws_secret_access_key_contextarm redacts the ENTIRE span (AKIA id, up to 200 chars of interposed text, and the 40-char value), so non-secret prose between an access-key id and a 40-char token on one line is removed from redacted output (at base that shape leaked the actual secret value instead); an=-labeled AWS secret whose value is LONGER than 40 chars has its first 40 chars redacted and the tail survives (real AWS secret keys are exactly 40 chars); quarantined rows no longer emitverdict_row_pipe_recoverytelemetry (the row is diverted before the emit; the receipt entry carries the same fact). - #3397 item 2 residuals (the fix-or-disclose set): ghost-owner BLOCKED wording can overstate owner intent for never-launched lanes (RC-01);
output_length/transcript_incompleteprovider terminals are typedresourceas an evidence grade, not an exact cause (RC-04/14/40); an internal fault inside validation is typedverdict_contractbecause the lane's contract check did fail — with this change an ABORTED VERDICT lane is never typed, which is the authenticity boundary (RC-41); pre-widening records parse as classless by intentional legacy tolerance (RC-19/36); coherence entries can outlive an interrupted settlement pass, cosmetic only (RC-09); the BLOCKED attempts sentence and theverdictAttemptsprojection render uncapped per-item lists (RC-11/12/22); the council-dispatch behavior change from #3388 is hereby disclosed (RC-13); stale retry-advice wording was removed by #3388 and any reappearance is a new defect (RC-16); latent schema-versioning gaps remain a tracked design debt for future persisted kinds (RC-15/17); test-quality gaps around exhaustion boundaries and projection pins remain (RC-25/26/28), and the five dispatch-lanes suites' temp-cleanup debt is untouched (RC-38). RC-20 (verdictAttemptsdocumented indocs/architecture.md) is fixed in this PR.operator_cancelledexclusion remains a recorded product decision awaiting maintainer confirmation.
Known limitation
Quarantine evidence in analysis.actual still renders arities only (quarantinedArities) on the FAILURE path; the success path now carries the capped, itemized entries on the receipt surface.
Shell write-detector: launcher prefixes, quoted payloads, canonical spellings, and cd/pipe/Windows evasions closed
What
The shell write-scope gate classifies a dozen previously-admitted evasion
shapes identically to their canonical forms (#3474, #3475). Every change is
additive detection (fail-closed); the only newly-admitted shapes are the
correctly-tracked ones below.
- Launcher prefixes no longer hide builtin writes:
command rmdir ../x,
builtin rmdir ../x,exec rmdir ../x, andtimeout 5 rmdir ../xare
detected and scope-checked like their unprefixed spellings, and
command rm -rf ../xnow reaches the destructive-command layer's
recursive-delete check like its unprefixed spelling.command -v/
-Vare query forms and stay admitted. - Single-quoted
sh -c 'rmdir ../x'/bash -c '...'payloads unwrap like
double-quoted ones, including payloads that contain the other quote kind. - Versioned and path-qualified spellings of the write-vector sets classify
canonically:/usr/bin/curl -o,curl8.5 -o,/usr/bin/sed -i,
/usr/bin/tar -xzf,bzip2.5 -d,/usr/bin/cp(three-tier version
lookup — canonicalbzip2stays itself). cd a && cd b && …chains track everycd(;-chains already did): the
final tracked directory resolves redirect targets AND eval working
directories, socd src && cd .. && echo x > escape.txtand
cd src && cd .. && python -c …now block for a src/-scoped coder.
||chains keep their previous (conservative) tracking.- Windows-grammar interpreter evals:
C:\Python311\python.exe -c "…"(and
the forward-slash spelling) classify asinterpreter_evalunder both cmd
and PowerShell grammars, with the raw-word operator so a spelling both
grammars see keeps its tracked context. A backslash-only spelling
authorizes against the command's starting directory (the workspace root —
outside every proper declared scope). - Inline-literal pipe sources feeding a bare (or
--flag) interpreter
classify as eval:echo 'open("../x","w")' | pythonand
printf '…' | python -block for scoped coders. Dynamic producers
(grep … | python) and every script-path form (cat f.py | python,
python f.py,python -m …,python - < f.py) keep the #1901/#1902
carve-out (maintainer ruling on #3474). - Mid-word
#operands no longer truncate:cp src/x.txt src/x.txt#../../../zzresolves the FULL operand (bash-parser's
quote-removal stage used to stop the word at the#), so the in-scope
prefix can no longer mask an out-of-scope target. Word-start#comments
are unaffected. - A leading-
=rmdiroperand loses the bare-rmdirexemption on every
host (zsh=-expansion resolves the word to a PATH location the static
containment check never saw; decision on #3475).
Why
Every one of these shapes was an execution-verified fail-open of the write
guard: a coder scoped to src/ could write outside the scope (and sometimes
outside the workspace) by spelling the write differently. The detector's
own residuals — read-only classification, the #1901 script-path carve-out,
and the bare-rmdir exemption's plain-line contract are unchanged
(command -v queries, timeout 5 make test, cat script.py | python,
in-workspace plain rmdir, and the destructive-command layer's original
analysis are all pinned by new suites). The exemption itself narrowed in
one disclosed way: a leading-= operand loses it (above). Repeated
Windows-grammar interpreter evals are now each retained with an occurrence
suffix instead of collapsing into one row (matching the POSIX behavior).
Migration
None. touch and mkdir remain undetected write commands (parity with the
pre-fix behavior for both their plain and prefixed spellings); making them
write-vector commands would change the bare-rmdir exemption's documented
mkdir x && rmdir x example and is out of scope.
Coders can remove an empty directory outside their declared scope
What
The shell write-scope gate no longer blocks a bare rmdir <dir> just because
the directory is outside the coder's declared scope, as long as the directory
is inside the workspace.
- Admitted:
rmdir <dir>with no options and exactly one literal, unquoted
operand whose location, with every symlink or junction in its parent chain
resolved, is strictly inside the session workspace, when the command runs
under a POSIX shell (thebashtool, or theshelltool on a non-Windows
host). The target must not exist yet or must be a real directory. - The exemption applies only to a plain command line, judged on the
command text after the gate's existingeval/sh -cunwrapping.
Launcher prefixes (command,builtin,exec,timeout DURATION,stdbuf) are unwrapped before that judgement, and a single-quotedsh -c/bash -cpayload unwraps like a double-quoted one, socommand rmdir ../outsideandsh -c 'rmdir ../outside'get the ordinary declared-scope check (#3474/#3475). An operand beginning with=also loses the exemption (zsh=-expansion makes the static literal unsound there). That
text may hold only letters, digits, spaces, newlines and the characters
_ . / - = & | ; < >, and every command in it must be one ofrmdir,
mkdir,echo,true,falseortest. None of these commands can
change the shell's working directory, and without$, quotes,
backslashes, backticks, parentheses, braces, brackets or#nothing in
that text can expand, be evaluated, be quoted, define a function or start
a comment, sormdirruns in the directory the gate checks and removes
exactly the operand the gate checks.&&/||/;lists, pipelines,
redirections such as2>/dev/null,FOO=1 rmdir xprefixes andifand
loop bodies are fine:
rmdir .scratch-a/tmp1,rmdir dir/,rmdir ./dir,rmdir a && rmdir b,
mkdir x && rmdir x,rmdir a 2>/dev/null || true,
rmdir a && echo doneandFOO=1 rmdir akeep the exemption, and each
rmdirof a distinct operand is judged separately. - Every other line gives every
rmdirin it the ordinary declared-scope
check. That includes any text with a character outside the set above:
rmdir "dir",rmdir a && echo "done",echo $HOME; rmdir a,
[ -d src ] && rmdir a, subshells and brace groups ((rmdir a),
{ rmdir a; }),case,$(...), backticks, any#(rmdir d#/x
removes the one directoryd#/xin bash, andrmdir a # noteloses the
exemption too), and paths containing~ + , : @ %. An interior tab is
refused outright as unsafe command text, and an interior carriage return
is never exempt either. It also includes any other
command in the line:cd,pushd,popd, function
definitions,sourceand.,trap,alias,
an upper-caseRMDIR, and every other command
(ls d && rmdir d,cd build && rmdir tmp). A line with a
here-document normally loses the exemption too, because the gate reads
the body and delimiter lines as commands that must also be on the list
(a body on a plain line cannot expand anything). This closes code that
bash evaluates at run time, such as the bash 5.3 current-shell
substitutions${ cd ..; }and${| cd ..; }, including a quoted one
that arithmetic ($((x))) or atest -v/[ -vsubscript evaluates. - Unchanged (still blocked as before):
rmdir -p, any other option
(--ignore-fail-on-non-empty,--), several operands, globs, brace
expansion, variables or command substitution in the operand, targets
outside the workspace, symlink escapes, the workspace root itself, every
recursive form (rm -rf,rmdir /s,rd /s), a target that exists as a
file or symlink, and anyrmdirrun by a Windows executor (a
powershell -Command/pwsh -c/cmd /cwrapper, a PowerShell-shaped
command, or theshelltool on Windows). In PowerShellrmdiris an
alias ofRemove-Item, which also deletes files. - Never exempt (the ordinary declared-scope check applies):
- a sequence that names the same operand more than once, for example
rmdir a; cd ../other && rmdir aorrmdir a; rmdir -p a, because the
gate cannot tell those writes apart; - an operand with a
..path segment (link/../x), even on a plain
line; - repository control state: a target under the workspace's
.gitor
.swarmdirectory; the exemption never applies there for any role.
- a sequence that names the same operand more than once, for example
- File-authority checks still apply to the target and are unchanged; only
the declared-scope membership check is skipped. Default-protected prefixes such as.github/workflows,
docs/releasesandnode_modulesare not separately protected for a bare
rmdir(issue #3450 scopes the exemption to the whole workspace), so a
coder whose declared scope excludes them can now remove an empty
directory there. The target's parent directory must already exist when
the gate runs:mkdir x && rmdir xkeeps the exemption (the parent is the
workspace root), butmkdir -p d/e && rmdir d/ein one command still gets
the ordinary scope check becauseddoes not exist yet. /swarm guardrail explainhas no tool or platform flag and evaluates the
command as theshelltool, so on a Windows host it conservatively reports
blockfor a barermdirthat the livebashtool admits. The
scope_validateMCP advisory does not apply the exemption at all and also
stays conservative.
Why
rmdir(2) refuses to remove a non-empty directory (ENOTEMPTY), so a bare
rmdir cannot destroy file data. Blocking it protected nothing, but it left
agent-created empty scratch directories behind and forced a human to clean
them up. The gate does not check whether the directory is empty first: that
check would race with the command (TOCTOU). The kernel's refusal is the
authoritative check.
Migration
None.
Merge-queue failure diagnostics: an orphaned-entry detector and a self-hosted
stale-runner classifier.
scripts/check-merge-queue-progress.mjs --pr <n> names a stuck merge-queue
entry in one line (<VERDICT> pr=<n> age=<m> state=<state>): ORPHANED
(accepted past the 150-minute default threshold with zero active queue
builds and an OPEN PR — recovery line printed, exit 2; any other PR state
suppresses the recovery), BUILDING (any non-terminal queue build counts —
queued runs are active under runner contention), IDLE, or UNKNOWN (a
removed entry cross-checked against PR state, or a run window that cannot
reach the entry event). The detection authority is the last
queue|auto_merge timeline event plus a 100-run merge-group window on the
gh-readonly-queue/main/pr-<n>- prefix — never elapsed time alone, which
would force-cycle a healthy in-flight build.
scripts/ci/merge-group-stale-runner-classifier.ts --run <id> classifies a
failed merge-group batch from per-job step evidence (exit 0 for every
verdict, 1 for usage/gh errors): routed jobs that died at actions/checkout
with hosted legs green (modulo the unit-passed cascade) read
runner-stale-clone — the 2026-10-27 stale-runner signature that cost six
re-enqueue cycles — while any work-phase failure (Biome, a test, a coverage
threshold) reads test-failure and outranks a coexisting checkout death,
never excused by missing logs; unattributable failures read unknown.
Malformed job payloads degrade to unknown instead of crashing, and echoed
job names are control-character-sanitized.
The ci-fix-monitor protocol gains the orphaned-entry and batch-classification
steps under "Push and monitor".