home / skills / petekp / claude-code-setup / rust

rust skill

/skills/rust

This skill helps you apply robust Rust patterns for file I/O, parsing, and cross-language boundaries to build reliable systems.

npx playbooks add skill petekp/claude-code-setup --skill rust

Review the files below or copy the command above to add this skill to your agents.

Files (6)
SKILL.md
6.1 KB
---
name: rust
description: Robust Rust patterns for file-backed data, parsing, persistence, FFI boundaries, and system integration. Use when writing Rust that handles file formats, subprocess integration, PID/process management, Serde serialization, or UniFFI boundaries. Covers UTF-8 safety, atomic writes, state machines, and defensive error handling.
---

# Rust Engineering Guide

Patterns for building reliable Rust systems that handle file-backed data, external process integration, and cross-language boundaries.

## Core Philosophy

**Conservative by Default**: Inputs from files, subprocesses, and external systems are untrusted.

- Prefer false negatives over false positives
- Same input → same output (deterministic)
- Never panic on user machines due to bad input

**Canonical Model Ownership**: When Rust is the source of truth, maintain separate representations:

| Layer | Purpose | Characteristics |
|-------|---------|-----------------|
| Internal domain | Business logic | Expressive enums, rich types |
| FFI DTOs | Cross-language boundary | Flat, stable, `String`-heavy |
| File format | Persistence | Versioned, round-trippable |
| External input | Validation boundary | Strictly validated, never trusted |

**Safe Rust Only**: None of these patterns require `unsafe`. Use ecosystem crates for safe abstractions.

---

## Reference Guides

Load the relevant reference when working in that domain:

| Domain | Reference | When to Load |
|--------|-----------|--------------|
| **Data Modeling** | [references/data-modeling.md](references/data-modeling.md) | Serde patterns, UniFFI, strong types, versioned schemas |
| **File I/O** | [references/file-io.md](references/file-io.md) | Atomic writes, concurrency control, file watching |
| **Process Integration** | [references/process-integration.md](references/process-integration.md) | PID verification, subprocess handling, timestamps |
| **Text & Parsing** | [references/text-and-parsing.md](references/text-and-parsing.md) | UTF-8 safety, path normalization, state machines |
| **Testing** | [references/testing.md](references/testing.md) | Round-trip tests, fuzz testing, Clippy lints |

---

## Error Handling

### Library vs Application Errors

**Libraries** (public API): Use `thiserror` with granular error types per operation:

```rust
// File operations have their own error type
#[derive(thiserror::Error, Debug)]
pub enum ReadError {
    #[error("failed to read {path}")]
    Io { path: PathBuf, #[source] source: std::io::Error },

    #[error("parse error at line {line}: {message}")]
    Parse { line: usize, message: String },
}

// Subprocess operations have their own error type
#[derive(thiserror::Error, Debug)]
pub enum SubprocessError {
    #[error("failed to spawn process")]
    Spawn(#[source] std::io::Error),

    #[error("process exited with {code:?}: {stderr}")]
    NonZeroExit { code: Option<i32>, stderr: String },

    #[error("output not valid UTF-8")]
    InvalidUtf8(#[source] std::str::Utf8Error),

    #[error("timed out after {0:?}")]
    Timeout(std::time::Duration),
}
```

**Applications** (internal/binary): Use `anyhow` for context-rich errors:

```rust
use anyhow::{Context, Result};

fn load_config(path: &Path) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path.display()))?;
    // ...
}
```

### Graceful Degradation

Errors degrade functionality, not crash. But *log* when being lenient:

```rust
match parse_metadata(&line) {
    Ok(meta) => entries.push(meta),
    Err(e) => {
        tracing::warn!("skipping malformed entry at line {}: {}", line_num, e);
        // Continue processing other entries
    }
}
```

---

## Quick Reference

### Do

- Use `std::sync::LazyLock` for static regex (Rust 1.80+)
- Hold locks across entire read-modify-write cycles
- Add `#[serde(deny_unknown_fields)]` for strict external input
- Truncate strings with `.chars()` or graphemes, not byte slicing
- Write files atomically with `sync_all()` before rename
- Verify PID identity with process start time
- Use `saturating_sub` for time arithmetic
- Run `cargo clippy -- -D warnings` and `cargo fmt` before commit

### Don't

- Use `#[from]` without adding context (loses *which* file failed)
- Create monolithic error enums spanning unrelated operations
- Silently ignore errors without logging
- Slice strings with `&s[..N]` (panics on char boundaries)
- Assume directory iteration order is stable
- Trust subprocess output without validation
- Use `unsafe` (not needed for these patterns)

---

## Bugs This Guide Prevents

| Bug | Pattern | Reference |
|-----|---------|-----------|
| PID reuse "ghost sessions" | Store + verify process start time | [process-integration.md](references/process-integration.md) |
| Timestamp unit mismatch (sec vs ms) | Normalize on read | [process-integration.md](references/process-integration.md) |
| UTF-8 panic on truncation | Use `.chars().take(n)` | [text-and-parsing.md](references/text-and-parsing.md) |
| Lost updates under concurrency | Lock spans full read-modify-write | [file-io.md](references/file-io.md) |
| Corrupt file on power loss | `sync_all()` before rename | [file-io.md](references/file-io.md) |
| Silent metadata corruption | Anchor updates to heading lines | [text-and-parsing.md](references/text-and-parsing.md) |
| Old data breaks new code | `#[serde(default)]` + `alias` | [data-modeling.md](references/data-modeling.md) |

---

## Change Checklist

When modifying these systems, verify:

**Schema / Serde**
- [ ] New fields use `Option` + `#[serde(default)]`
- [ ] Old field names supported via `alias` (read) or `rename` (write)
- [ ] External input uses `#[serde(deny_unknown_fields)]`

**Concurrency**
- [ ] Mutex held across entire read-modify-write cycle
- [ ] Shared state uses `Mutex<T>`, not `thread_local!`
- [ ] File locking documents platform caveats if used

**Robustness**
- [ ] No panics on file I/O or parse errors
- [ ] Errors logged before being ignored
- [ ] Subprocesses have timeouts

**Quality**
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo fmt --check` passes
- [ ] No `unsafe` blocks (unless justified and audited)

Overview

This skill captures robust, production-ready Rust patterns for file-backed data, subprocess integration, FFI boundaries, and system-level concerns. It focuses on conservative validation, deterministic behavior, and defensive error handling to avoid panics and data corruption. Use it to design safe, maintainable Rust code that interacts with files, processes, or other languages.

How this skill works

The skill lays out layered ownership: expressive internal domain models, flat stable FFI DTOs, versioned file formats, and a strict external-input validation boundary. It prescribes safe Rust-only patterns—atomic writes with sync_all, PID verification with start times, Serde schema evolution, UTF-8-safe text handling, and clear, contextual error types for libraries and applications. It also recommends testing and linting workflows to keep behavior deterministic and auditable.

When to use it

  • Implementing file persistence or migrating on-disk schemas safely
  • Integrating with subprocesses, agents, or PID-managed daemons
  • Exposing Rust code across language boundaries (UniFFI/FFI DTOs)
  • Parsing user-controlled text or binary input where UTF-8 safety matters
  • Building concurrent read-modify-write flows that must avoid lost updates

Best practices

  • Treat all file and subprocess input as untrusted; validate strictly and log when skipping malformed entries
  • Keep separate representations for domain, DTOs, and persisted schemas; evolve schemas with serde defaults and aliases
  • Use granular thiserror types in libraries and anyhow with context in binaries to preserve actionable error info
  • Write files atomically: write, call sync_all(), then rename; hold locks across the whole read-modify-write cycle
  • Avoid byte-slice string truncation; operate on chars or grapheme clusters to stay UTF-8 safe
  • Verify PID identity using process start time and use time-saturating arithmetic to avoid overflow bugs

Example use cases

  • A daemon persists session metadata and must avoid ghost sessions after PID reuse
  • A CLI tool spawns a formatter subprocess, validates UTF-8 output, and surfaces contextual errors
  • A library exposes a stable FFI DTO layer to a Python consumer while keeping rich internal enums
  • Migration of a JSON config file with backward-compatible serde rules and round-trip tests
  • Concurrent updater that locks a file, reads state, applies a change, and atomically replaces the file

FAQ

Should I use unsafe for FFI performance?

No. The patterns avoid unsafe; prefer stable crates and flat DTOs to keep cross-language boundaries safe.

When do I use anyhow vs thiserror?

Use thiserror for library-level, typed errors and anyhow in applications/binaries where adding context and flexible propagation is valuable.