A hands-on technical reference for platform engineers operationalizing Nix-based SBOMs, CVE remediation, and supply chain controls in real C

One Nix Command That Activates a Full Dev Environment on macOS Linux and WSL2

One command activates the same development environment across macOS, Linux, and WSL2.

· 8 min read
Cover illustration for “One Nix Command That Activates a Full Dev Environment on macOS Linux and WSL2”
Environment Parity · October 29, 2026 · 8 min read · 1,909 words

On macOS, Linux, and WSL2, nix develop activates a project’s declared development environment from its committed flake.nix and flake.lock, once Nix is installed. One-command onboarding is therefore a retrieval operation against a shared specification.

Why "it works on my machine" is an environment problem, not a developer problem

A developer’s workstation often records a sequence of installation decisions. One project required a different compiler, another needed an older runtime, and a later operating system upgrade changed how those tools interacted. These decisions accumulate as machine state. Even careful developers cannot reliably reproduce decisions that were never recorded as a complete set of requirements. Treating the resulting failures as individual carelessness mistakes an organizational gap for a personal one.

Tools such as nvm, pyenv, rbenv, and sdkman each manage a language-specific slice, but they do not coordinate the complete toolchain. Their separate configurations can break when the operating system is upgraded. Meanwhile, local tools drift from CI over days and weeks, with the difference becoming visible only when the same code behaves differently elsewhere.

When macOS developers run Linux workloads through a VM layer, they can face file permission mismatches and trouble with volume mounts as well as version drift. Windows, WSL, and a Linux server can each evolve independently without a shared mechanism to reconcile them. Diagnosis then requires reconstructing the machine’s past before examining the code. Interactively assembled environments create a structural problem: they cannot reliably be shared, audited, or recovered.

What makes a declarative manifest a retrievable artifact rather than a setup script

A declarative manifest describes the complete intended environment rather than the actions someone once took to assemble it. A bash bootstrap script is a route from an assumed starting point, while a manifest specifies the destination. That distinction changes what successful activation means. The toolchain resolves the declared state instead of trusting that a sequence of installation commands will produce it on a machine with an unknown history.

Nix identifies package builds through cryptographic hashes of their inputs, including source code, dependencies, build flags, and compiler version. Store paths therefore depend on the build’s declared inputs, regardless of when it was installed. The project’s flake.nix defines the environment, while the generated flake.lock records exact revisions for its inputs. Together, those files provide a retrievable specification of the dependency graph. Retrieval may involve obtaining or building packages, but it does not depend on reconstructing undocumented installation choices.

Committing those files also gives environment changes a history. Git records when a package was added and who added it, with the reason available in the accompanying change record. A history of tools installed by hand cannot give the team that shared record. The environment becomes recoverable from its definition. This is the conceptual foundation for a command that activates a complete project environment.

How one command activates the same environment on macOS, Linux, and WSL2

With Nix installed, nix develop activates the development shell declared by the project’s flake. The invocation is the same on macOS, native Linux, and WSL2. Developers do not need separate bootstrap command sequences for each platform. Instead, the flake defines supported targets and resolves the declared tools for the platform executing it. The shared contract is the environment definition and its pinned inputs.

A single flake.nix can target x86_64-linux, aarch64-linux, and aarch64-darwin. These targets let Linux workstations, supported macOS laptops, and CI runners use the same declared tool versions, with builds for each platform. On Linux, a native multi-user Nix installation provides the declared shell directly. WSL2 supplies a Linux execution environment, and NixOS-WSL uses WSL’s native systemd support to run Linux services in the background. The platform runs the tools, with no separate list of tools to keep up to date by hand.

Activation is scoped to the project shell. Exiting it ends the shell’s environment changes rather than leaving conflicting global Node or Python selections behind. For an automatic workflow, direnv uses use flake in .envrc to activate the environment when the developer enters the directory and deactivate it on exit. These are two ways to consume the same definition. With either method, the developer has no need to remember which tools each project needs.

How do new engineers become productive on day one without manual setup steps?

Once the Nix prerequisite is in place, project onboarding becomes cloning the repository and running nix develop. The new engineer gets the tools and versions the team has declared. There is no separate package inventory to interpret. A requirement such as Terraform 1.7.5 belongs in the environment definition rather than in a message asking the new hire to install the right release. The distinction removes configuration work from the point where the engineer is trying to understand the project.

A wiki-based path makes the engineer reconcile instructions with the machine in front of them. It may span several package managers, include different caveats for macOS and Linux, and become stale as the project changes. A committed manifest changes with the project instead. The organizational benefit is fewer commands typed by the newcomer and less dependence on another developer’s memory to explain why the documented steps no longer produce a working toolchain.

The same steps work when a developer already on the team loses a machine. With the repository and the Nix prerequisite restored, environment recovery uses the same activation operation and download time as initial onboarding. External contractors and open-source contributors can consume that definition without inheriting the team’s tribal knowledge. Their access to a working toolchain no longer depends on an informal handover. Onboarding speed is one dividend, but the deeper one is that every developer begins from the same declared ground.

How identical environments across dev, CI, and production eliminate the "passes locally, fails in CI" failure mode

When CI consumes the same flake.nix and flake.lock as local development, it removes the independently maintained toolchain that causes environment-drift failures. The lock file is the shared contract. A pipeline cannot silently select a different declared dependency graph while still using those same pinned inputs. This eliminates the environment-drift form of “passes locally, fails in CI,” rather than guaranteeing that every possible difference between local and CI execution disappears.

Running nix flake update, either for selected inputs or for the whole flake, and committing the result exposes the update in git diff. Other flake commands can also update the lock file when needed, so the committed result defines what the team shares. GitHub Actions, GitLab CI, and other runners can consume the same flake. With Nix store caching, later runs can fetch build results that are already available without having to rebuild them.

Production packaging can derive from the same definition without replacing the development shell workflow. Nix can build minimal, reproducible OCI images containing the dependencies specified by the relevant closure. Containers hold the artifact to deploy, while the shell is the interactive development environment. They solve different parts of the same problem. Where Nix rebuilds manage system state, atomic switching and rollback also keep an update that has only partly been applied from becoming the active environment.

Why the environment's content-addressed inputs are also a supply chain audit trail

The hashes that identify build inputs also provide a basis for tracing software provenance. Changing a declared dependency changes its identity and the identities of downstream builds that depend on it. Silent substitution conflicts with that recorded graph. Successful activation therefore has a provenance dimension, although an input-derived path is not, by itself, a complete security attestation. The useful property is the connection between the environment being activated and the inputs declared for it.

Nix derivations expose build instructions, while sandboxed builds restrict arbitrary network and filesystem access. The dependency graph records build-time inputs and the runtime closure. This gives an SBOM generator more than a language package list. Native dependencies such as glibc and openssl are visible as part of the environment, rather than being omitted because a tool examines only Rust or Go packages. The build grounds the inventory before deployment.

The sbomnix toolset, originating from the Ghaf Framework, generates CycloneDX and SPDX SBOMs from a flake reference or store path. Its provenance tool generates SLSA v1.0 compliant provenance attestation files in JSON format for Nix flakes or derivations. Metadata still needs careful handling. CycloneDX expresses component classification through type, while SPDX 3.0.1 uses primaryPurpose, illustrating why a dependency graph alone does not settle every reporting requirement. Metadata completeness and translation remain an area for further work.

For enterprises, an input-derived hash can serve as a deployment fingerprint. Connecting that fingerprint to the dependency inventory supports CVE discovery and targeted blocking or patching. The security benefit follows from the same declared graph used for activation.

How AI coding agents inherit the same environment guarantees as human developers

AI coding agents are another consumer of the project environment. Their execution depends on inherited shells, installed toolchains, available binaries, and sandbox configuration. An agent cannot compensate for an undeclared toolchain difference merely because its prompt is unchanged. When agents run against different local environments, those differences become another source of inconsistent results. Declarative activation removes that source of differences in the same way it does for human developers.

Running an agent within the declared environment gives it the same project binaries as the developer using that flake. Tool versions become controlled inputs. This makes the agent’s execution environment reproducible across machines and team members. It does not make every model response deterministic. The guarantee concerns the tools available to execute, test, and validate the agent’s work.

The numtide/llm-agents.nix repository packages more than fifty AI coding agents as Nix flakes with daily automated updates. Its packages include Codex CLI, Claude Code, and Gemini CLI, allowing evaluation in isolated environments without global state pollution. Package resolution also needs accurate inputs. The mcp-nixos server reduces hallucinated Nix package names by querying live data sources. Because a wrong attribute path can break a build, accurate resolution is a correctness requirement within the same reproducibility model.

How platform engineers can make the activation command the golden path for every team

For platform engineers, the lesson is to publish versioned base environments that teams consume and extend. A base can define shared security scanners, linters, cloud CLIs, and kubectl versions, while project teams add their own dependencies. Teams share the base without making copies of it. Updates can then reach consumers through their next pull of the shared environment.

Scoped package groups make that composition manageable. Groups for shell utilities, Kubernetes tools, a Python stack, or Azure CLI can be additive, with dependencies resolved within each scope. Where a hosted control plane offers named environment versions, teams can fetch a complete stack and see what changed between generations. Atomic rollback makes a previous generation an explicit recovery target rather than a set of instructions to reverse manually.

The shared definition also extends the platform team’s visibility. Deterministic SBOMs derived from it give security teams a fleet-wide basis for tracing vulnerable dependencies and coordinating patches without separate manual inventories for every project. NixOS publishes AMIs to all AWS regions for x86_64 and arm64 with weekly updates, extending the declarative approach to cloud instances. Existing container workflows can use the same defined inputs for deployment alongside the shell workflow.

The organizational result reaches beyond workstation convenience. New developers, contributors, CI runners, and cloud instances can reach a declared working state through a common retrieval model. The manifest establishes what a correct environment means, and activation puts that definition into use.

Sources

  1. Download

More in Environment Parity