Skip to content

fix(file): dash-safe args, real error status, symlink delete, streaming read#14

Merged
sebyx07 merged 2 commits into
mainfrom
fix/file-tool-correctness
Jul 2, 2026
Merged

fix(file): dash-safe args, real error status, symlink delete, streaming read#14
sebyx07 merged 2 commits into
mainfrom
fix/file-tool-correctness

Conversation

@sebyx07

@sebyx07 sebyx07 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

File-tool correctness fixes from the bug hunt, all with tests:

  1. Argument injection into grep/ls. Patterns/paths were passed positionally with no --: grep for -> failed with invalid option, and a pattern like -r silently shifted arguments (the path became the pattern, recursing cwd — wrong results, no error). find has no --, so a leading-dash relative path is anchored with ./.
  2. Failures reported as successes. sh() ignored the exit status, so file(action="list", path="/does/not/exist") returned a success whose body was ls: cannot access .... Non-zero exit is now an error; grep's exit-1 (zero matches) maps to an explicit [grep: no matches] result instead of an ambiguous empty string.
  3. delete followed symlinks. fs::metadata classified a dir-symlink as a directory (remove_dir_all refuses the top-level link → undeletable) and errored ENOENT on dangling links. Now stats with symlink_metadata and unlinks the link itself.
  4. read slurped whole files. Reading 200 lines of a multi-GB log materialized the entire file (2–3× its size in RAM) before paginating — an OOM risk that takes down every client. Now streams line-by-line, holding only the requested window; identical page format and ceilings.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • File reading now handles large files more efficiently and preserves proper paging when viewing file contents.
    • Deleting a link now removes the link itself instead of affecting the target, including broken links.
    • Search results now correctly distinguish between “no matches” and actual command errors.
    • File and pattern handling is more reliable for names that begin with a dash.

…ng read

- grep/ls get a `--` separator (find gets a ./ anchor): a pattern like
  `->` errored, and `-r` silently recursed with the path as pattern.
- sh() now returns Err on non-zero exit instead of reporting failures as
  MCP successes whose body happens to contain the error text; grep exit 1
  maps to an explicit "[grep: no matches]" result.
- delete uses symlink_metadata: a dir-symlink is unlinked (not refused by
  remove_dir_all), a dangling symlink is deletable at all.
- read streams line-by-line holding only the requested window, instead of
  materializing the whole file (an OOM risk on multi-GB files) before
  paginating. Same page format and byte/line ceilings via jobs::paginate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9bfc2eee-2e02-409c-addf-6eea1f4c614b

📥 Commits

Reviewing files that changed from the base of the PR and between 3f60526 and 6c6edf5.

📒 Files selected for processing (2)
  • src/tools/files.rs
  • src/tools/files/tests.rs

Walkthrough

Changes to src/tools/files.rs rework file-reading to stream lines via an async BufReader rather than loading whole files, switch delete to symlink_metadata to correctly unlink symlinks, and revise list/grep shell execution to sanitize arguments and return a new ShError on command failure instead of Ok. Tests were added accordingly.

Changes

File tools hardening

Layer / File(s) Summary
Streaming line-based read
src/tools/files.rs
read now streams the file line-by-line via async BufReader/read_until, tracks total line count, collects only the requested page window, and paginates that window with updated next_cursor logic instead of reading the whole file upfront.
Symlink-safe delete
src/tools/files.rs
delete uses fs::symlink_metadata instead of fs::metadata, so symlinks (including dangling ones) are unlinked rather than followed.
Safer shell exec for list/grep
src/tools/files.rs
Leading-dash paths are anchored with ./, commands use -- delimiters, grep distinguishes “no matches” from real errors, and the sh helper now returns a new ShError on non-success exit instead of Ok.
Tests for new behaviors
src/tools/files.rs
Tests added for dash-starting grep patterns, no-match vs. error distinction, shell failures surfacing as Err, and symlink deletion including dangling symlinks.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant list_grep as list/grep
  participant sh
  participant Shell

  Caller->>list_grep: invoke with path/pattern
  list_grep->>sh: run command with -- delimiters
  sh->>Shell: execute
  Shell-->>sh: exit code, stdout, stderr
  alt grep exit 1, empty stdout
    sh-->>list_grep: "[grep: no matches]"
  else non-success exit
    sh-->>list_grep: ShError
  else success
    sh-->>list_grep: Ok(output)
  end
  list_grep-->>Caller: result or error
Loading

Related Issues: None found in the provided context.

Related PRs: None found in the provided context.

Suggested labels: rust, bugfix, security

Suggested reviewers: None found in the provided context.

Poem:
A rabbit streams lines, no more full-file spree,
Unlinks the symlink, dodging the tree,
Dashes get anchored, grep learns to be calm,
Errors now surface, no false "Ok" balm,
Tests hop in neatly — a well-guarded palm. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main file-tool fixes, including safe args, proper error handling, symlink deletion, and streaming reads.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/file-tool-correctness

Comment @coderabbitai help to get the list of available commands.

@sebyx07 sebyx07 enabled auto-merge (squash) July 2, 2026 17:10
@sebyx07

sebyx07 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/tools/files.rs`:
- Around line 293-365: The test code currently lives inline in the main files
module, pushing the file over the LOC limit. Move the existing `#[tokio::test]`
cases out of `src/tools/files.rs` into a new `tests` submodule and wire it up
with `#[cfg(test)] mod tests;` so the production module stays under 300 LOC.
Keep the test names like
`grep_pattern_starting_with_dash_is_a_pattern_not_an_option`,
`grep_no_matches_is_ok_and_distinguishable`,
`failed_shell_command_is_an_error_not_a_success`, and
`delete_unlinks_symlinks_instead_of_following` unchanged in the new
`src/tools/files/tests.rs` file.
- Around line 41-56: The pagination logic in the file-reading path can still
buffer an entire oversized line before trimming, so `read_until` in the
line-reading flow should be changed to enforce a bounded per-line read strategy.
Update the line handling in `src/tools/files.rs` around the `read_until` loop so
it either reads in capped chunks or stops accumulating once a maximum line size
is reached before applying `cursor` and `limit`, and add a regression test for a
very long file with no newline to verify memory stays bounded.
- Around line 163-166: Update the ShError handling in files-related command
execution so the grep no-match case stays distinct from signal-based exits; keep
the existing Option<i32> status representation, or introduce a separate signal
variant instead of collapsing everything into i32. Also derive thiserror::Error
for ShError and adjust the matching in the function around the grep/status
handling to preserve the separate no-match path while keeping error conversion
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2cab304e-d345-4321-92c2-1192ff9a7d8c

📥 Commits

Reviewing files that changed from the base of the PR and between d8b41a0 and 3f60526.

📒 Files selected for processing (1)
  • src/tools/files.rs

Comment thread src/tools/files.rs Outdated
Comment thread src/tools/files.rs Outdated
Comment thread src/tools/files.rs Outdated
- read() streams via fill_buf/consume with a 64 KiB per-line cap
  (append_capped) so a no-newline multi-GB file can't OOM; regression
  test added
- ShError derives thiserror::Error; split Status/Signal variants make
  illegal states unrepresentable, grep no-match path stays distinct
- move test module to src/tools/files/tests.rs; files.rs now 263 LOC
  (was 444), under the 300 cap

Co-Authored-By: Claude <noreply@anthropic.com>
@sebyx07 sebyx07 merged commit ba72a75 into main Jul 2, 2026
5 checks passed
@sebyx07 sebyx07 deleted the fix/file-tool-correctness branch July 2, 2026 18:23
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.

1 participant