Skip to content

fix(workspace-cleanup-guard): keep quoted rm text native in composite commands - #624

Merged
tt-a1i merged 2 commits into
openpi-dev:mainfrom
cuipengcx90:fix/guard-quoted-rm-text
Sep 26, 2026
Merged

tt-a1i merged 2 commits into
openpi-dev:mainfrom
cuipengcx90:fix/guard-quoted-rm-text

Conversation

@cuipengcx90

Copy link
Copy Markdown
Contributor

Problem

workspace-cleanup-guard blocks read-only commands that merely mention rm inside quotes, whenever the command also contains a shell separator.

containsRmReference matches that text anywhere in the command. When the command contains ;, &, |, > or a backtick, standaloneShellTokens cannot tokenize it, and containsExecutableRmReference answers before looking at where the text sits:

const tokens = standaloneShellTokens(source);
if (!tokens) return true;

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:

rg -n --fixed-strings 'rm -rf' . | head -20
echo "rm permission required" | cat
mysql -e "SELECT 1 FROM t_role_menu rm ON 1=1" ; echo done
rg -n --fixed-strings 'rm -rf' -g '!*.map' . | head -60

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-executable rm text in ordinary arguments stays native. The user-visible symptom is that no rewrite of the command can avoid the block: mentioning rm at 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:

  1. pipelineSegments splits on unquoted \n, ;, &, |.
  2. segmentRunsRm returns true when a segment cannot be tokenized, or when its first non-assignment word is rm or a command forwarder. This keeps xargs rm, find -exec rm, bash -c '... rm ...', eval '... rm ...' and if true; then rm ...; fi blocked.
  3. containsRunnableRmReference treats 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, so echo "$(rm x)" and cmd=rm; "$cmd" x stay blocked.

The change is confined to the if (!tokens) branch. Tokenizable commands, directRmTargets, heredoc handling, the confirmation flow and the block result 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 that echo "rm $(printf keep.txt)" still blocks.
  • Guard cases that must keep blocking still do: 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 in tests/extensions/workflows/execute.e2e.test.ts that time out waiting for the checkout hook on this machine.

Impact

  • User-visible behavior: commands that mention rm only inside non-executing quotes are allowed again; every deletion path keeps its previous decision.
  • Model-visible context/tools: none.
  • Runtime/lifecycle: none.
  • Persisted config/data: none.
  • Compatibility/risk: the allowed set grows only for commands whose rm text sits in inert quoted text. A command with an unquoted rm, a command forwarder in any segment, or command substitution still fails closed.

… 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.
@cuipengcx90

Copy link
Copy Markdown
Contributor Author

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:

  • Codex identifies deletion from the parsed argv: argv[0]'s basename plus whether the rm arguments include a force flag (/bin/rm matches, rm -r does not). It explicitly does not flag rm -rf text when it sits inside a quoted string (bash -lc "echo 'rm -rf /tmp/x'") or when it is assembled from a variable (cmd=rm; $cmd -rf ... -> no match), and it keeps regression tests for exactly those cases. (is_dangerous_command.rs)
  • Claude Code splits a command into sub-commands, keeps a built-in read-only set, and when a command cannot be parsed it asks the user instead of refusing it. Its rm handling is a path analysis of the deletion target, and it runs only after a recursive delete has been identified. (permission modes)
  • OMP (Oh My Pi) ships a narrow list of critical shell shapes (rm -rf /..., --no-preserve-root, sudo rm) and otherwise defers to user-configured command rules; an unparseable command falls back to the previous matching path rather than being rejected. (approvals)

In all three, "cannot be statically parsed" resolves to ask or fall back, and none of them treats the presence of the word rm as a deletion reference on its own. That is the difference this PR addresses: the fail-closed decisions stay, but they should be reached from a deletion that was actually identified, not from a word that happens to appear in a search pattern or a SQL alias.

@tt-a1i tt-a1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tt-a1i tt-a1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final integration head 6b7a5ef preserves the reviewed quote-aware cleanup inspection and regression tests. All required checks passed after integrating current main.

@tt-a1i
tt-a1i merged commit 42383f0 into openpi-dev:main Sep 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants