Rust Dependency Management and Governance
Authority: Cargo — Specifying Dependencies, Cargo — Dependency Resolution, Cargo — Source Replacement, Cargo — Credentials, cargo-deny, cargo-audit, RustSec.
This skill owns the governance of dependencies: how to declare them safely, what they pull in, who is allowed in, and what to do when one goes wrong. It does not own the Cargo.toml field reference (rust-cargo-build), semver (rust-semver), or workspace topology (rust-workspace).
Capability Boundaries
✅ Strengths
- Choosing the right version requirement (
"1"vs"1.2"vs"=1.2.3") - Evaluating dependency sources (crates.io / git / path / private registry)
- Setting up source replacement for vendored or mirrored registries
- Configuring
cargo-deny(4 tables: advisories, licenses, bans, sources) - Running
cargo-auditand responding to RustSec advisories - Diagnosing dependency cycles and duplicate-version problems via
cargo tree - Setting MSRV-aware resolver behavior
- Automating dependency updates with Renovate / Dependabot
- Establishing supply-chain policy for an organization
⚠️ Prerequisites
- Cargo manifest basics — see
rust-cargo-build - Semver vocabulary — see
rust-semver
❌ Out of Scope
- Cargo.toml field-by-field reference →
rust-cargo-build - Workspace layout →
rust-workspace - Clippy / lint policy →
rust-style-clippy - Testing dependency injection →
rust-testing
Data Privacy
This skill does not collect, store, or transmit user data. Private registry access and credential use are user-authorized actions; never log credentials or expose tokens in build artifacts.
Part 1: Version Requirement Syntax
Cargo uses SemVer version requirements (not exact versions) by default.
| Syntax | Meaning | Compatibility |
|---|---|---|
"1" or "^1" |
>=1.0.0, <2.0.0 |
Default — auto-update within major |
"1.2" or "^1.2" |
>=1.2.0, <2.0.0 |
At least 1.2.x |
"1.2.3" or "^1.2.3" |
>=1.2.3, <2.0.0 |
At least 1.2.3, may pull 1.5.0 |
"=1.2.3" |
Exactly 1.2.3 | Pin — for reproducibility |
"~1.2" |
>=1.2.0, <1.3.0 |
Within minor only |
"~1.2.3" |
>=1.2.3, <1.3.0 |
Within minor only |
">=1.0, <2.0" |
Range | Explicit bounds |
"*" |
Any | Forbidden on crates.io — too loose |
"0.1" or "^0.1" |
>=0.1.0, <0.2.0 |
Pre-1.0: minor treated as major |
"0.0.1" |
>=0.0.1, <0.0.2 |
Pre-0.1: patch treated as major |
The default is caret (^)
[dependencies]
serde = "1" # caret by default
serde = { version = "1" } # same
This is usually right. You get bug fixes and non-breaking features automatically.
When to deviate
| Need | Use | Example |
|---|---|---|
| Pin for reproducibility | = |
"=1.2.3" |
| Restrict to minor only | ~ |
"~1.2" (no 1.3.x) |
| Bounded range | >=, < |
">=1.0, <1.5" |
| Pre-1.0 with breaking minors | minor pin | "0.7.3" (Cargo treats as <0.8) |
| Git head | git | { git = "..." } |
Caret rule for pre-1.0
# These are equivalent and both mean <0.2:
my-crate = "0.1"
my-crate = "^0.1"
# 0.0.x is even more restrictive:
my-crate = "0.0.3" # means >=0.0.3, <0.0.4 — almost a pin
The pre-1.0 rules exist because the SemVer spec treats 0.x as "anything goes." Cargo encodes community convention: minor bump in 0.x = breaking.
Part 2: Dependency Sources
crates.io (default)
[dependencies]
serde = "1"
Public, immutable, audited via crates.io. This is what 99% of dependencies should use.
Git
[dependencies]
# Branch / tag / rev / default
my-crate = { git = "https://github.com/user/repo", branch = "dev" }
my-crate = { git = "https://github.com/user/repo", tag = "v1.0.0" }
my-crate = { git = "https://github.com/user/repo", rev = "abc1234" }
my-crate = { git = "https://github.com/user/repo" }
Use sparingly: git deps make Cargo.lock non-portable, slow down CI, and cannot be published to crates.io unless the git dep is also published. Prefer crates.io, a fork published under a different name, or cargo vendor.
Path (local / workspace)
[dependencies]
my-core = { path = "../core", version = "0.1" }
Required for workspace internal deps. Always pair with a version for publishing (otherwise cargo publish fails).
Private registry
# .cargo/config.toml
[registries.my-registry]
index = "sparse+https://my-registry.example.com/index/"
[registry]
default = "my-registry" # optional: make this the default
# Cargo.toml
[dependencies]
my-private-crate = { version = "1.0", registry = "my-registry" }
Source replacement (transparent)
Cargo Source Replacement lets you replace crates.io with a mirror — without editing each Cargo.toml:
# .cargo/config.toml
[source.my-mirror]
registry = "sparse+https://mirrors.example.com/crates.io-index"
[source.crates-io]
replace-with = "my-mirror"
Use cases: enterprise proxy, China mirrors (RsProxy, tuna), vendored offline builds.
Vendored
cargo vendor vendor/ # downloads all deps to vendor/
# .cargo/config.toml
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"
For air-gapped / reproducible builds. Check vendor/ into git (it's large but stable).
Part 3: Dependency Tree Analysis
cargo tree
cargo tree # full tree
cargo tree --depth 2 # limit
cargo tree --invert --package X # what depends on X?
cargo tree -e features # show feature unification
cargo tree -e no-dev # exclude dev-deps
cargo tree -e no-build # exclude build-deps
Finding duplicates
cargo tree --duplicates # show crates with multiple versions
If you see serde at both 1.0.180 and 1.0.195, that's two versions in the tree. Common causes:
- An old transitive dep requires an old version
- A git dep pulled in a specific version
- Your own version requirement is too tight
Resolving duplicates
cargo update -p serde # bump to latest matching req
cargo update -p serde --precise 1.0.200
If a transitive dep pins an old version, you can either:
- Update the transitive dep (
cargo update -p that-crate) - Live with the duplicate (usually fine)
- Replace the transitive with a fork
Detecting cycles
Cargo forbids cycles in the dependency graph. If you see:
error: cyclic package dependency: package `a` depends on `b`. package `b` depends on `a`.
You have a real design bug — restructure (often by extracting a shared crate c that both depend on).
Part 4: Dependency Update Governance
Use rust-cargo-build for current Cargo.lock version-control guidance and the exact behavior of --locked, --offline, and --frozen. The current Cargo Guide recommends committing Cargo.lock when in doubt; do not apply an application-versus-library prohibition here.
This skill owns how resolved dependency changes are proposed, reviewed, and approved:
cargo update # bump all to latest within reqs
cargo update --precise 1.0.200 -p serde # pin a specific version
cargo update --dry-run # see what would change
- Prefer package-scoped updates over unrelated graph churn.
- Review lockfile source, checksum, version, feature, and duplicate-version changes.
- Run advisories, licenses, bans, and source-policy checks on the proposed graph.
- Keep automated update pull requests bounded and observable.
- Use a separate scheduled compatibility lane when libraries need to test newly resolved dependency ranges.
- Never add an unconditional
cargo updateto required CI merely to make a stale lockfile pass.
Part 5: cargo-deny (Supply-Chain Governance)
cargo-deny is the de-facto tool for dependency policy. Four checks:
5.1 advisories — RustSec
# deny.toml
[advisories]
db-urls = ["https://github.com/rustsec/advisory-db"]
yanked = "deny"
ignore = [
# "RUSTSEC-2024-0001", # ignore specific advisory with justification
]
cargo deny check advisories
5.2 licenses — policy
# deny.toml
[licenses]
allow = [
"MIT",
"Apache-2.0",
"BSD-3-Clause",
"ISC",
"Unicode-DFS-2016",
]
confidence-threshold = 0.93
[[licenses.exceptions]]
allow = ["Zlib"]
name = "some-zlib-crate"
version = "1.0"
cargo deny check licenses
Default is permissive; tighten to your org's policy.
5.3 bans — forbidden crates
# deny.toml
[bans]
multiple-versions = "warn"
wildcards = "deny" # don't allow "2.*" requirements
[[bans.deny]]
name = "openssl" # prefer rustls
version = "*"
[[bans.deny]]
name = "chrono"
version = "<0.5" # only old versions
[bans.workspace-dependencies]
duplicates = "deny" # require [workspace.dependencies]
cargo deny check bans
5.4 sources — where deps come from
# deny.toml
[sources]
unknown-registry = "deny" # disallow non-crates.io registries
unknown-git = "deny" # disallow non-allowlisted git sources
allow-registry = ["crates-io"]
allow-git = []
cargo deny check sources
All-in-one
cargo deny check # runs all four
CI integration:
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
Part 6: cargo-audit
cargo-audit scans Cargo.lock against the RustSec advisory database.
cargo install cargo-audit --locked
cargo audit # scan
cargo audit --deny warnings # fail CI on warnings
cargo audit --ignore RUSTSEC-XXXX # ignore specific advisory
Difference from cargo-deny:
cargo-auditis advisory-only (security focus)cargo-denycovers advisories + licenses + bans + sources
Most projects use cargo-deny as a superset. Some use both (defense in depth).
Responding to an advisory
- Run
cargo auditto identify the vulnerable crate + version - Check RustSec for the fixed version
cargo update -p vulnerable-crate --precise X.Y.Z- If no fix exists: switch to a maintained fork, remove the dependency, or disable the affected feature
- File an issue upstream if it's a new vulnerability
Part 7: Automation
Renovate
Renovate auto-opens PRs when deps update. Supports Cargo out of the box:
// .renovaterc.json
{
"extends": ["config:recommended", ":semanticCommits"],
"schedule": ["before 6am on Monday"]
}
Dependabot
Dependabot is GitHub-native:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
Cargo's own auto-update
There's no built-in "cargo update PR" tool, but you can script it:
cargo update
git diff Cargo.lock
# Commit + push to a branch
Part 8: Feature Minimization
[dependencies]
serde = { version = "1", features = ["derive"] } # only what you use
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
Each enabled feature increases compile time, binary size, and attack surface. Disable defaults when you only need a subset:
[dependencies]
tokio = { version = "1", default-features = false, features = ["rt", "net"] }
Inspecting features
cargo tree -e features # which features are enabled, where
cargo tree -e features -p tokio # just tokio's features
If tokio shows up with [full] from a transitive dep, you're paying for more than you use.
Workspace dep governance
# root Cargo.toml
[workspace.dependencies]
serde = { version = "1", features = ["derive"] } # pin features once
# crates/abc/Cargo.toml
[dependencies]
serde.workspace = true # inherits feature set
Prevents feature drift across members.
Workflow
- Inventory —
cargo tree -e features,cargo tree --duplicates - Audit —
cargo audit(advisories) +cargo deny check(full policy) - Tighten versions — replace
"*"with"1"; pin exact only when reproducible - Minimize features —
default-features = false, list only what you use - Decide sources — prefer crates.io; reserve git/path for legitimate cases
- Configure CI —
cargo build --locked,cargo deny check, scheduledcargo audit - Automate updates — Renovate or Dependabot with semantic commits
Decision Shortcuts
| Question | Answer |
|---|---|
| Pin or caret? | Caret ("1") by default; pin ("=1.2.3") for reproducibility only |
| crates.io or git? | crates.io unless you have a specific reason |
| Use latest or wait? | Wait 1-2 weeks for new majors; auto-patch for patches |
Allow "*" requirements? |
No — crates.io rejects them, and they hide upgrades |
How many versions of serde? |
One. If duplicates, trace via cargo tree --duplicates |
| Commit Cargo.lock for a library? | No — downstream owns the lock |
CI: --locked or --frozen? |
--locked for normal CI; --frozen for air-gapped |
Resources
- Cargo — Specifying Dependencies — full syntax reference
- cargo-deny Configuration Cookbook — 4-table policies
- Private Registry Setup — credentials, source replacement
examples/golden-deps/— a small crate with a minimal, audited dep set