Rust

Writes, debugs, and optimizes Rust code, crates, and Cargo builds: ownership, lifetimes, traits, async, unsafe, and FFI. Use when the borrow checker rejects code, when rustc report…

Iván

@ivangdavila

What This Skill Does

A Rust development assistant that writes, debugs, and optimizes Rust code, crates, and Cargo builds. It handles ownership, lifetimes, traits, async, unsafe, and FFI, and diagnoses compiler errors, runtime panics, deadlocks, and build failures.

Replaces manually tracing borrow checker errors, async runtime issues, and Cargo dependency conflicts by providing targeted diagnostics and fixes for common Rust pain points.

When to Use It

  • Fix a borrow checker error where a value is moved or cannot be borrowed as mutable
  • Resolve a missing lifetime specifier or trait bound not satisfied in a function signature
  • Diagnose why a future cannot be sent between threads safely across an await point
  • Debug a deadlock, BorrowMutError panic, or unwrap failure in production code
  • Optimize slow Cargo builds, resolve feature unification issues, or fix CI-only build failures
  • Write unsafe code, C FFI bindings, proc macros, serde derives, or no_std firmware

Install

$ openclaw skills install @ivangdavila/rust

User preferences and memory live in ~/Clawic/data/rust/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/rust/ or ~/clawic/rust/), move it to ~/Clawic/data/rust/.

When To Use

  • Writing or reviewing Rust: functions, traits, error types, async code, public crate APIs
  • Fighting the compiler: moves, borrows, lifetimes, trait bounds, dyn compatibility, type inference
  • Diagnosing runtime failures: panics, deadlocks, BorrowMutError, memory growth, segfaults through unsafe or FFI
  • Cargo work: features, workspaces, lockfiles, MSRV, slow builds, CI-only failures, duplicated dependencies
  • Targets beyond the host: cross-compilation, static musl binaries, no_std firmware, wasm
  • Not for C++ (cpp) or language-agnostic concurrency theory (async-patterns) — this is Rust the language and its toolchain

Quick Reference

SituationPlay
Compiler says a value moved and you still need itBorrow instead of consuming, or copy the small part you need out first — clone is rung 5, not rung 1 (→ Borrow Checker Escape Ladder)
Two mutable borrows refused on code that looks fineThe checker is field-sensitive on direct field access and field-insensitive through a method call — destructure or split_at_mutownership.md
A returned reference "does not live long enough"Nothing borrowed from a frame outlives it: return owned data, or an index → lifetimes.md
missing lifetime specifier on a signatureTwo input references and no elision rule applies — name which one the output borrows from → lifetimes.md
"trait bound not satisfied" on a type that clearly implements itMissing use for the trait, or &T supplied where T was required — read the required by chain bottom-up → traits.md
"future cannot be sent between threads safely"A !Send value (std MutexGuard, Rc, RefCell ref) is alive across an .awaitasync.md
Async program stalls with no error and no progressA blocking call is occupying a runtime worker → async.md
Program deadlocksOne thread locks the same lock twice, or two threads take two locks in opposite order → concurrency.md
already borrowed: BorrowMutError at runtimeOverlapping RefCell borrows — the check you moved from compile time to runtime → ownership.md
String vs &str, a panic on a string slice, UTF-8, or path handlingByte indices are not characters and paths are not UTF-8 → strings-and-text.md
A match is unreadable, non-exhaustive, or the type allows a state you never wantModel it as an enum, then let exhaustiveness find the sites → pattern-matching.md
Nothing in the logs explains what the service was doingNo subscriber, or context in strings instead of span fields → observability.md
"Where does this type go, and what should be public"Module tree, pub(crate), re-exports, lib vs bin split → modules-and-layout.md
Error names two identical-looking types (expected Uri, found Uri)Semver-incompatible duplicate of one crate in the graph: cargo tree -dcargo.md
Builds locally, fails in CIToolchain, lockfile, or feature-unification drift → cargo.md
Build is slowcargo build --timings first; linker and codegen settings before restructuring code → cargo.md
Program is slowProfile a release build with symbols; allocation and clone before algorithms → performance.md
Segfault, corrupted data, or nondeterministic "works on my machine"Only reachable through unsafe or FFI — run it under Miri → unsafe.md
Anything elseRead the whole rustc output bottom-up including note: and help:, then rustc --explain E0xxxcompiler-errors.md

Depth on demand: compiler-errors.md error code → real cause → first move · ownership.md moves, borrows, interior mutability · lifetimes.md elision, variance, 'static, HRTB · traits.md coherence, generics vs dyn, associated types · pattern-matching.md match ergonomics, let else, exhaustiveness, Option/Result combinators, modeling states · errors.md Result design, ?, panics, backtraces · async.md runtimes, Send bounds, cancellation · concurrency.md threads, locks, channels, atomics · strings-and-text.md String/&str, UTF-8, formatting, Cow, paths · collections.md Vec, maps, sets, queues, iterators · performance.md profiling, allocation, build settings · observability.md tracing, spans, metrics, panic hooks · cargo.md features, workspaces, lockfiles, build times · modules-and-layout.md module tree, visibility, lib/bin/tests layout, when to split a crate · testing.md layout, doctests, property tests, Miri, fuzzing · unsafe.md UB rules, raw pointers, safety comments · ffi.md C interop in both directions · macros.md macro_rules! and proc macros · api-design.md public APIs and semver hazards · serde.md derive traps and format edge cases · embedded-no-std.md firmware and no_std · wasm.md browser and WASI targets · cross-compilation.md target triples, musl, linkers, containers.

Core Rules

  1. Fix a borrow error at the lowest rung that compiles. The ladder (full version below) runs shorten → split → take → copy → clone → interior mutability → arena → unsafe. Every rung down trades a compile-time guarantee for a runtime one; reaching for Rc<RefCell<T>> against a rung-1 problem is the most common finding in Rust review.
  2. Borrowed in arguments, owned in returns. A parameter typed String forces every caller to allocate or surrender ownership; &str accepts String, &String, and literals through deref coercion. Escape hatch: impl Into<String> when the function genuinely stores the value.
  3. Overflow checks are ON in debug and OFF in release by default. i32::MAX + 1 panics under cargo test and wraps silently in the shipped binary. For arithmetic on unbounded or untrusted input use checked_* (returns Option), saturating_* (clamps), or wrapping_* (documents intent) — or set overflow-checks = true in [profile.release] and measure that cost instead of assuming it.
  4. Measure release builds only. Debug builds skip optimization and keep overflow and bounds instrumentation: arithmetic-heavy loops commonly run 10-50× slower, so a debug timing ranks nothing. Profile with [profile.release] debug = true — full optimization plus symbols, so the profiler can name frames.
  5. One error type per boundary: enum outward, dynamic inward. A library returning anyhow::Error denies callers the ability to match on the failure; a binary with 40 enum variants pays for matching nobody does. Default: a thiserror enum in library crates, anyhow::Result with .context() in binaries (errors.md).
  6. main prints errors with Debug, not Display. fn main() -> Result<(), MyError> on failure emits Error: <Debug repr> and exits 1 — the Display message you wrote never appears. Either print it yourself and std::process::exit(1), or return an error type whose Debug renders the full report (anyhow::Error does).
  7. Bind every guard to a real name. let _ = mutex.lock(); releases the lock on that same line — _ is not a binding, so the guard drops immediately; let _guard = mutex.lock()?; holds it to end of scope. Same trap with RefCell borrows and every other RAII handle.
  8. Features are additive and unified across the entire graph. default-features = false in your manifest is a request, not a guarantee: if any crate in the tree enables tokio/full, the whole build gets full. Find the enabler with cargo tree -e features -i <crate> before arguing with the manifest.
  9. Pin the toolchain and the lockfile wherever the build must reproduce. rust-toolchain.toml plus cargo build --locked in CI. Without --locked, CI resolves newer semver-compatible dependencies than your Cargo.lock and the difference arrives as a compile error nobody can reproduce locally.

Compiler Error Codes

The code names the subsystem that refused, which names the file to open. rustc --explain E0382 prints the canonical explanation; the column below is the first move that is usually right.

CodeWhat it actually meansFirst move
E0382Use after move — consumed by a for loop, a by-value method, or a closure captureBorrow (&v, .iter()) or copy the needed field out
E0499Two &mut to the same place alive at onceSplit the borrow or shorten the first one (rungs 1-2)
E0502A &mut and a & overlapBind the read to a let before the mutation
E0505 / E0506Move out of, or assign to, something still borrowedEnd the borrow first; std::mem::take if you need the value now
E0507Move out of a reference or an index expression.clone(), .take(), mem::replace, or match by reference
E0515Returning a reference to a localReturn owned data or an index — the frame dies at return
E0597Borrowed value dropped too earlyHoist the owner's let above the borrower's
E0716Temporary dropped while borrowedBind the temporary: let s = make(); let r = &s;
E0106Missing lifetime specifierElision cannot choose among multiple inputs — name it (lifetimes.md)
E0308Mismatched typesCompare the two full paths in the message: usually &T vs T, or two versions of one crate (cargo.md)
E0277Trait bound not satisfiedMissing use of the trait, or a bound to add — read required by bottom-up
E0038Trait is not dyn compatible (formerly "object safe")Generic method, Self return, or associated const — use generics or split the trait (traits.md)
E0599No method foundTrait not in scope, or the receiver does not deref to the type that has the method
E0072Recursive type has infinite sizeBox the recursive field
E0117 / E0119Orphan rule / conflicting implsNewtype wrapper; either the trait or the type must be local (traits.md)
E0282 / E0283Type annotations needed / ambiguousTurbofish (collect::<Vec<_>>()) or a typed binding
E0596Cannot borrow as mutableMissing mut on the binding, or a &self method that needs &mut self
Anything elseEvery code is stable and documentedrustc --explain E0xxx, then compiler-errors.md for the symptoms that have no code

Pointer And Container Choice

Pick the weakest tool that expresses the need — every row costs something the row above does not.

NeedUseCost or trap
Single owner, size knownTThe default; most code never leaves this row
Read-only view in a parameter&T, &str, &[T]No allocation, caller keeps ownership (rule 2)
Heap indirection, recursion, or a large value moved oftenBox<T>One allocation; Box<dyn Trait> also erases the type
Borrow when possible, allocate only on modificationCow<'a, str>Removes "clone just in case" from hot paths (strings-and-text.md)
Shared ownership, one threadRc<T>Not Send; reference cycles leak — break them with Weak
Shared ownership, multiple threadsArc<T>Atomic refcount; Arc<T> by itself is still read-only
Mutate shared state, one threadRc<RefCell<T>>Borrow checking moves to runtime: BorrowMutError panics
Mutate one Copy value, one threadCell<T>Get/set only, no borrows, therefore no panic
Mutate shared state, multiple threadsArc<Mutex<T>>Poisoning on panic; lock ordering is yours to enforce
Many readers, rare writerRwLock<T>Writer starvation and re-entrant read deadlock; measure before preferring it to Mutex
Initialize once, then read foreverOnceLock<T> (rust >=1.70), LazyLock<T> (rust >=1.80)Replaces lazy_static and most once_cell uses with no dependency
A counter or a flagAtomicUsize, AtomicBoolCheapest sharing available; memory ordering is a real decision (concurrency.md)
Anything elseStart at T and &TMove down a row only when the compiler proves you must

Borrow Checker Escape Ladder

Ordered by cost. Take the first rung that compiles.

  1. Shorten the borrow. NLL ends a borrow at its last use, not at end of scope: let n = v.len(); v.push(n); compiles where v.push(v.len()) does not.
  2. Split the borrow. let Foo { a, b } = &mut foo; yields independent &mut to distinct fields; slice::split_at_mut does the same for slices. A &mut self method borrows the whole struct — that asymmetry explains most "obviously fine" rejections.
  3. Take the value out. let buf = std::mem::take(&mut self.buf); hands you an owned value and leaves Default::default() behind; put it back when done. mem::replace when there is no sensible default.
  4. Copy the small thing. An index, an id, a length: copying a usize costs nothing and removes the borrow entirely. Index-based access is a legitimate design, not a defeat.
  5. Clone. Honest, and unobservable in setup, config, and error paths. A clone in a hot loop is a rung-1 problem in disguise; profile rather than guess which one you have.
  6. Interior mutability. Cell for Copy, RefCell for the rest, Mutex across threads. You now own the invariant the compiler used to check, and violations arrive as runtime panics.
  7. Handles instead of pointers. Vec<Node> plus u32 indices. This is the standard answer for graphs and trees with back-edges; Rc<RefCell<Node>> is the version that gets rewritten a year later.
  8. unsafe. Only with a // SAFETY: comment naming the invariant, and Miri in CI (unsafe.md).

Output Gates

Before emitting Rust code, verify:

  • Every unwrap/expect outside tests is justified in a comment, or replaced by ?
  • Public parameters take &str, &[T], or impl AsRef<Path> wherever the function only reads them
  • Errors follow rule 5: enum at library boundaries, #[from] conversions, no Box<dyn Error> in a published signature
  • No std guard, Rc, or blocking call is alive across an .await
  • Arithmetic on untrusted or unbounded values uses checked_* or saturating_*, never debug-only overflow checks
  • Public enums and structs that may grow carry #[non_exhaustive] (api-design.md)
  • Each new unsafe block carries a // SAFETY: comment naming the invariant it upholds
  • The code passes cargo clippy -- -D warnings, not merely cargo build

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/rust/config.yaml.

VariableTypeDefaultEffect
edition2015 | 2018 | 2021 | 20242024Selects syntax and semantics in emitted code (if let temporary scope, reserved keywords, unsafe attribute forms) and which cargo fix --edition advice applies
msrvtext (version, e.g. 1.75)none (current stable)Gates which stabilized APIs and syntax may appear; suppresses suggestions newer than the floor and drives rust-version in Cargo.toml
async_runtimetokio | async-std | smol | embassy | nonetokioChooses the runtime API in every async example and the spawn, timer, and IO types used from async.md
error_stylethiserror | anyhow | std-enum | box-dyn | autoauto (thiserror in libraries, anyhow in binaries)Sets the error type generated at each boundary and the ? conversion strategy in errors.md
unsafe_policyforbid | reviewed | allowreviewedforbid emits #![forbid(unsafe_code)] and refuses ladder rung 8; reviewed requires a SAFETY comment plus a Miri step; allow drops the Miri requirement
lint_leveldefault | pedantic | deny-warningsdefaultControls which clippy lints the Output Gates enforce and whether generated CI fails on warnings
explanation_depthfix-only | standard | teachingstandardfix-only emits the corrected code and one line of why; standard adds the rung or rule that applies; teaching walks the compiler message and the alternatives that were rejected

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:

  • Tooling: cargo test vs cargo nextest, criterion vs a custom harness, cargo vs just/make wrappers, linker choice (lld, mold) — affects the commands in testing.md and performance.md
  • Conventions: module layout (mod.rs vs name.rs), builder vs plain constructors, re-export policy at the crate root, doc-comment strictness — affects modules-and-layout.md and api-design.md
  • Platform: default target triple, libc flavor (gnu vs musl), hosted vs no_std, wasm target — affects cross-compilation.md, embedded-no-std.md, wasm.md
  • Dependencies: appetite for adding a crate vs writing it, vetted-crate list, license and audit regime (cargo deny, cargo audit) — affects every "add a crate" recommendation
  • Registries and sources: crates.io vs a private or mirrored registry, cargo vendor plus source replacement, air-gapped or offline builds, git vs path dependencies for internal crates — affects the manifest, .cargo/config.toml, and every install command in cargo.md
  • Work order and gates: cargo check in the edit loop vs full builds, whether clippy runs pre-commit or only in CI, whether cargo semver-checks gates release PRs, test-before-fix discipline — affects the command sequences in performance.md, testing.md, and api-design.md
  • Output format: depth of explanation vs a direct fix (see explanation_depth), whole file vs diff, comment density in emitted code, whether to show the rejected alternatives — affects the shape of every answer
  • Safety posture: how proactively to surface panic, overflow, and unwrap risks vs answering only what was asked — affects Output Gates verbosity
  • Cadence: dependency update and audit frequency, MSRV bump policy — affects the maintenance advice in cargo.md

Traps

TrapWhy it failsDo instead
let _ = mutex.lock(); to hold a lock_ is not a binding; the guard drops on that line and the lock is free immediatelylet _guard = mutex.lock().unwrap(); (rule 7)
Rc<RefCell<T>> for a tree with parent pointersCycles never free, and every access can panic with BorrowMutErrorArena: Vec<Node> plus index handles (rung 7)
.clone() inside a loop to appease the borrow checkerTurns an O(1) borrow into a copy per iterationFix the scope first (rungs 1-3), then clone deliberately
#[derive(Clone)] on every generic typeDerive adds T: Clone to every type parameter, and that bound becomes part of your public APIHand-write the impl, bounding only what actually needs it
String parameters in a public APIEvery caller allocates just to call you&str, or impl Into<String> when you store it (rule 2)
mem::transmute to change a lifetimeErases the exact property the compiler was checking; UB the moment the data diesRestructure, or a raw pointer plus a SAFETY comment (unsafe.md)
async fn on functions that never .awaitColors the whole call graph and wraps sync work in a state machineasync only where an await point exists (async.md)
Benchmarking with cargo run or cargo testDebug build: the number measures instrumentation (rule 4)--release, criterion, and black_box on the inputs
cargo clean to fix a slow buildDeletes the incremental cache and guarantees the slowest possible next buildcargo build --timings and read the graph (cargo.md)
.iter().collect::<Vec<_>>() just to loop over itAllocates a vector to walk it onceIterate the iterator; collect only when the collection is kept
unwrap() on Mutex::lock() everywhere by reflexRight most of the time, but hides that poisoning means another thread panicked mid-invariantHandle or document the poison case (concurrency.md)
Adding lifetime parameters until it compilesProduces signatures nobody can call and hides the real design errorReturn owned data first; add lifetimes once you can state what borrows from what (lifetimes.md)

Where Experts Disagree

  • anyhow in libraries. Purists never allow it: callers cannot match on the failure. Pragmatists accept it in application-adjacent crates nobody programs against. The boundary both sides accept: if any consumer must branch on the failure kind, the enum is not optional.
  • Clone-first vs lifetime-first. One camp writes owned data everywhere and optimizes later; the other treats every clone as debt. The line the evidence supports: clones in setup, config, and error paths are noise, clones inside hot loops are the first thing a profiler finds — so the disagreement only bites where you have measured.
  • unsafe avoidance. #![forbid(unsafe_code)] teams vs teams accepting reviewed unsafe for measured wins. Neither side accepts unsafe without a SAFETY comment and Miri coverage — that is not a tradeoff, it is a defect.
  • Generics vs dyn. Monomorphization is faster per call, slower to compile, and multiplies code size per instantiated type; dyn costs an indirect call and keeps one copy. Default to generics in leaf code and dyn at plugin-shaped boundaries, or wherever compile time or binary size is the measured constraint (performance.md).

Related Skills

More Clawic skills, get them at https://clawic.com/skills/rust (install if the user confirms):

  • cpp — when the other side of the FFI boundary is C++, or when comparing ownership models
  • go — the other systems language on the same team; different concurrency tradeoffs
  • async-patterns — language-agnostic concurrency and cancellation theory behind async.md
  • error-handling — cross-language error design behind rule 5

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/rust.

Top skills in this category