A Cargo subcommand that updates dependencies only when their latest stable release has aged past a configurable threshold. Useful for teams that want to avoid pulling in freshly-published crate versions before the ecosystem has had a chance to shake out bugs, supply-chain issues, or yanks.
For each dependency in your Cargo.toml, cargo-aged queries the crates.io API for the newest non-prerelease, non-yanked version. If that version was published at least N days ago, it runs cargo update -p <crate> --precise <version> to pin your Cargo.lock to it.
From source (from this repo):
cargo install --path .Or install directly from a git checkout:
cargo install --git https://github.com/YannickLeRoux/cargo-aged.gitOnce installed, the cargo-aged binary lives in ~/.cargo/bin/, which Cargo picks up as the cargo aged subcommand.
Run inside any Cargo project:
cargo aged # default: 30-day minimum age
cargo aged --min-age 14 # more aggressive
cargo aged --min-age 90 --dry-runUse --check as a read-only gate in CI to fail the build when Cargo.lock drifts to a version younger than the configured threshold:
cargo aged --check # inherits min-age from .cargo/config.toml
cargo aged --check --min-age 14 # or set it inlineExit codes:
| Code | Meaning |
|---|---|
0 |
Every direct registry dep is either already locked to an age-eligible version or skipped for a legitimate reason (path/git dep, =-pin, no stable release, no age-eligible version exists, or crates.io lookup failed). |
1 |
At least one direct registry dep is locked to a version younger than min-age and an older age-eligible version exists on crates.io — i.e. running cargo aged (without --check) would pin something back. Any other error (missing threshold, bad manifest, HTTP failure) also uses exit 1. |
--check is mutually exclusive with --dry-run and --iterate: both are non-mutating, but --check is the one that sets a status code.
cargo-aged reads the same config key that upcoming Cargo -Zmin-publish-age support (tracking issue #17009, RFC #3923) uses. Put this in your project's .cargo/config.toml:
[registry]
min-publish-age = "14 days"Accepted values: "N days" / "N day" / "N weeks" / "N week", or a bare integer meaning days.
Precedence (highest wins):
--min-age <DAYS>on the CLI[registry].min-publish-agein.cargo/config.toml— searched from the manifest's directory up to the root, then$CARGO_HOME/config.toml(default~/.cargo/config.toml)
If neither is set, cargo-aged exits with an error rather than guessing a default — you have to opt in to a specific threshold. Use plain cargo update if you don't want any age filtering.
When the value comes from a config file, the effective threshold and its source are printed at the top of the run.
Once Cargo's -Zmin-publish-age is stable, the same config file will govern both the resolver and this tool.
| Flag | Default | Description |
|---|---|---|
--min-age <DAYS> |
see below | Minimum release age in days before a crate is eligible for update. Overrides .cargo/config.toml. Required if no config file provides one — the tool exits with an error otherwise. |
--manifest-path <PATH> |
./Cargo.toml |
Path to the Cargo.toml to read. |
--dry-run |
off | Print what would be updated without changing Cargo.lock. |
--verbose |
off | Also print the publish timestamp for each crate. |
--iterate |
off | Repeat passes until a full pass makes no changes (bounded at 10 passes). Useful for tightly-coupled dep families like serde + serde_json that can only be downgraded in stages. |
--check |
off | Read-only CI gate. Exits 1 if Cargo.lock has any direct registry dep locked to a version younger than the threshold and an age-eligible replacement exists on crates.io. Does not modify Cargo.lock or shell out to cargo update. Mutually exclusive with --dry-run and --iterate. |
-h, --help |
Print help. | |
-V, --version |
Print version. |
- Path dependencies (
path = "...") — nothing to fetch from crates.io. - Git dependencies (
git = "...") — same. =-pinned requirements (serde = "=1.0.210") — the pin is treated as an explicit choice and left alone.- Crates whose latest stable version is younger than
--min-age. - Crates already locked to an age-eligible version — reported as
= serde 1.0.210 — already age-eligibleand left alone. Note this meanscargo-agedwon't proactively upgrade one age-eligible version to a newer age-eligible version within the same major; pair it with a plaincargo updatefirst if you want to move forward before aging back. - Crates that 404 on crates.io or whose API call fails — a warning is printed and the crate is skipped.
Yanked releases and pre-release versions (anything with a - suffix, e.g. 1.0.0-rc.1) are ignored when picking the "latest stable" version.
Checking 12 dependencies (min-age: 30 days)...
✓ serde 1.0.210 — 45 days old, updating...
✗ tokio 1.38.0 — 8 days old, skipping
✗ reqwest (git dep) — skipping
✓ clap 4.5.4 — 62 days old, updating...
...
Summary: 3 updated, 9 skipped.
With --dry-run, the updating... lines change to would update (dry-run) and no cargo update is invoked.
- Parses your
Cargo.toml(including[dev-dependencies],[build-dependencies],[target.*.dependencies], and[workspace.dependencies]). - For each registry dependency, GETs
https://crates.io/api/v1/crates/<name>with a descriptiveUser-Agent(per the crates.io fair-use policy). - Picks the newest version that is not yanked and not a pre-release.
- Reads
Cargo.lockand skips the crate if any age-eligible version is already locked (prevents redundant work and lets--iterateconverge). - If
(now - published_at) >= min_age, shells out tocargo update -p <crate> --precise <version> --manifest-path <path>. - If that
cargo updatefails (typically because another direct dep transitively constrains this crate to a newer range), retries with the next-older age-eligible version — up to 5 attempts per crate — and reports the successful pin, or the last error if all attempts fail. - With
--iterate, repeats the whole pass until a pass produces zero updates (fixed point), bounded at 10 passes. - Prints a summary of updated vs skipped counts.
The age constraint is applied only to the direct dependencies you declare in Cargo.toml. Transitive deps are left to Cargo's normal resolver.
Your Cargo.toml is never modified — only Cargo.lock is touched, via cargo update.
cargo build
cargo run -- aged --dry-run # test against this repo's own manifest
cargo run -- aged --dry-run --manifest-path ../other-project/Cargo.tomlMIT OR Apache-2.0