agentleFS
sssemilsssemil/SKILLS/skills/misc/rust-pr-review/SKILL.md

rust-pr-review

Skill2 starsChanged 4 months agoMITView on GitHub

Review a Rust PR, branch, or local diff against main. Focus on correctness, smaller code, idiomatic Rust, ownership, error handling, unnecessary clones, needless allocation, async safety, tests, and maintainability.

The file

Show its rules and text

We split this skill into the rules it gives an agent

59 rules. About 0 are common, 42 are distinctive, and 17 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈42

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 17

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when asked to review a Rust pull request, branch, patch, or local diff.

    line 16
  2. Distinctive:

    Prefer concrete fixes. Avoid style churn.

    line 20
  3. Distinctive:

    Prefer feedback that makes the code:

    line 26
  4. Very specific:

    Do not nitpick formatting.

    line 36
  5. Distinctive:

    Do not optimize for fewer lines by itself.

    line 38
  6. Distinctive:

    Do not rewrite unrelated files.

    line 44
  7. Distinctive:

    Do not suggest broad architecture changes unless the diff introduced the problem.

    line 45
  8. Distinctive:

    Do not ask for refactors outside the touched area unless they are required for correctness or safety.

    line 46
  9. Distinctive:

    Do not change tests to make a refactor pass.

    line 47
  10. Distinctive:

    Do not remove error handling, logging, metrics, or user-facing behavior just to make code shorter.

    line 48
  11. Distinctive:

    When in doubt, prefer a small targeted comment over a broad rewrite.

    line 50
  12. Very specific:

    If main...HEAD is empty, check staged and unstaged changes:

    line 64
  13. Distinctive:

    If main is missing, inspect remotes and choose the likely base:

    line 73
  14. Distinctive:

    If the repository uses master instead of main, compare against master.

    line 81
  15. Distinctive:

    Before making findings, inspect the Rust project shape:

    line 95
  16. Very specific:

    Read the relevant Cargo.toml files and note:

    line 101
  17. Distinctive:

    Read the changed files directly, not just the diff.

    line 111
  18. Very specific:

    Prefer the project's own commands first.

    line 126
  19. Distinctive:

    If all features are impossible because of mutually exclusive features, use the feature set relevant to the changed crate.

    line 143
  20. Distinctive:

    If checks fail, distinguish existing failures from failures caused by the diff.

    line 154
  21. Distinctive:

    Do not bury compiler or Clippy findings under speculative review comments.

    line 156
  22. Very specific:

    If input can come from a file, CLI, network, environment variable, or user input, this should return an error with context.

    line 184
  23. Very specific:

    If user_supplied can contain absolute paths or .., this may escape base.

    line 190
  24. Distinctive:

    delete dead branches and unused helpers

    line 264
  25. Very specific:

    remove intermediate Vecs when an iterator can flow directly

    line 267
  26. Very specific:

    use entry() for map insert/update logic

    line 268
  27. Very specific:

    use then_some / then for simple conditional values

    line 269
  28. Very specific:

    use matches! for simple enum checks

    line 270
  29. Very specific:

    use let Some(x) = ... else { ...

    line 271
  30. Very specific:

    use strip_prefix, strip_suffix, split_once, or rsplit_once instead of manual indexing

    line 272
  31. Very specific:

    use retain instead of rebuilding a collection when mutating in place is clearer

    line 273
  32. Very specific:

    use mem::take or Option::take when moving out of a field intentionally

    line 274
  33. Very specific:

    use of std::sync::Mutex in async hot paths

    line 355
  34. Distinctive:

    Only raise performance issues that are visible from the code or relevant to the changed path.

    line 386
  35. Distinctive:

    Do not request micro-optimizations without a concrete reason.

    line 412
  36. Distinctive:

    read the full unsafe block and safe API around it

    line 418
  37. Distinctive:

    check aliasing, lifetimes, initialization, alignment, provenance, and panic safety

    line 420
  38. Distinctive:

    verify the safe wrapper prevents misuse

    line 421
  39. Distinctive:

    Check whether changed behavior has tests.

    line 454
  40. Distinctive:

    Prefer behavior-focused test names:

    line 469
  41. Distinctive:

    use real network services unless explicitly integration tests

    line 482
  42. Distinctive:

    use broad snapshots that will churn constantly

    line 483
  43. Distinctive:

    only test the new happy path when error behavior changed

    line 484
  44. Distinctive:

    Only for issues that can break correctness, safety, security, data integrity, public API compatibility, or production behavior.

    line 492
  45. Distinctive:

    test or build failure caused by the PR

    line 501
  46. Distinctive:

    Should fix before merge unless there is a good reason.

    line 507
  47. Distinctive:

    Do not inflate severity. Many Rust idiom comments are nice-to-have unless they affect API design, correctness, or likely performance.

    line 533
  48. Distinctive:

    Start with the result:

    line 537
  49. Very specific:

    Use changes requested only when there is at least one blocking issue or an important issue that should clearly block merge.

    line 543
  50. Very specific:

    If there are no findings in a section, write None.

    line 586
  51. Distinctive:

    Prefer patches over vague advice.

    line 628
  52. Distinctive:

    Do not report uncertain issues.

    line 630
  53. Distinctive:

    When suggesting code, keep the patch minimal.

    line 634
  54. Distinctive:

    Do not rewrite the whole module unless the bug requires it.

    line 644
  55. Distinctive:

    When changing signatures, mention likely call-site impact.

    line 646
  56. Distinctive:

    When suggesting a more idiomatic construct, include the exact replacement and why it preserves behavior.

    line 648
  57. Distinctive:

    Use this checklist during review:

    line 652
  58. Distinctive:

    run large unrelated refactors

    line 675
  59. Distinctive:

    remove explicit code just because it is longer

    line 680

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust PR Review

Use this skill when asked to review a Rust pull request, branch, patch, or local diff.

The job is to review changed code against main, then report only findings that are worth the author's time.

Prefer concrete fixes. Avoid style churn.

What this skill optimizes for

Review the diff as a Rust reviewer.

Prefer feedback that makes the code:

  • correct
  • smaller without becoming clever
  • more idiomatic Rust
  • easier to reason about
  • safer around ownership, lifetimes, errors, and async boundaries
  • cheaper in allocations, clones, locks, and repeated work
  • better covered by tests

Do not nitpick formatting. Let rustfmt and Clippy handle mechanical style.

Do not optimize for fewer lines by itself. Optimize for code that preserves behavior, reduces complexity, and is harder to misuse.

Scope

Review only changed code and the surrounding context needed to understand it.

Do not rewrite unrelated files. Do not suggest broad architecture changes unless the diff introduced the problem. Do not ask for refactors outside the touched area unless they are required for correctness or safety. Do not change tests to make a refactor pass. Do not remove error handling, logging, metrics, or user-facing behavior just to make code shorter.

When in doubt, prefer a small targeted comment over a broad rewrite.

Step 1: Identify the review target

For a local branch:

git status --short
git branch --show-current
git diff --stat main...HEAD
git diff main...HEAD
git log --oneline main..HEAD

If main...HEAD is empty, check staged and unstaged changes:

git diff --staged --stat
git diff --staged
git diff --stat
git diff

If main is missing, inspect remotes and choose the likely base:

git remote -v
git branch -a
git merge-base origin/main HEAD 2>/dev/null || git merge-base origin/master HEAD

If the repository uses master instead of main, compare against master.

For a GitHub PR, prefer a separate worktree or read-only checkout. Do not switch the user's current branch unless explicitly asked.

Useful PR commands when gh is available:

gh pr view --json title,body,baseRefName,headRefName,author,commits,files
gh pr diff
gh pr checks

Step 2: Get Rust context

Before making findings, inspect the Rust project shape:

find . -name Cargo.toml -maxdepth 4

Read the relevant Cargo.toml files and note:

  • Rust edition
  • rust-version / MSRV, if present
  • workspace layout
  • crate type: library, binary, proc macro, test helper, example
  • relevant features
  • lint settings
  • major dependencies: tokio, serde, thiserror, anyhow, tracing, clap, database clients, HTTP clients

Read the changed files directly, not just the diff.

For any serious finding, read the whole function, trait impl, module, or unsafe block around the changed line.

Also inspect nearby code for:

  • existing helper functions
  • existing error types
  • established naming patterns
  • existing test style
  • public API call sites
  • feature-gated code paths

Step 3: Run deterministic checks when practical

Prefer the project's own commands first. Check README, CI config, justfile, Makefile, .github/workflows, xtask, or repo docs for canonical commands.

Common Rust checks:

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

For workspaces, narrow the package if the full workspace is too large:

cargo clippy -p <package> --all-targets --all-features -- -D warnings
cargo test -p <package> --all-features

If all features are impossible because of mutually exclusive features, use the feature set relevant to the changed crate.

Other useful checks:

cargo check --all-targets
cargo test -p <package> <test_name>
cargo test --doc
cargo bench --no-run

If checks fail, distinguish existing failures from failures caused by the diff.

Do not bury compiler or Clippy findings under speculative review comments. Deterministic tool output comes first.

Step 4: Review priorities

1. Correctness

Look for:

  • behavior changes not covered by tests
  • panics on user input, config, filesystem, network, or parsed data
  • silent error swallowing
  • changed ordering, filtering, retries, deduping, or default behavior
  • overflow, truncation, sign conversion, and lossy casts
  • path handling problems
  • UTF-8 assumptions
  • partial writes, partial reads, cancellation, cleanup, and rollback bugs
  • serialization/deserialization compatibility changes
  • public API behavior changes without migration notes or tests
  • feature flag behavior changes
  • concurrency behavior that differs between single-threaded and multi-threaded runtimes
  • nondeterminism introduced into tests or production behavior

Examples worth flagging:

let value = input.parse::<u64>().unwrap();

If input can come from a file, CLI, network, environment variable, or user input, this should return an error with context.

let path = base.join(user_supplied);

If user_supplied can contain absolute paths or .., this may escape base.

2. Idiomatic ownership and borrowing

Prefer:

  • &str over &String
  • &[T] over &Vec<T>
  • &Path over &PathBuf
  • borrowed parameters when the function only reads
  • owned parameters when the function stores, consumes, or transfers ownership
  • moving values instead of cloning when ownership is already available
  • Cow<'a, str> or Cow<'a, [T]> when ownership is conditional
  • Arc<T> only when shared ownership is real
  • Rc<T> for single-threaded shared ownership, if already used and appropriate
  • lifetime elision when explicit lifetimes add no useful information

Flag:

  • .clone() used only to appease the borrow checker
  • .to_string() / .to_owned() on hot or repeated paths
  • cloning inside loops
  • collecting into Vec just to iterate once
  • taking String, PathBuf, or Vec<T> when a borrow would work
  • broad 'static bounds used to dodge lifetime design
  • unnecessary lifetime parameters
  • APIs that force callers to allocate
  • converting borrowed data to owned data too early

Good comment:

This takes `String`, but the function only reads it. Taking `&str` avoids forcing callers to allocate and still lets existing `String` callers pass `name.as_str()`.

3. Error handling

Prefer:

  • Result<T, E> for expected failures
  • ? for propagation
  • thiserror for library error types
  • anyhow / eyre at app boundaries, if already used
  • .with_context(...) or equivalent at I/O, parse, network, and config boundaries
  • ok_or_else / map_err when errors need construction or context
  • typed errors when callers need to branch on failure kind

Flag:

  • production unwrap
  • production expect on recoverable failure
  • panic paths in library code
  • string errors where callers need structure
  • errors logged and then discarded
  • map_err(|_| ...) that destroys useful diagnostics
  • returning Option when the caller needs to know why something failed
  • using anyhow::Error deep inside reusable library APIs when a typed error would be better

Acceptable uses of unwrap / expect:

  • tests
  • examples where failure would obscure the example
  • constants or invariants that cannot fail, with a clear explanation
  • code where the invariant is already checked immediately before

Even then, prefer comments that explain the invariant.

4. Smaller and clearer code

Suggest simplifications when they preserve behavior.

Good simplifications:

  • early returns instead of deeply nested branches
  • delete dead branches and unused helpers
  • merge duplicate match arms
  • replace manual Option / Result plumbing with ?, ok_or_else, map, map_err, transpose, flatten, or is_some_and
  • remove intermediate Vecs when an iterator can flow directly
  • use entry() for map insert/update logic
  • use then_some / then for simple conditional values
  • use matches! for simple enum checks
  • use let Some(x) = ... else { ... }; when it makes the happy path clearer
  • use strip_prefix, strip_suffix, split_once, or rsplit_once instead of manual indexing
  • use retain instead of rebuilding a collection when mutating in place is clearer
  • use mem::take or Option::take when moving out of a field intentionally

Bad simplifications:

  • dense iterator chains that hide stateful logic
  • clever one-liners
  • removing error context
  • changing user-facing strings, logs, metrics, or serialized output unless asked
  • changing tests to fit a refactor
  • refactoring unrelated code
  • replacing a clear loop with an iterator chain that needs side effects and mutation anyway

Example useful simplification:

let Some(config) = maybe_config else {
    return Ok(None);
};

let value = parse_config(config)?;
Ok(Some(value))

This is often clearer than nested match when the happy path is the main path.

5. Traits and APIs

Prefer:

  • small, cohesive traits
  • From for infallible conversions
  • TryFrom for fallible conversions
  • newtypes for domain identifiers and validated values
  • #[must_use] on builders and meaningful return values
  • pub(crate) before pub
  • sealed traits when external impls would be a breaking-compatibility trap
  • impl Trait or generics when dynamic dispatch is not needed
  • Box<dyn Trait> only when runtime heterogeneity or object safety is actually needed
  • explicit ownership semantics in names and signatures

Flag:

  • primitive obsession in public APIs
  • public fields that should be invariant-protected
  • blanket trait bounds wider than needed
  • needless Send + Sync + 'static
  • Deref used as inheritance
  • APIs that force allocation on callers
  • public APIs that expose implementation details
  • trait methods that make the trait impossible to object-safe without need
  • unnecessary generic parameters that leak complexity into callers

Examples:

fn read_config(path: &Path) -> Result<Config>

is usually better than:

fn read_config(path: PathBuf) -> Result<Config>

when the function only reads the path.

pub(crate) struct Parser

is better than pub struct Parser unless downstream crates need it.

6. Async and concurrency

Look for:

  • holding MutexGuard, RwLockGuard, or borrowed state across .await
  • blocking I/O in async functions
  • unbounded channels without a reason
  • spawned tasks with ignored JoinHandles
  • cancellation paths that leak work or lose errors
  • use of std::sync::Mutex in async hot paths
  • lock ordering problems
  • retries without backoff or cancellation
  • select! branches that drop important futures unexpectedly
  • async functions that never yield in long loops
  • accidentally serial work where concurrency was intended
  • accidentally concurrent work where ordering matters

Prefer:

  • small lock scopes
  • clone/move before .await when it releases a lock
  • tokio::task::spawn_blocking for blocking work
  • try_join! when failures should short-circuit
  • JoinSet when managing many tasks
  • bounded channels unless unbounded growth is intentional
  • cancellation-safe APIs where needed
  • explicit timeouts for network calls when the project uses timeouts elsewhere

Example issue:

let mut state = self.state.lock().await;
let response = self.client.fetch().await?;
state.last_response = Some(response);

The lock is held during network I/O. Prefer fetching outside the lock, then locking only to update state.

7. Performance and allocation

Only raise performance issues that are visible from the code or relevant to the changed path.

Look for:

  • repeated allocation in loops
  • needless format!
  • collect::<Vec<_>>() only to iterate
  • repeated regex/parser construction
  • O(n²) scans introduced in changed code
  • avoidable string copies
  • large enum variants
  • missing with_capacity when size is known
  • avoidable locks on hot paths
  • cloning large structs
  • repeated filesystem or network calls inside loops
  • using contains on a Vec in repeated membership checks where a HashSet would be clearer and cheaper

Prefer:

  • with_capacity when size is known
  • entry() for map insert/update
  • lazy iterators without intermediate collections
  • write! into an existing buffer instead of repeated format! in hot paths
  • references and slices for zero-copy paths
  • caching compiled regexes or parsers when construction is expensive

Do not request micro-optimizations without a concrete reason.

8. Unsafe

For any changed unsafe:

  • read the full unsafe block and safe API around it
  • identify the safety invariant
  • check aliasing, lifetimes, initialization, alignment, provenance, and panic safety
  • verify the safe wrapper prevents misuse
  • require a // SAFETY: comment that explains why the block is valid

Unsafe issues are blocking when the invariant is wrong or unenforced.

Look for:

  • invalid pointer casts
  • unchecked indexing without a proven bound
  • assuming layout without repr(...)
  • incorrect Send / Sync impls
  • creating references to uninitialized or aliased memory
  • panic paths that leave data structures invalid
  • safe APIs that allow callers to violate the unsafe invariant

9. Serialization and compatibility

For changes involving serde, wire formats, config, CLI args, database rows, or public structs, check:

  • renamed fields
  • default behavior
  • skipped fields
  • enum representation
  • backward compatibility
  • migration behavior
  • unknown-field handling
  • optional vs required fields
  • stable ordering when output is compared or cached

Flag changes that break existing users without tests or migration notes.

10. Tests

Check whether changed behavior has tests.

Look for:

  • happy path
  • error path
  • empty input
  • boundary values
  • malformed input
  • async timeout/cancellation
  • serialization compatibility
  • public API examples or doctests
  • feature-specific behavior
  • regression tests for fixed bugs

Prefer behavior-focused test names:

returns_error_when_config_file_is_missing
does_not_retry_after_permanent_failure
preserves_existing_order_when_deduping

Flag tests that:

  • assert implementation details instead of behavior
  • rely on wall-clock timing without control
  • depend on test order
  • use real network services unless explicitly integration tests
  • use broad snapshots that will churn constantly
  • only test the new happy path when error behavior changed

Severity

Use these buckets.

Blocking

Only for issues that can break correctness, safety, security, data integrity, public API compatibility, or production behavior.

Examples:

  • panic on external input
  • unsound unsafe
  • data loss
  • race condition
  • swallowed critical error
  • test or build failure caused by the PR
  • public API break in a library crate without versioning or migration plan
  • security-sensitive path, auth, permission, or parsing bug

Important

Should fix before merge unless there is a good reason.

Examples:

  • unnecessary clone in a hot path
  • missing error context at an important boundary
  • public API forces allocation
  • fragile async cancellation
  • missing tests for changed behavior
  • confusing ownership API
  • duplicated code that already caused inconsistent behavior
  • avoidable O(n²) behavior on expected input sizes

Nice-to-have

Small idiom, cleanup, or maintainability improvements.

Examples:

  • &String could be &str
  • duplicated branches
  • minor iterator cleanup
  • entry() would simplify map update logic
  • a small helper would remove repeated parsing code
  • a test name could be more behavior-focused

Do not inflate severity. Many Rust idiom comments are nice-to-have unless they affect API design, correctness, or likely performance.

Output format

Start with the result:

Verdict: approve | approve with nits | changes requested

Use changes requested only when there is at least one blocking issue or an important issue that should clearly block merge.

Then use this structure:

### Blocking

- `path/to/file.rs:123` — Title
  What is wrong.
  Why it matters.
  Suggested change:

  ```rust
  // replacement
  ```

### Important

- `path/to/file.rs:456` — Title
  What is wrong.
  Why it matters.
  Suggested change:

  ```rust
  // replacement
  ```

### Nice-to-have

- `path/to/file.rs:789` — Title
  Small cleanup or idiom suggestion.

### Things I would not change

Mention tempting rewrites that are not worth doing.

### Checks

- `cargo fmt --check`: pass/fail/not run
- `cargo clippy --all-targets --all-features -- -D warnings`: pass/fail/not run
- `cargo test --all-features`: pass/fail/not run

If there are no findings in a section, write None.

Comment style

Be specific.

Bad:

This is not idiomatic Rust.

Good:

This takes `String`, but the function only reads it. Taking `&str` avoids forcing callers to allocate.

Bad:

Can this be simpler?

Good:

Both branches parse the same value and only differ in the fallback. Pulling the parse before the branch removes the duplicate error path.

Bad:

Use iterators.

Good:

This `collect::<Vec<_>>()` is only used by the next `for` loop. Keeping the iterator lazy avoids an allocation and removes the temporary vector.

Prefer patches over vague advice.

Do not report uncertain issues. If it might be a problem but you cannot prove it from the diff and surrounding code, put it under “Nice-to-have” or skip it.

Suggested patch style

When suggesting code, keep the patch minimal.

Good:

fn normalize_name(name: &str) -> String {
    name.trim().to_ascii_lowercase()
}

Do not rewrite the whole module unless the bug requires it.

When changing signatures, mention likely call-site impact.

When suggesting a more idiomatic construct, include the exact replacement and why it preserves behavior.

Rust idiom checklist

Use this checklist during review:

  • Can owned parameters become borrowed parameters?
  • Can &String, &Vec<T>, or &PathBuf become &str, &[T], or &Path?
  • Are there unnecessary .clone(), .to_string(), .to_owned(), or format! calls?
  • Is a Vec collected only to be iterated once?
  • Can nested match / if let become ?, let else, transpose, flatten, or early returns?
  • Is unwrap or expect reachable from production input?
  • Are errors preserving enough context?
  • Are trait bounds narrower than necessary?
  • Is visibility narrower than necessary?
  • Is dynamic dispatch actually needed?
  • Are locks held across .await?
  • Are blocking calls inside async code?
  • Are spawned tasks observed, cancelled, or joined?
  • Are unsafe invariants documented and enforced by the safe API?
  • Are tests covering error behavior, not just the happy path?
  • Did the PR change serialization, CLI, config, or public API compatibility?

Non-goals

Do not:

  • run large unrelated refactors
  • debate formatting choices handled by rustfmt
  • suggest nightly-only APIs unless the project already uses nightly
  • suggest dependencies for tiny helpers
  • change MSRV assumptions without checking rust-version
  • remove explicit code just because it is longer
  • replace simple loops with complex iterator chains
  • make user-facing behavior change as part of cleanup
  • turn every String into Cow by default
  • turn every trait object into generics by default
  • turn every clone into lifetime complexity

The best review is small, accurate, and easy for the author to apply.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.