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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rust-ownership Installs to ~/.agents/skills/rust-ownership/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rust-ownership The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rust-ownership Installs to .agents/skills/rust-ownership/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rust-ownership.zipunzip -o rust-ownership.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rust-ownership PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The error is about Send or a guard held across .await in async code: use rust-tokio-async
- The code already uses raw pointers or unsafe blocks: use rust-unsafe-review
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.
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
| Code | Meaning | First fixes to try, in order |
|---|---|---|
| E0502 | Mutable 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 |
| E0499 | Two mutable borrows at once | Split struct fields, split_at_mut, get_disjoint_mut on slices and HashMap (1.86+), or restructure into two passes |
| E0505 | Move out while borrowed | Finish using the reference before the move; move a clone of the small part; restructure ownership |
| E0382 | Use after move | Borrow (&x) instead of moving; move into the last use only; .clone() if the value is small or Copy-like; return the value back |
| E0597 / E0716 | Borrowed value does not live long enough / temporary dropped | Bind the temporary to a let; return owned data (String, Vec) instead of a reference to a local |
| E0106 | Missing lifetime specifier | Return an owned type, or tie output to the one input it borrows from (fn f<'a>(x: &'a T, y: &U) -> &'a V) |
| E0373 | Closure may outlive borrowed value | move closure; clone an Arc into it first for threads/tasks |
| E0507 | Cannot move out of borrowed content | std::mem::take, mem::replace, Option::take, or .clone() the field |
| E0596 | Cannot borrow as mutable | Add 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
| Need | Use | Not |
|---|---|---|
One owner, value on the heap (recursive type, large or dyn Trait) | Box<T> | Rc |
| Read-only access for a while | &T | clone() |
| Shared ownership, single thread | Rc<T> | Arc (needless atomic cost) |
| Shared ownership across threads or tokio tasks | Arc<T> | Rc (not Send) |
| Mutate through a shared handle, single thread | Rc<RefCell<T>>, or Cell<T> for Copy values | unsafe |
| Mutate through a shared handle, multi thread | Arc<Mutex<T>> / Arc<RwLock<T>>, atomics for counters | RefCell (not Sync) |
| Sometimes borrowed, sometimes owned (e.g. maybe-escaped string) | Cow<'a, str> | always allocating |
| Parent back-pointer | Weak<T> | Rc cycle (leaks) |
| Cheap immutable shared string or slice | Arc<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. 'staticbound on a generic (T: 'static) means “contains no non-static borrows”, not “lives forever”.Stringsatisfies it.&'static strreturn types for computed strings are wrong; returnStringorCow<'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 Traitnow 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 letscrutinee temporaries now drop before theelsebranch, and tail-expression temporaries drop before locals. Code that relied on a lock guard living throughelsemust bind it explicitly.cargo fix --editionhandles 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 ofselfborrowed through a laterself.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
Dropimpl keeps its borrow alive until the end of scope. Calldrop(guard)explicitly. for x in vecmoves the Vec;for x in &vecborrows;for x in &mut vecborrows mutably.- Returning
&mutfrom a method locks the wholeselffor 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.unsafeto silence the borrow checker is almost never correct: use$rust-unsafe-reviewif 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.yaml
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.