Rust Guardrails
These guardrails apply to all Rust code you write, review, or modify. They exist because LLM-generated Rust tends to repeat the same classes of mistakes: lazy unwrap() calls, unnecessary cloning, overly broad visibility, and sloppy dependency management. Following these rules produces code that compiles cleanly, passes clippy, and looks like it was written by someone who actually ships Rust in production.
Error Handling
Rust's error handling is one of its greatest strengths — but only when used properly. The ? operator exists so you don't have to write boilerplate match arms, and crates like thiserror and anyhow exist so you don't have to hand-roll Display and Error impls.
- Never use
unwrap() or expect() in production code. These are fine in tests and examples where a panic is acceptable, but in library or application code they're a ticking time bomb. Use ? to propagate, or handle the error explicitly.
- Always define structured error types with
thiserror first. Every module that can fail should have its own error enum with #[derive(Debug, Error)] and #[error(...)] variants. This gives callers something to pattern-match on and produces clear error messages. thiserror is the right choice for both libraries and applications — the distinction is about where anyhow fits in, not whether to define error types.
- Use
anyhow only at the application boundary — in main(), CLI entry points, or top-level orchestration where you're collecting errors from multiple subsystems and just need to report them. anyhow::Result wraps your thiserror types; it never replaces them. If you find yourself writing anyhow::Result on an internal function, stop and define a proper error type instead.
- Propagate errors with
? — don't write match result { Ok(v) => v, Err(e) => return Err(e) } when result? does the same thing.
- Add context with
.context() / .with_context() from anyhow at the boundary, or use map_err to convert between your thiserror types deeper in the stack.
Ownership & Borrowing
Unnecessary cloning is the most common sign of "fighting the borrow checker" — it compiles, but it's wasteful and hides design problems. Think about whether a function actually needs to own data before taking it by value.
- No unnecessary
clone() — if you clone, add a comment explaining why (e.g., // clone needed: value used after move into spawned task).
- Prefer
&T over owned T in function parameters unless the function genuinely needs ownership (e.g., storing the value in a struct, moving it into a thread).
- Use
Cow<'_, str> when a function might or might not need to allocate — this avoids forcing the caller to allocate when they already have a &str.
- Prefer
&str over String in function parameters. If the caller has a String, they can pass &s; the reverse isn't free.
- Prefer
impl Iterator or impl IntoIterator over Vec in function parameters — this lets callers pass any iterator without collecting first.
Visibility
Rust defaults to private for good reason. Every pub item is a commitment — it's part of your API surface and can't be removed without a breaking change.
- Minimum visibility by default — use
pub(crate) instead of pub unless the item is genuinely part of the public API. Leave items private when they don't need to be accessed outside their module.
#[must_use] on all functions returning Result or Option — a silently ignored error is a bug waiting to happen.
#[non_exhaustive] on public enums and structs that might grow — this lets you add variants or fields without breaking downstream code.
Patterns
- Edition 2021 minimum — there's no reason to target older editions in new code.
#[derive(Debug)] on all types — everything should be debuggable. Add Clone, PartialEq, Eq, Hash etc. when semantically appropriate.
- Prefer exhaustive
match over wildcard _ — when you enumerate all variants explicitly, the compiler tells you when a new variant is added. Use _ only when the set is truly open-ended or when matching on values like integers/strings.
- Use
if let / let else over match for single-variant checks — if let Some(x) = val { ... } is cleaner than a full match with a _ => {} arm.
- Use
HashMap::entry() for insert-or-get patterns — never write if !map.contains_key(&k) { map.insert(k, v); } when map.entry(k).or_insert_with(|| v) does the same thing in one lookup instead of two. The Entry API is also the right tool for counters (*entry.or_insert(0) += 1), default values, and conditional insertion. It's more efficient and signals intent clearly.
- Builder pattern for structs with more than 3 fields — constructors with many positional arguments are error-prone and hard to read.
- Prefer
impl Trait over dyn Trait when the concrete type is known at compile time — monomorphization gives better performance and the code is often simpler.
Dependencies
Bad dependency choices haunt a project for years. A few minutes of due diligence saves a lot of pain.
- Prefer well-maintained, widely-used crates:
serde, tokio, tracing, clap, thiserror, anyhow. These have large userbases, good docs, and responsive maintainers.
- No yanked or deprecated crates — check before adding a dependency.
- Pin major versions in Cargo.toml — write
tokio = "1" not tokio = "*". Wildcard versions invite breakage.
- Use
smol-toml for TOML parsing unless you need to preserve formatting/comments during editing (in which case toml_edit is appropriate). Avoid the toml crate — smol-toml is lighter and faster.
- Disable default features and enable only what's needed — e.g.,
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }. This keeps compile times and binary size down.
Formatting & Style
cargo fmt compliant — never fight rustfmt. If a formatting choice feels wrong, it's still better than inconsistency across the codebase.
cargo clippy clean — no #[allow(clippy::...)] unless you have a genuine reason, documented with a comment explaining why the lint doesn't apply.
- Doc comments (
///) on all public items — types, functions, methods, constants. Explain what it does and when to use it, not how it works internally.
- Module-level doc comment (
//!) in lib.rs — this becomes the crate-level documentation.
Unsafe
Rust's safety guarantees are its core value proposition. unsafe should be a last resort, not a convenience.
- No
unsafe unless absolutely necessary — and "the safe version is slightly slower" is almost never a valid reason.
- Every
unsafe block must have a // SAFETY: comment explaining the invariant that makes this safe. This isn't optional — it's how you prove to reviewers (and your future self) that the code is sound.
Testing
#[cfg(test)] module in the same file for unit tests — keep tests close to the code they test.
- Integration tests in
tests/ directory — these test public API behavior from the outside.
- Use
assert_eq! / assert_ne! over assert!(x == y) — the former gives you the actual vs. expected values on failure, which makes debugging far easier.
- Test error cases, not just the happy path — if a function returns
Result, write tests that verify the error variants too.
1---2name: rust3description: Mandatory guardrails for writing idiomatic, production-quality Rust code. This skill enforces zero-tolerance rules for unwrap()/expect(), correct thiserror vs anyhow usage, ownership and borrowing best practices, visibility minimalism (pub(crate) by default), and idiomatic patterns like the Entry API. Consult this skill for ANY task that writes, modifies, reviews, or refactors Rust code — including .rs files, Cargo.toml edits, CLI tools, libraries, error handling rewrites, and porting code to Rust. It catches the recurring mistakes that LLM-generated Rust makes and produces code that passes clippy and looks like it was written by someone who ships Rust in production.4---56# Rust Guardrails78These guardrails apply to all Rust code you write, review, or modify. They exist because LLM-generated Rust tends to repeat the same classes of mistakes: lazy `unwrap()` calls, unnecessary cloning, overly broad visibility, and sloppy dependency management. Following these rules produces code that compiles cleanly, passes `clippy`, and looks like it was written by someone who actually ships Rust in production.910## Error Handling1112Rust's error handling is one of its greatest strengths — but only when used properly. The `?` operator exists so you don't have to write boilerplate `match` arms, and crates like `thiserror` and `anyhow` exist so you don't have to hand-roll `Display` and `Error` impls.1314- **Never use `unwrap()` or `expect()` in production code.** These are fine in tests and examples where a panic is acceptable, but in library or application code they're a ticking time bomb. Use `?` to propagate, or handle the error explicitly.15- **Always define structured error types with `thiserror` first.** Every module that can fail should have its own error enum with `#[derive(Debug, Error)]` and `#[error(...)]` variants. This gives callers something to pattern-match on and produces clear error messages. `thiserror` is the right choice for both libraries *and* applications — the distinction is about where `anyhow` fits in, not whether to define error types.16- **Use `anyhow` only at the application boundary** — in `main()`, CLI entry points, or top-level orchestration where you're collecting errors from multiple subsystems and just need to report them. `anyhow::Result` wraps your `thiserror` types; it never replaces them. If you find yourself writing `anyhow::Result` on an internal function, stop and define a proper error type instead.17- **Propagate errors with `?`** — don't write `match result { Ok(v) => v, Err(e) => return Err(e) }` when `result?` does the same thing.18- **Add context with `.context()` / `.with_context()`** from `anyhow` at the boundary, or use `map_err` to convert between your `thiserror` types deeper in the stack.1920## Ownership & Borrowing2122Unnecessary cloning is the most common sign of "fighting the borrow checker" — it compiles, but it's wasteful and hides design problems. Think about whether a function actually needs to own data before taking it by value.2324- **No unnecessary `clone()`** — if you clone, add a comment explaining why (e.g., `// clone needed: value used after move into spawned task`).25- **Prefer `&T` over owned `T` in function parameters** unless the function genuinely needs ownership (e.g., storing the value in a struct, moving it into a thread).26- **Use `Cow<'_, str>`** when a function might or might not need to allocate — this avoids forcing the caller to allocate when they already have a `&str`.27- **Prefer `&str` over `String`** in function parameters. If the caller has a `String`, they can pass `&s`; the reverse isn't free.28- **Prefer `impl Iterator` or `impl IntoIterator` over `Vec`** in function parameters — this lets callers pass any iterator without collecting first.2930## Visibility3132Rust defaults to private for good reason. Every `pub` item is a commitment — it's part of your API surface and can't be removed without a breaking change.3334- **Minimum visibility by default** — use `pub(crate)` instead of `pub` unless the item is genuinely part of the public API. Leave items private when they don't need to be accessed outside their module.35- **`#[must_use]`** on all functions returning `Result` or `Option` — a silently ignored error is a bug waiting to happen.36- **`#[non_exhaustive]`** on public enums and structs that might grow — this lets you add variants or fields without breaking downstream code.3738## Patterns3940- **Edition 2021 minimum** — there's no reason to target older editions in new code.41- **`#[derive(Debug)]` on all types** — everything should be debuggable. Add `Clone`, `PartialEq`, `Eq`, `Hash` etc. when semantically appropriate.42- **Prefer exhaustive `match`** over wildcard `_` — when you enumerate all variants explicitly, the compiler tells you when a new variant is added. Use `_` only when the set is truly open-ended or when matching on values like integers/strings.43- **Use `if let` / `let else`** over `match` for single-variant checks — `if let Some(x) = val { ... }` is cleaner than a full `match` with a `_ => {}` arm.44- **Use `HashMap::entry()` for insert-or-get patterns** — never write `if !map.contains_key(&k) { map.insert(k, v); }` when `map.entry(k).or_insert_with(|| v)` does the same thing in one lookup instead of two. The Entry API is also the right tool for counters (`*entry.or_insert(0) += 1`), default values, and conditional insertion. It's more efficient and signals intent clearly.45- **Builder pattern for structs with more than 3 fields** — constructors with many positional arguments are error-prone and hard to read.46- **Prefer `impl Trait` over `dyn Trait`** when the concrete type is known at compile time — monomorphization gives better performance and the code is often simpler.4748## Dependencies4950Bad dependency choices haunt a project for years. A few minutes of due diligence saves a lot of pain.5152- **Prefer well-maintained, widely-used crates**: `serde`, `tokio`, `tracing`, `clap`, `thiserror`, `anyhow`. These have large userbases, good docs, and responsive maintainers.53- **No yanked or deprecated crates** — check before adding a dependency.54- **Pin major versions** in Cargo.toml — write `tokio = "1"` not `tokio = "*"`. Wildcard versions invite breakage.55- **Use `smol-toml` for TOML parsing** unless you need to preserve formatting/comments during editing (in which case `toml_edit` is appropriate). Avoid the `toml` crate — `smol-toml` is lighter and faster.56- **Disable default features and enable only what's needed** — e.g., `tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }`. This keeps compile times and binary size down.5758## Formatting & Style5960- **`cargo fmt` compliant** — never fight rustfmt. If a formatting choice feels wrong, it's still better than inconsistency across the codebase.61- **`cargo clippy` clean** — no `#[allow(clippy::...)]` unless you have a genuine reason, documented with a comment explaining why the lint doesn't apply.62- **Doc comments (`///`)** on all public items — types, functions, methods, constants. Explain what it does and when to use it, not how it works internally.63- **Module-level doc comment (`//!`)** in `lib.rs` — this becomes the crate-level documentation.6465## Unsafe6667Rust's safety guarantees are its core value proposition. `unsafe` should be a last resort, not a convenience.6869- **No `unsafe` unless absolutely necessary** — and "the safe version is slightly slower" is almost never a valid reason.70- **Every `unsafe` block must have a `// SAFETY:` comment** explaining the invariant that makes this safe. This isn't optional — it's how you prove to reviewers (and your future self) that the code is sound.7172## Testing7374- **`#[cfg(test)]` module in the same file** for unit tests — keep tests close to the code they test.75- **Integration tests in `tests/` directory** — these test public API behavior from the outside.76- **Use `assert_eq!` / `assert_ne!`** over `assert!(x == y)` — the former gives you the actual vs. expected values on failure, which makes debugging far easier.77- **Test error cases, not just the happy path** — if a function returns `Result`, write tests that verify the error variants too.