Rust Ownership skill for Claude Code
Satisfy the borrow checker without clone spam. Claude reads rustc's three spans, finds where a borrow can end sooner or a value can have a single owner, and fixes errors structurally instead of cloning or wrapping everything in Rc<RefCell>. It applies the thiserror-for-libraries, anyhow-for-binaries split, sets up edition 2024 workspaces with shared lints, and keeps clippy at zero warnings.
- name
rust-ownership - size201 lines · ≈2,150 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Resolves Rust borrow checker and lifetime errors by restructuring ownership instead of cloning blindly, and writes idiomatic Rust with thiserror/anyhow error handling, smart pointer choices (Box, Rc, Arc, RefCell, Mutex), cargo workspaces, and clippy-clean code. Use when rustc reports E0382, E0499, E0502, E0505, E0597, or lifetime errors, when designing structs that hold references, choosing error types, or setting up cargo, clippy, and rustfmt. Try asking
- rustc says cannot borrow self.items as mutable because it is also borrowed — fix it properly
- Design error types for this crate: library plus CLI in one workspace
- This struct holds a &str and lifetimes are spreading everywhere — should it own the data?
Use it when
- rustc reports a borrow, move, or lifetime error you cannot explain
- Choosing between Box, Rc, Arc, RefCell, Mutex, or Cow
- Defining error types or replacing unwrap calls
- Setting up a cargo workspace with clippy and rustfmt in CI
Skip it when
- Writing generic async concurrency limits in any language — use async-concurrency
- Upgrading many crates at once — use dependency-upgrades
How to check the result
cargo check, cargo clippy --all-targets -- -D warnings and cargo test pass, and every clone added in the diff has a stated reason.
The common mistake it prevents
Reaching for clone or Rc<RefCell<T>> at the first borrow error, which compiles but moves the conflict to runtime panics and needless allocations.
Done well means: the code compiles without .clone() used as a sedative, each value has
one obvious owner, lifetimes appear only where a struct or function truly borrows,
errors carry context, and cargo clippy --all-targets -- -D warnings is clean.
Ground rules
- Read the full rustc message, including the “borrow later used here” span. The fix is usually moving that later use, not adding a clone.
- Prefer restructuring (shorter borrows, splitting structs, passing indices or IDs) over
Rc<RefCell<_>>. Interior mutability is a design choice, not an error fix. - Owned types in structs by default (
String,Vec<T>). Reach for&'a strfields only for short-lived views where profiling shows allocation matters. - Function parameters: borrow (
&str,&[T],&T) unless the function stores or consumes the value; then take ownership (String,Vec<T>,T). - No
unwrap()/expect()in library code paths that can fail on input.expectwith a message stating the invariant is acceptable for true invariants.
Workflow for a borrow checker error
- [ ] 1. Identify the error code (rustc --explain E0502 for the canonical example)
- [ ] 2. Mark the three spans: where the borrow starts, the conflicting action, the later use
- [ ] 3. Ask: can the later use move earlier, or the borrow end sooner?
- [ ] 4. Ask: does this need two owners, or one owner and a reference passed down?
- [ ] 5. Apply the smallest structural fix from the table below
- [ ] 6. Re-run cargo check; if a new error appears in a different place, repeat
- [ ] 7. Run clippy and tests
For the error-code catalog with before/after fixes, read references/borrow-errors.md when the error is not one of the common patterns below.
Fix patterns, cheapest first
| Symptom | Fix |
|---|---|
| Mutate while iterating a collection | Collect indices/keys first, then mutate; or use retain, iter_mut, drain(..) |
Borrow one field mutably, another immutably via &mut self method | Borrow fields directly (let a = &mut self.a; let b = &self.b;) or split into a helper taking the fields |
| Value moved into closure, used after | Clone before the closure only the cheap part, or restructure so the closure runs last; move + Arc for threads |
Value used after move into function | Pass &value; if callee needs ownership, clone at that call or return the value back |
| Returning reference to a local | Return the owned value (String, Vec) |
| HashMap get-then-insert | map.entry(k).or_insert_with(..) |
Option field needs taking out of &mut self | self.field.take() or std::mem::replace / mem::take |
| Self-referential struct | Store indices or offsets instead of references; or separate the owner from the view |
| Graph / cyclic data | Arena + usize IDs (or slotmap); Rc + Weak only if arenas do not fit |
// E0502: cannot borrow `v` as mutable because it is also borrowed as immutable
let first = &v[0];
v.push(4); // conflict
println!("{first}");
// Fix: end the shared borrow before mutating (copy the value out if it is Copy)
let first = v[0];
v.push(4);
println!("{first}");
Choosing ownership containers
| Need | Use |
|---|---|
| Heap allocate, single owner, recursive type | Box<T> |
| Shared read-only, single thread | Rc<T> |
| Shared read-only, across threads | Arc<T> |
| Shared + mutable, single thread | Rc<RefCell<T>> (runtime borrow panics possible) |
| Shared + mutable, threads, short critical sections | Arc<Mutex<T>> (parking_lot or std) |
| Many readers, rare writers | Arc<RwLock<T>> |
| Simple counter/flag across threads | AtomicU64, AtomicBool |
| Maybe-owned string (avoid alloc on fast path) | Cow<'a, str> |
| Cheap clones of large immutable strings/slices | Arc<str>, Arc<[T]> |
Never hold a std::sync::Mutex guard across .await; use tokio::sync::Mutex only
when the lock must span an await, otherwise scope the guard in a block.
Lifetimes
- Elision covers most functions: one input reference, or
&self, gives the output the same lifetime. Write lifetimes only when the compiler asks. - Name lifetimes by meaning when there are several:
'src,'buf. T: 'staticmeans “owns all its data or holds only ‘static references”, not “lives forever”. Thread spawns andtokio::spawnrequire it; pass owned data orArc.- Struct holding a reference:
struct Parser<'a> { input: &'a str }. Every method returning slices ofinputshould return&'a str, not&strtied to&self. impl Traitin return position captures all in-scope lifetimes in edition 2024; use+ use<'a>precise capturing when you need to restrict it.
Error handling
| Crate kind | Use |
|---|---|
| Library (others match on errors) | thiserror enum per module or crate |
| Binary / application | anyhow::Result with .context() at each layer |
| Both in one workspace | thiserror in lib crates, anyhow in the bin |
// library
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("config file not found at {path}")]
NotFound { path: std::path::PathBuf },
#[error("invalid TOML in config")]
Parse(#[from] toml::de::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
// application
use anyhow::{Context, Result};
fn load(path: &std::path::Path) -> Result<Config> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config {}", path.display()))?;
let cfg = toml::from_str(&text).context("parsing config")?;
Ok(cfg)
}
?converts viaFrom; add#[from]only when the conversion is unambiguous.- Do not stringify errors (
map_err(|e| e.to_string())); it destroys the source chain. maincan returnanyhow::Result<()>; print with{:#}or{:?}to see the chain.
Cargo and tooling
cargo new --lib mylib # or --bin
cargo add serde --features derive
cargo add --dev proptest
cargo check # fastest feedback loop
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo test --all-features
cargo tree -d # duplicate dependency versions
cargo update -p some-crate # bump one lockfile entry
Workspace root Cargo.toml:
[workspace]
resolver = "3" # edition 2024 default
members = ["crates/*"]
[workspace.package]
edition = "2024"
rust-version = "1.85"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
thiserror = "2"
anyhow = "1"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
module_name_repetitions = "allow"
Member crates use serde.workspace = true and [lints] workspace = true.
Idioms clippy and reviewers expect
- Iterators over index loops:
items.iter().filter(..).map(..).collect::<Vec<_>>(). if let Some(x) = opt/let Some(x) = opt else { return ... };(let-else).impl From<A> for Brather than ad-hocto_b(); implementDisplayfor user-facing types, deriveDebugeverywhere.- Accept
impl AsRef<Path>or&Pathfor paths;impl Into<String>for constructors that store strings. #[must_use]on builders and functions whose result must not be ignored.- Newtypes (
struct UserId(u64);) to stop mixing IDs.
Gotchas
.clone()on anArcis cheap; on aVec<String>it is not. Clippy’sredundant_clone(nursery) and reading the type tell you which.RefCellborrow errors are runtime panics (already mutably borrowed); they appear in production, not incargo check.async fnin traits is stable, but the returned futures are notSendby default for generic callers; use thetrait_variantcrate or explicitimpl Future + Send.- Deadlocks: acquiring two
Mutexes in different orders in different code paths. Define a lock order or merge the state. unsafeto bypass the borrow checker is almost never correct; if you think you need it, document the invariant and runcargo miri test.- Edition 2024 changes temporaries in
if letand tail expressions; when migrating, runcargo fix --editionand review drop-order sensitive code (locks, guards).
Verification
cargo checkandcargo build --all-targetssucceed.cargo clippy --all-targets --all-features -- -D warningsclean.cargo testpasses; for concurrency changes also run tests under--release.- Count clones you added:
git diff | grep -c '\.clone()'. Justify each in the summary. - For
unsafeor tricky lifetimes:cargo +nightly miri teston the affected module.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/borrow-errors.md
# Borrow checker error catalog
Run `rustc --explain <CODE>` for the official explanation. Below: what the error really
means and the structural fix to try first.
## E0382 — use of moved value
The value was moved (into a function, a `let`, a closure, or a `for` loop) and used
again.
```rust
let names = vec!["a".to_string()];
for n in names { println!("{n}"); } // moves names
println!("{}", names.len()); // E0382
```
Fixes, in order: iterate by reference (`for n in &names`); pass `&names`; if the callee
truly needs ownership and you need it after, clone at the call site and say why.
## E0499 — two mutable borrows at once
```rust
let a = &mut self.items[0];
let b = &mut self.items[1]; // E0499
```
Fixes: `let [a, b, ..] = &mut self.items[..] else { return };` (slice patterns), or
`split_at_mut`, or `get_disjoint_mut` on slices/HashMap (stable since 1.86). For struct
fields, borrow distinct fields directly; the checker tracks fields, not methods.
## E0502 — mutable borrow while shared borrow alive
The shared borrow is still used later. Move the later use before the mutation, copy the
needed value out, or shrink the scope with a block.
## E0505 — move out while borrowed
```rust
let r = &s;
drop(s); // E0505
println!("{r}");
```
Fix: finish using `r` first; or make `r` own a copy.
## E0506 — assign to borrowed value
Same shape as E0505 but with assignment. End the borrow first.
## E0507 — cannot move out of borrowed content
Moving a field out of `&self` or out of a `Vec` by index.
Fixes: `.clone()` if cheap; `std::mem::take(&mut self.field)` (leaves `Default`);
`Option::take()`; `vec.swap_remove(i)` / `vec.remove(i)`; iterate with `into_iter()` if
you own the collection.
## E0515 — return reference to local data
Return the owned value instead. If you are returning a slice of a parameter, tie the
lifetime to that parameter.
## E0597 — borrowed value does not live long enough
A reference outlives its owner, often stored into a struct or collection that lives
longer than a loop iteration.
```rust
let mut refs: Vec<&str> = vec![];
for line in reader.lines() {
let line = line?;
refs.push(&line); // E0597: line dropped at end of iteration
}
```
Fix: store owned values (`Vec<String>`), or keep the owners in an outer collection that
outlives the references.
## E0716 — temporary value dropped while borrowed
```rust
let s: &str = &String::from("x").to_uppercase(); // ok (temporary lifetime extension)
let parts = text.to_lowercase().split(','); // E0716 later when used
```
Fix: bind the temporary: `let lower = text.to_lowercase(); let parts = lower.split(',');`
## E0373 — closure may outlive the current function (thread/task spawn)
Add `move` and make captured data owned or `Arc`. For `std::thread::scope`, borrows of
locals are allowed without `'static`:
```rust
std::thread::scope(|s| {
for chunk in data.chunks(1024) {
s.spawn(move || process(chunk));
}
});
```
## E0106 — missing lifetime specifier
Struct field or return type references need a named lifetime. Before adding `<'a>`,
ask whether the struct should own the data instead.
## E0596 — cannot borrow as mutable
The binding is not `mut`, or you have `&T` where `&mut T` is needed. Add `mut` to the
binding or change the parameter type; if behind `Rc`, that is the signal for `RefCell`
or a redesign.
## "cannot return value referencing function parameter" with iterators
Returning `impl Iterator<Item = &T>` built from a local `Vec`. Return the `Vec`, or take
the collection as a parameter and return `impl Iterator<Item = &'a T> + 'a`.
## Async-specific
- "future cannot be sent between threads safely": a non-Send type (`Rc`, `RefCell`
guard, `std::sync::MutexGuard`) is alive across an `.await`. Drop it in a block before
awaiting or switch to `Arc` / `tokio::sync::Mutex`.
- "borrowed value does not live long enough" with `tokio::spawn`: spawned tasks need
`'static`; move owned data or `Arc` clones in.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.