SKILLS.md for Rust development

Anyone augmenting their AI driven coding with skill files directly aimed at writing more idiomatic, well architected Rust? What has been your experience with getting AI to conform to best practices to create reasonable crate or workspace layouts? I've personally found that Claude's Rust output isn't very pretty and it tends to write untestable code unless it's being babysat.

I haven't found a way other than babysitting and manually cleaning up the damage.

I've tried supplying code style guidelines, but they get applied inconsistently. Sometimes they're completely ignored, sometimes taken too literally and make LLM uncritically rewrite existing code in a twisted parody of the coding style.

I've tried supplying lists of best practices and typical refactorings, and tell an agent to find where they can be applied. The results were equally mediocre. It only had a very shallow understanding, and only wanted to add getters/setters and newtypes everywhere.

I've tried telling LLM it has a role of a Software Architect and told it to manage subagents to systematically review and improve the codebase. It was like children playing Doctor - they've imitated the words and motions.

I've tried supplying the code structure as pseudocode, but that didn't get far either. Writing top-down goes off the rails when an agent writes without feedback from unit tests, but when writing bottom up it quickly stops seeing the forest through the trees.

I've tried splitting project into smaller crates. The LLMs struggle to make cross-cutting changes across crates. They keep treating internal APIs as set in stone (despite being told otherwise) and keep hacking around the gaps in their own APIs instead of fixing the APIs.

My 2 cents: I am slowly transitioning back to writing the code myself for any important project (i.e., where I care about the quality of the code). I am tired of babysitting! My current view: LLMs are wonderful for prototyping, creating code snippets where I am too lazy to get the syntax right, or developing "simple" stuff (i.e., no more than a few thousand lines of code), where I only care to get the correct output (and it is clear if something is wrong), but I do not care about the code.

Edit: my main motivation for ditching the LLM approach is to keep on learning Rust; vibe coding defeat that purpose.

I'm with @kornel and @pzometa: my personal experience has been that AI-assisted programming just isn't worth the trouble. The one exception that I will make is writing rustdoc-type function comments. I ask the AI to write something, which I then use a first draft for writing my own. That gives me two things: it gets the markup syntax "in the right place" (because I can never remember from one language to the next), and it gets me over the writer's block I inevitably face. On this second point, were it not for that AI-provided first draft, my rustdoc comments would all end up being variations of "This does stuff. Just read the code.". This is technically correct, although not terribly useful.

Of course, I'm willing to accept that I'm just a terrible "prompt engineer".

Shamefully I have hardly written any code in half a year. Was busy experimenting with LLM in the warp terminal. Produced half a dozen useful but not critical and not so big tools in Rust. That all worked out very well, for example creating real-time 3d data visualisations using WGPU that run on Windows, Linux and Mac and also in the browser with Leptos. That at least got me off the ground with Leptos and WGPU quickly, neither of which I knew anything about before hand.

I have never had any SKILLS.md file. I just say what I want, in some detail in any old file, say spec.md and get the LLM to read that. Detail includes stateing the inputs, expected outputs, language to use etc. Oh and a requirement that cargo fmt and cargo clippy be used. I guess that amounts to a skill file. That spec.md file grows as the project continues so that details learned along the way don't get forgotten.

Looking over the code it has mostly seemed reasonable. But I do worry how far I can push this before it all goes into a mess tat the LLM cannot fix and I hardly know the workings of.

All in all I'm not ready to let LLM loose on our serious product. But can I still remember how to code after all this time....

In my opinion, terms like idiomatic, well-architected, best practices (my favorite!), pretty, and reasonable are highly subjective.

They depend on time, domain, experience, team culture, constraints, and context.

So expecting someone else, human or LLM, to produce code exactly as you would have written it yourself is unrealistic.

I've been dabbling with LLMs for coding lately (mostly out of laziness), and I've been rather unimpressed with the results.

On one hand, it has suggested to me (unprompted) the very function I was about to write, as if by magic. That side of things can be very convenient and nice.

On the other hand, it's been consistently doing things like introducing logic bugs and introducing unnecessary casts (like usize -> usize). It also tends to get hung up on details I've told it to ignore, and repeating exactly the mistakes I told it to fix.

Overall, if AI was a person I contracted to write code for me, I'd quickly become frustrated with their ineptitude and refusal to own and correct mistakes. Perhaps I was just getting the poor man's experience (the best model I used was the Gemini business tier), but I wouldn't trust AI to write anything I didn't already understand completely, and even then...

There are some on github for rust, but seem a bit ad hoc. I noticed modular released skills for mojo (which is nearing version 1.0).

The syntax has changed a lot - they don't have "fn" for compiled function any more, now its all "def" like python. So there is a syntax skill for that.

I don't use a SKILL.md but if I did I would have one that told it what the latest version of the rand crate is. It keeps breaking and the AIs are always behind and mistakenly point out "there is an error there...".

I only used it on code bases where there's already a huge amount of code written by me alone and, in my experience, after several corrections + putting stuff it consistently gets wrong in memory files it kind of learned and writes the wiring / large scale refactors, my beloved use case, pretty neatly and without problems.

Stockhastic parrot[1] can pull similar things from the wast pile of knowledge that it accumulated.

Because these require an actual understanding of what you are doing.

Which is entirely normal: it's very hard for a pattern matching to ignore anything and the fact that LLMs sometimes may actually ignore things is amazing, but don't expect too much from them. Consider it more of a party trick, of it works.

it's really amazing how much can one do with just a pattern matching with the brain of a goldfish, but please, please don't try to teach said goldfish to think… it's not designed for thinking! That would come later, much later.


  1. That's not an insult, that's high praise: existing models are closer to golden fish in complexity than to parrot. ↩︎

I fully agree, AI is just a jumble of weights and biases that ends up mimicking human speech (not human thought). That's kind of my point: both in theory and in practice (my experience), generic pattern matching only gets you so far. That said, pattern matching is still a very useful mechanism for a number of applications, where the topic is already well-documented online (so it has a better base against which to pattern match).

Precisely. For some reason people imagine this:

As if AI is destined to be built in the same way human is grown.

Remember how Isaac Asimov, the really foreseeing guy, imagined it, before we started: understanding → hearing → speech. Like child does.

Reality: speech (half-century ago) → hearing (couple of decades ago) → understanding (in progress). Opposite movement.

Same with understanding, too! LLM starts where human finishes!

The proper, if imperfect, but pretty good analogue is not child. Nope. Proper analogue is someone more knowledgeable than Einstein, superb scientist with knowledge of all things simultaneously… at the age of 150. With advanced Alzheimer dementia. And… that's it.

Yes, it may not be appealing image like child, it may not help with planning for the future (because AI moves to toward death like aforementioned person would, but in opposite direction), but it very efficiently explains what happens with tools that are available here and now: don't try to teach LLM anything, it doesn't have the capability to learn… but give it short, clear, instructions — and it may use these… if wouldn't mix them or forget because of 150 years brain…

It still can be used and pretty decently used, just don't expect to use it like you would use child or intern. It's entirely opposite: things that people expected that AI would struggle with (emotions, easy to read forms speech, etc), shown in countless movies and comics and everywhere are trivial for it — while things that were supposed to be trivial (logic and math) are hard. Tremedos intuition, near zero ability to think.

Yes, that's now what we were promised for 100+ years, sure… but that's what we have, danm it. Why try to apply some analogues that clearly don't work?

This looks like a misinterpretation of what they said. They didn't say the child would learn, they said it was only imitating.

Skills look like just md files describing various useful approaches or structure of the project. I write some by hand, AI also likes them and tries to read immediately. It can also write such files if asked, but of course you need to review.

Relying on the LLMs to interpret the rules correctly is often hit and miss. Sometimes, there's too much text, they'll just ignore them. So a predictable and deterministic program is much better.

I am vibe-coding my own linter. The linter code is going to be absolutely atrocious, but it doesn't matter as long as it enforces consistency upon the codebases I care about.

Sorry for being verbose, but since you asked, the following rules made my AI agent to generate much better rust code. These rules were developed incrementally, while working on my project. I am not sure that all these rules are idiomatically correct, I made them based on my personal understanding of what is idiomatically correct. Whenever AI did something wrong from my point of view, I asked it to correct and remember the rule. This significantly improved the code quality (from my point of view). Here are the rules:

Idiomatic Rust Patterns

Use Concise Control Flow

  • Prefer ? operator over explicit match statements for error propagation:

    // Good
    let result = operation()?;
    
    // Avoid
    match operation() {
        Ok(val) => val,
        Err(e) => return Err(MyError::from(e)),
    }
    
  • Use if let instead of match when handling single patterns:

    // Good
    if let Some(value) = option {
        do_something(value);
    }
    
    // Avoid
    match option {
        Some(value) => do_something(value),
        None => {}
    }
    
  • Use while let for loop patterns:

    // Good
    while let Some(item) = iterator.next() {
        process(item);
    }
    
    // Avoid
    loop {
        match iterator.next() {
            Some(item) => process(item),
            None => break,
        }
    }
    

Iterator Patterns

  • Prefer iterator chains over manual loops:

    // Good
    let results: Vec<_> = items
        .iter()
        .filter(|item| item.is_valid())
        .map(|item| item.process())
        .collect();
    
    // Avoid
    let mut results = Vec::new();
    for item in &items {
        if item.is_valid() {
            results.push(item.process());
        }
    }
    
  • Use functional combinators (filter_map, flatten, fold, etc.) instead of intermediate collections

Error Handling

  • Use ? operator over explicit error conversions when error types can be automatically converted via From trait or #[from] attribute:
    // Good: Use ? when NoteError has #[from] std::io::Error
    let file = File::open("data.txt")?;
    
    // Avoid: Explicit map_err when automatic conversion works
    let file = File::open("data.txt").map_err(NoteError::from)?;
    
  • Use map_err only when you need custom error transformation that cannot be handled by the From trait:
    // Good: Custom error context that can't be expressed via From
    let file = File::open("data.txt").map_err(|e| NoteError::IoWithContext(e, "failed to open config"))?;
    
  • Prefer anyhow or thiserror for application-level error handling
  • Use Result extensions like ok_or, and_then, or_else for complex flows

Other Idiomatic Patterns

  • Prefer destructuring assignments where appropriate
  • Use .. spread operator in struct updates
  • Leverage From/Into traits for conversions
  • Use AsRef/AsMut bounds for flexible parameter types

Import Grouping

  • Group imports from same crate using curly braces:

    // Good: Grouped imports
    use mdnotes_lib::{NoteError, Result};
    use mdnotes_lib::repository::{NoteRepository, fs::FileSystemRepository};
    
    // Avoid: Multiple separate imports
    use mdnotes_lib::NoteError;
    use mdnotes_lib::Result;
    use mdnotes_lib::repository::NoteRepository;
    use mdnotes_lib::repository::fs::FileSystemRepository;
    
  • Use qualified paths sparingly - prefer imports over fully-qualified names in function signatures:

    // Good: Using imported types
    pub async fn handle_create(title: String, content: Option<String>) -> Result<()> {
        // ...
    }
    
    // Avoid: Fully-qualified in signatures (verbose)
    pub async fn handle_create(title: String, content: Option<String>) -> mdnotes_lib::Result<()> {
        // ...
    }
    

Project Philosophy

Avoid Overengineering

  • Implement exactly what's required by task specifications and acceptance criteria
  • Cover specified edge cases but avoid adding extra functionality unless explicitly requested
  • Focus on essential features - resist the urge to add bells and whistles
  • Keep changes minimal and targeted - only modify what's necessary to fulfill requirements
  • Refactor only when essential or explicitly requested

Commands

Build

cargo build
cargo build --release

Test

cargo test
cargo test --all

Run

cargo run -- create "Test Note" --content "Hello"
cargo run -- list
cargo run -- search "test"

Check

cargo check
cargo clippy

Code Style

Rust Standards

  • Follow standard Rust naming conventions (snake_case for functions/variables, CamelCase for types)
  • Use thiserror for custom error types
  • Prefer ? operator over match for error propagation
  • Use async/await for I/O operations
  • Derive common traits: Debug, Clone, PartialEq where applicable

Documentation

  • Use /// for public API documentation
  • Include examples in doc comments
  • Document panics and errors

Testing

  • Write unit tests in #[cfg(test)] modules
  • Use tempfile for filesystem tests
  • Use assert_cmd for CLI integration tests
  • Follow test-first development (Constitution II)

Quality Assurance

Upon completion of each task that involves Rust code, AI agents MUST run the following commands to ensure code quality and consistency:

Required Checks

# Format all Rust code
cargo fmt

# Run linter and static analysis
cargo clippy

Why These Checks Matter

  • cargo fmt: Ensures consistent code formatting across the entire codebase, following Rust's standard style guide
  • cargo clippy: Catches common mistakes, idiomatic issues, and provides suggestions for better Rust code

When to Run

  • After completing any implementation task
  • Before marking a task as done
  • Before creating a pull request or committing changes
  • After any code modification in the crates/ directory

Handling Errors

If cargo fmt or cargo clippy report issues:

  1. Fix all warnings and errors reported by clippy
  2. Re-run formatting if needed
  3. Verify the fixes don't break existing tests
  4. Re-run both commands to confirm compliance