Universal baseline rules for AI coding agents on Rust projects.
Merge project-specific instructions below the ## Project Overrides section.
These rules are mandatory. Do not skip, reorder, or silently ignore any item. When a project-specific rule conflicts with a baseline rule, the project-specific rule wins — but the conflict must be noted in a comment.
- Read before writing. Understand the existing code, conventions, and
Cargo.tomlbefore making any change. - Smallest correct change wins. If two solutions solve the problem equally well, prefer the shorter one.
- Leave the codebase cleaner than you found it — but only within the scope of the current task.
- Every claim must be verifiable. "It should work" is not acceptable. Run the checks. Show the output.
- Fail loudly and early. A compile error now is cheaper than a silent bug in production.
- Start with the smallest plausible interpretation of the request.
- If uncertain, ask one clarifying question — never assume the larger interpretation.
- Surface tradeoffs. Push back when a simpler approach exists.
- Name what is unclear and stop. Do not hide confusion behind an elaborate plan.
- Do the minimum that solves the problem. Nothing speculative or "just in case."
- If a task fits in 1–3 commands, do it directly without over-planning.
- No features, abstractions, or error handling beyond what was explicitly requested.
- If planning an options table with more than three rows, pause — you may have misread the request.
- Touch only what the request requires. Do not improve adjacent code, comments, or formatting.
- Do not refactor things that are not broken. Match the existing style exactly.
- Every changed line must trace directly to the user's request.
- Clean up only what your changes made unused. Never remove pre-existing dead code unless explicitly asked.
- Transform vague requests into verifiable goals before starting.
- Define what "done" looks like. Loop until verified.
- For multi-step work, state a brief plan with a verification step after each stage.
- Strong success criteria let the agent work independently. Weak criteria require constant clarification — clarify first.
- Ask one question at a time. Never chain questions.
- When presenting a recap, summary, or plan:
- Print it as formatted text (numbered list, table, or markdown block).
- Ask a single short confirmation: "Proceed?" or "Any changes?"
- Never embed the recap inside the question itself.
- Make a recommendation, summarize, confirm once. Stop there.
- If the user provides new information mid-task, re-evaluate the plan before continuing.
Run these in order. Do not skip any step. Show output or declare clean.
cargo fmt --check # formatting
cargo check # type-check without building
cargo clippy -- -D warnings # no warnings allowed
cargo test # all tests must pass
cargo doc --no-deps 2>&1 | grep warning # no doc warningsIf any step fails, fix the issue before reporting completion. Never report completion with known failures.
- Always specify
edition = "2024"(or the edition explicitly required by the project). - Pin direct dependencies to
major.minor(e.g.,serde = "1.0"). Avoid*versions. - Separate
[dependencies],[dev-dependencies], and[build-dependencies]cleanly. - Use
[profile.release]tuning only when performance requirements are documented. - Document why each dependency exists via an inline comment when it is not obvious.
[dependencies]
serde = { version = "1.0", features = ["derive"] } # serialization
thiserror = "2" # structured errors in lib code
tokio = { version = "1", features = ["full"] } # async runtime- Keep a root
Cargo.tomlwith[workspace]for all multi-crate projects. - Share common dependencies via
[workspace.dependencies]and inherit them in members. - Each crate must have a clear, single responsibility stated in its
Cargo.tomldescriptionfield.
- Prefer borrowing (
&T,&mut T) over cloning unless ownership transfer is semantically correct. - Use
Cow<'_, str>when a function sometimes owns and sometimes borrows string data. - Avoid
clone()inside loops; refactor to pass references or restructure data flow.
- Library crates: use structured errors with
thiserror. One error enum per public module boundary. - Binary / application crates: use
anyhowfor rich context propagation. - Always propagate errors with
?. Never swallow them silently. - Never use
unwrap()orexpect()in non-test code without a documented invariant.- If an invariant truly cannot be violated, use
expect("invariant: <explain why this is safe>").
- If an invariant truly cannot be violated, use
- Map external errors to domain errors at crate boundaries — do not leak implementation details.
// ✅ Correct: structured library error
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("invalid token at position {pos}: {msg}")]
InvalidToken { pos: usize, msg: String },
}
// ✅ Correct: application context
fn load_config(path: &Path) -> anyhow::Result<Config> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config at {}", path.display()))?;
Ok(toml::from_str(&raw)?)
}- Derive
Debug, Clone, PartialEq, Eqwhere meaningful. AddHashwhen the type will be used in collections. - Avoid
Copyunless the type is trivially copyable (plain data, no heap allocation, ≤ 16 bytes). - Use newtypes to enforce domain invariants at compile time.
- Prefer
Option<T>over sentinel values (empty string,-1,0used as "none"). - Use
NonZeroU*types when a value is semantically nonzero.
// ✅ Newtype pattern
pub struct UserId(u64);
pub struct OrderId(u64);
// Prevents mixing up IDs at compile time- Avoid
unsafeunless strictly required by FFI, raw pointer arithmetic, or performance-critical hot paths. - Every
unsafeblock must have a// SAFETY:comment explaining why the invariants hold. - Encapsulate
unsafeinside a safe abstraction. Never expose raw pointers in public APIs. - Add a test that exercises every code path touching
unsafeblocks.
- No
panic!,todo!,unimplemented!, orunreachable!in production paths. unreachable!is acceptable only when a match arm is provably unreachable by construction; document why.- Replace
todo!()with a compile-time error if the feature is not yet implemented:compile_error!("feature X is not yet implemented — see issue #42");
- Prefer iterators and iterator adaptors over manual index loops.
- Avoid allocating in hot paths. Use
SmallVec,ArrayVec, or stack buffers where appropriate. - Profile before optimizing. Do not introduce complexity for hypothetical gains.
- Use
#[inline]sparingly — only when profiling confirms a benefit.
- Keep
lib.rs/main.rsthin: re-exports and top-level wiring only. Delegate to focused modules. - Use
pub(crate)for internal APIs. Only markpubwhat is part of the intentional public surface. - Group related types, traits, and functions in the same module or submodule.
- Avoid deeply nested module trees (more than 3 levels) unless the codebase is genuinely large.
- Use
mod.rs-less module layout (file-per-module, e.g.,src/parser.rsnotsrc/parser/mod.rs) unless the module has sub-modules. - Re-export carefully: a flat public API is easier to use than deep paths.
src/
├── lib.rs # pub use, top-level types
├── error.rs # unified Error / Result
├── config.rs
├── parser/
│ ├── mod.rs
│ ├── lexer.rs
│ └── ast.rs
└── db/
├── mod.rs
└── queries.rs
- Use
tokioas the default async runtime unless the project specifies otherwise. - Prefer
async fnover manualimpl Futureunless fine-grained control is needed. - Never block inside an async context: replace
std::thread::sleepwithtokio::time::sleep,std::fswithtokio::fs, etc. - Use
tokio::spawnfor background tasks. Always.awaitor store theJoinHandle. - Set timeouts on all I/O:
tokio::time::timeout(duration, future). - Structure concurrency with
tokio::select!,FuturesUnordered, orJoinSet— never busy-loop. - Propagate cancellation: check for cancellation at every
.awaitpoint in long-running tasks.
- Write unit tests in
#[cfg(test)]modules alongside the implementation. - Cover: happy path, edge cases (empty, boundary values), and error propagation.
- Name tests descriptively:
test_<unit>_<scenario>_<expected_outcome>.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_input_returns_ok() { ... }
#[test]
fn parse_empty_input_returns_error() { ... }
#[test]
fn parse_oversized_input_truncates_correctly() { ... }
}- Place integration tests in
tests/at the crate root. - Each integration test file should test one feature or flow end-to-end.
- Use
rstestfor parameterized tests when the same logic needs multiple input sets.
- Use
#[tokio::test]for async unit tests. - Use
#[tokio::test(flavor = "multi_thread")]only when concurrency is part of what is being tested.
- No
unwrap()in tests — use?with-> Result<(), Box<dyn Error>>return type, orassert!(result.is_ok()). - Clean up test side effects (temp files, DB state) using RAII guards or
tempfilecrate. - Tests must not depend on external services unless explicitly marked
#[ignore]with a comment.
- Document all
pubitems with///. Include:- What it does (one line).
- Parameters / fields (if non-obvious).
- Return value and error cases.
- A
# Examplessection with a runnable\``rust` block for non-trivial APIs.
- Use
//!module-level docs in every module explaining its purpose and responsibility. - Keep docs accurate — outdated docs are worse than no docs.
- Run
cargo doc --no-deps --openperiodically to verify rendered output.
/// Parses a raw configuration string into a [`Config`].
///
/// # Errors
///
/// Returns [`ParseError::InvalidToken`] if the input contains an unrecognized key.
///
/// # Examples
///
/// ```rust
/// let cfg = parse_config("timeout = 30")?;
/// assert_eq!(cfg.timeout, 30);
/// ```
pub fn parse_config(raw: &str) -> Result<Config, ParseError> { ... }- Never log secrets, tokens, API keys, or PII.
- Validate and sanitize all external input before use (file paths, user strings, network data).
- Use
PathBuf::canonicalize()and verify paths stay within expected roots to prevent traversal. - Prefer
secrecy::Secret<T>for sensitive values to prevent accidental debug-printing. - Audit
Cargo.lockwithcargo auditbefore each release. Treat high-severity advisories as blockers. - Avoid
unsafefor parsing untrusted data — always use safe, validated parsers.
Before merging to main or cutting a release, confirm all of the following pass:
cargo fmt --check
cargo check --all-targets
cargo clippy --all-targets -- -D warnings
cargo test --all-features
cargo doc --no-deps
cargo audit # requires cargo-auditFor releases, additionally:
- Bump version in
Cargo.tomlfollowing SemVer. - Update
CHANGELOG.mdwith a summary of changes. - Tag the commit:
git tag -s v<version> -m "Release v<version>". - Verify
cargo publish --dry-runsucceeds before publishing to crates.io (if applicable).
When applying these standards to an existing codebase, follow this sequence:
- Audit — Run
cargo clippy -- -W clippy::allandcargo fmt --check. List all violations. Do not modify code yet. - Prioritize by tier:
- Tier 1 (Safety & Correctness):
unwrapwithout invariant docs, missing error handling,unsafewithout// SAFETY:comments, panics in production paths. - Tier 2 (Idiomatic Patterns): module visibility, trait usage, error type structure, clone-in-loop, blocking in async.
- Tier 3 (Style & Docs): formatting, missing
///docs, test coverage gaps.
- Tier 1 (Safety & Correctness):
- Surgical execution — Refactor file-by-file or module-by-module. After each change:
cargo check && cargo test && cargo clippy -- -D warnings
- Verify completeness — Re-run the full CI checklist from §11.
- Document — Update inline docs and
CHANGELOG.mdto reflect structural changes.
| Anti-pattern | Preferred alternative |
|---|---|
unwrap() / expect("") without docs |
? with structured errors, or expect("invariant: …") |
clone() inside a loop |
pass references; restructure ownership |
Returning String for errors |
Return a typed error enum |
Vec<Box<dyn Trait>> for homogeneous collections |
Use a concrete type or enum |
pub on everything |
pub(crate) or pub(super) for internals |
std::sync::Mutex in async code |
tokio::sync::Mutex |
Blocking I/O inside async fn |
tokio::fs, tokio::io, spawn_blocking |
println! for diagnostics in library code |
tracing or log crate |
| Magic numbers / string literals | Named constants with const |
Ignoring #[must_use] results |
Always handle or explicitly let _ = with a comment |
Add project-specific rules here. Conflicting rules override the baseline above. Format:
[OVERRIDE §<section>] <rule>