clean-rust
Skill7 starsChanged 18 days agoMITView on GitHub
Idiomatic Rust 2024 rules: ownership, error handling, async, unsafe, traits, iterators, clippy.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- FilenCloudDienste/filen-rstheir own version, first seen Oct 9, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- FilenCloudDienste/filen-rs★ 104same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- askamaSkill
- cargo-toml-optimizationSkill
- clean-codeSkill
- clean-flutterSkill
- clean-shellSkill
- docs-syncSkill
- emulator-testingSkill
- maudSkill
- mcp-statelessSkill
- name-checkSkill
- ratatui-patternsSkill
- terminal-uxSkill
- todoSkill
- ux-patternsSkill
- webapp-testingSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 34
No sentinel returns (-1, "") for failure.
- Very specific: line 42
Every .clone() is an explicit cost, never one to silence the borrow checker.
- Very specific: line 86
No redundant #[cfg] inside a module that's already cfg-gated at its mod declaration.
- Very specific: line 97
Never truncate a String at a raw byte index.
- Very specific: line 109
Every lint suppression is justified: #[expect(lint, reason = "...")] over bare #[allow].
- Distinctive: line 116
No formatting-only churn in feature PRs: the diff should match the description.
- Very specific: line 120
Default posture: unsafe_code = "forbid" until a concrete need exists.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Clean Rust
Rust-specific conventions for writing, reviewing, and refactoring. The core rules below always apply. Load the one reference file matching the task's domain; don't load them all.
If the clean-code skill is installed, its language-agnostic principles (function size, naming hygiene, comment discipline) still apply. Where generic advice conflicts with Rust idiom, this skill wins. Classic examples: "prefer exceptions over error codes" maps to Result, never panics; "replace switch with polymorphism" maps to exhaustive match, which in Rust is a feature, not a smell.
| Task touches | File |
| Error types, propagation helpers, failure semantics, retries | references/error-handling.md |
async/await, tokio, select!, channels, spawned child processes | references/async.md |
| Threads, atomics, lock ordering, drop order | references/concurrency.md |
| Process-global mutable state (env vars, global overrides) across threads | references/edition-2024.md |
| Public API shape, builders, newtypes, typestate, serde | references/api-design.md |
Any unsafe block, FFI, raw pointers, exported symbols | references/unsafe.md |
| Writing tests or debugging test infrastructure | references/testing.md |
| Hot paths, allocations, string building, hasher choice | references/performance.md |
Benchmarks, criterion, #[bench] | references/performance.md |
| Secret files on disk, keys and tokens at rest | references/security.md |
| Logging, tracing spans, log levels | references/observability.md |
| Edition 2024 migration or 2024-specific behavior changes | references/edition-2024.md |
Errors
Result+?everywhere; no.unwrap()/.expect()outside tests and one-time init of hand-audited literals (LazyLock<Regex>on a fixed pattern). The rare justifiedexpectcarries a message naming the invariant that makes it safe.- No sentinel returns (
-1,"") for failure.Option<T>for absence that isn't an error. thiserrorfor library errors (callers can match),anyhow/eyreat the application boundary. NeverBox<dyn Error>orStringas a library error type.- Bind a
Resultonce withmatch/if let/let-else, never.is_ok()followed by separate access or a downstream.unwrap(). - Pick one failure semantics per operation: best-effort (warn per item,
Ok(())at end) or fail-fast (propagate first error). Never warn-per-item and then fail at the end; that surprises exit-code consumers.
Ownership & Borrowing
- Borrow by default:
&str,&[T],impl AsRef<Path>for read-only params. Never&Stringor&Vec<T>. - Every
.clone()is an explicit cost, never one to silence the borrow checker.Cow<'_, str>when allocation is conditional;Arc<T>for shared ownership across threads. - Unsigned types for counts, ports, sizes:
jobs: u32, notjobs: i32. The type documents the constraint. - Let lifetime elision work. A struct with 3+ lifetime params is a design smell: consider owned data.
Naming
RFC 430 casing (snake_case items, CamelCase types, SCREAMING_SNAKE_CASE consts) plus semantic method prefixes:
| Prefix | Contract |
into_ | consumes self, returns owned |
as_ | cheap borrowed view, no allocation |
to_ | possibly expensive, may allocate |
try_ | fallible variant returning Result |
is_ / has_ | boolean query |
with_ | builder-style configuration |
_mut suffix | mutable variant |
- No
get_for plain field access:fn name(&self) -> &str. Reservegetfor fallible lookups (get(key) -> Option<&V>). - Concrete names:
cmdnotexecutor,typnotty. Single letters only in tiny scopes. - Names must not lie about the data: no
_listsuffix on aHashMap, noowned_on a&str. - Wrap semantically distinct primitives in newtypes (
UserId(u64),OrderId(u64)) so the compiler catches argument swaps.
Control Flow & Iterators
matchon enums stays exhaustive: no_ => {}arm in dispatchers. Spell out every variant: an empty arm with a comment beats a wildcard that silently swallows the next variant added.let-elsefor guard-style early returns; it reads linearly where.ok().and_then(...).is_some_and(...)chains don't.- Iterator chains for value-producing pipelines. When the result is discarded (
let _ = ...), the chain is control flow in disguise; write theforloop. - Return
impl Iterator<Item = T>when callers consume sequentially; premature.collect()allocates for nothing. - Group one logical filter into one
.filter_map()closure (using?and earlyreturn None) instead of fragmented.filter_map().filter().filter_map()chains. - Don't reimplement the standard library:
split_once('=')over.splitn(2, '=').collect(),unwrap_or_default()overunwrap_or_else(|| T::default()). Don't reimplement an in-scope codebase helper either: a fix for a type or signature error lets the existing helper's return type flow through the signature (return the helper'sPathBufwhere the call site asked for a name) instead of mirroring the helper's body under new return values. Reuse in this order: an in-scope helper, an existing generic config knob, a sibling-code precedent, the standard library, a well-known crate, then hand-rolled; a hand-rolled fallback is fine only when the standard option has a concrete documented limit, and carries an in-code comment naming it. - One fluent chain over three named single-use intermediates; declare variables next to first use, not at the top of the function.
- Prefer chained builder calls over
let mutnamed variables for linear configuration sequences. When each step returnsSelf, naming the intermediate creates a variable-mixing bug class: two similar blocks in one function, copy-paste producesfirst.configure()wheresecond.configure()was meant, and the compiler accepts it. Chaining makes each pipeline self-contained.
Types & Traits
- Derive liberally where semantically correct:
Debugon effectively every public type;Clone,PartialEq,Eq,Hashas meaning allows (ifa == bthenhash(a) == hash(b)must hold);Defaultwhen a meaningful empty value exists. dyn Traitonly for genuinely open sets (plugins, user extension). A closed set of a few known types is an enum: simpler and faster.- Typed
#[derive(Deserialize)]structs overserde_json::Valueindexing: field typos become compile errors instead of silent runtimeNones. impl Traitin argument position for simple bounds;whereclauses when signatures grow. Don't make a function generic when one concrete type is ever used.
Platform & cfg
- Short forms:
#[cfg(windows)],#[cfg(unix)], not#[cfg(target_os = "windows")]. - No redundant
#[cfg]inside a module that's already cfg-gated at itsmoddeclaration. - Helpers used on only one platform must be gated (or
#[allow(unused)]) so every target compiles warning-free. Cross-platform CI fails on the platform that doesn't use them. - An
#[allow(dead_code)]/#[allow(unused)]on an item whose only callers sit incfg-gated modules is load-bearing (the item is live on those platforms, the allow keeps the others warning-free); in a binary cratepubdoes not exempt an item fromdead_code; a sibling's same-file allow is the precedent signal; on such an item keep the bare#[allow]:#[expect(dead_code)]firesunfulfilled_lint_expectationson the platforms where the item is live.
Strings & IO
static RE: LazyLock<Regex>for compiled-once regexes; never compile inside a loop or per call.- ASCII character classes (
[a-zA-Z0-9]) over POSIX[[:alnum:]]: the explicit range reads unambiguously and ports to engines where POSIX classes are locale-sensitive (in Rust'sregexthey're ASCII-only either way). - Inline format args:
debug!("found {name:?}"), notdebug!("found {:?}", name). - Forward child-process output with
io::stdout().write_all(&output.stdout)?;println!mangles encoding and panics on broken pipes. - Long-form flags when spawning external commands (
--force, not-f): the call site is its own documentation. - Never truncate a
Stringat a raw byte index.&s[..n]panics mid-codepoint on non-ASCII; the "safe"s.get(..n).unwrap_or(s)is worse: it returns the whole string whennlands mid-codepoint, so oversize input escapes the cap. Usefloor_char_boundary(n)or walkchar_indices().
Modules & Visibility
- Private by default;
pub(crate)for internal sharing. Fields with invariants stay private behind methods. - Organize by domain (
order/,user/), not by kind (models/,services/). pubitems before private helpers in a file;usestatements at the top, never inside functions.
Comments & Docs
- Comments explain why: hidden constraints, upstream bug links, invariants the reader can't see. Never what the next line does. Delete commented-out code; git remembers.
- Public APIs get
///docs with# Examples(runnable), plus# Errors,# Panics,# Safetywhen applicable. - Every lint suppression is justified:
#[expect(lint, reason = "...")]over bare#[allow]. - Non-obvious string input formats get documented above the signature (
// accepts "name", "name:tag", or "ns/name:tag").
Tooling
cargo fmtandcargo clippyclean in CI, no exceptions. Fix warnings; don't blanket-suppress.- Libraries:
#![warn(clippy::pedantic)]and selectively allow, never a blanket#![allow(clippy::all)]. Considerunwrap_used/expect_usedatwarn(seereferences/testing.mdfor keeping test code exempt). - No formatting-only churn in feature PRs: the diff should match the description.
Unsafe
Default posture: unsafe_code = "forbid" until a concrete need exists. When it does exist, every block carries a // SAFETY: comment and stays minimal; full discipline in references/unsafe.md.
Process
- One PR = one focused change; formatting fixes, new workflows, and features travel separately.
- A technically-correct change that breaks a user contract (CLI flags, config keys, env semantics) keeps the legacy path, ships the new one alongside, and milestones the removal for the next major. Warn only on genuine old-vs-new conflicts: end users on only-old or only-new see nothing. A change to an existing default ships opt-in/off by default, unless the old behavior is broken for every user (then on by default with an opt-out) or the behavior it breaks is incidental/unsupported (then it ships as the new default without an opt-in).
- Feature-detect external tools by parsing
--versionoutput; where that's unreliable, sniff for specific stderr messages. Either way, probe with plain.output()so a failed probe stays non-fatal. - Unexpected or unparseable external-tool output fails loud, with a message naming the command, never a silent fallback; a clean skip is only for a known not-applicable state (missing binary, wrong OS), distinct from "ran, found nothing" and from failure.
Pre-Submit Checklist
- No
.unwrap()outside tests/compile-time constants; no.is_ok()+ separate access - Test expectations hand-computed, never recomputed by the code under test; no re-pinned paths
- Failure semantics consistent: best-effort or fail-fast, not mixed; cleanup always runs
- Borrows by default; no
&String/&Vec<T>; every.clone()intentional - Method prefixes honest (
into_/as_/to_/try_); newtypes for swappable primitives - No
_ => {}in enum dispatchers;let-elseover combinator gymnastics impl Iteratorover prematureVec; no reimplementations of stdlib or in-scope helpers- Typed
DeserializeoverValue;dynonly for open sets #[cfg(windows)]short form; single-platform helpers gated for all-target CILazyLockregexes; inline format args;write_allfor child output- Comments say why; suppressions carry
reason; no commented-out code - fmt + clippy clean; no formatting churn outside the change
- One focused change per PR; breaking changes keep the legacy path, removal milestoned
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

