I run several coding agents at once, and each one works in its own git worktree: a separate checkout of the same repository, on its own branch, so the agents don't trip over each other's changes. It works well for Ruby and JavaScript. It works less well for Rust.
Our AWS Lambda workers are written in Rust, and every worktree that touches them builds them in its own target/ folder. One worktree's build takes 5 to 15 GB, and almost all of that is the same third-party crates, compiled again with the same versions and the same flags. In two days, about ten worktrees built the workers, and their build folders went from 11 GB to about 80 GB. My laptop's disk was already pretty full, and by then I had only 2 GB free. I had to regularly run Mole (an awesome free disk-cleaning utility), plus a dedicated Claude Code session to help me free up disk space a few times per week. (Rust isn't the only culprit here. I also need to regularly clear Docker containers and images, old worktrees, system caches and many other things.)
One way to fix this Cargo disk usage problem is to point every worktree at a shared build folder. Cargo does have a setting for this, but when I tried it, one worktree would silently run another worktree's code, because Cargo gave our crate the same file name in both worktrees. Adding a few more Cargo settings can fix that: each worktree gets its own build of our crates, and all of them share one build of the dependencies.
Where this stands, as of 13 November 2026: our laptops have built this way since 13 November, with Cargo 1.99.1 on macOS, and our worktree setup script writes the config for every new worktree. I tested it on our Lambda workers and on a minimal example first. It needs two unstable Cargo features, so we only use it on developer machines. Our CI builds the normal way.
A shared build directory has problems #
You can share a build folder with CARGO_TARGET_DIR, or, since Cargo 1.91, with the build.build-dir setting. I'd use build-dir. It moves only the intermediate files (compiled crates, fingerprints, incremental data) and leaves the final binaries in each worktree's own target/, so two worktrees never overwrite each other's target/debug/my-app.
In this minimal example, two copies of the same crate stand in for two worktrees. They differ in one line: one prints "A" and the other prints "B". Both use one shared build directory:
[build]
build-dir = "/tmp/shared-build"
$ (cd wtA/app && cargo build -q && ./target/debug/app)
A
$ (cd wtB/app && cargo build -q && ./target/debug/app)
A
Worktree B built without an error and printed "A", because Cargo gave it worktree A's build.
That's much worse than a slow build. An agent in worktree B can change the code, run the tests, see them pass and push, and nothing tells it that the tests ran another branch's code.
Why sharing Cargo's target folder breaks #
Cargo names every compiled unit with a hash of what went into it: the package, its version, features, profile, the compiler version and its dependencies' hashes. The hash appears in file names like libmy_app-0d7a8db30f960835.rlib. When two builds would produce the same hash, Cargo treats them as the same unit.
For a package that comes from a path, the hash includes the package's path, but relative to the workspace root, not the absolute path. Cargo's source explains why, in the comment on SourceId::stable_hash:
/// For path dependencies, we do these to help reproducible builds:
///
/// * Remove the workspace prefix,
/// so the same source will give the same hash in different locations.
That's a good rule for reproducible builds: the same source checked out in two places should produce the same artifact. Two worktrees are the same repository checked out in two places, but with different changes in each. Cargo can't tell them apart, so our crate in worktree A and our crate in worktree B get the same hash. I checked our real worktrees, and two of them had built a file with the same name, liblambda_pdf_handler-0d7a8db30f960835.rlib.
In a shared build directory, that means the two worktrees share one fingerprint for our crate. Cargo decides whether a unit is up to date by comparing its recorded build against the modification times of its source files. When worktree B builds after worktree A and none of B's files are newer than A's build, Cargo finds a fingerprint that looks current and reuses A's output.
Third-party crates don't have this problem, and they take up most of the space. Cargo identifies a crate from crates.io by its registry and version, so two worktrees building serde 1.0.228 with the same features really are building the same thing.
Giving each worktree its own hash #
Our own crates need a different hash in each worktree, and the dependencies need to keep the one they have. My first idea was a fork of Cargo that hashed the absolute path. It would work, but I'd be rebuilding that fork for every Rust release. Then I found that the hash already has an input we can set per package.
Cargo puts a unit's rustflags into the hash in its file name. (It leaves them out of the -C metadata value that goes into symbol names, so changing flags doesn't change symbols.) Cargo profiles can set rustflags too, for every package or for only some of them. So each worktree can give its own crates a harmless flag, with a value unique to that worktree:
[profile.dev]
rustflags = ["--cfg", "worktree=\"455b9757ada83113\""]
[profile.dev.package."*"]
rustflags = []
The first table gives the flag to every package. The second takes it away from "*", which in a profile means every package outside your workspace: everything from crates.io or git. That leaves the flag on your own crates.
--cfg worktree="..." sets a configuration value that no code checks, so the compiled code stays the same and only the hash changes. Our crate gets a different file name in each worktree, and the dependencies keep theirs.
With those two tables, the minimal example prints "A" and then "B". Worktree B's first build compiles only its own crate and takes every dependency from worktree A's build.
Building at the same time #
Cargo also locks the whole build directory while it builds, so two worktrees that build at once take turns:
Blocking waiting for file lock on build directory
With several agents running tests, that adds up to a lot of waiting. Cargo's unstable fine-grained locking locks each unit separately instead. With -Zfine-grain-locking, two worktrees build at the same time and still share the units they both need. I ran the test builds of two of our worktrees together. Each compiled only our crate and finished in 35 seconds, without waiting for the build directory's lock.
Turning on the unstable features #
Profile rustflags (the profile-rustflags feature) and fine-grained locking are both unstable. Stable Cargo allows unstable features only when it thinks it's a nightly build, and you can get there two ways:
- Use a nightly Cargo for local builds.
- Keep your stable toolchain and set
__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS=nightly. That's a variable Cargo uses in its own test suite, and the name tells you how the Cargo team feels about anyone else using it. It changes only Cargo's own check:rustcand build scripts still see a stable compiler.
We use the variable, set in each worktree's environment. It's unsupported, which is why it stays on our laptops. Our CI doesn't set it and doesn't share a build directory, so if a Rust release breaks this setup, only our local builds break.
The setup #
Each worktree gets its own .cargo/config.toml at its root, ignored by git. Cargo reads config files from the current directory and every directory above it, so this applies to every Cargo project in the worktree. This script writes one:
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$1" && pwd -P)
shared="${CARGO_SHARED_BUILD_DIR:-$HOME/.cache/cargo-shared-build}"
hash=$(printf '%s' "$root" | shasum -a 256 | cut -c1-16)
mkdir -p "$root/.cargo"
{
printf '[build]\nbuild-dir = "%s"\n\n' "$shared"
printf '[unstable]\nprofile-rustflags = true\nfine-grain-locking = true\n'
for profile in dev release; do
printf '\n[profile.%s]\n' "$profile"
printf 'rustflags = ["--cfg", "worktree=\\"%s\\""]\n' "$hash"
printf '\n[profile.%s.package."*"]\nrustflags = []\n' "$profile"
done
} > "$root/.cargo/config.toml"
You don't list your crates anywhere, so a new crate in the workspace gets the flag without anyone having to remember. Profiles that inherit from dev or release get the flag too, custom ones included. cargo test builds with the test profile, which inherits from dev, so each worktree's test binaries get their own hashes.
A shell that missed the setup can't build in a configured worktree: without the environment variable, Cargo stops with "feature profile-rustflags is required".
The numbers #
I measured these on an M2 Pro MacBook with our Lambda workers. Each worktree makes two builds: the test build (instrumented for coverage) and a plain build of the document worker that our Rails app runs in development. sccache was on for every run, so the cold builds are faster than they'd be without it.
| Separate build folders | Shared build directory | |
|---|---|---|
| First worktree | 8.2 GB, 99 seconds | 8.2 GB, 118 seconds |
| Each worktree after that | 8.2 GB, 99 seconds | 2.7 GB, 22 seconds |
| Ten worktrees | 82 GB | 33 GB |
A second worktree compiled one crate per build, our own, instead of about 400. What's left for each worktree is its build of our crate (about 2 GB in the shared directory) and its final binaries (0.7 GB in its own target/). These are fresh builds, and a worktree's build grows with daily use.
Switching our existing worktrees over freed 46 GB. Ten worktrees take 33 GB, which is still a lot of data, but at least it's a bit more manageable, and I won't have to free up disk space quite as often.
What to watch out for #
Only workspace members get the flag. A path dependency inside your package's folder is a member only if the root Cargo.toml has a [workspace] table. I found that out when my minimal example's util crate, in a subfolder with no [workspace] above it, printed "A" in worktree B. Add a [workspace] table and Cargo makes every path dependency under its folder a member. A path dependency outside the workspace's folder is safe already, because Cargo hashes it by its absolute path (unless you turn on trim-paths).
Don't add --remap-path-prefix to those flags. Cargo leaves a unit's rustflags out of its hash when they contain --remap-path-prefix, so that remapping paths can't make a build less reproducible. Put it in the same flags, and the per-worktree value stops changing the hash, which brings the bug back.
If you add .cargo/ to .gitignore, branches from before that change still show the file as untracked, and a git add -A on one of them commits it. Add /.cargo/ to the repository's .git/info/exclude as well. Every worktree reads that file, whatever its branch.
The shared directory only grows. Removing a worktree doesn't remove its builds, and old dependency versions stay behind after a Cargo.lock update. Cargo's own clean-up for build directories is still in development, so you'll need to clear it out yourself. Deleting the whole directory is safe while nothing is building: the next build compiles the dependencies again. Our script that removes a worktree also automatically deletes the shared directory once it's over 50 GB.
Unstable features change, and fine-grained locking is still being worked on. When you update Rust, check that a new worktree still compiles only its own crates.
Until Cargo has a shared cache #
The Cargo team is working on a build cache shared between workspaces, and it's one of the Rust project's goals for 2026. The plan starts with a nightly prototype for simple crates. When that's stable and handles crates like ours, we can drop this setup. Until then, our worktrees compile the Lambda workers' dependencies only once, and a new worktree's first build takes 22 seconds instead of 99.