You want winget to keep most apps updated automatically — but not all of them. Maybe you need to stay on Node 20 LTS, or your team standardised on a specific Python version, or one app's latest release broke your workflow. That's what winget pin is for.
TL;DR
# Pin Node LTS at major version 20.x
winget pin add --id OpenJS.NodeJS.LTS --version "20.*"
# Pin Python 3.13 exactly
winget pin add --id Python.Python.3.13 --version "3.13.5"
# Now upgrade everything else
winget upgrade --all
Pinned apps get skipped silently. Other apps update normally.
What pin actually does
winget pin add adds an entry to the pin database that tells winget upgrade (without an explicit --id) to ignore that package. Three pin modes:
- Pinning (default) — soft pin; bulk upgrades skip, manual upgrades still work
- Version pin — locks to a specific version or range
- Gating — hard pin; ANY upgrade attempt fails until you unpin
Step 1 — Pin a single app
The simplest case: stop winget upgrade --all from touching a specific app.
winget pin add --id Microsoft.PowerToys
Now winget upgrade will skip PowerToys until you unpin. Confirm:
winget pin list
Output:
Name Id Version Pin Type
------------------------------------------------------------------
PowerToys Microsoft.PowerToys 0.92.1 Pinning
Step 2 — Pin to a specific version
Use --version to fix at a single version:
winget pin add --id OpenJS.NodeJS.LTS --version "20.18.0"
Now winget upgrade will only upgrade to 20.18.0 — never above.
Step 3 — Pin to a version range (wildcards)
The most useful flavour. Pin to "stay on the 20.x LTS line but get patches":
winget pin add --id OpenJS.NodeJS.LTS --version "20.*"
Now winget will upgrade 20.17.0 → 20.18.0 → 20.19.0 but refuse to jump to 22.x.
Other examples:
# Stay on Python 3.13.x
winget pin add --id Python.Python.3.13 --version "3.13.*"
# Pin to anything below 2.0 (legacy app)
winget pin add --id Some.Legacy.App --version "1.*"
# Pin to a major + minor range
winget pin add --id App.Tool --version "1.5.*"
Step 4 — Gating (hard pin)
If you want to completely block ANY upgrade, even manual ones, use --gated:
winget pin add --id Critical.Server.Tool --gated
Now even winget upgrade --id Critical.Server.Tool will fail with "Package has been pinned". To upgrade, you'd need to remove the pin first. Useful for production servers where uncontrolled upgrades are a release-management problem.
Step 5 — List and remove pins
List all pins:
winget pin list
Remove a specific pin:
winget pin remove --id OpenJS.NodeJS.LTS
Remove all pins (nuclear option):
winget pin reset --force
Real-world recipes
Recipe 1 — LTS Node + Python
Many dev teams standardise runtime versions. Pin them once:
winget pin add --id OpenJS.NodeJS.LTS --version "20.*"
winget pin add --id Python.Python.3.13 --version "3.13.*"
Then your team's daily winget upgrade --all won't accidentally bump to Node 22 mid-sprint.
Recipe 2 — Freeze IDE for a project
You're shipping a build with VS Code 1.118 for compatibility reasons:
winget pin add --id Microsoft.VisualStudioCode --version "1.118.*" --gated
Gating means even an explicit upgrade attempt fails — forces a conscious decision to unpin.
Recipe 3 — Pin Microsoft system apps
If you don't want winget upgrading critical Microsoft-bundled apps:
winget pin add --id Microsoft.PowerShell
winget pin add --id Microsoft.WindowsTerminal
These will stay on whatever Microsoft Store / Windows Update gives you, and winget won't override.
Recipe 4 — Bulk pin from a file
Save IDs to pins.txt:
OpenJS.NodeJS.LTS
Python.Python.3.13
Microsoft.PowerShell
Apply:
Get-Content pins.txt | ForEach-Object {
winget pin add --id $_
}
What pinning doesn't do
- Doesn't lock the running installer version — only what
winget upgradewill install going forward. If someone manually downloads a new installer from the publisher's site and runs it, winget can't stop them. - Doesn't roll back — pinning to 1.0.0 when you've already installed 2.5.0 just means winget won't upgrade further. To revert, uninstall and reinstall the older version.
- Doesn't sync across machines — pins are local. If you want a team-wide pin policy, distribute the settings.json or use a DSC configuration.
Combining with import/export
Pins are saved in your winget settings, NOT in the export JSON. So winget export followed by winget import on a new machine doesn't transfer pins.
Workaround: copy your settings file:
%LOCALAPPDATA%\Microsoft\WinGet\Settings\settings.json
Or script the pins as part of your machine setup:
# setup-pins.ps1
winget pin add --id OpenJS.NodeJS.LTS --version "20.*"
winget pin add --id Python.Python.3.13 --version "3.13.*"
Run this after winget import on every new machine.
Common errors and fixes
"Package already pinned"
You're trying to pin something already pinned. Remove the existing pin first:
winget pin remove --id <ID>
winget pin add --id <ID> --version "<new spec>"
"Package upgrade was pinned"
This is the error you'll see during winget upgrade --all for a gated pin — and it's exactly the behaviour you wanted. To upgrade anyway:
winget pin remove --id <ID>
winget upgrade --id <ID>
"Pin not found"
The pin ID doesn't exist. List pins with winget pin list to see exact IDs.
Why use pins at all?
Most users don't bother — they let winget upgrade --all do its thing and accept new versions. But these scenarios make pins worth it:
- LTS dependencies — Node 20.x, Python 3.13.x, Java 21 LTS
- Production servers — upgrade only on a release cadence, not on a developer whim
- Compatibility frozen — your tooling broke at version N+1 and you're not ready to debug
- Team standardisation — everyone on the same version for reproducible builds
- Recovery — you got burned by an upgrade and want a buffer to evaluate before adopting
If none of those apply, skip pins and run plain winget upgrade --all weekly. If any do, pin the relevant apps and sleep better.
What's next?
- Winget commands cheatsheet → — full reference
- How to update all Windows apps → — the upgrade workflow
- How to use winget import → — pair pins with reproducible setups
- 30 essential winget commands → — extended reference
