declarative environment managers for teams spanning multiple operating systems
Declarative manifests eliminate environment drift across macOS, Linux, and WSL2.

Environment drift is a reliability and security problem because it hides in the dependency graph, not in the code a team actually writes and reviews. Developers on a team spanning macOS, Linux, and WSL2 pin the top-level library version they care about, but the sub-dependencies that library pulls in keep updating on their own schedule, and the environment quietly diverges without anyone noticing it happen. This is why the gap between "works on my machine" and "works in CI" so rarely traces back to a bug in the code itself. It's almost always a version mismatch buried somewhere in that graph.
Environment drift as a reliability and security problem
The mechanism gets worse once you add more than one chip architecture into the picture. A transitive dependency that resolves cleanly on x86 Linux may resolve to a different version on Apple Silicon, making the divergence invisible until a cross-platform bug appears. Fleet's 2026 analysis of enterprise device management found the same structural problem one layer up, at the OS level: disconnected dashboards are where visibility gaps and patch lag hide, because nobody has a single place to see what is actually installed across the fleet. One layer down, inside the development environment itself, the same dynamic plays out: package managers and shell scripts stand in for the dashboard, and they give you just as little visibility into what has quietly changed.
This is not a mere annoyance, because it carries a security exposure. An unpinned transitive dependency is an unaudited dependency, and if a CVE lands in that component, a team may not find out until well after a release has already shipped. Drift raises an open question about what code is actually running across every operating system and chip architecture a team supports, which compounds the security exposure rather than remaining a cosmetic difference between two developers' laptops.
What a declarative environment contract specifies
A declarative environment contract solves this by encoding the entire dependency graph, not just the packages a developer thinks to name, into version-controlled configuration, so that every developer on every operating system resolves to the exact same set of inputs. The mechanism that makes this guarantee real rather than aspirational is a content-addressed store: every package is identified by a cryptographic hash of its own inputs, including source code, its dependencies, build flags, and compiler version, so that identical inputs always produce identical outputs. Two machines building from the same manifest are not just configured similarly. They are mathematically guaranteed to end up with the same thing.
That guarantee only holds if the manifest actually contains everything that could affect the build. A manifest that pins only the top-level packages while letting the transitive graph float is a partial specification, and the distinction matters enormously once a team starts comparing tools against each other, because a tool that looks "declarative" on the surface can still leave most of the actual dependency graph unaccounted for.
The payoff for getting this right extends past day-to-day convenience. Because the configuration is version-controlled, environment changes appear as reviewable diffs in the commit history, rollbacks are atomic instead of best-effort, and you can audit the full history of what actually ran in production long after the fact. A hash of a Dockerfile cannot offer any of that, because it tells you an image was built, not what went into it or how to reconstruct it precisely if something goes wrong.
The specific requirements a cross-OS team imposes that a single-platform tool cannot meet
A team spanning macOS, Linux, and WSL2 has to work with an environment manager that runs natively on all three, resolves system dependencies appropriate to each platform on its own, and produces the same logical environment everywhere, without requiring the same binary everywhere. "Platform-appropriate" resolution means macOS developers should get Clang and the Apple system frameworks their tools expect, Linux developers should get GCC and Valgrind, and WSL2, since it runs on an actual Linux kernel, should be treated the same as native Linux rather than as some separate third category. A manifest that handles this well does it without branching into separate platform-specific files that someone has to keep in sync by hand⟶.
Chip architecture adds a second axis to the same problem. Apple Silicon (aarch64), Intel Mac (x86_64), and Linux ARM servers are all realistic parts of a single team's footprint today, so if a tool needs emulation or QEMU to bridge one of those legs, it brings back the exact fragility the declarative approach was supposed to remove. Emulated builds are slower, they can behave subtly differently from native ones, and they add a failure mode that a genuinely cross-platform manifest should never need.
WSL2 deserves its own callout because it trips up tools in a specific, practical way. Developers running WSL2 on Windows expect a Linux environment, and they expect their tools to treat them as Linux, so if a tool quietly handles them differently from a native Linux user, it has created a hidden class of drift that nobody set out to add. Many teams decide that trade-off isn't worth it for everyday development, even if they accept containers at the deployment stage.
How the main declarative environment managers handle cross-platform teams
Weighed against those requirements, the declarative tools available to teams today sit along a spectrum that trades setup effort for strength of guarantee, and where a team should land depends on how much of that setup cost it can absorb. Flox, a single-manifest environment manager built on Nix that lets teams share and run identical dependency sets across laptops, CI, and production without requiring anyone to learn Nix directly, sits near the reproducibility end of that spectrum.
Nix flakes sit at the reproducibility end of that spectrum. Nix runs natively on both Linux and macOS across x86_64 and aarch64, and because WSL2 runs an actual Linux kernel, Nix installs there too, though it typically needs some WSL-specific configuration steps along the way. Cross-compilation is treated as a first-class feature rather than an afterthought: targeting ARM from an x86 machine is documented and supported without reaching for custom images or brittle toolchain scripts. Nix is also where AI agent tooling is advancing fastest: the mcp-nixos MCP server queries live Nix package data instead of relying on the package names baked into an AI model's training data, which models frequently get wrong, and the numtide organization keeps a flake repository of AI coding agent tools that it updates daily.
Devbox takes the same underlying guarantee and wraps it in a friendlier interface, retaining the Nix input-addressed store as its reproducibility foundation rather than approximating it. Its learning curve is moderate rather than steep, and its CI story is strong: caching the Nix store through the GitHub Actions cache, by setting enable-cache: true, makes later environment setup nearly instant. It also supports containerless service orchestration through Process-Compose, which replicates the behavior teams expect from Docker Compose without actually running containers; the Dwarves Foundation engineering team used this to move local development off Docker entirely while keeping Docker for deployment. The research behind this piece concludes that, as of mid-2026, Devbox is the option that delivers the core cross-OS promise with the most approachable setup for most teams, a conclusion specific to the research's own comparison rather than a universal ranking.
mise occupies a different point on the spectrum. It manages language and tool versions, environment variables, and tasks, and as of 2026 it also reaches into system-level dependencies, meaning packages, services, and files, through its mise bootstrap command, though its OS-level reproducibility still falls short of what Nix or Devbox provide. It's easy to adopt and gives you medium reproducibility, so it suits teams whose drift problem is confined mostly to language versions and who handle system dependency parity through some other layer. It does not meet the full-dependency-graph requirement laid out earlier, so if a team needs true OS-level consistency, it will eventually hit its ceiling.
Dev containers package the operating system and its tools inside a container image, so you get high reproducibility and an easy entry point if your team is already comfortable with container-based workflows.
What CI/CD environment parity requires
A declarative environment only removes "passes locally, fails in CI" failures when the CI pipeline draws from exactly the same manifest as local development. The most common way this breaks down is a pipeline that installs its own tools independently of the team's declared environment, through a separate apt-get or brew step that grabs whatever version happens to be current at run time rather than the version the manifest actually pins. That single gap is enough to reintroduce every problem the manifest was built to prevent, because now CI is testing against a different environment than the one developers actually use.
To close that gap, the environment manager's own store or cache has to be the source of truth for how CI installs its tools, rather than deferring to the pipeline runner's default package manager. Store caching is what makes this practical rather than theoretical: when the store is cached by content hash, later CI runs restore the exact environment without rebuilding anything, so setup becomes close to instant while staying fully reproducible. Internal developer platform guidance frames this as one of the platform's core jobs: matching environment-agnostic developer configurations to the right infrastructure profile at deployment time, so a single manifest stays valid across every stage of the delivery pipeline rather than needing a rewrite at each one. GitOps tooling reinforces the same discipline one layer further downstream: tools like Argo CD maintain a declared desired state and reconcile any drift from it automatically, applying at the deployment layer the same logic a declarative environment manager applies to the developer's own toolchain.
Declarative environments as supply chain controls
Everything said so far about reproducibility turns into a security argument the moment a team has to answer the question of what it actually shipped. A fully specified, content-addressed dependency graph is what accurate SBOMs and CVE tracking depend on, and without one, an organization simply cannot answer that question with any confidence. Much of the research on this points to the same gap: many SBOMs get generated too late in the development lifecycle, lack any real context about how their components are actually used, or fail to reflect what was truly shipped, and an SBOM built from a partially specified environment inherits all of that incompleteness by default.
A declarative environment changes that equation because it hashes every input as a matter of course. The dependency graph it produces is the SBOM, not a document generated after the fact to approximate one. You can attach that input-derived hash directly to a deployment and use it for fast CVE discovery and hash-level blocking, so nobody needs to grep through logs or rescan container images after the fact. The European Union's Cyber Resilience Act requires software makers to create and maintain SBOMs in a commonly used, machine-readable format, and that requirement is already in force for any team shipping into EU markets, not something on a future roadmap.
Precision matters about what SBOMs do and don't cover here. SLSA and SBOM solve related but different problems: SLSA is about build provenance, while SBOM generation is about enumerating components, and teams need both, with a declarative environment supplying the input traceability that makes each of them tractable rather than aspirational. None of that is reliable unless the build's inputs were fully declared and pinned in the first place.
Manifest requirements for holding the contract across developer machines, CI, and production
Holding this contract across macOS, Linux, and WSL2 requires a manifest that declares far more than package names: toolchain, shell hooks, environment variables, and service definitions all belong in it, because anything left out becomes its own vector for drift. Every direct dependency needs a pin, and wherever the tool allows it, the transitive graph needs one too, since top-level pins sitting on top of floating transitive dependencies are not a real contract at all, just a partial one dressed up to look complete. Local services like databases and message queues need to live in the same manifest or in a companion configuration such as Process-Compose, so that developers are never unknowingly running different service versions against the same codebase. A well-built manifest also handles platform-specific needs, such as Apple frameworks on macOS or Valgrind on Linux, directly inside itself rather than pushing that logic out into separate files or into a README full of caveats that nobody reads until something breaks. Internal developer platform guidance treats this as the platform team's core deliverable: golden-path base environments that developers can extend on their own, without needing to loop in the platform team every time something small needs to change.
How declarative environments extend to AI coding agents
If AI coding agents work inside an undeclared environment, they inherit every drift risk already described in this piece, and they add new failure modes on top of it, including hallucinated package names and sandbox states that nobody can reproduce after the fact, so only a fully specified environment can prevent both. The clearest example of this occurs with Nix itself: AI models are known to hallucinate Nix package names, and a single wrong attribute path is enough to cascade into a broken build that gives the agent no useful signal about what actually went wrong.


