Governance Models for Rust Codebases
Governance is the set of rules a team encodes into its build, its review process, and its dependency policy so that risk gets caught before it ships instead of after. In a Rust codebase that shows up as three interlocking domains: coding standards that keep the code reviewable, dependency and unsafe governance that keeps the supply chain and memory-safety boundary honest, and edition/toolchain upgrades that keep the compiler itself from becoming a liability. None of these exist because someone likes rules. Each one is a direct response to a failure mode a team has already hit, or can see coming, and this page is the mental model that ties the rest of this section's pages together.
Summary
- Governance is risk management encoded into tooling and process, converting failure modes that would otherwise surface in production into checks that fail fast in CI or review.
- Insight: Without it, small individual decisions (an
unwrap(), an unvetted crate, a lagging toolchain) compound silently until a single incident forces an expensive, reactive fix. - Key Concepts: risk surface, enforcement layer, review gate, ratchet, supply-chain trust boundary, migration train.
- When to Use: Bootstrapping a new repo from a template, onboarding a team past 3-4 engineers, preparing for a security questionnaire or audit, or absorbing a CVE in a transitive dependency.
- Limitations/Trade-offs: Every rule has an enforcement cost - review latency, CI time, or onboarding friction - and rules that outlive their original failure mode become the bureaucracy critics warn about.
- Related Topics: coding standards, supply-chain security, code review culture, release management.
Foundations
At its simplest, governance is a policy plus an enforcement mechanism plus a documented reason. A rule with no enforcement is a suggestion, and a rule with no reason is bureaucracy waiting to be discovered by someone who deletes it in frustration. Good Rust governance always answers "what breaks if we skip this," and the answer is usually one of a small number of failure modes: unreviewable diffs, undefined behavior from unsafe, a compromised or abandoned dependency, or a compiler upgrade that silently changes behavior.
The analogy that holds up well is a building code. A building code is not there to make construction slower for its own sake; each clause traces back to a fire, a collapse, or a flood that happened before the clause existed. Rust governance works the same way: unsafe_code = "forbid" in a domain crate's lints traces back to a memory-safety bug that leaked past review, and cargo audit in CI traces back to a CVE that shipped in a transitive dependency nobody was watching. Reading a governance rule without its originating failure mode is like reading a building code clause with the history stripped out - it looks arbitrary even when it isn't.
The most basic mechanic is the enforcement layer: where a rule lives determines whether it survives contact with a deadline. A rule written only in a README competes with shipping pressure and usually loses. A rule enforced by clippy -D warnings, a CODEOWNERS entry, or a required CI check does not compete with anything - it simply fails the build, which is why the mature form of every governance rule eventually migrates from prose into tooling.
Mechanics & Interactions
The three governance domains in this section are not independent; they share a common shape and interact constantly. Coding standards define the baseline risk surface a reviewer has to reason about - naming, error handling, module boundaries - and every other governance layer builds on top of that baseline being predictable. Dependency and unsafe governance narrow the supply-chain trust boundary: every external crate and every unsafe block is a place where the compiler's guarantees stop and human judgment takes over, so both get the same treatment, a review gate plus continuous scanning. Edition and toolchain upgrades are the odd one out in cadence but not in kind - they are governance over the compiler itself, since a stale toolchain is its own accumulating risk (missed security fixes, expired MSRV promises, incompatible tooling).
The control flow that ties them together is a pipeline, not a single check:
PR opened
-> rustfmt / clippy -D warnings (coding standards gate)
-> cargo audit + cargo deny (known-CVE + license gate)
-> cargo vet (if configured) (new/unreviewed-crate gate)
-> unsafe diff? -> second reviewer (human judgment gate)
-> merge -> SBOM attached to releaseEach stage catches a different failure mode, and skipping a stage does not remove the risk, it just moves the discovery point later and more expensive: from a red CI check, to a code review comment, to a production incident, to a customer security questionnaire.
The most useful mental model for why risk needs continuous, not one-time, checking is that a dependency's trustworthiness is a property of a moment in time, not a permanent fact. A crate vetted and pinned today can be taken over by a new maintainer, have a CVE disclosed against an old version still in the lockfile, or simply go unmaintained next quarter. That is why cargo audit runs on every main build instead of once at adoption time, and it is the single most common reasoning pitfall: treating a governance check as a gate you pass once rather than a property you have to keep re-verifying.
# workspace-level lint policy - encodes the rule in the build, not in a doc
[workspace.lints.clippy]
unwrap_used = "deny" # panics on the request path are an incident, not a style nit
await_holding_lock = "deny" # a known deadlock shape under loadThe reason this lives in Cargo.toml rather than a style guide is the enforcement-layer idea from Foundations made concrete: a lint that fails the build cannot be skipped under deadline pressure the way a documented convention can.
Advanced Considerations & Applications
At scale, governance has to account for cases the simple pipeline does not cover cleanly. FFI crates concentrate unsafe in one place deliberately, so a fleet-wide #![forbid(unsafe_code)] has to carve out an explicit exception with its own heavier review bar (Miri runs, a named second reviewer group) rather than being disabled fleet-wide. Monorepos and multi-repo fleets diverge in how governance propagates: a monorepo can enforce a single deny.toml and lint set at the workspace root, while a fleet of many repos needs a template-and-drift-detection model, since nothing forces each repo to pull in policy updates automatically.
Supply-chain governance in particular has evolved quickly in response to real attacks - typosquatted crate names, compromised maintainer accounts, and build-script payloads have all happened in open-source ecosystems, which is why cargo vet (evidence that a specific human reviewed a specific crate version) exists as a layer above cargo audit (which only catches known, disclosed vulnerabilities, not first-seen malicious code). SBOM generation follows the same logic one step further outward: it does not prevent a bad dependency, it makes it possible to answer "were we affected" quickly when the next disclosure happens, which is often the actual deliverable a security questionnaire is asking for.
Edition and MSRV governance interacts with this too. A newer edition often ships new clippy lints and stricter defaults that surface latent issues standards governance was already trying to prevent, so migration trains double as a scheduled opportunity to pay down lint debt that individual PRs could never justify tackling alone.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Documented convention only | Fast to write, zero tooling cost | Loses to deadline pressure, drifts silently | Very early prototype, single contributor |
| CI-enforced lint/deny gate | Cannot be skipped, catches issues before review | Needs upkeep as rules evolve; false positives frustrate | Fleet-wide coding standards, license/CVE checks |
| Human review gate (unsafe, ADR) | Catches judgment calls tooling can't express | Slower, depends on reviewer expertise and availability | unsafe blocks, architectural exceptions |
| Automated dependency bot + audit | Continuous, low-effort steady state | Needs a human triage step or it becomes noise | Routine dependency bumps at any team size |
Common Misconceptions
- Governance is bureaucracy that slows shipping. It is understandable to feel this from the inside of a slow PR review, but the rules that survive are the ones mapped to a real incident; the fix for slow governance is pruning stale rules, not removing enforcement wholesale.
unsafecode is inherently the enemy.unsafeis a visibility marker, not a defect; the goal of governance is to make everyunsafeblock reviewed and justified, not to reach zero of them in code that genuinely needs FFI or manual memory layout.cargo auditpassing means the dependency tree is safe.cargo auditonly flags known, disclosed vulnerabilities against a database; it says nothing about a first-seen malicious crate or an unmaintained one, which is exactly the gapcargo vetandcargo deny's unmaintained warnings are meant to close.- Edition upgrades are cosmetic syntax churn. They frequently ship default-lint and lifetime-inference changes that are load-bearing for correctness, and they are often the only practical trigger for an MSRV bump that unblocks a needed security fix.
- Coding standards are about taste and style preference. Naming and formatting rules are the low-stakes tip of the standard; the parts that matter operationally are error-handling boundaries and layering, which determine whether a bug is caught by a type or discovered in production.
FAQs
What does "governance" mean for a Rust codebase, concretely?
- A set of rules (lint policy, dependency vetting, review gates, upgrade cadence)
- Each rule enforced somewhere real: CI, CODEOWNERS, or a documented human review step
- Each rule traceable back to a specific failure mode it prevents
Why not just trust the compiler and skip most of this?
The compiler enforces memory and type safety inside safe Rust, but it has nothing to say about supply-chain trust, panics on hot paths, license compliance, or whether a team's public API is coherent over time - those are exactly the gaps governance is designed to cover.
How does a rule actually get enforced day to day?
Most durable rules live in one of three places: a [workspace.lints] block that fails the build, a required CI job (cargo audit, cargo deny), or a CODEOWNERS entry that forces a specific reviewer on sensitive paths like **/unsafe/**.
How do coding standards and dependency governance actually interact?
A new dependency has to satisfy the same layering rules as hand-written code - for example, a domain crate that forbids framework imports also has to reject a dependency that would pull axum into that layer transitively, so vetting includes checking what a crate brings with it, not just the crate itself.
What's the difference between `cargo audit`, `cargo deny`, and `cargo vet`?
cargo audit checks the dependency tree against a database of known, disclosed vulnerabilities. cargo deny enforces policy (license allowlists, banned crates, duplicate-version bloat). cargo vet requires a human to have actually reviewed a specific crate version, which is the only one of the three that can catch a first-seen malicious or unreviewed dependency.
When is it not worth adding a governance rule?
When there is no team large enough to drift, no external consumer or compliance requirement, and no incident history motivating it - a solo prototype repo does not need a cargo vet policy any more than it needs a change-advisory board.
What's the trade-off of enforcing everything in CI?
CI-enforced rules cannot be skipped under pressure, which is their strength, but every added check adds pipeline time and a new way for an unrelated PR to get blocked by a flaky or overly strict gate, so gates need owners who keep them tuned.
Isn't `unsafe` forbidden entirely in well-governed Rust?
No - it is isolated and reviewed, typically confined to a small ffi-shim or similarly named crate with #![forbid(unsafe_code)] everywhere else, so the audit surface stays small and explicit rather than pretending unsafe doesn't exist.
Why do edition upgrades need a dedicated "migration train" instead of a normal PR?
An edition change affects lint defaults and sometimes inference across an entire workspace at once, so mixing it into unrelated feature work makes it impossible to tell whether a bug came from the edition change or the feature, which is why teams isolate it to its own branch and soak window.
How does governance change as a team or company grows?
Early on, a handful of CI checks and a style guide cover most risk; past a certain size, teams add SBOMs, formal vet policies, and CODEOWNERS-based review because the coordination cost of an undocumented tribal-knowledge rule grows faster than the team does.
Who should own governance rules?
Typically a platform or language guild proposes and maintains the baseline, while individual teams can add stricter local rules but rarely relax the shared baseline unilaterally, since relaxing a rule silently reintroduces the failure mode it was added to prevent.
How do you know when to remove a governance rule instead of adding one?
If a rule hasn't caught a real issue in a long time, if its underlying tooling has been superseded, or if the team it protects no longer exists in that form, it is a candidate for a periodic governance retro to prune - unused rules erode trust in the rules that still matter.
Related
- Coding Standards & API Guidelines - the baseline risk surface every other governance layer builds on
- Unsafe & Dependency Governance - the review gates and CI scanning for the supply-chain trust boundary
- Edition & Toolchain Upgrades - governance over the compiler itself, run as planned migration trains
- Spikes, PoCs & Adopting New Crates - how a new dependency earns its way past the trust boundary
- Governance Best Practices - a condensed checklist drawn from every page in this section
Stack versions: This page is conceptual and references governance tooling (
cargo audit,cargo deny,cargo vet) that applies across the fleet's pinned Rust 1.97.0 (edition 2024) toolchain; it is not otherwise tied to a specific stack version.