Nova Labs is currently on pause. New product purchases are unavailable. The blog remains live as an archive of the experiment.
Back to blog

Claude Code for Rust and Go projects: CLAUDE.md configs and real workflows

April 12, 2026 11 min read

Most Claude Code documentation uses JavaScript or Python examples. That makes sense for reach, but it leaves Rust and Go developers without concrete guidance on the configuration that actually matters for their stacks. The defaults work poorly for both languages without some upfront setup.

This covers what to put in CLAUDE.md for Rust and Go projects, which pitfalls keep coming up, and how to structure a testing workflow that doesn't fight the language toolchain.

Why Rust and Go need custom CLAUDE.md configs

Both languages have strong opinions baked into their toolchains. Rust has clippy, the borrow checker, and a compiler that rejects code other languages would happily run. Go has gofmt, go vet, and a community that cares a lot about error handling style.

Without explicit instructions, Claude Code produces code that compiles but drifts from your conventions. It might clone when borrowing would work, or add unwrap() calls you'd never accept in production. In Go it might use blank identifier error suppression when you want explicit handling. These aren't hard to fix individually, but they accumulate and become a review burden.

A good CLAUDE.md sets the constraints once and they apply to every session.

CLAUDE.md configuration for Rust projects

Here is a starting template for a Rust project. Adjust the specifics to your codebase:

# Project: [Name]

## Stack
- Rust (stable toolchain, minimum 1.75)
- Cargo workspace with crates: [list them]
- Async runtime: Tokio (if applicable)

## Build and check commands
- Check: cargo check --all-targets
- Lint: cargo clippy --all-targets -- -D warnings
- Test: cargo test
- Format check: cargo fmt --check
- Always run clippy before calling a task complete

## Code conventions
- No unwrap() or expect() in library code; use proper error propagation with ?
- unwrap() is acceptable in tests and main() with a comment explaining why it's safe
- Prefer borrowing over cloning; if you clone to satisfy the borrow checker, add a comment
- unsafe blocks must include a SAFETY comment explaining the invariants being upheld
- Use thiserror for library error types, anyhow for application-level error handling

## Lifetime rules
- Prefer explicit lifetime annotations over introducing unnecessary cloning
- When lifetimes become complex, add a comment explaining the ownership model
- Avoid 'static bounds unless the type genuinely needs static lifetime

## What NOT to do
- Do not add allow(dead_code) or allow(unused) to silence warnings; fix them instead
- Do not use unwrap() as a placeholder unless the code is clearly marked as a draft
- Do not write unsafe code without explicit instruction

The clippy rule matters more than it looks. Without -D warnings, clippy suggestions are advisory. With it, Claude Code will fix them because the build fails otherwise. That single flag shifts the default from "suggestions" to "requirements."

Handling the borrow checker with Claude Code

The most common mistake is pasting only the error line. Borrow checker errors are contextual. The issue is often not where the compiler points, but several lines earlier where you moved a value or took a reference. Give Claude the full compiler output:

cargo build 2>&1 | head -80

Then ask Claude to read the error and trace where the conflicting borrows originate. It's generally good at this. Where it struggles is with complex pin + async combinations (common in custom futures), and with lifetime variance in trait objects. For those, the fix Claude suggests is often correct but the explanation may gloss over the actual variance issue. Worth double-checking.

A useful prompt when you're stuck: "Explain why this borrow checker error occurs, then suggest the minimal fix that doesn't involve cloning." The minimal fix constraint prevents the shortcut of cloning everything into silence.

Testing setup for Rust

Rust tests live in the same file as the code under a #[cfg(test)] module, which is a different pattern from most other languages. Claude handles this correctly, but your CLAUDE.md should clarify whether you also use a tests/ directory for integration tests, and whether you use cargo-nextest instead of the default test runner.

## Testing
- Unit tests: inline #[cfg(test)] modules in each file
- Integration tests: tests/ directory, one file per major feature area
- Run unit tests: cargo test --lib
- Run all tests including integration: cargo test
- Test runner: cargo-nextest (install if not present: cargo install cargo-nextest)
- nextest run: cargo nextest run
- Mocking: mockall crate (already in dev-dependencies)
- Do not add test utilities to non-test code paths

If you use cargo-nextest, say so explicitly. It has different output format and different flags. Claude will default to cargo test otherwise and the output parsing differs.

CLAUDE.md configuration for Go projects

Go configuration is slightly different because the language is more permissive by default. The compiler won't stop Claude from writing code that is technically valid but stylistically wrong for your project. Explicit rules matter more here:

# Project: [Name]

## Stack
- Go 1.22+
- Module path: github.com/yourorg/yourrepo
- Database: [postgres/sqlite/none]
- HTTP framework: [net/http stdlib / chi / gin / echo]

## Build and check commands
- Build: go build ./...
- Vet: go vet ./...
- Test: go test ./...
- Lint: golangci-lint run (config in .golangci.yml)
- Format: gofmt -w . (all new code must be gofmt-clean)
- Always run go vet before marking a task complete

## Error handling conventions
- Always handle errors explicitly; never use _ to discard errors except in defer
- Wrap errors with context: fmt.Errorf("doing X: %w", err)
- Return errors to the caller; do not log and return nil
- Sentinel errors in pkg/errors/errors.go; check with errors.Is() not string comparison
- Custom error types implement the error interface with Error() string

## Context handling
- All functions that do I/O must accept context.Context as the first parameter
- Name it ctx, not context or c
- Do not store context in structs; pass it through function calls

## Goroutine conventions
- Every goroutine must have a clear owner responsible for waiting on it
- Use errgroup for goroutine fans that can fail
- Do not start goroutines in init() or package-level variable initialization

## What NOT to do
- Do not use panic() for error handling outside of truly unrecoverable states
- Do not use global variables for state that should be passed as parameters
- Do not write tests that depend on external network resources without a build tag

Goroutine patterns: where Claude Code gets it wrong

The most common goroutine problem Claude Code produces without guardrails is the unowned goroutine. It will write:

go func() {
    doSomething()
}()

That goroutine has no owner. If doSomething() panics, the program crashes. If the function returns before the goroutine finishes, you have a data race. Adding the errgroup convention to your CLAUDE.md makes Claude use golang.org/x/sync/errgroup by default when you need concurrent work:

g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
    return doSomething(ctx)
})
if err := g.Wait(); err != nil {
    return fmt.Errorf("concurrent work failed: %w", err)
}

That pattern is composable, handles cancellation, and gives the caller a clean error. It takes one line in CLAUDE.md to make it the default.

Testing setup for Go

Go's testing package is minimal by design. Whether you use testify, table-driven tests, or something else should be documented:

## Testing
- Framework: testify (assert and require packages in dev deps)
- Pattern: table-driven tests for functions with multiple input/output cases
- Test files: foo_test.go next to foo.go in the same package
- Integration tests: tag with //go:build integration and run with -tags integration
- Run unit tests only: go test ./... (no build tags)
- Run integration tests: go test -tags integration ./...
- Use t.Parallel() for tests that don't share state
- Do not use time.Sleep in tests; use channels or sync primitives to wait

The t.Parallel() note is important. Claude will add it correctly when you ask for it, but without instruction it will omit it, and your test suite runs slower than it needs to.

Generate your stack config in 30 seconds

If you want a complete CLAUDE.md for your Rust or Go project without building it from scratch, the ContextKit Generator produces a full config based on your stack inputs. Already have a config? Score it out of 10 with the Analyzer to find gaps.

Common pitfalls summary

For Rust: paste full compiler output, not just the error line. Add -D warnings to clippy. Require SAFETY comments on unsafe blocks. Distinguish library error handling from application error handling.

For Go: define error handling convention explicitly. Require context threading on I/O functions. Mandate errgroup for concurrent work. Separate integration tests with build tags so unit test runs stay fast.

Both languages reward upfront investment in CLAUDE.md. The time spent writing these rules pays back within the first session when Claude stops producing code you have to fix on review.

Want to build your own AI OS?

The AI OS Blueprint gives you the complete system: 53-page playbook, working skills, and a clonable repo. Starting at $47.

30-day money-back guarantee. No subscription.