Rust Ownership skill for Codex

Satisfy the borrow checker without cloning everything. Codex reads the whole rustc diagnostic, names the two conflicting uses, and reaches for structural fixes first: ending a borrow early, splitting fields, mem::take, the entry API, index-based graphs. Clone and Rc<RefCell> become deliberate, justified choices rather than reflexes, and Rust 2024 edition capture and temporary-scope changes are handled explicitly.

  • Languages
  • $rust-ownership
  • 175 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- rust-ownership

Installs to ~/.agents/skills/rust-ownership/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $rust-ownership, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Fixes Rust borrow checker errors (E0499, E0502, E0505, E0382, E0597, E0716, E0106, E0373) by restructuring ownership instead of sprinkling clone, and chooses between references, lifetimes, Box, Rc, Arc, RefCell, Mutex and Cow with clear rules. Covers struct lifetimes, iterator invalidation, self-referential designs, closures and Rust 2024 edition capture changes. Use when rustc rejects a borrow, a function needs a lifetime annotation, or someone asks whether to use Rc or Arc, RefCell, or clone.

Use it when

  • rustc rejects code with a borrow, move or lifetime error
  • A pull request adds many clone() calls or Rc<RefCell<..>> to make code compile
  • You are designing a struct that holds references or a graph with back-pointers
  • Code broke after migrating to the Rust 2024 edition

Use something else when

How to check the result

cargo clippy --all-targets -D warnings and cargo test pass, and every clone added by the change is listed with a reason.

The mistake it prevents

Wrapping everything in Rc<RefCell<T>> to silence the compiler, which trades a compile error for a runtime BorrowMutError panic.

~/.agents/skills/rust-ownership/SKILL.md

--- name: rust-ownership description: Fixes Rust borrow checker errors (E0499, E0502, E0505, E0382, E0597, E0716, E0106, E0373) by restructuring ownership instead of sprinkling clone, and… license: MIT ---

Done well means: the code compiles because the data flow is right, not because every conflict was cloned away or wrapped in Rc<RefCell<_>>. Each fix keeps the original behaviour, names the rule it satisfies, and leaves no new clone() on a hot path or large value without a reason in the report.

Workflow

- [ ] 1. Read the full rustc error (cargo build 2>&1 | less), including every "note" and "help"
- [ ] 2. Identify the two (or more) uses that conflict and the span where each borrow lives
- [ ] 3. Classify: shared vs mutable overlap, use after move, outlives, missing lifetime
- [ ] 4. Pick the smallest structural fix from the tables below
- [ ] 5. cargo check, then cargo clippy --all-targets, then cargo test
- [ ] 6. Report the error, root cause and why the fix is sound

Run rustc --explain E0502 (works offline) when a code is unfamiliar. Check edition in Cargo.toml first; Rust 2024 (1.85+) changes temporary scopes and impl Trait capture.

Error code to fix

CodeMeaningFirst fixes to try, in order
E0502Mutable borrow while a shared borrow is live (or reverse)End the shared borrow earlier (copy out the value, use an index, compute before mutating); split the borrow into fields
E0499Two mutable borrows at onceSplit struct fields, split_at_mut, get_disjoint_mut on slices and HashMap (1.86+), or restructure into two passes
E0505Move out while borrowedFinish using the reference before the move; move a clone of the small part; restructure ownership
E0382Use after moveBorrow (&x) instead of moving; move into the last use only; .clone() if the value is small or Copy-like; return the value back
E0597 / E0716Borrowed value does not live long enough / temporary droppedBind the temporary to a let; return owned data (String, Vec) instead of a reference to a local
E0106Missing lifetime specifierReturn an owned type, or tie output to the one input it borrows from (fn f<'a>(x: &'a T, y: &U) -> &'a V)
E0373Closure may outlive borrowed valuemove closure; clone an Arc into it first for threads/tasks
E0507Cannot move out of borrowed contentstd::mem::take, mem::replace, Option::take, or .clone() the field
E0596Cannot borrow as mutableAdd mut to the binding or take &mut self; for shared state see interior mutability below

Structural fixes that beat clone

// E0502: reading len while pushing
let n = v.len();          // shared borrow ends here
v.push(n);

// E0499 on struct fields: borrow fields, not the whole struct
let Self { items, index, .. } = self;
for it in items.iter() { index.insert(it.id, it.name.clone()); }

// Mutating while iterating: collect decisions, then apply
let stale: Vec<usize> = map.iter().filter(|(_, v)| v.expired()).map(|(k, _)| *k).collect();
for k in stale { map.remove(&k); }
// or in one call when the API exists:
map.retain(|_, v| !v.expired());

// E0507: take ownership out of a &mut
let old = std::mem::take(&mut self.buffer);   // leaves Default in place
let job = self.pending.take();                // Option<T> -> leaves None

// Entry API instead of get-then-insert double lookup (and borrow conflicts)
*counts.entry(word).or_insert(0) += 1;

Index-based designs (Vec<Node> plus usize ids, or an arena crate) beat Rc<RefCell<Node>> graphs for trees with parent links, graphs, and ECS-style data.

Choosing an ownership type

NeedUseNot
One owner, value on the heap (recursive type, large or dyn Trait)Box<T>Rc
Read-only access for a while&Tclone()
Shared ownership, single threadRc<T>Arc (needless atomic cost)
Shared ownership across threads or tokio tasksArc<T>Rc (not Send)
Mutate through a shared handle, single threadRc<RefCell<T>>, or Cell<T> for Copy valuesunsafe
Mutate through a shared handle, multi threadArc<Mutex<T>> / Arc<RwLock<T>>, atomics for countersRefCell (not Sync)
Sometimes borrowed, sometimes owned (e.g. maybe-escaped string)Cow<'a, str>always allocating
Parent back-pointerWeak<T>Rc cycle (leaks)
Cheap immutable shared string or sliceArc<str>, Arc<[T]>Arc<String> (double indirection)

RefCell moves borrow checking to runtime: a second borrow_mut() while one is live panics. Keep RefCell borrows short and never hold one across a call that might re-enter.

Lifetimes

  • Elision covers most functions: one input reference, or &self, gives the output its lifetime. Add explicit lifetimes only when there are several inputs and the output borrows from a specific one.
  • A struct holding a reference (struct Parser<'a> { src: &'a str }) is right for short-lived views (parsers, iterators). For long-lived or stored values, own the data.
  • 'static bound on a generic (T: 'static) means “contains no non-static borrows”, not “lives forever”. String satisfies it.
  • &'static str return types for computed strings are wrong; return String or Cow<'static, str>.
  • Self-referential structs (a field borrowing another field) do not work with plain references. Store indexes or ranges into the owned buffer, or use a crate built for it (ouroboros, self_cell) only after simpler designs fail.

Rust 2024 edition changes that surface as borrow errors

  • Return-position impl Trait now captures all in-scope lifetimes by default. If a returned iterator should not borrow an argument, say so with precise capturing: fn ids<'a>(&'a self, filter: &str) -> impl Iterator<Item = u32> + use<'a>.
  • if let scrutinee temporaries now drop before the else branch, and tail-expression temporaries drop before locals. Code that relied on a lock guard living through else must bind it explicitly.
  • cargo fix --edition handles most migrations; review its diff rather than accepting it blind.

Closures, threads and async

let config = Arc::new(load_config()?);
for id in ids {
    let config = Arc::clone(&config);          // clone the handle, not the data
    handles.push(std::thread::spawn(move || work(id, &config)));
}

// Scoped threads borrow locals without Arc (std::thread::scope, Rust 1.63+)
std::thread::scope(|s| {
    for chunk in data.chunks(1024) {
        s.spawn(|| process(chunk));
    }
});

tokio::spawn and std::thread::spawn need 'static: move owned data or Arc clones in. For async-specific Send errors (a guard held across .await), use $rust-tokio-async.

When clone is the right answer

Clone is fine and often best when the value is small (String ids, small structs), cloned once outside a loop, an Rc/Arc handle, or when the alternative is a lifetime parameter that would spread through ten types. Say so explicitly in the report. Clone is wrong inside hot loops over large data, or when it hides a real aliasing bug (two parts of the code believing they mutate the same state).

Gotchas

  • A reference returned by self.map.get(k) keeps all of self borrowed through a later self.update() call; copy out what you need (.copied(), .cloned() on a small value) or look the key up again after the mutation.
  • Non-lexical lifetimes end a borrow at its last use, but a value with a Drop impl keeps its borrow alive until the end of scope. Call drop(guard) explicitly.
  • for x in vec moves the Vec; for x in &vec borrows; for x in &mut vec borrows mutably.
  • Returning &mut from a method locks the whole self for as long as the result lives; return owned data or split the API when callers need to touch other fields.
  • Rc<RefCell<T>> everywhere is a design smell from GC languages; reach for it last.
  • unsafe to silence the borrow checker is almost never correct: use $rust-unsafe-review if it is already in the codebase.

Verification

cargo check --all-targets
cargo clippy --all-targets -- -D warnings   # catches redundant_clone, needless_borrow
cargo test

Count clone() calls added by the change (git diff | grep -c '\.clone()') and justify each one. If the fix changed a public signature (added a lifetime, returned owned data), note it as an API change.

Report template

**Error:** E0502 in src/index.rs:88 (`self.entries` borrowed while `self.rebuild()` mutates)
**Cause:** iterator over entries held across a &mut self call
**Fix:** collect stale keys first, then mutate; no clones added
**API impact:** none
**Checks:** cargo clippy -D warnings clean, cargo test 214 passed

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Rust Ownership"
  short_description: "Satisfy the borrow checker without cloning everything"
  default_prompt: "$rust-ownership cannot borrow self.entries as mutable because it is also borrowed as immutable, fix it properly"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc