Cargo Workspaces skill for Codex
One lockfile, additive features, ordered releases. Codex sets up virtual manifests with workspace-inherited versions, dependencies and lints, picks the right resolver, and designs features that stay additive using dep: and weak ? syntax. It traces surprise features and duplicate versions with cargo tree, checks members alone and in feature powersets, and only publishes to crates.io after you have read a clean dry run.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rust-cargo-workspaces Installs to ~/.agents/skills/rust-cargo-workspaces/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rust-cargo-workspaces The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rust-cargo-workspaces Installs to .agents/skills/rust-cargo-workspaces/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rust-cargo-workspaces.zipunzip -o rust-cargo-workspaces.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rust-cargo-workspaces PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $rust-cargo-workspaces, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Structures and maintains Cargo workspaces - virtual manifests, workspace.package and workspace.dependencies inheritance, shared lints, resolver 2 and 3, additive feature design with dep: and weak features, feature unification debugging with cargo tree, MSRV via rust-version, and publishing interdependent crates to crates.io in order with semver checks. Use when splitting a Rust crate into several, when builds pull unexpected features or duplicate versions, when adding a feature flag, or before running cargo publish.
Use it when
- A Rust project is growing into several crates that share dependencies
- Builds enable unexpected features or compile two versions of a crate
- You are adding or redesigning Cargo feature flags
- Interdependent crates need to be versioned and published to crates.io
Use something else when
- You need build tooling across many languages in one repo: use monorepo-tooling
- You are deciding version numbers and changelog policy in general: use semantic-versioning
How to check the result
cargo check passes for the workspace with all features and with no default features, members build alone with -p, cargo tree -d lists only accepted duplicates, and cargo publish --dry-run is clean.
The mistake it prevents
Leaving default-features on in workspace.dependencies and trying to disable them in a member, which Cargo ignores, so the defaults still compile everywhere.
Done well means: one Cargo.lock and one place to bump each shared dependency, every
crate builds and tests on its own (-p), each feature combination you advertise compiles,
cargo tree -d shows only duplicates you accept, and publishing is a dry run the user has
read before anything goes to crates.io.
Inspect first
cat Cargo.toml # virtual manifest or root package?
cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | "\(.name) \(.version) \(.manifest_path)"'
cargo tree -d --depth 1 # duplicate versions
rustc -V; grep -rn 'rust-version\|edition' --include=Cargo.toml .
Read AGENTS.md for the release flow (release-plz, cargo-release, manual) and CI matrix.
cargo operations that fetch the index need network; --offline works when the registry
cache already has everything.
Layout and root manifest
repo/
├── Cargo.toml # virtual manifest (no [package])
├── Cargo.lock # committed, shared by all members
└── crates/
├── core/ # library, published
├── cli/ # binary, published
└── xtask/ # dev automation, publish = false
# Cargo.toml (root)
[workspace]
members = ["crates/*"]
resolver = "3" # set explicitly in a virtual manifest; "3" needs Rust 1.84+
[workspace.package]
version = "0.8.2"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
repository = "https://github.com/acme/widget"
[workspace.dependencies]
serde = { version = "1.0.219", default-features = false }
tokio = { version = "1.45", default-features = false }
widget-core = { path = "crates/core", version = "0.8.2" } # path + version for publishing
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
dbg_macro = "warn"
unwrap_used = "warn"
[profile.release]
lto = "thin"
codegen-units = 1
# crates/cli/Cargo.toml
[package]
name = "widget"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
widget-core.workspace = true
serde = { workspace = true, features = ["derive"] } # adds to, never removes, root features
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[lints]
workspace = true
Notes:
- A virtual manifest has no edition, so set
resolverexplicitly. Resolver “2” stops dev-dependency and build-dependency features leaking into normal builds; “3” (default for edition 2024 packages) adds MSRV-aware version selection usingrust-version. default-features = falsemust be set in[workspace.dependencies]; a member cannot turn defaults off if the workspace entry leaves them on.- Profiles are only honoured in the root manifest.
Feature design
| Rule | Example |
|---|---|
| Features are additive; enabling one must never remove behaviour or change an API | json adds a module, never swaps a type |
Optional dependencies get explicit features with dep: so the dep name is not a public feature | json = ["dep:serde_json"] |
Forward features to a dep only if it is enabled: weak syntax ? | serde = ["dep:serde", "chrono?/serde"] |
Keep default small; libraries rarely need defaults beyond std | default = ["std"] |
| Mutually exclusive backends are a design smell | Prefer runtime choice or separate crates; if unavoidable, compile_error! on conflicts |
| Never feature-gate items in a way that breaks downstream when another crate enables a feature | Unification is global to the build |
[features]
default = ["std"]
std = ["serde?/std"]
json = ["dep:serde_json", "serde"]
serde = ["dep:serde"]
[dependencies]
serde = { workspace = true, optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
Document features in the crate README and with #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
plus [package.metadata.docs.rs] all-features = true so docs.rs shows them.
Debugging features and duplicates
cargo tree -e features -i serde -p widget # who enables which serde features
cargo tree -d # duplicates; follow with -i <crate>@<ver>
cargo tree -p widget-core --no-default-features -e normal
cargo build -p widget-core --no-default-features # a member alone, minimal features
cargo hack check --feature-powerset --no-dev-deps -p widget-core # needs cargo-hack installed
Features unify across everything built together. cargo build at the root may compile a
crate with more features than cargo build -p that-crate; test both in CI so a missing
feature declaration does not hide behind a sibling.
Duplicate major versions (syn 1 and syn 2) are often unavoidable; duplicates within one
major usually mean a member pins =x.y.z or an outdated lock. cargo update -p <crate>
first, then look for exact pins.
Common commands
| Goal | Command |
|---|---|
| Build or test everything | cargo test --workspace --all-features (plus a --no-default-features job) |
| One member | cargo test -p widget-core |
| Exclude a member | cargo test --workspace --exclude xtask |
| Run a binary | cargo run -p widget -- args |
| Add a dep to a member | cargo add -p widget anyhow (then move it to workspace.dependencies if shared) |
| Check MSRV | cargo +1.85 check --workspace (installing that toolchain needs network) |
| Local override of a crates.io dep | [patch.crates-io] foo = { path = "../foo" } in root only |
Publishing crates
Publishing is public and permanent: a version can be yanked but never deleted or replaced.
Get explicit confirmation from the user before any command without --dry-run.
- [ ] 1. Bump versions (workspace.package.version and path deps' version fields)
- [ ] 2. Update CHANGELOG; run cargo semver-checks for libraries (needs cargo-semver-checks)
- [ ] 3. cargo package --list -p <crate> # confirm no secrets, fixtures or huge files ship
- [ ] 4. cargo publish --workspace --dry-run (Rust 1.90+), or per crate in dependency order
- [ ] 5. Show the dry-run summary to the user; wait for "yes"
- [ ] 6. Publish; then tag the release
- Every path dependency of a published crate needs a
version; crates.io rejects path-only deps. Dev-dependencies with path only are stripped and allowed. cargo publish --workspace(stable since Cargo 1.90) publishes members in dependency order and skipspublish = falsecrates. On older toolchains publish leaf crates first, one-pat a time.- Use
include = [...]orexclude = [...]in[package]to keep test data out of the.crate; the 10 MB crates.io limit applies to the compressed package. - Prefer CI-based publishing (crates.io trusted publishing from GitHub Actions, or a scoped API token stored as a secret) over tokens on laptops. Never print or commit a token.
- Release tools:
release-plz(PR-based, changelog from commits) andcargo-releaseautomate version bumps, tags and ordered publishing; follow whichever the repo uses.
Gotchas
- Moving a type between crates is a breaking change for anyone importing the old path;
re-export it (
pub use widget_core::Thing;) for a deprecation cycle. - Glob
members(crates/*) silently pick up scratch or vendored crates; remove them withexclude = [...]or list members explicitly in larger repos. cargo test --workspacecompiles all dev-dependencies of all members; slow CI is often one heavy dev-dep in a single crate.- Inherited
version.workspace = truemeans every crate shares one version; if crates release independently, set versions per crate instead. rust-versionset lower than what the code actually needs is a lie the resolver acts on; test the MSRV in CI or do not declare it.
Verification
cargo check --workspace --all-targets --all-features
cargo check --workspace --all-targets --no-default-features
cargo test --workspace
cargo tree -d
cargo publish --workspace --dry-run # only when releasing
Report template
**Workspace:** 4 members (core, cli, macros, xtask[publish=false]), resolver 3, edition 2024
**Shared deps moved to workspace.dependencies:** serde, tokio, tracing, thiserror
**Features:** core: std (default), json, serde; powerset checks pass
**Duplicates:** syn 1/2 (accepted, via darling); removed duplicate hashbrown 0.14/0.15
**Publish:** dry run clean for core 0.9.0 and widget 0.9.0; awaiting your go-ahead What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Cargo Workspaces"
short_description: "One lockfile, additive features, ordered releases"
default_prompt: "$rust-cargo-workspaces Split this Rust crate into core, cli and macros crates in one workspace" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.