Claude Code Rust CLAUDE.md rust-analyzer AI Coding 2026

Claude Code for Rust Projects: CLAUDE.md Template and Workflow Guide (2026)

The Prompt Shelf ·

Rust is the language where AI assistance can go most spectacularly wrong. The ownership system, borrow checker, and strict type guarantees mean that code that looks plausible — the kind an AI confidently generates — will fail to compile for reasons the agent didn’t anticipate. Without explicit guidance in your CLAUDE.md, Claude Code falls back to patterns learned from millions of lines of code in other languages. Those patterns don’t transfer to Rust.

We’ve run Claude Code against real Rust projects: a CLI tool, a tokio-based API server, and two library crates. The failure modes without a configured CLAUDE.md are consistent:

  • .unwrap() calls scattered through library code instead of propagated errors
  • Mixing Arc<Mutex<T>> and raw Rc<RefCell<T>> in the same async context
  • unsafe blocks added without safety comments or justification
  • Clippy warnings left in generated code because the agent wasn’t told to care
  • New error types defined inline instead of using the project’s existing thiserror definitions

A well-written CLAUDE.md eliminates these. Here’s exactly what to put in it.

Why Rust + Claude Code Works (But Needs Config)

Claude Code understands Rust. It can reason about lifetimes, help with trait design, and navigate the borrow checker in ways that save genuine time. The problem isn’t comprehension — it’s defaults.

By default, Claude Code optimizes for code that compiles and passes tests. In Rust, that’s a lower bar than it sounds. Code that compiles with .unwrap() everywhere will pass tests until it hits an edge case in production. Code that uses unsafe without documentation is a maintenance liability even if it works today.

The CLAUDE.md file shifts Claude’s optimization target. Instead of “code that compiles,” it produces “code that matches this project’s conventions.” That’s the difference between generated code you can ship and generated code you have to rewrite before review.

The Complete CLAUDE.md Template for Rust

This template is the starting point we use for new Rust projects. Copy it, delete what doesn’t apply, and add your project-specific notes.

# Rust Project: [ProjectName]

## Build & Test Commands
- Build: `cargo build`
- Test: `cargo test`
- Test with output: `cargo test -- --nocapture`
- Lint: `cargo clippy -- -D warnings`
- Format: `cargo fmt`
- Check (fast, no codegen): `cargo check`
- Audit dependencies: `cargo audit`
- Documentation: `cargo doc --open`

## Rust Edition and Toolchain
- Edition: 2021 (always specify in Cargo.toml)
- MSRV: 1.75.0 (stable, pinned in rust-toolchain.toml)
- Do NOT use nightly features

## Error Handling
- Libraries: use `thiserror` — derive `Error` for all custom error types
- Applications/binaries: use `anyhow` — propagate with `?`, add context with `.context()`
- Never use `.unwrap()` or `.expect()` in library code
- `.expect()` is acceptable in binary main() for setup failures (file not found, etc.)
- Never use `.unwrap()` in tests — use `?` with `#[tokio::test]` or return `Result<(), Box<dyn Error>>`
- Propagate errors with `?` unless there is a documented reason to handle locally

## Clippy Policy
- Treat all warnings as errors: `cargo clippy -- -D warnings`
- Run clippy before committing, not just before merging
- Do not use `#[allow(clippy::...)]` without a comment explaining why

## Code Style
- All public items (structs, enums, functions, traits, modules) must have doc comments (`///`)
- Private items: doc comments are encouraged but not required
- Prefer explicit type annotations on public function signatures
- Do not use wildcard imports (`use foo::*`) except in test modules

## Dependency Policy
- Prefer minimal dependencies — justify each new crate in the PR description
- Pin exact versions for production binaries (`=1.2.3` in Cargo.toml)
- Use `^` (caret) for library crates (allows compatible updates)
- Run `cargo audit` before every release
- Do not add a crate to solve a problem that can be solved with std in under 20 lines

## Testing
- Unit tests: `#[cfg(test)]` module at the bottom of each file
- Integration tests: `tests/` directory
- Test naming: `test_<function_name>_<scenario>` pattern
- Use `cargo test -- --nocapture` when debugging output
- Mock external dependencies using trait objects, not concrete types
- Aim for 80%+ coverage on library crates; focus on edge cases not happy paths

## Memory Safety and Unsafe
- No `unsafe` blocks without a `// SAFETY:` comment explaining the invariants upheld
- If `unsafe` is required, isolate it in a dedicated module (e.g., `src/ffi/` or `src/sys/`)
- Prefer `Arc<Mutex<T>>` over raw pointers for shared state across threads
- Do not use `Rc<RefCell<T>>` in async code (not `Send`)
- If you need `unsafe`, ask before writing it — we may have a safe alternative

## Module Structure
- `src/lib.rs` or `src/main.rs` — crate root, minimal logic
- `src/error.rs` — all custom error types
- `src/types.rs` — shared types and newtypes
- Keep modules focused: one concern per file, not one per type

Build and Test Commands

The commands section is the most immediately useful part of CLAUDE.md. Claude Code reads it before running any terminal commands, which prevents the most common failure: running cargo build --release when you need a fast iteration cycle, or skipping cargo clippy entirely.

The distinction between cargo check and cargo build matters for development speed. cargo check performs type-checking and borrow-checking without code generation — it’s typically 3-5x faster and sufficient for catching errors during iteration. Tell Claude Code to prefer cargo check during code exploration and reserve cargo build for final verification.

## When to Use Each Command
- `cargo check` — fastest feedback during iteration, catches type errors and borrow checker issues
- `cargo build` — full build, use before testing or running
- `cargo clippy -- -D warnings` — run after any code change before committing
- `cargo test` — after all code changes; use `-- --nocapture` when debugging test output
- `cargo fmt` — before every commit; enforced in CI

Error Handling Rules in CLAUDE.md

Error handling is the highest-leverage section of CLAUDE.md for Rust projects. The thiserror / anyhow split is an established convention but Claude Code won’t apply it without instruction.

The mental model: thiserror for libraries (because callers need to match on error variants), anyhow for binaries (because you just need a displayable error to report to the user). Write this explicitly:

## Error Handling — Library Crate (this project)
- Define all error types in `src/error.rs`
- Use `thiserror::Error` derive macro for all custom errors
- Error variants must have descriptive messages via `#[error("...")]`
- Wrap external crate errors with `#[from]` — do not re-invent conversion
- Return `Result<T, crate::Error>` from all fallible public functions

Example pattern:
```rust
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("configuration key not found: {key}")]
    KeyNotFound { key: String },
    
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("parse error: {0}")]
    Parse(#[from] serde_json::Error),
}

Claude Code will follow this pattern when generating new error types, which eliminates the "define a new error type at every call site" anti-pattern.

## Memory Safety Policy

The `unsafe` block policy deserves its own section because it's the one area where AI-generated code poses the highest risk. Claude Code can write valid `unsafe` Rust — the borrow checker won't catch all unsafety — but it won't automatically document the invariants that make it sound.

```markdown
## Memory Safety
- Every `unsafe` block requires a `// SAFETY:` comment above it
- The comment must explain: (1) what invariant is being upheld, (2) why the code is sound
- Isolate all `unsafe` in `src/ffi.rs` or `src/sys.rs` — never inline in business logic
- Do NOT use `unsafe` to work around borrow checker errors you don't understand
- If you're unsure whether an `unsafe` block is sound, stop and ask

Example of acceptable unsafe:
```rust
// SAFETY: We hold exclusive access to `ptr` for the duration of this block.
// The pointer was obtained from `Box::into_raw` and has not been aliased.
let value = unsafe { Box::from_raw(ptr) };

## rust-analyzer Integration

Claude Code and rust-analyzer operate alongside each other, and understanding how they interact helps you configure both effectively.

rust-analyzer is your LSP — it provides real-time diagnostics, go-to-definition, and completion in your editor. Claude Code reads compiler output from `cargo check` and `cargo clippy` but does not talk directly to rust-analyzer's LSP protocol. They're complementary, not redundant.

The practical implication: use rust-analyzer's real-time feedback during exploration, then feed `cargo check` output to Claude Code explicitly when you want it to fix errors. Claude Code responds well to pasted compiler output:

$ cargo check 2>&1 | head -30 error[E0502]: cannot borrow data as mutable because it is also borrowed as immutable —> src/main.rs:15:5 | 14 | let r = &data[0]; | ---- immutable borrow occurs here 15 | data.push(42); | ^^^^^^^^^^^^^^ mutable borrow occurs here


Paste that output into Claude Code with "fix the borrow checker error" and it will produce correct code more reliably than if you describe the error in natural language.

Reference rust-analyzer in your CLAUDE.md so Claude Code knows what tooling exists:

```markdown
## LSP and IDE Integration
- LSP: rust-analyzer (configured in .vscode/settings.json or editor equivalent)
- rust-analyzer and cargo check produce equivalent diagnostics
- When showing compiler errors for Claude to fix, prefer `cargo check 2>&1` output
- rust-analyzer settings: proc-macro support enabled, target triple: [your target]

Automating with Hooks

Claude Code supports hooks that run shell commands after tool use. For Rust projects, a PostToolUse hook that runs cargo check after each file edit gives Claude Code immediate feedback on whether its changes compile.

Add to your Claude Code project settings (.claude/settings.json):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "cargo check 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}

With this hook active, Claude Code sees compiler output after every edit. When it introduces a borrow checker error, it immediately gets the diagnostic and can fix it in the next tool call — without you having to run cargo check manually and paste the output.

A stronger variant runs clippy instead of check:

{
  "command": "cargo clippy -- -D warnings 2>&1 | tail -30"
}

This is slower (clippy does more analysis than check) but catches style issues alongside correctness issues in the same feedback loop.

Async Rust with tokio

If your project uses async Rust, document the runtime choice and async patterns explicitly. Claude Code defaults to patterns that work with both tokio and async-std, which in practice means it sometimes generates code that compiles with neither without adjustment.

## Async Runtime: tokio
- Runtime: tokio with `#[tokio::main]` for the binary entry point
- Test runtime: `#[tokio::test]` for all async test functions
- Use `tokio::spawn` for tasks that should run concurrently with the current task
- Use `tokio::select!` for racing futures — not nested `.await` chains
- Use `tokio::sync::Mutex` (not `std::sync::Mutex`) in async contexts
- CPU-bound work: wrap in `tokio::task::spawn_blocking` to avoid blocking the executor
- Do NOT mix `tokio` and `async-std` runtimes in the same binary

Example spawn pattern:
```rust
// Preferred: structured concurrency with JoinSet
let mut set = tokio::task::JoinSet::new();
for item in items {
    set.spawn(process(item));
}
while let Some(result) = set.join_next().await {
    handle(result?)?;
}

## AGENTS.md Version for Cross-Tool Compatibility

If your team uses multiple AI coding tools alongside Claude Code (Cursor, Copilot Workspace, Codex), maintain an AGENTS.md at the project root. It uses the same format as CLAUDE.md and is read natively by all major agents.

```markdown
# AGENTS.md — [ProjectName]

## Build Commands
- `cargo check` — type check without codegen (fast)
- `cargo clippy -- -D warnings` — lint, treat warnings as errors
- `cargo test` — run all tests
- `cargo fmt` — format code (run before committing)
- `cargo audit` — security audit for dependencies

## Code Conventions
- Rust edition 2021
- Error handling: `thiserror` (library), `anyhow` (binary)
- Never `.unwrap()` in library code — use `?`
- Every `unsafe` block requires a `// SAFETY:` comment
- All public items need `///` doc comments
- Clippy lint level: deny warnings

## Directory Layout
- `src/error.rs` — all custom error types
- `src/types.rs` — shared data types
- `src/lib.rs` — public API surface, minimal logic
- `tests/` — integration tests only; unit tests live in `#[cfg(test)]` modules

## What Agents Should NOT Do
- Add `.unwrap()` to library code
- Write `unsafe` blocks without `// SAFETY:` comments
- Create new error types outside `src/error.rs`
- Add crates without noting the justification in the PR

Common Mistakes to Avoid

After running AI agents against Rust codebases, these are the patterns that appear most often without explicit CLAUDE.md guidance:

Unwrap propagation. The path of least resistance for an AI making code compile is to add .unwrap(). It works, the tests pass, and the error is silently swallowed in production. The fix is explicit in CLAUDE.md: “never .unwrap() in library code.”

Thread-safety confusion. Mixing Rc<RefCell<T>> (not Send) with tokio::spawn (requires Send) is a compile error, but the agent generates it anyway because it knows Rc<RefCell<T>> is a common interior mutability pattern. Specify Arc<Mutex<T>> for shared async state in your CLAUDE.md.

Silent unsafe. Claude Code can write sound unsafe Rust. The problem is it doesn’t automatically document why it’s sound. An undocumented unsafe block that works today becomes a maintenance liability when the surrounding code changes. The // SAFETY: requirement in CLAUDE.md forces this documentation.

Error type proliferation. Without guidance, Claude Code defines MyNewError wherever it needs an error type. Over time you end up with 15 incompatible error types in one crate. One src/error.rs with a single Error enum, and a rule to add variants there instead of creating new types, keeps this under control.

Generic-when-you-wanted-dyn. Claude Code defaults to generic bounds (fn process<T: Trait>(t: T)) when you might have wanted dynamic dispatch (fn process(t: &dyn Trait)). These have different tradeoffs. Specify your preference in CLAUDE.md:

## Polymorphism Policy
- Prefer generics when: performance matters, trait bounds are simple, single-crate use
- Prefer trait objects (`Box<dyn Trait>`) when: return types need erasure, heterogeneous collections, cross-crate boundaries
- Ask before converting between the two — it affects the public API surface

Putting It Together

The CLAUDE.md template above isn’t exhaustive — it’s a starting point. The most effective CLAUDE.md files grow from caught mistakes: every time Claude Code generates something you have to revert, that pattern belongs in CLAUDE.md.

Rust’s strictness is the language’s main feature, not a burden. A well-configured CLAUDE.md makes AI assistance align with that strictness rather than fight it. The result is generated code that treats .unwrap() as a code smell, documents its unsafe blocks, and drops neatly into your existing error handling hierarchy.

Browse Rust configuration examples in our rules gallery for more CLAUDE.md and AGENTS.md snippets from real projects.


Frequently Asked Questions

Does Claude Code understand Rust’s borrow checker? Claude Code has strong Rust knowledge and can reason about ownership, lifetimes, and borrowing. Where it struggles is applying your project’s conventions — error handling patterns, test structure, clippy policy — without explicit instruction. CLAUDE.md bridges that gap.

Should I use CLAUDE.md or AGENTS.md for a Rust project? Use CLAUDE.md if Claude Code is your only agent. Add AGENTS.md if you also use Cursor, Copilot Workspace, or Codex — those tools read AGENTS.md natively. The Rust template above works in either file.

How does rust-analyzer interact with Claude Code? They run independently. rust-analyzer provides real-time LSP features in your editor. Claude Code reads cargo check / cargo clippy terminal output. Paste compiler output directly to Claude Code when you want it to fix specific errors.

Can Claude Code help with unsafe Rust? Yes, but only with the right guardrails. The // SAFETY: comment requirement in CLAUDE.md is load-bearing: it forces Claude Code to reason about why the unsafe block is sound, which catches errors that would otherwise be invisible until runtime.

What Rust error handling library should I specify in CLAUDE.md? The standard recommendation: thiserror for library crates (callers need matchable variants), anyhow for binary crates (propagate with ?, add context with .context()). If you use something else, specify it explicitly — Claude Code will follow whatever you document.


Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules