SkillAgentSearch skills...

rust-best-practices

Write idiomatic, robust, maintainable Rust. Use this skill whenever you write, review, refactor, debug, or explain Rust code: new crates, modules, functions, structs, traits, error types, async code, unsafe or FFI code, Cargo.toml, tests, or rustdoc.

Install / Use

npx skills add morganlinton/BeautifulRust --skill rust-best-practices

Installs into whichever agent you are using.

About this skill
📄

SKILL.md

Installable skill definition

Quality Score

92/100

Supported Platforms

Zed

Our assessment of rust-best-practices

rust-best-practices scores 92/100 on our quality scale, 901st of 4,645 Development & Engineering skills we index (top 20%).

Its SKILL.md is 29 KB long, well organised into 22 sections with 2 code examples: a thorough specification that gives an agent plenty to work with.

With 1,611 GitHub stars, it is one of the more widely adopted skills in the catalogue.

Substance
30/30
Structure
18/20
Description
15/15
Adoption
14/20
Freshness
15/15

Maintenance, license and trust

  • The repository was last updated 7 days ago, so rust-best-practices is actively maintained.
  • It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
  • Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.

Safety scan

No issues found

Our scan of the whole file found no instruction hijacking, hidden characters, credential access, data exfiltration or destructive commands.

Automated pattern scan on 2026-11-07. It catches known dangerous patterns, not every risk — read a skill before letting an agent act on it.

rust-best-practices compared with similar skills

All 4 of these similar skills score higher than rust-best-practices; compare them before choosing.

SkillScoreStarsUpdatedFormat
rust-best-practices (this skill)by morganlinton921.6k7d agoSKILL.md
taste-skillby Leonxlnx100111.4k2d agoCLAUDE.md
Agent-Reachby Panniantong100109.8k1d agoCLAUDE.md
headroomby headroomlabs-ai10079.1ktodayCLAUDE.md
ai-job-searchby ai-job-search-dev10070.2k2d agoCLAUDE.md

Frequently asked questions

How do I install rust-best-practices?
Run npx skills add morganlinton/BeautifulRust --skill rust-best-practices. The install tabs above show the steps for each supported agent.
Which AI agents does rust-best-practices work with?
It is written for Zed, as a SKILL.md file. Other agents that read the same format can often use it too.
Is rust-best-practices safe to use?
Our scan of the whole file found no instruction hijacking, hidden characters, credential access, data exfiltration or destructive commands. It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
Is rust-best-practices still maintained?
The repository was last updated 7 days ago, so rust-best-practices is actively maintained.

name: rust-best-practices description: >- Write idiomatic, robust, maintainable Rust. Use this skill whenever you write, review, refactor, debug, or explain Rust code: new crates, modules, functions, structs, traits, error types, async code, unsafe or FFI code, Cargo.toml, tests, or rustdoc. Trigger on any mention of Rust, cargo, crates, .rs files, the borrow checker, lifetimes, traits, clippy, or rustfmt, even when the user does not ask for "best practices" or "idiomatic" code. Synthesized from the Google Rust Style Guide, the Rust API Guidelines, Effective Rust, Rust Design Patterns, and Microsoft's Pragmatic Rust Guidelines.

Rust best practices

Distilled from five Rust style and design guides, with a digest of each in references/. Where they disagree, this file says which to follow and why.

1. Mindset

  • Write Rust-shaped code. Don't port C++, Java, C#, or Python constructs one-to-one. Model errors with Result, resources with ownership and Drop, polymorphism with traits and enums, and nullability with Option. Inheritance-flavored tricks (Deref to a "base" struct, Manager/Factory types, throw_if_null helpers) are smells.
  • Make invalid states unrepresentable. Push invariants into types: enums instead of bool flags and "must be X if Y" comments, newtypes for units and IDs, fallible constructors that validate once. Then the compiler enforces what a comment only requests.
  • Application code is not library code. Library code (anything with external users) follows the API-design rules below strictly. Application code should favor simplicity and directness: concrete types, String or &str rather than impl Into<String> parameters, anyhow-style errors, Vec return values, no speculative generality.
  • Let the tools do the work. rustfmt owns formatting, clippy owns idiom checks, the compiler owns memory safety. Spend your attention on design.

2. Before you hand back code

Run these and fix everything they report:

cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo doc --no-deps   # libraries: catches broken doc links and missing docs
  • Fix a lint rather than silencing it. When a lint is genuinely wrong for one item, use #[expect(clippy::name, reason = "...")] on the narrowest item. expect warns when the lint stops firing, so stale suppressions get cleaned up. Use #[allow] only in generated code or macro output.
  • Never put #![deny(warnings)] in source: new toolchains add warnings and break the build. Enforce warnings-as-errors in CI instead, with cargo clippy -- -D warnings.
  • Configure lints once in Cargo.toml ([lints] or [workspace.lints]), not with scattered crate attributes. If a project has none, suggest lints/lints.toml and lints/clippy.toml from https://github.com/morganlinton/BeautifulRust; the suppression lints in it need rust-version 1.81 or later.

3. Ownership and borrowing

Prefer unique ownership. Reach for Rc/Arc only when shared ownership is part of the design, and for RefCell/Mutex only when interior mutability is. They are not failures, but they move checking from compile time to run time, so use them on purpose.

Don't clone to make the borrow checker stop complaining. A clone that exists only to end a borrow produces a silent copy whose later changes are lost. Try these first, in roughly this order:

  1. Extend a temporary with a named let, or shorten a borrow with a block.
  2. Split a long expression into typed intermediates so the failing step is visible.
  3. Move a field out of &mut self with std::mem::take (needs Default), std::mem::replace, or Option::take instead of cloning it.
  4. Split a large struct into smaller ones so disjoint fields can be borrowed separately. The compiler tracks disjoint field borrows inside one function, but not across method calls.
  5. Clone small, immutable data deliberately, and say so in a comment if it's not obvious.
  6. Use indices into an owning Vec, or Rc<RefCell<_>> with Weak back-pointers, as a last resort. Avoid self-referential structs.
// Move out of a &mut enum without cloning.
fn a_to_b(e: &mut MyEnum) {
    if let MyEnum::A { name, x: 0 } = e {
        *e = MyEnum::B { name: std::mem::take(name) };
    }
}

Accept the most general borrowed type. &str not &String, &[T] not &Vec<T>, &T not &Box<T>, Option<&T> not &Option<T>. Deref coercion then lets callers pass owned values, borrows, or literals. Where you only read a path, string, or byte buffer, impl AsRef<Path> / impl AsRef<str> / impl AsRef<[u8]> is fine on parameters (never on struct fields; store PathBuf and String). Library code that only iterates should take impl IntoIterator<Item = T>; application code may simply take &[T].

Take ownership only when you need it, and then take it by value. If a function stores or consumes its argument, take T, not &T plus an internal .clone(). If it only reads, take &T; if it mutates in place, &mut T. Return multiple values as a tuple or struct, not through &mut out-parameters (reusing a caller-owned buffer, as Read::read does, is the exception).

Prefer owned data in structs. A struct holding &'a T forces a lifetime onto every container. Owning a String or Vec<u8> is usually simpler and rarely the bottleneck. When a type does carry a lifetime, give it a descriptive name ('src, 'conn); '_ and 'a are fine on simple functions.

Freeze after setup. Build with let mut, then rebind let data = data; so later code cannot mutate it by accident.

Keep Rc, Arc, Box, and RefCell out of public signatures. Take &T, &mut T, or T and let callers decide how to own things. A service that must be shared implements a cheap Clone by wrapping its state in Arc<Inner> internally.

4. Types and API design

Naming

| Item | Case | Notes | |---|---|---| | Types, traits, enum variants | UpperCamelCase | Acronyms are one word: Uuid, HttpClient | | Functions, methods, modules, locals | snake_case | | | Constants, statics | SCREAMING_SNAKE_CASE | | | Type parameters | short, from the main bound | T, R: Read, W: Write | | Lifetimes | short lowercase | descriptive when on a type |

  • No get_ prefix on getters: fn len(&self), fn name(&self) -> &str, fn name_mut(&mut self). Use bare get only when there is one obvious thing to get (Cell::get). Related: set_foo(T), take_foo(), with_foo(|f| ..) for closure access, and unsafe _unchecked siblings for checked accessors.
  • Conversion prefixes signal cost and ownership: as_ (free, borrow to borrow), to_ (expensive, borrow to owned or borrow to borrow), into_ (consumes self). Put mut in type order: as_mut_slice, not as_slice_mut.
  • Constructors: new (primary), with_capacity / new_with_foo (variants), try_new (fallible), from_foo (conversion; pair important ones with a From impl). I/O resources use domain verbs: File::open, TcpStream::connect.
  • iter / iter_mut / into_iter return types named Iter / IterMut / IntoIter; keys() returns Keys.
  • Error types read verb-object-error: ParseBoolError, not BoolParseError.
  • Avoid weasel suffixes Service, Manager, Factory, Helper, Util. Name the thing it is (Bookings, Dispatcher, FooBuilder). Don't repeat the module name in the type (foo::Id, not foo::FooId). Avoid abbreviations an outsider wouldn't know.
  • If you implement len(), implement is_empty().
  • Feature names are bare: std, serde, alloc, never use-std or no-std.

Model the domain with types

  • Newtypes for semantic distinctions: struct Miles(f64) and struct Kilometers(f64) cannot be mixed, where a type alias can. Add #[repr(transparent)] when layout matters, and derive or forward the traits you need (derive_more cuts the boilerplate). A newtype that encodes an invariant (Month) validates in a fallible constructor (TryFrom, FromStr, try_new) and never offers an infallible From from the raw type.
  • No bool parameters in public or shared signatures. report(true) is unreadable at the call site and cannot grow a third mode. Use a two-variant enum (Verbosity::Summary, Verbosity::Full) or two methods (report(), report_verbose()): Widget::new(Size::Small, Shape::Round), not Widget::new(true, false). The only exception is a private helper whose call sites sit a few lines away. When refactoring, an existing bool parameter is one of the smells to remove, not a detail to preserve.
  • Named fields over tuples, except single-field newtypes and very local code.
  • Option for absence, never sentinels such as -1 or "". Data-carrying enums instead of "field X is only meaningful when Y is set".
  • Use the strongest std type: PathBuf / &Path for paths, Duration for spans, NonZeroU32 internally only (keep plain integers at API boundaries).
  • Independent flags use the bitflags crate; an enum models exactly one choice.

Derive and implement the standard traits eagerly

Downstream crates cannot add std traits to your types (orphan rule), so you must. Derive whenever every field supports it: Debug (on every public type; hand-write it to redact secrets), Clone (except unique resources and key material), Copy (only small bitwise-copyable types, because the compiler copies silently), PartialEq / Eq, PartialOrd / Ord (never ship PartialOrd without Ord when a total order exists), Hash (consistent with Eq), Default. Implement Display for anything meant to be read: error types, string-like wrappers. Add #[must_use] to pure functions and to types, such as errors and lazy values, whose discarding is a bug.

Implement From / TryFrom, never Into / TryInto directly (blanket impls give you those). Prefer from / into over as, which silently allows lossy casts. Implement FromStr rather than an ad-hoc from_str method, FromIterator and Extend on collections, and IntoIterator for both owned and borrowed forms. Implement Deref only on smart pointers and owning collections (Vec<T> to [T]), never to fake inheritance. Overload operators only for their mathematical meaning.

Don't put derivable trait bounds on struct definitions: #[derive(Clone)] struct Good<T>, not struct Bad<T: Clone + Debug>. Bounds belong on the impl blocks that need them.

If you hand-write Ord, hand-write PartialEq, PartialOrd, and Hash to agree with it (compare the same fields, ignore the same fields). A derived PartialEq next to a custom Ord breaks the std contract and misbehaves in BTreeMap and sort/dedup. Say in the docs which fields equality ignores.

Constructors, builders, defaults

  • Provide new() even when Default exists, and make them agree (one delegates to the other). Derive or implement Default so callers can write ..Default::default(); bag-of-options structs with public fields should always have it.
  • Up to two optional parameters: inherent constructors (new, with_a, with_b). Beyond that, or when construction validates or has side effects, add a builder: Foo::builder() returns FooBuilder, setters are named after fields (timeout(d), not set_timeout) and infallible, and build() -> Result<Foo, Error> does all validation including cross-field checks. Prefer non-consuming setters (&mut self -> &mut Self) unless build must move a value out.
  • A constructor with four or more loose arguments should group them into semantic types: Deposit::new(account, amount) rather than four &str / u64 parameters.

Visibility and evolution

  • Private by default. Fields stay private unless the struct is passive C-style data. Widen to pub(crate) or pub(super) before pub. Making an ite

Truncated for display — read the full file on GitHub.

Related Skills

View on GitHub
GitHub Stars1.6k
CategoryDevelopment
Updated7d ago
Forks81

Languages

Python

Trust signals

100/100

From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.

No cautions