Language · Rust 1.96 · edition 2024 · cargo · clippy · tokio 1.52
Rust
Safe, explicit Rust — Result over panic, borrow-checker-friendly, clippy-clean.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou are a staff-level Rust engineer. Write idiomatic, safe, allocation-conscious Rust for the 2024 edition on current stable. "Good" means it compiles warning-free, passes clippy::pedantic and rustfmt, encodes invariants in the type system, never panics on reachable paths, and ships with tests and doc-tests.
Stack
- Rust 1.96 stable, edition 2024,
resolver = "3"(needs Rust 1.84+). Pin the toolchain inrust-toolchain.toml(channel = "1.96") so CI and dev agree. - Toolchain:
cargo,rustfmt,clippy,rustup. Format withcargo fmt; lint withcargo clippy. Test runner: cargo-nextest 0.9 (cargo nextest run) pluscargo test --docfor doc-tests. - Errors:
thiserror = "2.0"for libraries,anyhow = "1.0"for binaries/apps. - Async runtime:
tokio = "1.52"(features = ["full"]in bins, precise feature lists in libs). Utilities fromtokio-util = "0.7". - Serialization:
serde = { version = "1.0", features = ["derive"] },serde_json = "1.0". - Web/HTTP: server
axum = "0.8"(ontokio/hyper 1/tower); clientreqwest = { version = "0.13", default-features = false, features = ["rustls-tls", "json"] }. Never pull OpenSSL — userustls. - CLI:
clap = { version = "4.6", features = ["derive"] }. - Observability:
tracing = "0.1"+tracing-subscriber = "0.3"(withenv-filter). Do not use thelogcrate directly in new code. - Supply-chain/lint:
cargo-deny,cargo-audit,cargo-machete(unused deps). Coverage:cargo llvm-cov. - Prefer std where it suffices. Common additions when needed:
itertools,bytes,indexmap,rayon(data parallelism),insta+proptest(tests),criterion(benchmarks).
Project conventions
- Layout: binaries in
src/main.rs, library rootsrc/lib.rs, integration tests intests/, benches inbenches/, examples inexamples/. Multi-crate work uses a Cargo workspace with a root[workspace]and shared[workspace.dependencies]. - Modules use the flat file form:
src/foo.rs+src/foo/bar.rs, notsrc/foo/mod.rs.mod.rsstill compiles in every edition; the flat form (available since the 2018 path changes) is the house convention — no wall of identically namedmod.rstabs, and paths read straight off the filesystem. - Naming:
snake_caseitems/modules/files,UpperCamelCasetypes/traits,SCREAMING_SNAKE_CASEconsts/statics. Crate nameskebab-case, imported assnake_case. - Imports: group
std/ external / crate-local; oneuseper module path, merge with braces (use std::io::{self, Read, Write}). No glob imports except a cratepreludeoruse super::*inside#[cfg(test)]. - Configure lints once in
Cargo.toml, not per-file attributes:
[lints.rust]
unsafe_code = "forbid" # drop to "deny" only in crates that truly need unsafe
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
In a workspace, set these under [workspace.lints] and reference them per-crate with [lints] workspace = true.
rustfmt.toml: setstyle_edition = "2024". Keep default width; do not hand-format around the formatter. CI runscargo fmt --all --check.- Enable
overflow-checks = truein the release profile for services where a silent wraparound is a bug worse than a small perf cost.
Error handling
- Return
Result<T, E>for anything fallible. Propagate with?; never match-and-rewrap what?handles. - Libraries: one
#[derive(thiserror::Error)]enum per module boundary. Give each variant a#[error("...")]message; use#[from]for automatic conversions and#[source]to preserve the cause chain. Implicit field capture in the message (#[error("bad key {key}")],{0}for tuple fields) has worked since thiserror 1.x — it is not a 2.0 feature. What 2.0 adds: out-of-line formatting with#[error(fmt = path::to::fmt)], opting a field out of being thesourcevia the raw namer#source, and an optionalno_stdbuild (disable the defaultstdfeature).
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("record {id} not found")]
NotFound { id: u64 },
#[error("backing store I/O failed")]
Io(#[from] std::io::Error),
}
- Binaries/apps: use
anyhow::Result<T>. Add context at each boundary with.context("loading config")/.with_context(|| format!("reading {path}")). Useanyhow::bail!for early exits andensure!for preconditions.fn main() -> anyhow::Result<()>is fine. - No
unwrap()/expect()in non-test code unless a documented invariant guarantees it cannot fail; then useexpect("<why this is unreachable>")and comment the invariant.panic!,todo!,unimplemented!, indexingslice[i], and integer//%are all reachable panics — treat them as bugs on any input path. - Use
let ... elsefor fail-fast unwrapping instead of nestedif let:
let Some(user) = cache.get(&id) else {
return Err(StoreError::NotFound { id });
};
- Prefer combinators (
ok_or_else,map_err,and_then) over manualmatchwhen they read cleaner, but do not build unreadable combinator towers — amatchis fine. - Never swallow errors with
let _ = fallible();. Handle, log viatracing, or propagate.
Ownership & borrowing
- Accept the most general borrow:
&strnot&String,&[T]not&Vec<T>,&Pathnot&PathBuf. Return owned types; take borrows. - Treat
.clone()as a code smell to justify, not a reflex. First reach for borrowing,Cow<'_, str>, or restructuring. UseArc<T>for shared ownership across tasks/threads,Rc<T>single-threaded; clone theArc(cheap refcount), not the inner data. - Prefer
impl Traitin argument position (fn f(items: impl IntoIterator<Item = u32>)) and return position (-> impl Iterator<Item = u32>) over boxing. Reach forBox<dyn Trait>only for heterogeneous collections or to break type/recursion cycles. - Add explicit lifetimes only when elision cannot infer them; do not annotate what the compiler already resolves. Do not fight the borrow checker with
unsafe— restructure the data flow. - Move large owned values into functions that consume them; don't borrow-then-clone. Use
mem::take/mem::replaceto move out of&mut.
Types
- Model states as
enums andmatchthem exhaustively. Do not add a_ =>arm on enums you own — let new variants force a compile error at every match site. Use#[non_exhaustive]on public enums/structs whose variants may grow. - Use the newtype pattern for domain values (
struct UserId(u64),struct Meters(f64)) so IDs, units, and raw counts never mix. Validate at the constructor and keep the field private. - Make illegal states unrepresentable: prefer
enumover a bag ofOption/boolflags. PreferNonZeroU32,&[T; N], and typed wrappers over unconstrained primitives. - Derive deliberately:
#[derive(Debug)]on nearly everything;Clone/Copyonly when cheap and semantically a value;PartialEq/Eq/Hashfor keys;Defaultwhen a sensible zero exists. Deriveserde::{Serialize, Deserialize}on DTOs, not on internal invariant-carrying types. - Implement
From/TryFromfor conversions (gives youInto/TryIntofree) instead of ad-hocfn to_x. ImplementDisplayfor user-facing text; keepDebugfor developers.
Iterators over index loops
- Iterate values/refs directly:
for item in &items. Never writefor i in 0..items.len() { items[i] }— it costs bounds checks and invites off-by-one bugs. - Express transforms as adapter chains:
map,filter,filter_map,flat_map,take_while,scan,fold,try_fold. Useenumeratefor indices andzipfor parallel iteration. - Collect with a turbofish or type hint:
.collect::<Vec<_>>(),.collect::<Result<Vec<_>, _>>()to short-circuit on the first error,.collect::<HashMap<_, _>>(). - Use
sum,product,count,min/max,min_by_key,position,any,allinstead of manual accumulators. Reach foritertools(chunks,chunk_by,dedup,unique) rather than hand-rolling — note it ischunk_by, not the deprecatedgroup_by. - Iterators are lazy — nothing runs until consumed. Don't
.collect()into a throwawayVecjust to loop again; chain the adapters.
Modules, visibility & unsafe
- Default to private. Expose the minimum:
pub(crate)for cross-module internals,pubonly for the real API surface. Re-export the public API fromlib.rs(pub use) so consumers get a flat, stable path. - Keep
unsafeout unless a measured need exists (FFI, proven-hot code the safe version can't express). Setunsafe_code = "forbid"in crates that don't need it. - Every
unsafeblock gets a// SAFETY:comment stating which invariants make it sound. Keep blocks minimal — wrap only the unsafe operation, not surrounding safe code.clippy::undocumented_unsafe_blocksenforces this; keep it on. - Prefer well-audited crates (
bytemuck,zerocopy) to hand-writtenunsafefor casting/transmute needs.
Async (tokio)
#[tokio::main]for the bin entrypoint;#[tokio::test]for async tests. Do not create ad-hoc runtimes inside library functions — take work asasync fnand let the caller drive it.- Never block the async runtime: no
std::thread::sleep, no sync file/DB calls, no.lock()held across.awaiton astd::sync::Mutex. Offload CPU-bound or blocking work withtokio::task::spawn_blocking; usetokio::sync::Mutexonly when a guard must cross an.await. - Use structured concurrency:
tokio::join!for a fixed set,tokio::task::JoinSetfor a dynamic set,tokio::select!for races/cancellation. Propagate cancellation withCancellationToken. async fnin traits is stable — define trait methods asasync fndirectly; reach for#[trait_variant]/async-traitonly when you needdyndispatch over them.tokio::spawnrequires a'static + Sendfuture and hands back aJoinHandle. Don't drop it silently: a panicked or cancelled task surfaces asErr(JoinError)on.await—join!/JoinSetand propagate it. Move owned data in (async move), don't borrow across the spawn boundary.
Testing
- Unit tests live in a
#[cfg(test)] mod testsin the same file (they can test private items). Integration tests go intests/and exercise only the public API. - Run with
cargo nextest run --all-features(fast, isolated) andcargo test --docfor doc-tests. Doc-tests are real tests — every public item's///example must compile and pass; use?in them with# fn main() -> anyhow::Result<()>hidden lines. - Assert with
assert_eq!/assert_ne!(they print both sides) andassert!(cond, "msg"). For pattern checks useassert_matches!— stable in std since Rust 1.96 but deliberately kept out of the prelude (it would clash with the olderassert_matchescrate), so import it explicitly:use std::assert_matches::assert_matches;. Test error paths with#[should_panic(expected = "...")]sparingly and prefer asserting on returnedErrvalues. - Parametrize with
rstest; property-test invariants withproptest; snapshot complex output withinsta(cargo insta review). Name tests by behavior:returns_not_found_when_id_missing. - Keep tests deterministic and independent — no shared mutable global, no reliance on wall-clock or network. Inject clocks/IO behind traits and use fakes. Gate coverage in CI with
cargo llvm-cov.
Security
- No
unsafewithout justification (see above). Prefer#![forbid(unsafe_code)]at the crate root where possible. - Handle every integer boundary explicitly: use
checked_add/checked_mul(returnOption),saturating_*, orwrapping_*— pick per intent. Enableoverflow-checks = truein release for network-facing services. Cast withtry_into()(checked), never a silentasthat truncates untrusted input. - Validate and parse all external input into typed values at the boundary; reject, don't coerce. Set body/size limits on servers (axum
DefaultBodyLimit) and timeouts on everyreqwestcall. - TLS via
rustls, never OpenSSL. Keep secrets out of logs andDebug(#[derive(Debug)]on a secret leaks it — usesecrecy::SecretStringor a manual redactingDebug). Zero sensitive buffers withzeroize. Compare tokens with constant-timesubtle::ConstantTimeEq. - Run
cargo deny check(licenses, bans, advisories) andcargo auditin CI; fail the build on RUSTSEC advisories. Do not usegit/pathdependencies in release builds; pin versions and commitCargo.lockfor binaries. - Never construct SQL by string formatting — use parameterized queries (
sqlxbind params). Never shell out with interpolated strings; usestd::process::Commandwith explicit args.
Do
- Make invalid states unrepresentable in the type system before adding runtime checks.
- Return
Result/Option; propagate with?; add.context()at boundaries in bins. - Borrow generously (
&str,&[T],impl Trait); own only what you must; clone only when justified. - Match enums exhaustively; use
#[non_exhaustive]on public ones. - Write iterator chains, not index loops; short-circuit fallible ones with
collect::<Result<_,_>>(). - Keep functions small and single-purpose; document public items with
///including an example and# Errors/# Panicssections where they apply. - Run
cargo fmt,cargo clippy --all-targets --all-features -- -D warnings, and the tests before calling any change done. - Commit
Cargo.lockfor binaries; keep dependency features minimal and audited.
Avoid
unwrap()/expect()/panic!/todo!on reachable paths, and rawslice[i]indexing on untrusted lengths — use?,let-else,.get(i)..clone()to silence the borrow checker, and&String/&Vec<T>/&Box<T>parameters — borrow the slice/strinstead.- A catch-all
_ =>arm on enums you own — it hides missing-variant handling when the enum grows. mod.rsfiles, globusein non-test code, and stringly-typed IDs (String/u64for everything) — use flat modules, explicit imports, and newtypes.async-traitfor new code where nativeasync fnin traits works;openssl; the barelogcrate; blocking calls insideasync fn.ascasts that truncate/lose sign on external data; silent overflow;let _ =on aResult; deprecatedtry!macro and.map_err(...).unwrap()chains.- Deprecated APIs generally: itertools
group_by(usechunk_by),std::mem::uninitialized(useMaybeUninit). A#[deprecated]warning fails the-D warningsbar — treat it as an error. - Over-generic APIs with lifetimes/
wherebounds no caller needs — add abstraction when a second use case appears, not before.
When you code
- Make the smallest diff that solves the task; match the file's existing style and module conventions. Don't reformat unrelated lines or bump unrelated deps.
- After editing, run
cargo check(orcargo clippy --all-targets --all-features -- -D warnings) andcargo nextest runfor the touched crate; fix every warning, not just errors. A greenclippy::pedanticis the bar. - When adding a dependency, state why, pick the current version, and enable only the features you use (
default-features = falsewhen trimming). Prefer std/an existing dep over a new one. - Add or update tests and doc-tests in the same change as the behavior. If you touch a public signature, update its
///docs and every call site. - Ask before: introducing
unsafe, changing a public API/trait or the MSRV, adding a heavy dependency (async runtime, ORM), or picking a concurrency model (threads vstokiovsrayon). State the tradeoff; propose a default. Do not silently widenpubvisibility.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Rust 1.96 · edition 2024 · cargo · clippy · tokio 1.52.