rust
Skill1 starsChanged 3 months agoMITView on GitHub
ALWAYS load for .rs files, Rust code, Cargo projects, or async/tokio work. Trigger vocabulary includes borrow checker, lifetime, ownership, trait bounds, async/await, futures, send/sync, pin, type-state, newtypes, anyhow, thiserror, axum, sqlx, reqwest, the tracing crate, tracing-subscriber, EnvFilter, winnow, parser combinators, clap, cargo workspace, criterion, insta, proptest — even when the user doesn't say "Rust" explicitly.
38 other projects use a version of this.
Adopted by
Most recently taken up by
- elodin-sys/elodintheir own version, first seen Nov 9, 2026
- crbnos/carbontheir own version, first seen Nov 7, 2026
- lawless-m/claude-skillstheir own version, first seen Nov 6, 2026
- inference-gateway/skillstheir own version, first seen Nov 5, 2026
- alexjmiller5/agent-config-publictheir own version, first seen Nov 1, 2026
- its-ash/skillstheir own version, first seen Nov 1, 2026
Used with
Projects with it also use node, golang and 6 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
mcollinaSkillnode
Provides domain-specific best practices for Node.js development with TypeScript, covering type stripping, async patterns, error handling, streams, modules, testing, performance, caching, logging, and more. Use when setting up Node.js projects with native TypeScript support, configuring type stripping (--experimental-strip-types), writing Node 22+ TypeScript without a build step, or when the user mentions 'native TypeScript in Node', 'strip types', 'Node 22 TypeScript', '.ts files without compilation', 'ts-node alternative', or needs guidance on error handling, graceful shutdown, flaky tests, profiling, or environment configuration in Node.js. Helps configure tsconfig.json for type stripping, set up package.json scripts, handle module resolution and import extensions, and apply robust patterns across the full Node.js stack.
Also in 3 projects that use this
rogue-dev-studioSkillgolang
Go development environment. Use when the project needs this capability or the user / team manifest asks for it. Use for specialized `golang` work when listed in TEAM.yaml or explicitly requested.
Also in 3 projects that use this
trotsky1997Skillast-grep
Guide for writing ast-grep rules to perform structural code search and analysis. Use when users need to search codebases using Abstract Syntax Tree (AST) patterns, find specific code structures, or perform complex code queries that go beyond simple text search. This skill should be used when users ask to search for code patterns, find specific language constructs, or locate code with particular structural characteristics.
Also in 3 projects that use this
OpenAISkillnextjs
Next.js App Router expert guidance. Use when building, debugging, or architecting Next.js applications — routing, Server Components, Server Actions, Cache Components, layouts, middleware/proxy, data fetching, rendering strategies, and deployment on Vercel.
Also in 3 projects that use this
SentrySkillcreate-pr
Create pull requests following Sentry conventions. Use when opening PRs, writing PR descriptions, or preparing changes for review. Follows Sentry's code review guidelines.
Also in 3 projects that use this
GoogleSkilltdd
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
Also in 5 projects that use this
Other versions38 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 38
- ccusage/ccusage★ 19ksame name
- ryoppippi/ccusage★ 19ksame name
- microsoft/fast★ 9.7ksame name
- pgdogdev/pgdog★ 5.6ksame name
- crbnos/carbon★ 2.8ksame name
- elodin-sys/elodin★ 553same name
- Mindrally/skills★ 275same name
- Moosync/Moosync★ 264same name
- wado-lang/wado★ 120same name
- databricks-solutions/apx★ 91same name
28 more not listed
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 repository18 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- clickhouseSkill
- distributed-systemsSkill
- flink-javaSkill
- flink-pythonSkill
- flink-sqlSkill
- gitSkill
- grokSkill
- java-prod-skillSkill
- javaSkill
- mermaid-skillSkill
- openapi_docSkill
- postgresSkill
- python-numericsSkill
- pythonSkill
- pytorchSkill
- reactSkill
- sqlSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
17 rules. 0 are common, 9 are distinctive, and 8 are very specific to this project.
0
Common
Most of the 31 edited versions we read keep these.
9
Distinctive
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Use Rust 2024 edition. Verify feature availability when uncertain.
- Distinctive: line 23
Delete aggressively — git preserves history
- Distinctive: line 25
No global variables or shared mutable state
- Distinctive: line 33
When a function exceeds 30-40 lines, look for natural phases to extract: validation, side-effect, verification.
- Distinctive: line 49
When Ritchie flags long-function, deep-nesting, or too-many-args, these are the canonical shapes:
- Distinctive: line 68
When a file grows past the soft limit, ask: *"what are the 2–3 distinct concerns this file mixes?"* Each concern becomes a sibling module.
- Distinctive: line 89
Reader test: read any function's signature aloud.
- Distinctive: line 140
When clarity is sacrificed for a measured hot path, comment the why (which benchmark, what gain).
- Distinctive: line 193
Use dynamic dispatch only when you need heterogeneous collections or type erasure:
- Very specific: line 225
Always .context("...") on the ?
- Very specific: line 270
When writing byte-indexed loops, slicing &str, or converting bytes to chars, read references/utf8.md (the push_char_at pattern, byte-loop …
- Very specific: line 373
Structured concurrency: prefer JoinSet over fire-and-forget tokio::spawn whose handle is dropped (silent error swallowing).
- Very specific: line 421
ALWAYS set timeouts — connect AND request.
- Very specific: line 508
Behavioral over interaction testing: Verify state and output, not method calls.
- Very specific: line 523
Every Rust project ships clippy.toml + [lints] config aligned to the skill's rules.
- Very specific: line 532
Read references/checklist.md before committing or completing a review.
- Very specific: line 539
Every service log line has trace_id / correlation_id
“Common” is measured: kept by at least 6 in 10 of the 31 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Version Policy
Use Rust 2024 edition. Verify feature availability when uncertain.
Philosophy
- Modular, clear, readable code with care for performance and efficiency
- Obvious over clever — if it needs a comment to be readable, simplify it first
- Ownership-first thinking, then borrowing
- Immutable by default —
letoverlet mut; iterator chains over mutating loops; localmutonly for buffers, builders, and accumulators - Make illegal states unrepresentable (newtypes, type-state, sum types)
- Each module does ONE thing well — clear, minimal interfaces
- Depend on abstractions, not implementations
- Compose small pieces into larger systems
- Flat over nested (early returns,
?operator,let-elseguard clauses) - Guard clauses over
expect/unwrap— return errors, don't panic - Solve the actual problem, not a generalized version
- Delete aggressively — git preserves history
debug_assert!for invariants- No global variables or shared mutable state
Function Discipline
- Max ~40 lines per function, 3-4 parameters, 2-3 nesting levels (deliberately looser than the global 2-level default — a match arm inside a loop is idiomatic Rust)
- Single responsibility: one reason to change
- Early returns over nested conditionals
When a function exceeds 30-40 lines, look for natural phases to extract: validation, side-effect, verification. Each phase becomes a self-naming helper. Linear flows (e.g., a transactional commit) can stay together if extracting would just be passing a context bag around.
Comments are for the person reading and reviewing the code: the public API, anything not obvious, and the reason behind the code as a plain sentence (no WHY: label). Keep the work plan out of comments — no phases, plan steps, ticket numbers, or words like for now / temporary / later. Those go stale and someone has to find and delete them. Unfinished work belongs in plan.md.
Any function that does real work gets a header comment above it: one line saying what it is for (this matters most for public APIs), plus its big-O for compute AND memory, derived DFS through the call tree: callees count toward the bound, and recursion counts its activation frames as memory. Tiny obvious helpers and trivially O(1)/O(n) glue are exempt. The big-O lives in the header, never on a line inside the body.
/// Merge sorted layers into one ordered run.
/// O(n log k) compute, O(n) memory — k-way heap merge over n total items.
fn merge_layers(layers: &[Layer]) -> Vec<Node> { /* ... */ }When you change a function, bring its comments to this same bar: keep the comments already there, fix any your change made wrong, and add the missing header / big-O / reason. Do this only for code you add or change, not for nearby code you did not touch.
Three Refactor Shapes (north star)
When Ritchie flags long-function, deep-nesting, or too-many-args, these are the canonical shapes:
| Pattern | Trigger | Fix |
| 1 — Phase Extraction | Function exceeds 30-40 lines | Extract validation / side-effect / verification phases into self-naming helpers; the entry fn reads as a 5-line narrative |
| 2 — Params Struct + Newtypes | 4+ args appearing in 3+ signatures (rule of three — don't preemptively bag) | Merge cohesive params into reused structs (13 args → 3) |
| 3 — Hoist Invariant Work + AHashMap Indexing | Loop-invariant work recomputed per iteration, or find() scans inside loops | Hoist invariants out of the loop; pre-build AHashMap indexes — O(N²) → O(N) |
The three patterns cascade: too-many-args produces long functions; long functions produce deep nesting; deep nesting hides quadratic work. Fix the signature first and the rest unravels. Full before/after pairs (with real codebase examples) live in references/decomposition.md.
Module Discipline
Per-file analog of Function Discipline. Long files almost always mix concerns — split by responsibility, not by type. Don't group every struct into types.rs and every trait into traits.rs; group code by what it does.
- Soft limit: ~800 source LOC per file (excluding tests).
- Hard smell beyond ~1500. That's not "Rust is verbose," it's a module that should be three.
- Tests inside a
#[cfg(test)] mod testsblock don't count toward the limit — they're allowed to be long. - Integration test modules are exempt for the same reason. Keep them as topic files under
tests/it/(modules of one entry binary), not astests/<topic>.rsfiles at the root — each root file is a separate binary that re-links the whole crate. See Testing →references/integration-test-layout.md.
When a file grows past the soft limit, ask: "what are the 2–3 distinct concerns this file mixes?" Each concern becomes a sibling module. Honest signals that a split is overdue:
- The module-level doc comment names more than one responsibility.
- Section banners (
// ── Foo ────────) divide the file into 200+ LOC blocks — those blocks want to be files. - A struct + its closely-related ADTs + the algorithm operating on them is a natural module boundary; extract them as a sibling module, leave the rest behind.
This is review-time guidance, not a build gate — no clippy lint exists for per-file LOC, and a script gate creates more friction than benefit at this scale. Ritchie checklist item #20 catches it before LGTM.
No Magic Constructs
Idiomatic Rust uses powerful features — Deref coercion, blanket impls, generics, ?. The rule isn't avoidance; it's deliberateness. If a construct hides non-obvious behavior, comment the why. If a reader can't follow it without footnotes, simplify.
| Construct | Idiomatic — use freely | Stop and comment / simplify |
Deref / AsRef / Into | Smart pointers (Arc<T>, Box<T>), DTO conversions | Hidden expensive conversions, unexpected method resolution |
Blanket impls (impl<T: Bound> Trait for T) | When the bound captures a real category | Just to avoid two explicit impl blocks |
| Generic signatures | 1-3 type params, single where line, reads aloud | >3 type params, HRTBs, multi-line where clauses without comments |
macro_rules! | After 3+ identical-shape duplications AND traits/generics can't unify the shape | First copy of a pattern; anything a function would handle; validated newtype families (use phantom-type generic — see Newtype Wrappers) |
| Proc macros | Crate authors, framework attrs (#[derive], #[tokio::main]) | Application logic |
unsafe | When truly required (FFI, no safe alternative, measured perf) | ALWAYS with // SAFETY: <invariant> comment explaining the why |
| Magic numbers / strings | Never | Use named const with units in the name (TIMEOUT_SECS) |
Reader test: read any function's signature aloud. If it requires footnotes, leave a comment or simplify.
Performance
- Correct first, then clear, then fast
- Measure before optimizing — intuition is often wrong
- Optimize hot paths only — 90% of code doesn't matter
- Simple code is often fast code (cache-friendly, predictable)
Allocation Discipline
Allocations are not free. In hot paths they dominate.
// PREFER: borrow over own
fn parse(input: &str) -> Result<Token> // not String
// PREFER: Cow when output is sometimes-owned
fn normalize(s: &str) -> Cow<'_, str> // borrow when no change, own only on edit
// PREFER: iterators that collect once
let total: u64 = items.iter().map(|i| i.price).sum();
// AVOID: cloning in a loop
for s in &strings {
process(s.clone()); // why clone?
}
// PREFER: pre-size collections you know the capacity of
let mut buf = String::with_capacity(items.len() * 16);
// PREFER: stack collections for bounded sizes (`smallvec`, `arrayvec`)
let mut codes: SmallVec<[u8; 8]> = SmallVec::new();
// PREFER: reuse buffers across iterations
let mut scratch = Vec::with_capacity(1024);
for input in inputs {
scratch.clear();
encode_into(input, &mut scratch)?;
write(&scratch)?;
}Hot Path Discipline
- Profile first (
cargo flamegraph,samply,tokio-consolefor async). Don't guess. #[inline]on small, frequently-called functions across crate boundaries — but only after measurement.- Lookup tables over computation when the input space is small (see the hex-encoding lookup table in
references/tracing.md). - Lock-free where possible:
AtomicUsizecounter, thread-local RNG, append-only logs. - Minimize work inside locks — read, copy out, drop, then process.
When clarity is sacrificed for a measured hot path, comment the why (which benchmark, what gain).
Hash Maps
Use ahash::AHashMap and ahash::AHashSet for all in-memory hash maps. ~2-3× faster than std::collections::HashMap (AES-NI on x86_64), and randomly keyed per instance, which raises the bar against HashDoS but is not a cryptographic guarantee like SipHash — do not rely on it for maps keyed by hostile input whose iteration order is observable (std's SipHash default is right for those).
use ahash::{AHashMap, AHashSet};
let mut index: AHashMap<&str, &Row> = AHashMap::new();
let mut seen: AHashSet<UserId> = AHashSet::new();One carve-out: for stable hashes (content-addressed storage, persistent dedup keys serialized to disk), AHashMap is wrong — its output is not stable across versions. Use SHA-256 or xxhash for those. For in-memory HashMap-style use, AHashMap is the default.
Mechanically enforced by assets/clippy.toml which disallows std::collections::HashMap and HashSet with a migration message.
Ownership & API Design
Borrow by default; own only when storing; Cow for sometimes-owned. Return owned values when creating; accept impl Trait (e.g. impl Iterator<Item = i32>) for flexibility.
| Situation | Use |
| Read-only access | &str, &[T], &T |
| Need to store/own | String, Vec<T>, T |
| Sometimes owned, sometimes borrowed | Cow<'_, str>, Cow<'_, [T]> |
| Shared ownership across threads | Arc<T> |
| Shared ownership single-threaded | Rc<T> |
| Heap-allocated trait object | Box<dyn Trait> |
Trait Design
Small, focused traits — compose via supertraits.
trait ReadRepo: Send + Sync {
async fn find(&self, id: UserId) -> Result<Option<User>>;
}
trait WriteRepo: Send + Sync {
async fn save(&self, user: &User) -> Result<()>;
}
trait UserRepo: ReadRepo + WriteRepo {}
impl<T: ReadRepo + WriteRepo> UserRepo for T {}Static dispatch is the default (zero-cost, monomorphized):
fn process(repo: &impl ReadRepo) -> Result<()>Use dynamic dispatch only when you need heterogeneous collections or type erasure:
fn handlers() -> Vec<Box<dyn Handler>>Associated types vs generic parameters:
trait Iterator { type Item; } // ONE natural choice per impl
trait From<T> { fn from(value: T) -> Self; } // a type can impl multiple timesType-State Pattern
Encode state transitions at compile time — invalid transitions don't compile. Put state-specific data inside the state struct, not as Options on a single struct.
pub struct Order<S> { id: OrderId, items: Vec<Item>, state: S }
pub struct Draft;
pub struct Confirmed { confirmed_at: DateTime<Utc> }
impl Order<Draft> {
pub fn confirm(self) -> Order<Confirmed> { /* move data into the next state */ }
}Anti-pattern: payment_id: Option<PaymentId> on a single struct + .expect("typestate invariant") at access. Defeats the point — runtime unwraps are exactly what type-state is meant to eliminate. Full worked example in references/newtypes.md.
Error Handling & Guard Clauses
anyhowfor binaries;thiserrorfor libraries.- Always
.context("...")on the?operator at fallible boundaries. unwrap()ONLY in tests — never in production code.unwrap_or/unwrap_or_defaultfor safe defaults.bail!/ensure!validate at boundaries;let-else,?, and earlyreturn Err(...)keep the happy path unindented and give callers aResultthey can handle.
use anyhow::{bail, Context, Result};
fn load_user(path: &Path, id: UserId) -> Result<User> {
let content = fs::read_to_string(path).context("failed to read user db")?;
let db: Db = serde_json::from_str(&content).context("failed to parse user db")?;
let Some(user) = db.users.get(&id) else {
bail!("user {id} not found");
};
Ok(user.clone())
}When expect is acceptable:
- Tests (
#[cfg(test)],tests/,benches/) — clippy auto-allows viaallow-expect-in-tests = trueinclippy.toml(paired with the workspace lintexpect_used = "warn") - Provably infallible: immediately after an
is_some()/is_ok()check you just did - Process startup (before serving requests) where panic = correct behavior
Library Errors (thiserror)
#[derive(Debug, thiserror::Error)]
#[non_exhaustive] // adding a variant stays non-breaking
pub enum LayoutError {
#[error("cycle detected involving node {0}")]
Cycle(NodeId),
#[error("invalid diagram source")]
Parse(#[from] ParseError),
}#[non_exhaustive]on public error enums — callers can't exhaustively match, so new variants don't break them.- Boundary rule: library crates export typed errors; the binary wraps them in
anyhowwith.context(). - Parsers: error types carry spans/byte offsets for diagnostics (see
references/winnow.md).
UTF-8 / Codepoint Discipline
Never index bytes out of a &str and cast to char in a byte loop (bytes[i] as char) — every high byte (0x80–0xFF) is reinterpreted as a Latin-1 codepoint and re-encoded, silently corrupting Arabic, CJK, and emoji. Push the full codepoint and advance by ch.len_utf8() instead. Truncation: &s[..N] panics on a multi-byte boundary — truncate by chars (s.chars().take(N)), never by bytes.
When writing byte-indexed loops, slicing &str, or converting bytes to chars, read references/utf8.md (the push_char_at pattern, byte-loop safety conditions, and the non-ASCII round-trip property test).
Newtype Wrappers
Private fields with new() validation + accessor methods. Public fields defeat encapsulation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositiveAmount(u64);
impl PositiveAmount {
pub fn new(value: u64) -> Result<Self> {
if value == 0 { bail!("amount must be positive"); }
Ok(Self(value))
}
pub fn value(&self) -> u64 { self.0 }
}
#[must_use] // Result is must_use by default; annotate non-Result returns where ignoring is a bug
pub fn checked_total(items: &[Item]) -> Option<Money>For families of validated string ids (IssueKey, Workspace, RepoSlug, ...), use the phantom-type Id-over-Spec pattern, not macro_rules! — all shared impls live in one place as plain types and traits, and each alias stays a distinct type. Read references/newtypes.md (includes the serde try_from validation wiring).
Serde Patterns
Default to snake_case wire format — matches Rust struct fields naturally, no rename_all needed, no implicit translation. Use deny_unknown_fields on input types to reject unexpected keys at the boundary. camelCase only when a non-Rust client (typically JS/TS frontend) requires it. For tagged enum events, attribute defaults, and the camelCase exception, read references/serde.md.
Where to Assert — Boundaries vs Invariants
Pick the assertion tool by who is at fault when it fires:
| Where | Tool | Reasoning |
Constructor (Type::new) | bail!/ensure! (returns Err) | Caller passed bad data — they need to handle it |
| Public API entry point | bail!/ensure! | Validate untrusted input at the boundary |
| Inside a private fn, after upstream validation | debug_assert! | Either we trust validation, or we have a bug |
| Pre-/post-condition on a private op | debug_assert! | Documents invariant; release skips the cost |
| Security-critical (auth, bounds, capability) | assert! (always-on) | Violation in release is worse than crashing |
Provably infallible (e.g., is_some() then unwrap) | expect("reason proven above") | Local proof; explicit message documents it |
| Return value that must not be ignored | #[must_use] on the fn | Result is must_use by default; annotate #[must_use] only on non-Result returns where ignoring is a bug |
Rule of thumb: at the boundary, the caller is responsible — return Result. Inside, we are responsible — debug_assert!. The only release-time panic is for invariants whose violation is worse than a crash (security, memory safety). Cheap O(1) invariant checks may use assert! (kept in release, effectively free); expensive checks (O(n) scans, conservation sums over collections) use debug_assert!.
pub fn transfer(from: &mut Account, to: &mut Account, amount: u64) -> Result<()> {
debug_assert!(amount > 0, "amount must be positive");
debug_assert!(from.id != to.id, "cannot transfer to same account");
let total_before = from.balance + to.balance;
from.withdraw(amount)?;
to.deposit(amount);
debug_assert_eq!(from.balance + to.balance, total_before, "balance must be conserved");
Ok(())
}Concurrency (tokio)
Shared State — Climb to the Simplest Rung
For shared mutable state, climb from simplest to most complex: ownership transfer (channels) → immutable share (Arc) → atomic primitive (AtomicUsize/AtomicBool) → lock (tokio::sync::Mutex/RwLock) → lock-free data structure (dashmap, crossbeam, arc-swap). Stop at the simplest rung that works. AtomicUsize is one type; Mutex<usize> is two types + lock-poisoning + an unwrap — lock-free is simpler, not more complex, when the state is a single cell.
For atomics, memory orderings, thread-local for hot paths, Send/Sync mental model, deadlock checklist, per-rung examples (channels, atomics, locks), and the lock-free crates table, read references/concurrency.md.
Right Primitive for the Job
use tokio::sync::{Mutex, RwLock, Semaphore, mpsc, oneshot};
use tokio::time::timeout;
Mutex<T> // Exclusive async access
RwLock<T> // Multiple readers OR one writer
Semaphore // Limit concurrent operations
mpsc::channel(n) // Bounded multi-producer, single-consumer
oneshot::channel() // Single value, single use
timeout(dur, future) // ALWAYS wrap external calls
// Parallel with join
let (users, orders) = tokio::join!(fetch_users(), fetch_orders());Channels are always bounded (mpsc::channel(n), never unbounded_channel) — backpressure over unbounded memory growth. CPU-bound work goes to rayon (par_iter) or tokio::task::spawn_blocking, never the async executor. For the JoinSet + Semaphore bounded-concurrency pattern, read references/async-rust.md.
Inside an async runtime, prefer tokio::sync::* over std::sync::* — the std primitives can block the executor if a critical section is long. Short, await-free critical sections on std::sync::Mutex are acceptable when documented.
Async Rust
Most async-Rust complexity is library complexity, not application complexity. Pin, manual Future impls, Box::pin — these exist for runtime/middleware authors. Application code is async fn + .await + tokio::sync::* and that covers ~95% of cases.
Avoid Pin<Box<dyn Future>> in application code — it's the right pattern for tower::Service::Future and similar framework interfaces, suspect anywhere else. The avoidance has a corollary: safely conceal, don't replace. Don't trade Pin for dyn everywhere (lost optimization), or wrap every async fn in indirection, or reflexively reach for Arc<Mutex<...>>. Use language and runtime features that already hide pinning without changing semantics — async fn, impl Future, tokio::spawn, axum::Router. They internally use Pin and expose clean types.
Application pitfalls that bite (with examples in references/async-rust.md):
.awaitwhile holding a lock → drop the guard before.awaittokio::spawnrequires'static→ cloneArc, move ownership; never borrow- Cancel safety —
select!/timeoutdrop the loser at any.await→ put the commit last block_oninside async deadlocks the runtime → usespawn_blockingfor sync workArc<Mutex<...>>cargo-cult → check the sharing ladder above first
Async traits: native async fn in trait has been stable since Rust 1.75 — default to it. Reach for #[async_trait] only when you need dyn-compatibility (heterogeneous trait objects), and accept the per-call heap allocation it adds.
Structured concurrency: prefer JoinSet over fire-and-forget tokio::spawn whose handle is dropped (silent error swallowing). JoinSet bounds child lifetimes to the parent's scope; aborting the set aborts in-flight tasks.
For full worked examples (lock-across-await, cancel-safety wrong-then-right, async traits, JoinSet + Semaphore for concurrent iteration, Stream vs Iterator), read references/async-rust.md.
Graceful Shutdown
Pattern: active-request counter + oneshot signal + drain timeout via tokio::select!. For canonical middleware + wiring, read references/graceful-shutdown.md. Never process::exit.
Resilience
Retries with exponential backoff for transient failures; timeouts on every external call; circuit breakers when one dependency can take down others. For canonical retry_with_backoff and circuit-breaker patterns, read references/resilience.md.
Parsers (winnow)
Writing or modifying parsers (winnow combinators, grammar work, text formats): read references/winnow.md FIRST — winnow 1.0's API (&mut input advanced in place, ModalResult, cut_err commitment) diverges hard from nom and from winnow ≤ 0.6, and stale habits don't compile. It also covers zero-copy &'src str ASTs, spans for diagnostics, and insta/proptest/conformance testing for parsers.
Project Structure
- Always
cargo add— never hardcode versions inCargo.toml. - Before adding a dependency, check
cargo tree -i <crate>— duplicates cause feature flag conflicts and lock file churn. edition = "2024"for every new package.- Pre-commit hooks (cargo-husky): any non-trivial project gets a
cargo-huskydev-dep +.cargo-husky/hooks/pre-commitrunningcargo fmt --checkandcargo clippy --workspace --all-targets -- -D warnings. Setup snippet, hook script, and edge cases inreferences/project-setup.md.
Workspaces (multi-crate)
[workspace]
members = ["crates/*"]
resolver = "3" # edition-2024 default (Rust 1.84+); virtual workspaces must set it explicitly
[workspace.package] # version/edition declared once; members inherit via `edition.workspace = true`
edition = "2024"
[workspace.dependencies] # shared deps declared once; members use `foo = { workspace = true }`- Lints live in
[workspace.lints.rust]/[workspace.lints.clippy]at the root, with[lints] workspace = truein every member crate. - Layering rule: lower crates never depend on higher (parse → layout → render → cli).
- Each crate exposes a minimal
pubsurface.
The directory tree and starter dependency lists live in references/project-setup.md.
Build Profiles
Two opposite goals, two profiles. Never opt-level = 1 everywhere — slower to compile than 0, slower at runtime than 3. Dev: your crates at 0, deps at 2, build-override at 0. Release: opt-level = 3 + thin LTO + codegen-units = 1 + panic = "abort". sccache can't cache incremental builds — on a single dev machine, incremental-without-sccache wins; reach for sccache only in CI. Creating a Cargo.toml, tuning compile times, adding SIMD, or writing benchmarks: read references/build-profiles.md.
HTTP Client (reqwest)
ALWAYS set timeouts — connect AND request. A connect timeout alone won't catch a slow body. For canonical client setup with fetch_json helper, retry integration, and error mapping, read references/http-client.md.
Web Framework (Axum)
Routes thin, business logic in services. AppState carries pool + clients. ALWAYS layer TimeoutLayer and TraceLayer. For the canonical handler/state/router pattern with custom error type and IntoResponse, read references/axum.md.
Database (sqlx)
Compile-time-checked queries (query!/query_as!); explicit transactions for multi-statement work; pool sizing based on concurrency, not "more is better". For canonical pool setup, transaction patterns, and migration discipline, read references/sqlx.md.
Logging (tracing)
In network services and long-running daemons, every log line carries a trace_id or correlation_id.
For web services: read X-Correlation-Id from the incoming request, validate it, fall back to a generated 16-char hex id if missing or invalid. Propagate via tracing::Instrument across every async boundary in the request. Write the same id back to the response in X-Correlation-Id so clients can join logs.
Non-service posture: libraries emit events and never install a subscriber; binaries own subscriber installation. CLIs log human-readable diagnostics to stderr — stdout is reserved for pipeable program output.
Naming convention: identifiers and acronyms use Id (not ID) capitalization throughout — header names (X-Correlation-Id, X-Request-Id), type names (UserId, OrderId, PaymentId), field names (trace_id, correlation_id in snake_case for fields).
For canonical setup (correlation-id middleware, fast lock-free trace_id generator, formatter, instrumentation patterns), read references/tracing.md.
Style
- Fields first, message last:
tracing::info!(field = val, "message") %for Display,?for Debug, bare for primitives /&'static str"-"sentinel for optional fields- snake_case fields, lowercase messages
skip(self, body, params)in#[instrument]- Secrets never logged — even at TRACE level
tracing::info!(version = env!("CARGO_PKG_VERSION"), address = %addr, "starting server");
tracing::info!(tool, duration_ms, "tool completed");
tracing::warn!(from = %prev, to = "OPEN", "circuit breaker state change");Defense in depth (non-negotiable)
- Pretty dev formatter, not plain
fmt::layer()— dim context, bright content; skeleton and rationale inreferences/tracing.md. - Secrets never reach a log line, masked or not —
secrecy::SecretString, explicit masking at the call site, no global redaction layer; rationale inreferences/tracing.md. - Per-HTTP-attempt logging > silent retry middleware — each attempt emits
service,method,path,status,duration_ms,attempt; rationale inreferences/tracing.md. - Boot logs emit twice — intent in
main.rsbefore bind, actual (local_addr()) after bind; rationale inreferences/tracing.md.
Observability (logs + metrics + traces, zero-overhead)
Three pillars, one emission API, one join key. tracing is the single
instrumentation surface; the correlation id joins all three pillars so an
operator can pivot from a metric spike to the log line to the exact trace.
- Emit once, consume via layers. Backends (fmt, Loki, OTLP/Langfuse) are
subscriber layers attached at boot — never backend calls at call sites, and
never custom proc macros:
#[instrument]IS the instrumentation macro, and a span no layer wants costs one cachedInterestcheck (~1ns, never constructed). - Metrics: the
metricsfacade (histogram!/counter!) at seams — request/turn boundaries, external-call round-trips, error arms. Exporter (metrics-exporter-prometheus) runs its own listener port; the app's auth surface is untouched. Increments are atomics; scrape is pull-based. - Label discipline (non-negotiable): bounded label values only (channel, model, provider, stage, status). Per-user/per-request ids are a cardinality bomb — identity belongs in traces and logs, never in metric labels.
- LLM/agent work traces to Langfuse via an OTLP layer with
gen_ai.*attributes (model, token usage, optionally payloads behind a config flag, redacted). Adopt the service correlation id as the OTel trace id by zero-padding 64→128 bit (W3C-sanctioned) — one shared pad function, so logs and traces carry the same key. - Zero-overhead posture: exporters batch on background tasks; the hot path never awaits an exporter and never touches a DB for observability; a misconfigured backend warns and is skipped — it must never block boot or a request.
- A feature is done when its seams carry spans + metrics. New endpoints, external calls, and error paths land instrumented, not retrofitted.
Dependency Injection
Trait-based injection for testability; struct fields + constructor for wiring; no global service locator. Alias runtime singletons Arc<dyn Trait> as SharedTrait — the alias says "process-wide singleton: clone, don't construct":
pub type SharedTokenVerifier = Arc<dyn TokenVerifier>;
let verifier: SharedTokenVerifier = Arc::new(HsJwtVerifier::new(secret, iss, aud)); // only in mainConstructors live only in main or a build_* helper, landing on AppState as Shared* fields — never via lazy_static / once_cell / LazyLock. For legitimate process-wide constants (a compiled Regex, an interned table), use std::sync::LazyLock — never the once_cell or lazy_static crates. For the full pattern (generic constructor injection, when NOT to alias, concrete Arc<T> singletons, test stubs), read references/di.md.
Testing
Workflow: global TDD doctrine — red, green, vacuous mutation check; regression test lands before the fix.
Behavioral over interaction testing. Verify state and output, not method calls. unwrap() is fine in tests.
| Type | When | Example |
| Real | Always first choice | Real OrderValidator |
| Fake | In-memory impl | InMemoryUserRepo |
| Stub | Fixed responses | StubPaymentGateway |
| Mock | Last resort | External HTTP API (use WireMock) |
For testcontainers (DB tests), property-based testing (proptest), insta snapshot testing, cargo-fuzz targets, and concrete fixture patterns, read references/testing.md.
Integration test layout — consolidate under tests/it/. Every file at the tests/ root compiles to its own binary that statically links the whole crate plus its deps. With heavy deps (an ML runtime, onnxruntime, candle, a vendored C lib), N root files means N fat binaries and N link steps — the usual cause of a slow cargo test and a target/ dir that balloons to tens of GB. Cargo has no flag to merge them; the lean layout is to keep few files at the root and put the suite under tests/it/ as modules of one entry binary (tests/it.rs with #[path = "it/<f>.rs"] mod <f>; per file — #[path] is required because a tests/*.rs file is a crate root and resolves mod to siblings). Put white-box tests in src/ under #[cfg(test)]; reserve tests/ for black-box public-API tests. When setting up a new test suite, or whenever a crate's tests/ root grows past a handful of files, read references/integration-test-layout.md for the mechanics (shared common, feature grouping, include_*! depth) and the behavior-preserving migration recipe.
Lint Configuration
Every Rust project ships clippy.toml + [lints] config aligned to the skill's rules. The skill packages both as assets:
assets/clippy.toml— thresholds (function length ≤40, cognitive complexity ≤25, max bool params 1, type complexity 250), disallowed types (std::HashMap→ahash::AHashMap)assets/cargo-lints.toml— lint levels (unwrap_used = deny,await_holding_lock = deny,too_many_arguments = warn,cognitive_complexity = warn, ...);unsafe_code = "forbid"is the asset default — relax todeny+// SAFETY:per-crate only when FFI or measured perf requires it
Copy assets/clippy.toml to the project root; MERGE the lints sections from assets/cargo-lints.toml into Cargo.toml — in a workspace, as [workspace.lints.rust] / [workspace.lints.clippy] tables with [lints] workspace = true in every member. These turn ~10 skill rules into build failures rather than review findings. Test code carve-outs come from clippy.toml's allow-unwrap-in-tests / allow-expect-in-tests / allow-panic-in-tests flags (they cover #[cfg(test)], tests/, and benches/) — no per-file cfg_attr attributes needed.
Before Committing (Self-Review)
Read references/checklist.md before committing or completing a review. Quick top-of-mind:
- No
unwrap()outside tests,?with.context()everywhere - Borrowed where possible (
&strnotStringin params) - Timeouts on ALL external calls
- Bounded channels,
Semaphorefor concurrency limits - Tests verify state/behavior, not interactions
- Every service log line has
trace_id/correlation_id clippypasses with no warnings- Serde annotations consistent (
rename_all,deny_unknown_fieldson input types)
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.

