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.

  • Languages
  • $rust-cargo-workspaces
  • 208 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- rust-cargo-workspaces

Installs to ~/.agents/skills/rust-cargo-workspaces/ for every project on this machine.

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

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.

~/.agents/skills/rust-cargo-workspaces/SKILL.md

--- name: rust-cargo-workspaces description: Structures and maintains Cargo workspaces - virtual manifests, workspace.package and workspace.dependencies inheritance, shared lints, resolver 2 and… license: MIT ---

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 resolver explicitly. Resolver “2” stops dev-dependency and build-dependency features leaking into normal builds; “3” (default for edition 2024 packages) adds MSRV-aware version selection using rust-version.
  • default-features = false must 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

RuleExample
Features are additive; enabling one must never remove behaviour or change an APIjson adds a module, never swaps a type
Optional dependencies get explicit features with dep: so the dep name is not a public featurejson = ["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 stddefault = ["std"]
Mutually exclusive backends are a design smellPrefer 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 featureUnification 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

GoalCommand
Build or test everythingcargo test --workspace --all-features (plus a --no-default-features job)
One membercargo test -p widget-core
Exclude a membercargo test --workspace --exclude xtask
Run a binarycargo run -p widget -- args
Add a dep to a membercargo add -p widget anyhow (then move it to workspace.dependencies if shared)
Check MSRVcargo +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 skips publish = false crates. On older toolchains publish leaf crates first, one -p at a time.
  • Use include = [...] or exclude = [...] 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) and cargo-release automate 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 with exclude = [...] or list members explicitly in larger repos.
  • cargo test --workspace compiles all dev-dependencies of all members; slow CI is often one heavy dev-dep in a single crate.
  • Inherited version.workspace = true means every crate shares one version; if crates release independently, set versions per crate instead.
  • rust-version set 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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem