Rust Code Hygiene and Harness in the age of AI

September 25, 2026

On this page

AI agents are getting good at complex coding tasks. My job has shifted from typing every line to designing, architecting and reviewing what the agent wrote. The agent is fast and confident, and some of what it writes is slop: a stray unwrap, a lock held across an .await, a "fixed" regex that quietly re-ports the old bug.

A harness is what keeps slop out of the codebase.
The agent can write whatever it wants. It cannot merge it.

Every gate below exists because one of those got through once. The services that predate this list had exactly one gate: does the Docker image build. That is how slop gets in.

What I use at my job, in the Rust ecosystem

1. Clippy, configured in one place. Lints live in [workspace.lints] in Cargo.toml and every crate inherits them. Everything is warn locally so the editor stays readable, and CI runs with -D warnings so every warning is a failure. The groups that matter:

# Don't panic
unwrap_used = "warn"
expect_used = "warn"
panic = "warn"
indexing_slicing = "warn"
unwrap_in_result = "warn"

# Don't fail silently
let_underscore_future = "warn"
unused_result_ok = "warn"
map_err_ignore = "warn"

# Don't deadlock: the only two at deny
await_holding_lock = "deny"
await_holding_refcell_ref = "deny"

# Don't print: library code logs through tracing
print_stdout = "warn"
print_stderr = "warn"
dbg_macro = "warn"

# Don't allow your way around it
allow_attributes = "warn"
allow_attributes_without_reason = "warn"

clippy.toml covers what a lint cannot say: std::thread::spawn is banned in favour of a named thread::Builder, catch_unwind is banned because we build with panic = "abort", and dbg! is banned everywhere, tests included. A suppression must be #[expect(..., reason = "...")], so it fails the build the day it stops being needed.

2. Pinned toolchain. rust-toolchain.toml pins the exact version with rustfmt and clippy as components, so a lint added in next month's release does not break the build. rust-version in Cargo.toml is the MSRV and CI checks it in its own job.

3. mise. One file installs the tools at pinned versions (nextest, cargo-deny, prek, cargo-machete, taplo, gitleaks, actionlint) and defines the tasks. mise run ci runs hooks, fmt-check, lint, test, doc and deny in the same order CI does. A new machine, or a new agent, gets the whole harness from one command.

4. Git hooks. prek runs the pre-commit config. On commit: fmt, taplo, clippy with -D warnings, cargo-deny when the manifest changed, cargo-machete for unused deps, typos, gitleaks on the staged diff, detect-private-key, nothing over 500 KB. On push: nextest and doctests. On commit-msg: Conventional Commits via committed, and a script that rejects any AI attribution line. Hooks can be skipped with --no-verify, so they are a convenience. CI is the gate.

5. CI. GitHub Actions re-runs every hook, then rustfmt, clippy, the MSRV check, nextest on Linux and macOS, rustdoc with -D warnings, cargo-deny and a Docker build. Every cargo command runs --locked, so a drifted Cargo.lock fails. Actions are pinned to commit SHAs, permissions is read-only, and zizmor lints the workflows themselves.

6. cargo-deny. Licenses on an allow list, wildcard versions denied, crates only from crates.io, yanked versions denied. RustSec advisories run on every dependency change and on a weekly cron.

7. Version control: git and DVC. Code goes in git. The corpus the code is judged against goes in DVC: the reviewed expected.json for each case lives in git, the document bytes live behind a .dvc pointer in object storage. CI pulls the data, runs the engine over every case and compares the output to the expected file exactly. One difference fails the case, a diff is written, and a human reads it before anything is pushed. The same job also checks that the input data has not drifted, because the expected outputs are only meaningful against the exact inputs they were reviewed on.

The agent does not get to decide that a changed output is an improvement.
A person does, by reviewing the diff and marking the case verified.

8. Skills and AGENTS.md. The file the agent reads before it touches anything. Plan first and get the plan approved.

9. ASD-STE100. Simplified Technical English for docs, comments and commit messages: short sentences, one instruction per sentence, active voice, one meaning per word. An agent that writes in STE writes comments a reviewer can check in one read.

10. Caveman. The agent talks like one: short words, no fluff, no metaphors. Less text to review is less slop to miss.

11. Proptest. #[test] for examples, proptest for invariants that must hold for any input: no starvation, tier weights respected, idempotency. An agent will happily write ten example tests that all pass. A property test finds the input it did not think of.

12. Tests that gate behaviour. nextest with fail-fast = false, zero retries in CI, a two minute kill for slow tests. Doctests on every public item, because missing_docs is deny. A public_api integration test that fails at compile time when a signature changes, and a downstream job that runs cargo check on the two real consumer services against the pull request.

13. cargo-semver-checks. A breaking change to the public API without a version bump fails CI.

14. Criterion. Benchmarks run on tags and on demand, never on pull requests, because shared runners are noisy. Results go to gh-pages with a 130% alert threshold. Informational, so a reviewer sees the regression without a flaky gate.

15. Conventional Commits and a changelog. One logical change per commit, typed and scoped, checked by a commit-msg hook. Every behaviour change gets a changelog row: what changed, why, and what it affects. An empty cell, or "should not affect anything", does not pass review.

16. Supply chain basics. Dependabot with a seven day cooldown, so a compromised release is yanked before it reaches us. Distroless runtime image, non-root. A read-only key for the corpus bucket: a build that can overwrite the data it checks against is not a check.

The shape of it

Warn locally, deny in CI.
Pin everything that can drift: toolchain, tools, actions, data, dependencies.
Make the agent read the rules, and make the hooks run them.
Put a human between a changed output and the word "verified".