Install
openclaw skills install @ivangdavila/rustWrites, 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 reports value moved, cannot borrow as mutable, does not live long enough, missing lifetime specifier, trait bound not satisfied, or not dyn compatible; when a future cannot be sent between threads, a Mutex guard crosses an await, a task blocks the async runtime, or select! loses data; when code deadlocks, panics on unwrap, or hits BorrowMutError; when cargo builds are slow, features unify unexpectedly, two versions of one crate collide, or the build fails only in CI; when writing unsafe, C FFI, proc macros, serde derives, no_std firmware, or wasm; when cross-compiling to musl or another target; when profiling, benchmarking, or shrinking a Rust binary. Not for C++ or language-agnostic concurrency theory.
openclaw skills install @ivangdavila/rustUser 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/.
dyn compatibility, type inferenceBorrowMutError, memory growth, segfaults through unsafe or FFIno_std firmware, wasmcpp) or language-agnostic concurrency theory (async-patterns) — this is Rust the language and its toolchain| Situation | Play |
|---|---|
| Compiler says a value moved and you still need it | Borrow 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 fine | The checker is field-sensitive on direct field access and field-insensitive through a method call — destructure or split_at_mut → ownership.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 signature | Two 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 it | Missing 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 .await → async.md |
| Async program stalls with no error and no progress | A blocking call is occupying a runtime worker → async.md |
| Program deadlocks | One thread locks the same lock twice, or two threads take two locks in opposite order → concurrency.md |
already borrowed: BorrowMutError at runtime | Overlapping 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 handling | Byte 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 want | Model it as an enum, then let exhaustiveness find the sites → pattern-matching.md |
| Nothing in the logs explains what the service was doing | No 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 -d → cargo.md |
| Builds locally, fails in CI | Toolchain, lockfile, or feature-unification drift → cargo.md |
| Build is slow | cargo build --timings first; linker and codegen settings before restructuring code → cargo.md |
| Program is slow | Profile 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 else | Read the whole rustc output bottom-up including note: and help:, then rustc --explain E0xxx → compiler-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.
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.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.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.[profile.release] debug = true — full optimization plus symbols, so the profiler can name frames.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).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).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.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.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.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.
| Code | What it actually means | First move |
|---|---|---|
| E0382 | Use after move — consumed by a for loop, a by-value method, or a closure capture | Borrow (&v, .iter()) or copy the needed field out |
| E0499 | Two &mut to the same place alive at once | Split the borrow or shorten the first one (rungs 1-2) |
| E0502 | A &mut and a & overlap | Bind the read to a let before the mutation |
| E0505 / E0506 | Move out of, or assign to, something still borrowed | End the borrow first; std::mem::take if you need the value now |
| E0507 | Move out of a reference or an index expression | .clone(), .take(), mem::replace, or match by reference |
| E0515 | Returning a reference to a local | Return owned data or an index — the frame dies at return |
| E0597 | Borrowed value dropped too early | Hoist the owner's let above the borrower's |
| E0716 | Temporary dropped while borrowed | Bind the temporary: let s = make(); let r = &s; |
| E0106 | Missing lifetime specifier | Elision cannot choose among multiple inputs — name it (lifetimes.md) |
| E0308 | Mismatched types | Compare the two full paths in the message: usually &T vs T, or two versions of one crate (cargo.md) |
| E0277 | Trait bound not satisfied | Missing use of the trait, or a bound to add — read required by bottom-up |
| E0038 | Trait is not dyn compatible (formerly "object safe") | Generic method, Self return, or associated const — use generics or split the trait (traits.md) |
| E0599 | No method found | Trait not in scope, or the receiver does not deref to the type that has the method |
| E0072 | Recursive type has infinite size | Box the recursive field |
| E0117 / E0119 | Orphan rule / conflicting impls | Newtype wrapper; either the trait or the type must be local (traits.md) |
| E0282 / E0283 | Type annotations needed / ambiguous | Turbofish (collect::<Vec<_>>()) or a typed binding |
| E0596 | Cannot borrow as mutable | Missing mut on the binding, or a &self method that needs &mut self |
| Anything else | Every code is stable and documented | rustc --explain E0xxx, then compiler-errors.md for the symptoms that have no code |
Pick the weakest tool that expresses the need — every row costs something the row above does not.
| Need | Use | Cost or trap |
|---|---|---|
| Single owner, size known | T | The 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 often | Box<T> | One allocation; Box<dyn Trait> also erases the type |
| Borrow when possible, allocate only on modification | Cow<'a, str> | Removes "clone just in case" from hot paths (strings-and-text.md) |
| Shared ownership, one thread | Rc<T> | Not Send; reference cycles leak — break them with Weak |
| Shared ownership, multiple threads | Arc<T> | Atomic refcount; Arc<T> by itself is still read-only |
| Mutate shared state, one thread | Rc<RefCell<T>> | Borrow checking moves to runtime: BorrowMutError panics |
Mutate one Copy value, one thread | Cell<T> | Get/set only, no borrows, therefore no panic |
| Mutate shared state, multiple threads | Arc<Mutex<T>> | Poisoning on panic; lock ordering is yours to enforce |
| Many readers, rare writer | RwLock<T> | Writer starvation and re-entrant read deadlock; measure before preferring it to Mutex |
| Initialize once, then read forever | OnceLock<T> (rust >=1.70), LazyLock<T> (rust >=1.80) | Replaces lazy_static and most once_cell uses with no dependency |
| A counter or a flag | AtomicUsize, AtomicBool | Cheapest sharing available; memory ordering is a real decision (concurrency.md) |
| Anything else | Start at T and &T | Move down a row only when the compiler proves you must |
Ordered by cost. Take the first rung that compiles.
let n = v.len(); v.push(n); compiles where v.push(v.len()) does not.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.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.usize costs nothing and removes the borrow entirely. Index-based access is a legitimate design, not a defeat.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.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.unsafe. Only with a // SAFETY: comment naming the invariant, and Miri in CI (unsafe.md).Before emitting Rust code, verify:
unwrap/expect outside tests is justified in a comment, or replaced by ?&str, &[T], or impl AsRef<Path> wherever the function only reads them#[from] conversions, no Box<dyn Error> in a published signaturestd guard, Rc, or blocking call is alive across an .awaitchecked_* or saturating_*, never debug-only overflow checks#[non_exhaustive] (api-design.md)unsafe block carries a // SAFETY: comment naming the invariant it upholdscargo clippy -- -D warnings, not merely cargo buildUser-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/rust/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| edition | 2015 | 2018 | 2021 | 2024 | 2024 | Selects syntax and semantics in emitted code (if let temporary scope, reserved keywords, unsafe attribute forms) and which cargo fix --edition advice applies |
| msrv | text (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_runtime | tokio | async-std | smol | embassy | none | tokio | Chooses the runtime API in every async example and the spawn, timer, and IO types used from async.md |
| error_style | thiserror | anyhow | std-enum | box-dyn | auto | auto (thiserror in libraries, anyhow in binaries) | Sets the error type generated at each boundary and the ? conversion strategy in errors.md |
| unsafe_policy | forbid | reviewed | allow | reviewed | forbid emits #![forbid(unsafe_code)] and refuses ladder rung 8; reviewed requires a SAFETY comment plus a Miri step; allow drops the Miri requirement |
| lint_level | default | pedantic | deny-warnings | default | Controls which clippy lints the Output Gates enforce and whether generated CI fails on warnings |
| explanation_depth | fix-only | standard | teaching | standard | fix-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:
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.mdmod.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.mdno_std, wasm target — affects cross-compilation.md, embedded-no-std.md, wasm.mdcargo deny, cargo audit) — affects every "add a crate" recommendationcargo 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.mdcargo 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.mdexplanation_depth), whole file vs diff, comment density in emitted code, whether to show the rejected alternatives — affects the shape of every answercargo.md| Trap | Why it fails | Do instead |
|---|---|---|
let _ = mutex.lock(); to hold a lock | _ is not a binding; the guard drops on that line and the lock is free immediately | let _guard = mutex.lock().unwrap(); (rule 7) |
Rc<RefCell<T>> for a tree with parent pointers | Cycles never free, and every access can panic with BorrowMutError | Arena: Vec<Node> plus index handles (rung 7) |
.clone() inside a loop to appease the borrow checker | Turns an O(1) borrow into a copy per iteration | Fix the scope first (rungs 1-3), then clone deliberately |
#[derive(Clone)] on every generic type | Derive adds T: Clone to every type parameter, and that bound becomes part of your public API | Hand-write the impl, bounding only what actually needs it |
String parameters in a public API | Every caller allocates just to call you | &str, or impl Into<String> when you store it (rule 2) |
mem::transmute to change a lifetime | Erases the exact property the compiler was checking; UB the moment the data dies | Restructure, or a raw pointer plus a SAFETY comment (unsafe.md) |
async fn on functions that never .await | Colors the whole call graph and wraps sync work in a state machine | async only where an await point exists (async.md) |
Benchmarking with cargo run or cargo test | Debug build: the number measures instrumentation (rule 4) | --release, criterion, and black_box on the inputs |
cargo clean to fix a slow build | Deletes the incremental cache and guarantees the slowest possible next build | cargo build --timings and read the graph (cargo.md) |
.iter().collect::<Vec<_>>() just to loop over it | Allocates a vector to walk it once | Iterate the iterator; collect only when the collection is kept |
unwrap() on Mutex::lock() everywhere by reflex | Right most of the time, but hides that poisoning means another thread panicked mid-invariant | Handle or document the poison case (concurrency.md) |
| Adding lifetime parameters until it compiles | Produces signatures nobody can call and hides the real design error | Return owned data first; add lifetimes once you can state what borrows from what (lifetimes.md) |
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.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.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).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 modelsgo — the other systems language on the same team; different concurrency tradeoffsasync-patterns — language-agnostic concurrency and cancellation theory behind async.mderror-handling — cross-language error design behind rule 5Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/rust.