Repository navigation
v7.200.5
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. 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.
Fix: sentinel receipt conflicts report their real reason, not store_unavailable
What
Filing a second knowledge_receipt with trace_id: "none" (the no-retrieval
sentinel) in the same session used to be rejected as store_unavailable —
the retry-inviting infrastructure category — even though the rejection is a
permanent, deterministic identity conflict: within one session the first
"none" receipt fixes the recorded task_id/phase (and grace window), and
every later filing that disagrees conflicts with the immutable record.
The conflict is now reported with the precise, actionable reason from the
existing receipt taxonomy:
wrong_task— the filedtask_iddiffers from (or is omitted against, in
either direction) the recorded one;wrong_phase— the filedphasediffers from or is omitted against the
recorded one;duplicate_conflicting_terminal— the recorded grace window no longer
matches. Task and phase agree in this case; the trigger is a change to the
knowledge.receipt_close_grace_dayssetting between the two filings, so
re-filing cannot resolve it — an operator restores the original setting.
The rejection detail keeps the conflicting immutable empty retrieval for …
prefix and names the conflicting axis with the recorded and filed values.
Every interpolated value is sanitized through the same single-line diagnostic
sanitizer used elsewhere in the plugin (control and bidirectional characters
become ?), truncated on code-point boundaries with a visible … marker
when over the per-field budget, and the axis sentence is budgeted to survive
in full. The knowledge_receipt tool surfaces the same code as reason, and
its description now distinguishes the two cases: wrong_task/wrong_phase
are fixed by re-filing with the originally recorded task_id/phase
(idempotent), while duplicate_conflicting_terminal is not fixable by
re-filing at all.
Genuine store failures keep their existing codes unchanged — lock exhaustion
still returns lock_timeout, journal corruption store_corrupt, and
unclassified I/O failures store_unavailable — so the permanent-conflict
class and the store class stay distinguishable by code, and only the store
class invites retry. The phase-complete gate's receipt-failure classifier
recognizes the three conflict codes as a permanent RECEIPT_CONFLICT bucket
— checked ahead of the detail-string heuristics for both the ledger result
shape and the validator's rejection shape. Its recovery points at the
knowledge_receipt tool for the wrong_task/wrong_phase arms (re-file
with the recorded identity); the duplicate_conflicting_terminal grace arm
is named as operator-actionable, since re-filing cannot resolve it.
Why
The misfiled category violated the plugin's failure-channel separation
(AGENTS.md invariant 9): agents reading store_unavailable legitimately treat
the failure as a transient store condition and retry, but the task/phase
conflicts are permanent for the session horizon. The precise codes already
existed in the receipt taxonomy but were never produced for this class, and no
test covered the conflict at all.
Migration
No breaking changes:
- The immutability rule itself is unchanged — same-arguments retries remain
idempotent with identical event ids as long as the resolved grace window is
unchanged. - The rejection JSON's
reasonvalue on this path changes from
store_unavailableto one of the three precise codes (the output shape is
unchanged; the codes already existed in the receipt taxonomy). - No schema, journal-format, keying, or grace-policy change.