Mental-model reset for Rust. Use when writing or reviewing Rust code to shift from "it compiles" to "thinks in Rust." Triggers on Rust code review, "is this idiomatic", borrow-checker errors, API design, domain modeling, ownership, lifetimes, errors, traits, async/Tokio, unsafe, serde, FFI, tests, performance, Cargo structure, .rs files, Cargo.toml, rustc diagnostics, clippy findings, Result/Option, thiserror vs anyhow, newtype, typestate, enum vs trait, dyn Trait, Send/Sync, Pin, Miri, PyO3, napi-rs, cxx, UniFFI, wasm-bindgen, serde attributes, or feature unification.
87
80%
Does it follow best practices?
Impact
99%
1.05xAverage score across 3 eval scenarios
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./rust/SKILL.mdYou already know Rust syntax. Change the defaults you reach for first when modeling a domain, handling ownership, designing APIs, or crossing boundaries.
The core failure mode: writing Rust that compiles but thinks like Python, Java, TypeScript, or C. Bare String for domain types. bool for states. Trait objects for closed sets. Error(String) for everything. _ => in every match. Index loops. Sentinel values. Getters and setters on every field. clone() to quiet the compiler. unsafe to escape design pressure. These compile. They are wrong.
Most of these habits come from languages without sum types, ownership, zero-cost newtypes, or exhaustive matching. Recognizing where a pattern comes from helps you see why it is wrong in Rust.
When reviewing Rust, start with the shape of the program: what invariants are represented, who owns each value, which states are impossible, where errors cross boundaries, and whether any escape hatch is hiding a design problem.
Treat these as strong defaults, not rigid laws: when unsure, choose the approach that moves invariants into types and lets the compiler enforce them.
String erases domain knowledge. The compiler cannot distinguish an email from a username from a URL. Wrap it, validate at construction, keep the field private. See references/newtypes-and-domain-types.md.true and false carry no meaning at the call site and cannot extend to a third state. Replace flags with named variants. Applies to struct fields too: correlated booleans are a state machine in disguise. See references/bool-to-enum.md.Option<bool> has three states but none of them are named. Empty collections can mean "checked and empty" or "not checked yet." Make each state a named variant. See references/option-bool-to-enum.md._ => arms. Wildcards silence the compiler when you add variants. List every variant of enums you control. _ => is for foreign #[non_exhaustive] types and primitives. See references/exhaustive-matching.md.Error(String). String errors throw away structure. Callers cannot match, test, retry, translate, or recover. Libraries expose typed error enums; applications add context. See error-handling.md.kind field plus Option payload fields is always an enum waiting to be written. See references/enums-as-modeling-tool.md.dyn Trait only when the set is genuinely open. See traits.md.&str, &[T], and &Path unless you need to store, mutate, transform, or transfer ownership. See references/borrow-by-default.md.Rc<RefCell<T>>. RefCell trades compile-time borrow checking for runtime panics. First try split borrows, read-then-write phases, arenas/indices, or explicit ownership flow. See references/ownership-before-refcell.md.spawn_blocking for short blocking calls, and Rayon or dedicated threads for CPU-bound work. See async.md.# Safety docs, // SAFETY: comments, and Miri when validity or aliasing matters. See atomics.md and unsafe.md.for i in 0..v.len() risks off-by-one errors and obscures intent. Use .iter(), .enumerate(), .windows(), .zip(), and adapters that say what you mean. See references/iterators-over-indexing.md.Option over sentinel values. -1, "", 0, and u32::MAX as "no value" markers are invisible to the type system. Use Option<T>. See references/option-over-sentinels.md.self chains over &mut self setters. Reserve &mut self for live objects being operated on. See references/transform-over-mutate.md.impl blocks. A unit struct with only associated functions is a Java class in disguise. Use modules for free functions; use traits when method syntax matters. See references/impl-namespace.md.matches!() for boolean checks. if let for one variant. let ... else for early return. match for real alternatives. Exhaustive match when adding a variant should break the build. See references/pattern-matching-tools.md.pub. Do not write get_x()/set_x() that only forwards. When accessors protect invariants, use Rust naming: name(), not get_name(). See references/getter-setter.md.pub is a semver promise. Default to private or pub(crate), group modules by domain, and curate the public facade with pub use. See references/visibility-and-modules.md.pub struct Email(pub String)) → Make the field private; force construction through parse/new so invariants cannot be bypassed.Option<bool> or nested Option state → Name the states. Stop making callers decode truth tables.kind field plus Option payload fields → Replace with an enum carrying per-variant data; delete the impossible states.Result<(), E> and then forgets the proof → Parse once at the boundary into a domain type.Error(String) or a crate-wide error blob → Define structured errors for one unit of fallibility.anyhow::Error in a public library API → Use a library error type; reserve anyhow for binaries/apps.? in application code → Add .context() so the error says what you were doing.clone() as first response to E0382 → Ask who should own the data. Clone only when you can name why.'static added to silence a lifetime error → Fix the relationship between lifetimes; do not make your API less useful.&String, &Vec<T>, or &PathBuf in APIs → Accept &str, &[T], or &Path.Rc<RefCell<T>> or Arc<Mutex<T>> as first resort → Restructure ownership or use message passing.dyn Trait for a closed set → Use an enum. Interfaces are not free flexibility in Rust.async fn → Use async APIs, spawn_blocking, or Rayon/thread pool..await → Narrow the lock scope or redesign shared state.Ordering::Relaxed because it is faster → Write the proof; otherwise use Release/Acquire or SeqCst.serde_json::Value as the internal model → Use DTOs at the boundary and domain types inside.--release first; keep invariants until profiling proves otherwise.Option<bool> state? → Named enum variants.clone(), 'static, Rc<RefCell<_>>, or unsafe? → Rework ownership first.&str, &[T], or &Path.anyhow errors? → Structured public error type.dyn only for true erasure..await, or fans out unboundedly? → Move blocking work and bound concurrency.--release before cleverness.pub or feature-gated internally? → Curate the facade; keep features additive.| Code smell | Rust default move | Reference |
|---|---|---|
Bare String/u64 for domain values | Newtype with private field | references/newtypes-and-domain-types.md |
bool parameter or state field | Two-variant enum | references/bool-to-enum.md |
Option<bool> / nested Option | Named enum variants | references/option-bool-to-enum.md |
_ => on your own enum | List every variant | references/exhaustive-matching.md |
Error(String) in a library | Typed error enum scoped to the operation | error-handling.md |
| Validate then forget | Parse into a domain type | references/parse-dont-validate.md |
kind field + Option payloads | Enum with per-variant data | references/enums-as-modeling-tool.md |
Box<dyn Trait> for a closed set | Enum, or generics if the set is open | traits.md |
Function takes Vec<T>/String but only reads | Borrow &[T] / &str | references/borrow-by-default.md |
| Defensive clone or lifetime fight | Redesign ownership before adding escape hatches | ownership.md |
for i in 0..v.len() | Iterator chain | references/iterators-over-indexing.md |
| Magic number/string for absence | Option<T> | references/option-over-sentinels.md |
| Parallel collections with shared keys | Single collection of structs | references/struct-collections.md |
&mut self builder setters | Consuming self chains | references/transform-over-mutate.md |
| Unit struct with only associated functions | Module with free functions | references/impl-namespace.md |
One meaningful match arm | if let, let ... else, or matches! | references/pattern-matching-tools.md |
Trivial get_x() / set_x() | Public field or x() accessor with invariant | references/getter-setter.md |
Everything pub / one giant file | Modules plus curated visibility | references/visibility-and-modules.md |
| Blocking work or unbounded fan-out in async code | Async waits; CPU blocks elsewhere; bound everything | async.md |
| Atomic ordering chosen by vibe | Use atomics only with a written proof | atomics.md |
| Unsafe added to bypass compiler friction | Isolate unsafe and document the invariant | unsafe.md |
| Wire format leaking into internals | Translate DTOs into domain types | serde.md |
| FFI or host-runtime boundary | Keep the ABI small, typed, and panic-safe | interop.md |
| Crate/workspace/API shape unclear | Stay single-crate until the boundary has a name | project-structure.md |
Cow, clone discipline.thiserror vs anyhow, structured errors, context, combinators, panic boundaries.Send/Sync invariants.40067f1
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.