fix(workspace-cleanup-guard): keep quoted rm text native in composite commands - #624
Conversation
… commands A command that only mentions rm inside quotes (a search pattern, a SQL alias, a message) no longer fails closed when the command also contains shell separators. Composite commands keep failing closed when any pipeline segment starts with rm or a command forwarder, and when command substitution or an unquoted variable keeps the reference unverifiable.
|
For context on why this PR narrows the trigger rather than removing the guard, I checked how three comparable tools decide that a command is a deletion:
In all three, "cannot be statically parsed" resolves to ask or fall back, and none of them treats the presence of the word |
tt-a1i
left a comment
There was a problem hiding this comment.
Reviewed quote-aware composite-command inspection and quoted-text regressions. Executable rm and command-forwarder paths remain guarded; inert quoted search/SQL text stays native. Required CI passed; current-main integration is required before merge. Exact reviewed head: cc83006.
Problem
workspace-cleanup-guardblocks read-only commands that merely mentionrminside quotes, whenever the command also contains a shell separator.containsRmReferencematches that text anywhere in the command. When the command contains;,&,|,>or a backtick,standaloneShellTokenscannot tokenize it, andcontainsExecutableRmReferenceanswers before looking at where the text sits:Reproduced on 978a49d (0.9.0). All four commands below are blocked with
outside its supported direct rm command grammar, although none of them deletes anything:In one real workspace this produced 44 blocks over two days; 41 of them took this fail-closed path and none touched a pre-existing file.
This is the same family as #433 (a path segment ending in
rm), on a different branch of the reference detector, and it contradicts the guarantee in #201 that recognizable non-executablermtext in ordinary arguments stays native. The user-visible symptom is that no rewrite of the command can avoid the block: mentioningrmat all is enough.Value
Read-only commands such as search patterns, SQL aliases and messages stop being blocked, so the guard no longer interrupts ordinary work with a message the user cannot act on. Every decision that protects a real deletion is unchanged.
Approach
When the whole command cannot be tokenized, decide per pipeline segment instead of answering for the whole command:
pipelineSegmentssplits on unquoted\n,;,&,|.segmentRunsRmreturns true when a segment cannot be tokenized, or when its first non-assignment word isrmor a command forwarder. This keepsxargs rm,find -exec rm,bash -c '... rm ...',eval '... rm ...'andif true; then rm ...; fiblocked.containsRunnableRmReferencetreats single-quoted text and plain double-quoted text as inert, and returns true as soon as it sees command substitution or a variable inside double quotes, soecho "$(rm x)"andcmd=rm; "$cmd" xstay blocked.The change is confined to the
if (!tokens)branch. Tokenizable commands,directRmTargets, heredoc handling, the confirmation flow and theblockresult shape are untouched.Validation
bun run check→ passed (biome format, biome lint,tsc --noEmit).node --test tests/extensions/workspace-cleanup-guard/*.test.ts→ 25/25 passed. The new test covers the four commands above and asserts thatecho "rm $(printf keep.txt)"still blocks.target="keep.txt"; rm "$target",printf x | rm keep.txt,/bin/rm -rf build,echo temp/ | xargs rm,r''m keep.txt,if true; then rm keep.txt; fi,bash -c 'rm keep.txt',cat <<EOF\n$(rm keep.txt)\nEOF.bun run test(full suite) → 5 failures, all pre-existing and unrelated. They reproduce on 978a49d without this patch:tests/extensions/goal/goal-surface.integration.test.ts(1),tests/extensions/plan-mode/result-rendering.test.ts(2), and two worktree-cancellation cases intests/extensions/workflows/execute.e2e.test.tsthat time out waiting for the checkout hook on this machine.Impact
rmonly inside non-executing quotes are allowed again; every deletion path keeps its previous decision.rmtext sits in inert quoted text. A command with an unquotedrm, a command forwarder in any segment, or command substitution still fails closed.