Rust
Instructions
Shared Knowledge: This skill builds on
brain/knowledge/general-problem-solving.md,brain/knowledge/coding-general.md, andbrain/knowledge/testing.md. Always apply those principles alongside the language-specific guidance below. Language-Specific Testing: Seetesting-guidelines.mdin this skill folder for framework-specific testing patterns.
⛔ Hard Rules: Non-Negotiable
These bind every line of Rust you add or modify. They are the ONE exception to "follow the repo's
conventions": when the repo itself violates one of them, the rule still wins for the code you write.
Leave existing violations in untouched code alone (never mass-refactor), but nothing new may break
these. Softer conflicts between repo patterns and this skill go to the user per
brain/knowledge/coding-general.md §2, "When the repo and the guidelines disagree". Re-read this
list before writing code, and walk it again at handoff (§8).
anyhowfor binaries, applications, and internal workspace libs;thiserrorfor published library crates; nothing else. Application code returnsanyhow::Result<T>with.context(...)attached at every level, and a library crate internal to its workspace (consumed only by that workspace's own tools) follows the same pattern. A library crate published for external consumers exposes typed errors viathiserrorinstead, becauseanyhowerases the type callers need to match on; the consuming binary converts at its boundary with?and.context(...). An internal lib whose callers start matching on failure modes is the trigger to move that lib tothiserror. Noeyre, no hand-rolled error enums wherethiserrorfits, and never both patterns mixed inside one crate.- No
unwrap()orexpect()outside test code, even in a repo sprinkled with them. Errors propagate with?. A case you believe impossible still gets handled explicitly, with the reasoning stated where it's handled, never panicked through. tracingis the logging crate.tracing+tracing-subscriber(subscriber initialized at the binary entry point only), neverlog,env_logger, orprintln!/eprintln!diagnostics in library code. Events carry structured fields (info!(user_id = %id, "queued transfer")), not values interpolated into the message string, and async or hot-path fns that deserve span context get#[instrument].- The four gates run clean before handoff, warnings as errors:
cargo fmt --all -- --check,cargo clippy --all-targets --all-features -- -D warnings,cargo build,cargo test. Never silence a lint to get there: an#[allow(...)]without a stated, site-specific reason is a violation, not a fix. - File size: split new code into a new module; never mass-refactor an existing file for size. A
.rsfile you create, or a pre-existing one your change grows across 1,500 lines (the hard cap; the warn tier starts at 700), goes into a new cohesive module instead of sailing past the cap. The per-language tier table is inbrain/knowledge/coding-general.md§3 (File size). This binds your code only: a file already over the cap stays untouched for size; route your addition into a new module and name the oversized file in the handoff. The count is production lines: the trailing#[cfg(test)] mod testsblock is subtracted, so co-located unit tests never push a file over a tier, and test-only files (tests/,*_tests.rs) are warn-only. The new-code quality gate (§8) enforces the cap.
1. Design Principles
Rust-Specific:
- Prefer borrowing over cloning; pass
&strnot&String,&Pathnot&PathBuf,&[T]not&Vec<T> - Use enums, newtypes, and the type system to make illegal states unrepresentable
- Parse raw input into well-typed structures at the boundary, then pass typed data inward
- Follow the repo's conventions over personal preference (see
CONTRIBUTING.md), except where a ⛔ Hard Rule above says otherwise; Hard Rules beat repo conventions. A softer conflict between a repo pattern and this skill's guidance goes to the user, not to whichever side you prefer
Models & Project Setup:
#[derive(Debug, Clone)]on config structs; addCopy,PartialEq,Eqwhere appropriate- Enums for discrete modes/states;
Option<T>for optional fields - New crates: use
edition = "2021"(the standing default for these projects for now); setauthorsandrepositoryto match the project's conventions; add the crate to the rootCargo.tomlworkspacememberslist (if using workspaces) - Pin the lint policy in the repo with a
[workspace.lints](or per-crate[lints]) table inCargo.tomlandlints.workspace = truein member crates, so the clippy bar travels with the project instead of living in anyone's head - When you are the one creating that lint-policy table (not when a crate merely joins an existing workspace), add
[workspace.lints.clippy] too_many_lines = "warn". The lint is allow-by-default (pedantic), and under Hard Rule 4's-D warningsa"warn"entry becomes a hard error, so a function past clippy'stoo-many-lines-threshold(kept at the default 100, consistent with the ~50-line function guidance in §4) stops the gate. A function that is legitimately long escapes with#[allow(clippy::too_many_lines)]plus a one-line site-specific reason, the same sanctioned-exception shape as Hard Rule 4. If the same table also enables a lint group (for examplepedantic = "warn"), give the grouppriority = -1so the individualtoo_many_lineslevel still wins. This is a per-function guard; whole-file size is enforced by Hard Rule 5 and the new-code gate, since no file-level clippy lint exists.
2. Error Handling (anyhow by default, thiserror for published libraries)
Applications and internal workspace libs use anyhow; library crates published for external
consumers use thiserror (Hard Rule 1). No other error library, and never both patterns mixed
inside one crate.
In application/binary code and internal workspace libs:
- All fallible functions return
anyhow::Result<T> - Attach context at every level with
.context("message")or.with_context(|| format!(...)) - Propagate errors with
?; never useunwrap()orexpect()outside test code (Hard Rule 2) - Use
anyhow::anyhow!("message")oranyhow::bail!("message")for ad-hoc errors - At the entrypoint: print the error to
stderrwitheprintln!, log witherror!, and exit non-zero (if the workspace defines a shared error-exit helper, use it) - For tools needing distinct exit codes, define a custom
AppError { message, exit_code }struct (follow the workspace's existing pattern if one exists)
In a published library crate:
- Define one error enum per module or domain with
#[derive(thiserror::Error, Debug)], a#[error("...")]message per variant, and#[from]where a source error maps one-to-one - Return
Result<T, YourError>from the public API; let binaries wrap it intoanyhowat their boundary with?and.context(...) - Keep variants meaningful to the caller (what they can match on and react to), not one variant per internal call site
3. Logging (tracing required)
tracing is the logging crate; never log, env_logger, or println! diagnostics (Hard Rule 3).
- Initialize
tracing-subscriberonce, at the binary entry point; libraries only emit events, never install a subscriber - Structured fields over interpolation:
info!(user_id = %id, order_count, "queued transfers"), neverinfo!("queued {count} transfers for {id}"); fields survive as queryable data, interpolated strings don't #[instrument]on async fns and hot paths that deserve span context (skip trivial helpers);#[instrument(skip(large_arg))]to keep noisy arguments out- Levels:
error!for failures someone must look at,warn!for degraded-but-continuing,info!for meaningful flow milestones,debug!/trace!for diagnosis; don't narrate every step atinfo! - Log at the site that has the context; don't double-log an error at every level of the propagation chain (
.context(...)already carries the story upward)
4. Code Style
- Use the repo's
rustfmtandclippydefaults; never change an existing project's settings; fix all warnings. Setting the lint policy on a new crate or workspace you are creating is the one sanctioned exception (§1), including thetoo_many_linespin snake_casefor functions/variables/modules/files;PascalCasefor types;UPPER_SNAKE_CASEfor constants- Group imports:
crate::, external crates,std::; no glob imports in production code - Doc comments (
///) on all public items with# Errors/# Panicssections where applicable - Comments explain "why", not "what"; the code's why, never the edit's (no comments narrating the
fix, the request, or the old behavior;
brain/knowledge/coding-general.md⛔ Hard Rule 3); no commented-out code - Functions ~50 lines max; extract helpers when they grow
matchoverif letchains for multiple variants; guard clauses for early returns
5. Performance
- Use
BufReader/BufWriterfor file I/O; source buffer sizes from the workspace's shared constants if it defines them - Pre-allocate
Veccapacity when size is known; useCow<'_, str>to avoid unnecessary allocations - Use
std::sync::LazyLock(stable since Rust 1.80) for expensive one-time inits like compiled regexes; reach foronce_cellonly in a repo pinned below 1.80 - Profile before optimizing; add parallelism only as the need arises
6. Testing
- Unit tests:
#[cfg(test)] mod testsat the bottom of each file;use super::* - When a co-located
#[cfg(test)] mod testsgrows large, move it into a siblingtests.rssubmodule file (#[cfg(test)] mod tests;in the source file, the tests in the adjacenttests.rs) rather than letting it inflate the source. Private items still reach it through the module boundary. The size gate already subtracts the trailing test module from the production count, so co-located tests never fail the gate; this split is about keeping the source file readable, and it is distinct from the integration-testtests/dir - Integration tests: separate crates in a top-level
tests/dir, exercising the public API; shared helpers go intests/common/mod.rs. Do not mirrorsrc/'s folder layout into a separate test tree (that is a C#/pytest convention, not Rust's). Seetesting-guidelines.md. - File-based tests:
tempfile::tempdir()for temporary directories - Follow Arrange-Act-Assert; use helper functions to build test fixtures
- Use
assert!(matches!(...))for enum variant checks - Test happy paths, error conditions, and meaningful boundary values
- Add tests where it makes sense; don't add them purely for coverage numbers
- New-code quality gate:
scripts/rust_quality_gate.py --skip-mutantschecks diff coverage before handoff; the mutation half is opt-in and runs only after the user has committed the work themselves and asked for it; seetesting-guidelines.md§"New-code quality gate"
7. Forbidden
unsafecode unless explicitly approvedunwrap()/expect()in non-test code (Hard Rule 2)- Error libraries other than
anyhow(binaries and internal workspace libs) andthiserror(published libraries); noeyreand friends (Hard Rule 1) log,env_logger, orprintln!/eprintln!diagnostics wheretracingbelongs (Hard Rule 3; the entrypoint's final error print is the exception)- Changing
edition,rustfmt, orclippysettings in an existing project (setting the lint policy when you create a new crate or workspace is the one sanctioned exception, §1)
8. Quality Validation
Before completion:
- Walk the ⛔ Hard Rules block at the top item by item against your diff: error crates match the
crate kind, no new
unwrap()/expect(), events use structured fields. Verify by looking, not by assuming. - Run the four gates; all clean, warnings as errors:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo build
cargo test
In a large workspace, scope with -p <crate> while iterating, but the full unscoped run happens at
least once before handoff.
- Run the coverage half of the new-code quality gate where the toolchain is available
(
scripts/rust_quality_gate.py --skip-mutants; see §6 andtesting-guidelines.md). Its file-size check (Hard Rule 5) exits 4 when a new or cap-crossing production file hits the 1,500-line cap; the fix is to split the new code into a new module, never to mass-refactor an existing oversized one. The size phase runs on git and the filesystem alone, so it reports even where cargo is absent, and it subtracts the trailing#[cfg(test)]block from the count.
When to Use This Skill
- Writing new Rust CLI tools, libraries, or crates
- Adding functionality to existing tools or shared crates
- Refactoring, debugging, or reviewing Rust code
- Any Rust task where production-quality, idiomatic code is expected