name: rust-coding
description: Use when authoring, scaffolding, refactoring, or reviewing Rust code. Triggers on greenfield requests like "create a Rust app", "new Rust project", "scaffold a Rust crate", "cargo new", "start a Rust library", "build a Rust binary that...", or any "in Rust" / "using Rust" request to build from scratch. Also triggers on *.rs edits, Cargo.toml/Cargo.lock changes, unsafe blocks, async/tokio code, FFI bindings (extern "C", #[repr(C)], cxx, bindgen), serde derives, thiserror/anyhow error enums, clippy warnings, borrow-checker errors (E0382, E0502, E0505, E0597, E0716), and edition 2024 migration. Also triggers on questions like "is this idiomatic Rust", "Arc or Rc", "thiserror or anyhow", "is this unsafe sound", "tokio deadlock".
rust-coding
Opinionated decision rules for writing high-quality Rust. Consult the matching references/ file when the decision gets deep.
Reference map (progressive disclosure)
| When you're… | Read first |
|---|---|
| Designing a public API or trait | references/api-design.md |
| Writing new Rust code of any kind | references/idioms.md |
Building error types or ?-using code |
references/error-handling.md |
Writing or reviewing unsafe |
references/unsafe.md |
Writing async/await or tokio code |
references/async.md |
Writing extern "C", cxx, bindgen, cbindgen |
references/ffi.md |
| Optimizing a hot path or reading a flamegraph | references/performance.md |
| Migrating a crate edition or reading 2024 features | references/edition-2024.md |
Determinism boundary
The mechanically detectable slice of the rules below is script-owned, not prose-owned. When auditing existing code (not authoring new code), run the plugin's deterministic lane instead of grepping by hand:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh" <project-root> --json
validate-cargo.sh decides manifest invariants (edition enum, edition↔MSRV
consistency, wildcard deps, workspace members, lockfile, toolchain channel).
validate-safety.sh flags candidates for rules 1–7 below by stable rule id
(rust-unsafe-missing-safety-comment, rust-unwrap-outside-tests,
rust-unbounded-channel, rust-sync-lock-in-async-candidate,
rust-box-dyn-error-in-pub-api, rust-serde-missing-deny-unknown) — confirming
each candidate and writing the fix is judgment and stays here.
stack-report.sh produces the project Stack Report. See scripts/README.md.
Non-negotiable rules
These apply to every Rust change. No exceptions without an explicit // NOTE: explaining why.
- Every
unsafeblock carries a// SAFETY:comment stating the invariants the caller relies on. Minimize block scope — smaller block, smaller blast radius. - Never hold a lock across
.await. Release the guard or switch to a lock-free design. This serializes entire services under load. - Bound every channel.
tokio::mpsc::unbounded_channel()is for compile-time-known-small fanout only. Attacker-controlled fanout → DoS. #[serde(deny_unknown_fields)]on every type deserialized from external input. Fail closed.- Errors:
thiserrorin libraries,anyhowin binaries.Box<dyn Error>in public APIs is an anti-pattern. - No
.unwrap()/.expect()outsidemain, tests,OnceCell::get, or where a panic is the documented contract. Everything else uses?. &strand&[T]at API boundaries. Takeimpl AsRef<str>/impl Into<String>when you must; never demandStringyou don't own.- Iterators over index loops. LLVM optimizes them equal-or-better; they compose; they don't OOB.
- Run
cargo clippy -- -D warningsin CI. Treat clippy warnings as bugs. rustfmtdefault config. No bikeshedding; commitrustfmt.tomlonly when a specific hard constraint forces it.
Decision shortcuts
Which smart pointer?
- Single-owner, heap-allocated value →
Box<T> - Shared ownership, single-threaded →
Rc<T>(rare outside trees/graphs) - Shared ownership, threads →
Arc<T> - Interior mutability, single-threaded →
RefCell<T>,Cell<T> - Interior mutability, threads →
Mutex<T>/RwLock<T>(orparking_lotfor non-poisoning) - Default to owned
Tor&T.Rc/Arcis almost never the first answer.
Which error strategy?
- Library crate that others depend on →
thiserrorenum per module with variants that carry context fields, neverString - Binary crate at the top level →
anyhow::Result+.context("what I was doing")chains - FFI boundary → opaque error code +
thread_locallast-error buffer (seereferences/ffi.md)
impl Trait vs generic vs dyn?
- Return position:
impl Traitunless you need trait objects in a collection - Argument position: generic (
fn foo<T: Read>) unless dynamic dispatch is a hard requirement dyn Trait: heap-allocated trait objects; cost is one vtable indirection — acceptable when values are heterogeneous or the API already requiresBox
Async runtime?
tokiofor network services (ecosystem dominance, multi-thread scheduler)smol/async-stdonly when a tight dep surface is mandatory- Never mix runtimes. Crates that pick a runtime must document it.
Gotchas (from prior incidents — see vault [[Rust]])
#[cfg(test)]items in a library are invisible to integration tests intests/. Cargo compiles integration tests as a separate crate that links the lib as a downstream user. Fix: drop the gate and mark#[doc(hidden)], or gate behind atestingCargo feature with[[test]] required-features = ["testing"].BufRead::read_line() == 0means EOF, not "nothing yet". Treating it as no-op → infinite loop that appends empty strings until OOM. Alwaysbreakonn == 0.Command::new("x").arg("y")without.status()/.output()/.spawn()does nothing. The builder drops silently with no warning. Always chain an execution method.reqwest'sjsonCargo feature is not default.resp.json::<T>()fails compile with "method not found". Enable withreqwest = { version = "0.12", features = ["json"] }.- Circular module dependencies in workspaces are rejected by the compiler. Fix: move the shared item into the more semantically-appropriate module.
- NVPTX (CUDA) builds require nightly.
ptx-linkerwas removed from stable; usellvm-bitcode-linker. Oldptx-linkerinstalls break new builds. First NVPTX build takes minutes — use-vvto see progress. - Rust edition changes are opt-in.
cargo fix --editionwalks a crate forward one edition at a time. Edition 2024 (released with 1.85.0, 2025-02-20) changedif lettemp-scope rules, RPIT hidden-lifetime capture, and unsafe-attribute syntax. - Feature-flag deprecations cascade. Before a version bump,
grep -r 'features = \["'across the repo and align with upstream rename (e.g.,collab→multi_agentin matrix-rust-sdk).
When to hand off to the subagent
Invoke rust-expert (via /rust-review or /rust-idiomize) when:
- The change spans more than ~3 files — main-context authoring loses global view
- An
unsafeblock needs an extracted safety contract with Miri-runnable tests - A migration to edition 2024 is being planned
- Clippy output has more than ~5 lints and you want a prioritized triage
- Refactoring a module from imperative loops → iterator chains
- Typestate or sealed-trait design is on the table
Otherwise, apply the rules above inline and move on.
Related
- Authoring subagent:
rust-expert(in this plugin) - Slash commands:
/rust-review,/rust-idiomize - Project-wide audit: the existing
rust-projectskill (clippy + cargo audit + cargo deny + unused deps + cross-compile) - Security review:
sec-reviewskill →sec-review:rust-runnersubagent