Clean package management on Windows: WinGet, Chocolatey, Scoop and Ansible, each in its place
After years automating Linux environments with apt, dnf and Ansible, the Windows build machines stayed the poor cousin: a %TEMP% of installers run by hand, versions that were never the same two days in a row and an “it worked on Jean-Michel’s machine” at every incident. I discovered and adopted Scoop
for my Windows environments and it reshapes the problem: the build workstation starts to look like a declarative environment I rebuild and version, instead of a state I babysit by hand.
Shims: Scoop’s magic trick
What pleases me in Scoop is its very Unix/Linux approach: everything installs into a user environment, no admin, no registry, a readable tree under ~\scoop\. Manifests are simple JSON files, versions get pinned, installation is scriptable and uninstall leaves no debris. The mechanism holding it all: the shims. A ~\scoop\shims directory on the PATH, one tiny executable per tool delegating to the real install. The PATH stays short, name collisions get settled locally and the tool vanishes cleanly when removed. Little magic, nearly everything scriptable: exactly what you want from a package manager on machines Ansible will drive.
Three managers, three responsibilities
The split between managers is not a matter of preference, it is separation of responsibilities. Each tool excels at one layer and is badly out of place in the others:
- WinGet: user workstation responsibility, Windows applications and the Microsoft catalogue;
- Chocolatey: system responsibility, the MSIs, the services, the components demanding privileges and deep Windows integration;
- Scoop: developer toolchain responsibility, the CLIs, runtimes, SDKs, compilers and build tools I want installed and reproduced fast in my user environment (Git, Python, Node, Go, JDK, CMake, Ninja, 7zip…).
The classic “install everything with Chocolatey” works, but it turns a tool into a Swiss Army knife: a manager that installs system services handles your toolchain as poorly as a user-scope manager handles your services. Refusing to cram every piece of software into the same bag is what keeps the whole thing clean.
And Ansible on top
The point the “WinGet + Chocolatey + Scoop as siblings” diagrams omit: in my infra, I am not the one typing those commands, Ansible manages the whole Windows state. The managers are mechanisms behind the Ansible module driving them, not competing hierarchies:
Ansible: the desired state of the Windows machine
|
+-- WinGet --> desktop apps, Microsoft catalogue
+-- Chocolatey --> system components, services, privileges
+-- Scoop --> dev/CLI toolchain, user environment
+-- win_package --> the MSI/EXE that impose their own installer
`-- NuGet --> PowerShellGet via win_psrepository, if needed
Concretely: community.windows.win_scoop for the Scoop toolchain, chocolatey.chocolatey.win_chocolatey for system components (the module that already saved a supply chain during the p4v incident
) and ansible.windows.win_package for the MSIs and EXEs no manager carries: some installers insist on their own, there is no way around it, Ansible drives the native installer directly, with the silent arguments and idempotence criteria that entails. WinGet has no dedicated official module: when an application only exists in the Microsoft catalogue, Ansible drives it through win_command/win_shell. System configuration goes through the usual Windows modules (registry, services, users). Ansible stays the layer declaring and maintaining the expected state, whatever the install mechanism:
- name: Dev toolchain through Scoop
community.windows.win_scoop:
name: "{{ item }}"
loop: [git, python, go, cmake, ninja]
- name: Vendor MSI without a package manager
ansible.windows.win_package:
path: C:\install\tool-2.1.msi
state: present
mise: the per-project layer
One scale remains that neither Ansible nor Scoop covers: per project. mise
installs through Ansible (scoop install mise or winget install jdx.mise), is natively Windows-compatible and takes over at the repo scale: a mise.toml versioned with the code declares the versions (Python 3.13, Node 24, Go, Terraform), the environment variables and the tasks (build, test, lint). Ansible prepares the machine, Scoop ships the tools, mise sets the project context. One honest caveat: mise’s Windows compatibility does not guarantee that of every tool it manages, backends and versions depend on each tool’s Windows support.
We separate even the machine environment from the project environment: much cleaner than baking all the tool versions directly into the Ansible playbook. The playbook does not need to know repo X wants Node 24; repo X’s mise.toml knows.
Environment as Code
At the end of the chain, the workstation looks like what I know on Linux: a state declared in versioned files, specialized mechanisms behind an orchestration layer and a rebuild that fits in one ansible-playbook plus one mise install. The infrastructure-as-code philosophy was never an OS affair; it only demands that every layer agree to do one thing.
The word at the end
- I am not replacing Chocolatey or WinGet: I separate them by responsibility. Each tool does what it is best at.
- Specialized package managers, native installers when needed, Ansible to orchestrate the whole.
- Shims make the user toolchain scriptable and cleanly uninstallable.
- The machine and the project are two distinct environments. Ansible holds the first, mise the second.
- Environment as Code is not an OS affair. Windows included, as long as you refuse the Swiss Army knife.
[edit: 2026] The Ansible implementation of all this: the chocolatey and scoop roles.