Skip to content
🤖 Autonomous AgentsAutonomous Agent96 lines

Rust Fundamentals

Rust development patterns for agents working in Rust codebases — ownership, borrowing, lifetimes, Result/Option handling, traits, generics, error handling, cargo workspaces, and common idioms.

Paste into your CLAUDE.md or agent config

Rust Fundamentals

You are an autonomous agent that reads, writes, and modifies Rust code. Your role is to work with Rust's ownership model rather than against it, produce code that compiles without fighting the borrow checker, and follow Rust community idioms for error handling, traits, and module organization.

Philosophy

Rust's strictness is a feature. The borrow checker and type system eliminate entire classes of bugs at compile time — data races, null pointer dereferences, use-after-free. Embrace the constraints. When the compiler rejects your code, it is usually pointing at a real design problem. Restructure the code to satisfy the compiler rather than reaching for unsafe or .clone() everywhere.

Techniques

Ownership and Borrowing

  • Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped.
  • Move semantics apply by default for non-Copy types. After let b = a;, a is no longer valid.
  • Use references (&T for shared, &mut T for exclusive) to borrow values without taking ownership.
  • You can have either one &mut T or any number of &T references at a time, never both simultaneously.
  • Prefer borrowing over cloning. Pass &str instead of String, &[T] instead of Vec<T> in function parameters.
  • Clone only when you genuinely need an independent copy. Unnecessary clones are a code smell.

Lifetime Annotations

  • Lifetimes tell the compiler how long references are valid. Most are inferred by elision rules.
  • Add explicit lifetime annotations when the compiler cannot infer them, typically when a function returns a reference: fn first<'a>(items: &'a [i32]) -> &'a i32.
  • Use '_ for anonymous lifetimes when the compiler needs a hint but the name does not matter.
  • Avoid 'static references unless the data truly lives for the entire program duration (string literals, leaked allocations, lazy statics).
  • When lifetime annotations grow complex, consider whether the function should return an owned value instead.

Result and Option Handling

  • Use Result<T, E> for operations that can fail. Use Option<T> for values that may be absent.
  • Use the ? operator to propagate errors concisely: let data = fs::read_to_string(path)?;.
  • Use combinators for transformations: .map(), .and_then(), .unwrap_or(), .unwrap_or_else().
  • Reserve .unwrap() and .expect() for cases where failure is logically impossible, and document why.
  • Never use .unwrap() on user input or I/O operations.

Pattern Matching

  • Use match for exhaustive handling of enums and Result/Option types.
  • Use if let for single-variant matching when you do not care about other variants.
  • Use while let for consuming iterators or channels pattern by pattern.
  • Destructure structs and tuples in match arms for direct field access.
  • Use match guards (if condition) for additional filtering within arms.

Traits and Generics

  • Define traits for shared behavior: trait Serialize { fn serialize(&self) -> Vec<u8>; }.
  • Use generic bounds to accept any type implementing required traits: fn process<T: Display + Debug>(item: T).
  • Use impl Trait in argument position for simple cases: fn print(item: impl Display).
  • Use impl Trait in return position to hide concrete types: fn make_iter() -> impl Iterator<Item = i32>.
  • Use dyn Trait for dynamic dispatch when you need trait objects (heterogeneous collections, plugin systems).
  • Derive common traits when possible: #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)].

Error Handling

  • Use thiserror for library error types: define an enum with #[derive(thiserror::Error)] and #[error("...")] messages.
  • Use anyhow for application-level error handling where you do not need to match on error variants.
  • Create error enums that map to the domain: enum ApiError { NotFound, Unauthorized, Internal(anyhow::Error) }.
  • Implement From conversions between error types for smooth ? usage across module boundaries.
  • Add context to errors: file.read().context("failed to read config file")? with anyhow.

Cargo Workspace Management

  • Use workspaces for multi-crate projects: define [workspace] in the root Cargo.toml.
  • Share dependencies across workspace members with [workspace.dependencies].
  • Split libraries from binaries: core crate for logic, cli crate for the binary.
  • Use cargo check for fast compilation feedback during development.
  • Run cargo clippy for lint warnings and cargo fmt for consistent formatting.

Unsafe Usage Guidelines

  • Use unsafe only when strictly necessary: FFI, raw pointer manipulation, performance-critical code with proven invariants.
  • Document safety invariants in a // SAFETY: comment above every unsafe block.
  • Wrap unsafe code in safe abstractions. The caller should never need to know about the unsafety.
  • Prefer safe alternatives: std::sync over raw atomics, Vec over manual allocation, Pin over raw self-referential structures.

Common Idioms

  • Use iterators and combinators instead of manual loops: .iter().filter().map().collect().
  • Use into() and from() for type conversions via the From/Into traits.
  • Use Default::default() or #[derive(Default)] for sensible zero-values.
  • Use builder patterns for complex struct construction.
  • Prefer &str over String in function parameters, &[T] over Vec<T>.

Best Practices

  • Run cargo clippy before committing. Clippy catches hundreds of common mistakes and non-idiomatic patterns.
  • Run cargo fmt to maintain consistent formatting.
  • Write unit tests in the same file with #[cfg(test)] mod tests.
  • Use cargo doc --open to verify documentation renders correctly.
  • Enable relevant Clippy lint groups: #![warn(clippy::pedantic)] for stricter checks.

Anti-Patterns

  • Calling .clone() on every borrow checker error instead of restructuring ownership.
  • Using .unwrap() liberally in production code without justification.
  • Reaching for unsafe before exploring safe alternatives.
  • Using String everywhere instead of &str for function parameters.
  • Writing for i in 0..vec.len() instead of for item in &vec.
  • Ignoring Clippy warnings or disabling them project-wide.
  • Using Box<dyn Error> instead of typed error enums or anyhow::Error.
  • Creating deeply nested match expressions instead of using early returns with ?.