Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

rust-cargo-workspace-designer

Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool. Use when the user creates or restructures a Rust monorepo, asks how to organise crates, sees 'profiles for the non root package will be ignored', 'cyclic package dependency', 'believes it's in a workspace when it's not', resolver warnings, duplicate dependency versions, slow builds or failed publishes.

Programming / Rustv1.0.0Updated OCT 21 20267 references6 scripts

Design a Cargo workspace that builds fast, layers cleanly and publishes without surprises: decide where crate boundaries pay for themselves, pick a layout, put every shared setting in the root manifest exactly once (package metadata, dependency versions, lints, profiles), keep features additive, and make CI prove it with --locked. Review existing workspaces the same way: three small std-only Rust programs in scripts/ read every manifest (they never run cargo), flag 20 mechanical mistakes and print the internal dependency graph by layer.

Trigger Conditions

Activate this skill when the user:

  • Starts a multi-crate Rust project, converts a single crate into a workspace, or asks "how should I split this into crates?", "monorepo layout", "virtual manifest or root package?"
  • Edits a root Cargo.toml with [workspace], [workspace.package], [workspace.dependencies], [workspace.lints], resolver, members, exclude, default-members, or [profile.*]
  • Sees warning: profiles for the non root package will be ignored, virtual workspace defaulting to resolver = "1", cyclic package dependency, current package believes it's in a workspace when it's not, default-features is ignored for ..., or cargo publish failing on a path dependency without a version
  • Has the same dependency at different versions across crates, two copies of a crate in cargo tree -d, features that work with cargo build --workspace but not with cargo build -p one-crate
  • Wants faster builds (cargo build --timings), CI for a workspace, an MSRV policy, a Cargo.lock policy, an xtask crate, proc-macro crate placement, or a release/publishing flow for several crates

Workflow

text
0. BUILD     -> once: for t in crate_graph manifest_lint dep_audit; do
                  rustc -O --edition 2021 "$SKILL/scripts/$t.rs" -o "/tmp/$t"; done
1. INVENTORY -> /tmp/crate_graph .                  (crates, layers, internal edges, cycles, membership)
                cargo tree --workspace -e normal -d (duplicates), cargo build --timings (if a toolchain exists)
2. AUDIT     -> /tmp/manifest_lint . --strict; /tmp/dep_audit . --strict   (file:line + fix)
3. BOUNDARIES-> decide split/merge with the table below; target layering domain <- app <- infra <- bin
4. ROOT      -> one virtual manifest: resolver, [workspace.package], [workspace.dependencies],
                [workspace.lints], profiles; members inherit with `x.workspace = true`
5. FEATURES  -> additive only, defaults off at the root, dep:/?/ syntax; test each crate alone (cargo hack)
6. CI        -> fmt, clippy, nextest + doctests with --workspace --all-targets --locked; MSRV job;
                cargo deny; semver-checks for published crates
7. VERIFY    -> all three scripts with --strict exit 0; the graph has no cycles

When to Split a Crate

Reason to splitWhy it paysReason NOT to split
Compile time: a big crate where every edit recompiles everythingcrates compile in parallel; an edit only rebuilds the crate and its dependentsthe "split" crates all depend on each other (no parallelism, more link/metadata work)
Isolate heavy dependencies (DB drivers, gRPC, GUI, ML)crates that do not need them stop rebuilding when they changethe dependency is needed everywhere anyway
Proc-macrosmandatory: proc-macro = true crates can only export macros-
Enforce an architectural boundary (domain must not see HTTP/SQL)the compiler enforces it; pub(crate) stays privatea single module with pub(crate) enforces it well enough
Publish part of the code (SDK, client, macros)independent semver, smaller downloadinternal-only code with no second consumer
Several binaries share codeapps/* binaries over shared crates/*one binary: modules are enough
Isolate a build.rs / links / FFI -sys cratebuild script reruns only rebuild that crate-
Different platforms or no_std corethe core builds for embedded/wasm without std deps-

Costs of splitting: more manifests, orphan-rule friction (impl ForeignTrait for ForeignType is forbidden, so trait impls must live next to the type or the trait), non-generic functions are not inlined across crates without #[inline] or LTO, and every boundary is a public API you have to maintain. Start with modules; promote a module to a crate when one of the reasons above is real. Detail: crate-boundaries-and-layouts.md.

Layouts

LayoutUse whenShape
Flat crates/* virtual manifestlibraries + a few bins, the default for most projectsCargo.toml (no [package]), crates/<name>/
apps/ + crates/ (or libs/)several deployables share librariesapps/server, apps/cli, crates/domain, crates/infra
Root package + membersone main crate with helpers (foo, foo-macros, foo-derive)[package] and [workspace] in root Cargo.toml
Separate workspaces / excludefuzz targets, examples with conflicting deps, embedded firmwareexclude = ["fuzz"], own Cargo.toml + lockfile
text
shop/
├── Cargo.toml            # virtual manifest: [workspace] only, resolver, shared tables, profiles
├── Cargo.lock            # committed, one for the whole workspace
├── .cargo/config.toml    # aliases (xtask), no global rustflags
├── apps/shop-server/     # bin, publish = false
├── crates/shop-domain/   # pure types + rules, no IO          (layer 0-1)
├── crates/shop-macros/   # proc-macro = true                 (layer 0)
├── crates/shop-app/      # use cases, port traits            (layer 2)
├── crates/shop-infra/    # adapters: sqlx, reqwest, tokio     (layer 3)
└── xtask/                # cargo xtask codegen|dist|ci, publish = false

Name folders exactly like the crates (crates/shop-domain holds name = "shop-domain") and prefix every crate with the project name: crates.io has one global namespace and use shop_domain::Order reads clearly.

Root Manifest

toml
[workspace]
members = ["crates/*", "apps/*", "xtask"]
default-members = ["apps/shop-server"]      # what plain `cargo build` / `cargo run` at the root builds
exclude = ["fuzz"]
resolver = "3"                              # required in a virtual manifest; "2" if MSRV < 1.84

[workspace.package]
version = "0.3.0"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
repository = "https://github.com/acme/shop"

[workspace.dependencies]
shop-domain = { path = "crates/shop-domain", version = "0.3.0" }   # path + version: publishable
serde = { version = "1.0.200", default-features = false }           # defaults off; members opt in
tokio = { version = "1.38", default-features = false }
thiserror = "2"

[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }     # groups need a lower priority than single lints
unwrap_used = "warn"

[profile.release]
lto = "thin"
codegen-units = 1
[profile.dev.package."*"]
opt-level = 2                               # fast dependencies, debuggable own code

Member Manifest

toml
[package]
name = "shop-infra"
description = "Database and HTTP adapters for shop-app"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
shop-domain = { workspace = true, features = ["serde"] }   # features are ADDED to the workspace entry
tokio = { workspace = true, features = ["rt-multi-thread", "fs"] }

[lints]
workspace = true

A member may add features, optional = true (and default-features = false only if the workspace entry also disables defaults); it can never change the version, path or source. Full key reference: manifest-and-inheritance.md.

Core Rules

  1. Virtual manifest with an explicit resolver. A virtual manifest has no edition, so without resolver it silently uses resolver 1. Use "3" for edition 2024 / MSRV >= 1.84, else "2".
  2. Root-only sections live in the root. [profile.*], [patch], [replace] in a member are ignored.
  3. One version per dependency. Every external dependency used by two or more crates goes into [workspace.dependencies]; members write dep.workspace = true.
  4. Inherit metadata and lints. edition, rust-version, license, repository, version via .workspace = true; every member has [lints] workspace = true.
  5. Layers point one way. domain <- app <- infra <- bin; no cycles; shared types sink to the lowest crate; upper layers depend on traits defined below (ports), not the other way round.
  6. Internal deps carry path AND version if anything that depends on them is published; everything else is publish = false (apps, xtask, test-support, benchmarks).
  7. Features are additive. Defaults off at the root, std as an opt-out-able default, dep: for optional deps, pkg?/feat for weak features, no mutually exclusive features. Test each crate alone.
  8. Commit Cargo.lock (binaries and libraries alike, per the 2023 Cargo guidance) and build CI with --locked; declare rust-version and test it in CI.
  9. No global rustflags in .cargo/config.toml for things that change codegen of every crate unless you mean it: they invalidate caches, do not reach users of published crates, and are overridden by RUSTFLAGS.
  10. Measure before restructuring for speed. cargo build --timings shows the critical path; split the crate on it, not the crates that are already parallel.

Features and Unification

Cargo builds one copy of each dependency per build with the union of features requested by every package selected for that build. cargo build --workspace and cargo build -p shop-app can therefore compile tokio with different features: one passes, the other fails ("works in CI, fails when built alone"), and switching between them rebuilds. Resolver 2+ keeps build-dependency/proc-macro features, dev-dependency features (unless tests/examples are built) and other-platform dependencies out of the union.

toml
[features]
default = ["std"]
std = ["serde?/std"]          # weak: enables serde/std only if serde is already on
serde = ["dep:serde"]         # namespaced: no implicit "serde" feature is created
[dependencies]
serde = { workspace = true, optional = true, features = ["derive"] }

Check every crate with its own features: cargo hack check --workspace --each-feature --no-dev-deps. Pitfalls, cargo tree -e features -i, workspace-hack crates: features-and-unification.md.

Symptom -> Cause -> Fix

SymptomCauseFix
profiles for the non root package will be ignored[profile] in a member (CW002)move to root Cargo.toml
virtual workspace defaulting to resolver = "1"no resolver in virtual manifest (CW001)resolver = "2" / "3"
cyclic package dependencytwo crates depend on each other (CW011)move shared items down a layer; invert with a trait
current package believes it's in a workspace when it's notcrate under the root not in members (CW010)add to members or exclude
failed to load manifest for workspace memberstale member path / glob (CW009)fix members, delete the empty dir
cargo publish: dependency does not specify a versionpath dep without version (CW007){ path = "...", version = "x.y" } or publish = false
default-features is ignored for serdemember disables defaults the workspace enables (CW016)default-features = false at the root
Same crate twice in cargo tree -dmembers pin incompatible majors (CW003)[workspace.dependencies]; cargo update -p
Builds alone fail, --workspace passesfeature unification hides a missing featuredeclare the feature in that crate; cargo hack --each-feature
Everything rebuilds when switching -p / IDE vs CLIdifferent feature sets or RUSTFLAGS per invocationunify features (workspace-hack), same flags, separate target-dir for the IDE
One edit rebuilds the whole treeedited crate is at the bottom of the graph or one giant cratesplit the hot crate, move volatile code up the graph
lto, opt-level in member have no effectprofile in member (CW002)root profile, [profile.release.package.foo] for per-crate overrides
Old toolchain: feature edition2024 is requiredMSRV lower than manifest needs (CW020)raise rust-version or drop the feature
CI passes locally, fails with --lockedCargo.lock not committed / out of datecommit it; cargo update deliberately in a PR

Version Notes

RustCargo change that matters for workspaces
1.51resolver = "2"
1.56edition 2021 (implies resolver 2), rust-version
1.60dep: namespaced and ?/ weak features; cargo build --timings
1.64workspace inheritance: [workspace.package], [workspace.dependencies]
1.74[lints] / [workspace.lints]
1.84resolver = "3": MSRV-aware dependency resolution (incompatible-rust-versions = "fallback")
1.85edition 2024 (implies resolver 3; underscore keys and [project] rejected)

Full history: version-history.md.

Output Format

When designing or reviewing a workspace, answer with:

  1. Graph - crates by layer (from crate_graph), proposed moves/splits/merges and why.
  2. Findings - rule id, file:line, what breaks and when (now, on publish, on a fresh lockfile).
  3. Manifests - complete root Cargo.toml and one representative member, not fragments.
  4. Features - per crate: defaults, optional deps, what each feature enables; unification risks.
  5. CI and policy - the commands (--workspace --all-targets --locked), MSRV, Cargo.lock, release flow.
  6. Verification - the three scripts exit 0 with --strict, plus the cargo commands to run.

Scripts

Small, standard-library-only Rust programs: they read TOML and never run cargo. Build each once with rustc -O --edition 2021 scripts/<name>.rs -o /tmp/<name>. Input is a workspace directory, any Cargo.toml in it (the whole workspace is checked), or - for one manifest on stdin (no file-system checks).

ScriptRulesJob
scripts/crate_graph.rsCW009-CW011, CW018crates by layer, internal edges (--dot, --json), cycles, missing/stray/invalid default members
scripts/manifest_lint.rsCW001, CW002, CW004-CW006, CW008, CW012, CW017, CW019, CW020manifest contents: resolver, root-only sections, inheritance, edition, MSRV, publishability, lints, deprecated keys
scripts/dep_audit.rsCW003, CW007, CW013-CW016dependency declarations: duplicates, unversioned path/git deps, *, unpinned git, undefined inheritance, ignored default-features
scripts/toml.rs, scripts/workspace.rs, scripts/cli.rs-shared modules: TOML reader with line numbers, Cargo-style member discovery, options and report
bash
/tmp/crate_graph .                                  # layered graph + membership findings
/tmp/crate_graph . --dot | dot -Tsvg > crates.svg   # cycle edges drawn in red
/tmp/crate_graph . --json
/tmp/manifest_lint crates/core/Cargo.toml           # a member: checks its whole workspace
/tmp/dep_audit . --json                             # machine-readable findings
/tmp/manifest_lint . --strict                       # exit 1 on any finding (CI gate)
/tmp/dep_audit . --select CW003,CW007 --ignore CW014
cat Cargo.toml | /tmp/manifest_lint -               # one manifest from stdin
/tmp/dep_audit --list-rules                         # each script lists its rules with why and fix
RuleNameSeverity
CW001resolver-missing (virtual manifest, or root package on edition < 2021)high
CW002root-only-section-in-member ([profile], [patch], [replace])high
CW003duplicated-dependency (not using [workspace.dependencies])medium
CW004package-field-not-inherited ([workspace.package] key neither inherited nor set)low
CW005inconsistent-editionmedium
CW006missing-rust-versionlow
CW007unversioned-path-dependency (path/git dep without version in a publishable crate)high
CW008publish-not-set (no description/license yet no publish = false)low
CW009member-missing (path without Cargo.toml, glob matching nothing)high
CW010crate-not-in-workspacemedium
CW011dependency-cycle (normal/build edges)high
CW012lints-not-inheritedmedium
CW013wildcard-version ("*")high
CW014git-dependency-unpinned (no rev/tag)medium
CW015inherit-undefined (x.workspace = true with nothing to inherit)high
CW016default-features-ignoredmedium
CW017deprecated-underscore-key (dev_dependencies, default_features, [project])medium
CW018default-member-invalidhigh
CW019virtual-manifest-section ([dependencies], [features] in a virtual manifest)high
CW020msrv-below-manifest-feature (edition 2024 < 1.85, resolver 3 < 1.84, [lints] < 1.74...)medium

Members are found like Cargo does: members globs minus exclude, plus path dependencies of a root package (implicit members); nested workspaces, hidden directories and target/ are skipped. Exit codes: 0 clean (or findings without --strict), 1 findings with --strict, 2 usage error or missing/invalid root manifest; crate_graph also exits 1 whenever there is a dependency cycle. Unparsable member manifests are warned about and skipped.

Additional Resources

FileContent
crate-boundaries-and-layouts.mdWhen to split/merge, layouts, naming, layering, proc-macro and -sys crates, xtask, build.rs, tests/benches/examples
manifest-and-inheritance.mdEvery [workspace] key, inheritance rules, workspace deps, lints, profiles, .cargo/config.toml
features-and-unification.mdAdditive feature design, dep:/?/, resolver 2/3 rules, unification pitfalls, cargo-hack, workspace-hack
publishing-msrv-and-lockfiles.mdInternal path + version deps, publish order, release tools, semver-checks, Cargo.lock and MSRV policy
ci-and-tooling.mdGitHub Actions workflows, --locked, nextest, rust-cache, sccache, cargo-deny/machete/udeps/hack
build-performance.mdcargo build --timings, splitting hot crates, profiles, linkers, codegen bloat, CI cache strategy
version-history.mdCargo workspace/manifest features by Rust version, edition 2021/2024 changes
More in Rust

rust-borrow-checker-error-explainer

Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.

rust-error-handling-thiserror-anyhow-designer

Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.

rust-lifetime-annotation-advisor

Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.

rust-tokio-async-service-builder

Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.

rust-trait-design-reviewer

Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.

rust-unsafe-block-auditor

Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.