.claude/skills/safe-over-unsafe/SKILL.md
Use when designing safe public APIs that wrap unsafe Rust code, adding unsafe blocks to existing types, reviewing unsafe code for soundness, or creating new types backed by raw pointers, MaybeUninit, or FFI
npx skillsauth add ahrav/gossip-rs safe-over-unsafeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Module privacy is the soundness boundary — not the unsafe keyword. The
unsafe {} block is a syntax marker; the real safety mechanism is private
fields behind a safe public API, within a module (or crate) where all code
is trusted.
MaybeUninit, raw pointers, or FFIunsafe blocks to existing code for performanceSend/Sync/Drop for types with unsafe internalsFollow every step. Skipping any step is how soundness bugs happen.
< to <= in
a safe bounds check can make all unsafe blocks unsound.#![forbid(unsafe_code)]; unsafe lives in a dedicated crate.#![deny(unsafe_op_in_unsafe_fn)]
#![deny(clippy::undocumented_unsafe_blocks)]
Two kinds of invariants exist — conflating them causes bugs:
bool must be 0 or 1.)Vec's len <= cap.)Document both in a module-level //! or struct-level /// doc comment:
/// # Invariants
/// - `len <= CAPACITY` (safety)
/// - Slots `[head..head+len)` (mod cap) are initialized (safety)
/// - Initialized slots contain valid `T` values (validity)
Every invariant should be evaluated: can this be a const assertion, a NonZero type, or a typestate transition instead of a runtime check?
const { assert!(N > 0 && N.is_power_of_two()) }; // compile-time
Compile-time enforcement eliminates entire bug classes. Runtime
debug_assert! is the fallback — use assert! in public API
preconditions that guard unsafe code in the implementation.
Every // SAFETY: comment must argue invariant preservation, not just
state a fact. Use this structure:
// SAFETY: [what unsafe operation is being performed]
// Invariant: [which safety invariant holds here — and WHY from the code path]
// Implies: [which validity invariant this satisfies for the unsafe op]
// Preserved: [what invariants remain intact after this operation]
Bad: // SAFETY: index is in bounds
Good: // SAFETY: physical = idx & MASK, so physical < CAPACITY (mask clears // high bits). The element is initialized because idx < self.len, and our // safety invariant guarantees slots [head..head+len) are initialized. // This satisfies assume_init_ref's requirement that the value is valid T.
For unsafe fn, use # Safety doc sections documenting caller obligations.
Incorrect Send/Sync is the #1 real-world unsafe soundness bug (tokio RUSTSEC-2025-0023, lock_api RUSTSEC-2020-0070, windows RUSTSEC-2022-0008).
| Situation | Action |
|-----------|--------|
| Type uses [MaybeUninit<T>; N] | Auto-derive is correct. No PhantomData needed. |
| Type uses raw pointers | Add PhantomData<T> for owned data (covariant, correct Send/Sync). |
| Need manual Send/Sync | Red flag. Bound generics: unsafe impl<T: Send + Sync> Sync for MyType<T> {} |
| Unsure about variance | If the type owns T, it should be covariant (like Vec). Use PhantomData<T>. |
Always prefer auto-derivation. Add compile-time assertions:
const _: () = {
fn assert_send<T: Send>() { fn requires_send<S: Send>() {} requires_send::<MyType<T>>(); }
fn assert_sync<T: Sync>() { fn requires_sync<S: Sync>() {} requires_sync::<MyType<T>>(); }
};
If an operation modifies state in multiple steps and any step can panic
(e.g., T::drop(), T::clone()), use the decrement-before-drop or
guard pattern:
// Decrement-before-drop: len reflects reality even if drop panics
while self.len > 0 {
self.len -= 1;
unsafe { self.buf[self.len].assume_init_drop(); }
}
Test with a deliberately-panicking Drop impl via catch_unwind.
Unsafe code cannot trust safe trait implementations. T::drop() can
panic. T::clone() can panic. Ord impls can be inconsistent. Write
defensively so broken trait impls cause wrong results, never UB.
All three are required. No single tool is sufficient.
| Layer | Tool | What It Catches | Required |
|-------|------|----------------|----------|
| Dynamic UB | cargo +nightly miri test | Aliasing, uninit, alignment, use-after-free | Every test run |
| Aliasing model | MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test | Tree Borrows violations | CI |
| Reference model | proptest vs known-correct safe impl (e.g., VecDeque) | Logic errors, edge cases | Every test run |
| Drop correctness | DropTracker with Arc<AtomicUsize> | Double-drop, leaks | Every test run |
| Bounded proof | Kani proof harnesses | Exhaustive invariant verification | Critical code |
Before declaring unsafe code "done," verify each:
unsafe impl Send/Sync without restrictive bounds on genericsMaybeUninit: using write() for init, tracking init state, never mem::uninitializedlen/capacity updated atomically with the memory operationassert! (not just debug_assert!)| Pattern | When to Use | Exemplar |
|---------|------------|----------|
| Private fields + safe API | Always | std Vec |
| Crate-level #![forbid(unsafe_code)] | Multi-crate projects | Fuchsia, gossip-rs |
| Compile-time const assertions | Numeric invariants | RingBuffer power-of-2 |
| MaybeUninit + init tracking | Uninitialized storage | std MaybeUninit docs |
| PhantomData for variance | Raw pointer containers | Rustonomicon Vec |
| Guard struct for panic safety | Multi-step mutations | hashbrown HashMap |
| Typestate (generic state param) | State-dependent operations | serde Serializer |
| Sealed trait | Fixed impl set | zerocopy |
| unsafe trait as capability gate | External impls needed | bytemuck Pod |
| Lifetime-encoded validity | Temporal access control | crossbeam-epoch Guard |
development
Deep first-principles code explanation that builds real understanding through phased walkthroughs with diagrams. Covers algorithms, data structures, memory layout, concurrency patterns, and performance tricks — especially for systems code in Rust. Use whenever the user asks to explain, walk through, break down, deep dive into, or understand code. Trigger on "how does this work", "what's happening here", "teach me about this", "why is it done this way", or when the user references a file with @ and wants to understand it. Proactively use when examining code involving lock-free algorithms, atomics/CAS, memory ordering,
development
Use when creating implementation-ready beads tasks that need testing strategy, optimal implementation approach, and documentation requirements baked in — composes /create-task with parallel enrichment agents that analyze the codebase and produce concrete test specifications, algorithm/data-structure guidance, and doc quality standards so implementing agents don't need to re-research
development
--- name: autoresearch description: Autonomous Goal-directed Iteration. Apply Karpathy's autoresearch principles to ANY task. Loops autonomously — modify, verify, keep/discard, repeat. Supports bounded iteration via Iterations: N inline config. version: 1.9.11 --- # Claude Autoresearch — Autonomous Goal-directed Iteration Inspired by [Karpathy's autoresearch](https://github.com/karpathy/autoresearch). Applies constraint-driven autonomous iteration to ANY work — not just ML research. **Core id
development
Use when implementing a new feature and assessing coverage gaps, during periodic test hygiene, when test suites feel bloated, or before merging code that changes coordination or hot paths. Two-phase assess-then-improve testing pipeline.