Background
Mixing npm and pnpm in daily development easily causes chaos:
- The same global package gets installed by both managers, command shims overwrite each other, and uninstalling one accidentally deletes the other’s entry;
- You accidentally run
npmwhen installing a new package, resulting in a dependency layout inconsistent with whatpnpmmanages.
So the idea is: standardize on pnpm and disable the npm command. But pnpm can’t uninstall or update packages that npm installed globally — if you completely disable npm, those packages become unmanageable.
The solution has two layers:
- ps1 script (interactive terminal): the
npmcommand is intercepted (prompting to use pnpm), whilenpmais defined as a forwarder to the real npm, specifically for managing npm’s own global packages. - AGENTS.md instructions (agent environment): opencode’s shell starts with
-NoProfileand doesn’t load the profile, so the same rules need to be documented in AGENTS.md for the agent.
Part 1: The ps1 Script
Add the following to your PowerShell profile ($PROFILE, i.e. Microsoft.PowerShell_profile.ps1):
# Unified UTF-8 encoding to avoid garbled Chinese characters (console codepage may be GBK 936)[Console]::OutputEncoding = [System.Text.Encoding]::UTF8[Console]::InputEncoding = [System.Text.Encoding]::UTF8$OutputEncoding = [System.Text.Encoding]::UTF8
# Disable npm, standardize on pnpm (fall back to real npm when pnpm is unavailable to avoid blocking package management)function npm { if (Get-Command pnpm -ErrorAction SilentlyContinue) { Write-Error "npm has been disabled. Please use pnpm (use npma to forward to real npm for managing npm's own global packages)." throw "npm disabled: use pnpm instead" } $realNpm = @(Get-Command npm -All -ErrorAction SilentlyContinue | Where-Object { $_.CommandType -ne 'Function' -and $_.CommandType -ne 'Alias' } | Select-Object -First 1) if ($realNpm) { Write-Warning "pnpm not detected, calling real npm: $($realNpm.Source)" & $realNpm.Source @args return } Write-Error "npm has been disabled, and neither pnpm nor real npm was found" throw "npm disabled and no fallback npm found"}function npma { # npma = real npm forwarder: the npm name is intercepted by the function above, # use npma when managing global packages installed by npm (visible via npm ls -g, not managed by pnpm). $realNpm = @(Get-Command npm -All -ErrorAction SilentlyContinue | Where-Object { $_.CommandType -ne 'Function' -and $_.CommandType -ne 'Alias' } | Select-Object -First 1) if ($realNpm) { & $realNpm.Source @args return } Write-Error "npma: real npm not found" throw "npma: real npm not found"}How It Works
npmfunction: blocks execution as long aspnpmis detected; only falls back to the real npm if pnpm is absent (to avoid completely locking out package management).npmafunction: since thenpmname is occupied by the function above,npmaserves as the alias for the “real npm”. It usesGet-Command npm -Allto find the actual npm on disk (filtering out Functions and Aliases), then forwards arguments as-is.- Key detail:
Get-Command npm -Allwill include thenpmfunction defined above — you must filter withWhere-Object { $_.CommandType -ne 'Function' -and $_.CommandType -ne 'Alias' }to avoid an infinite loop.
Usage
npm install -g foopnpm add -g foonpma uninstall -g hexoGotcha: Command Name ≠ Package Name
If uninstalling reports “package not found”, you’re probably using the command name instead of the package name:
| Command Name | Actual Package Name (as shown in npm ls -g) |
|---|---|
mimo | @mimo-ai/cli |
gemini | @google/gemini-cli |
hexo | hexo-cli |
vercel | vercel (also provides a vc command) |
Always verify the package name via npm ls -g / pnpm ls -g, not the command name shown by Get-Command.
Part 2: AGENTS.md Instructions
opencode (or other agents) launches commands with pwsh -NoProfile -NonInteractive, which does not load your PowerShell profile — so the npm intercept function and npma from above don’t exist in the agent’s shell: npm in the agent is the real npm, and npma doesn’t exist.
Therefore, the same rules need to be documented in the global AGENTS.md (~/.config/opencode/AGENTS.md):
## Package Manager Conventions
The user's PowerShell profile has disabled the `npm` command (intercepted when pnpm is detected) and defined `npma` = real npm forwarder (for managing npm's own global packages). **The agent's shell starts with `-NoProfile` and does not load that profile** — so in the agent environment, `npm` is the real npm and `npma` does not exist. Based on this:
- Package management operations **default to `pnpm`** (install / update / uninstall global and project dependencies).- When managing **npm's own global packages** (listed by `npm ls -g`, not managed by pnpm), use the real npm directly: `npm uninstall/update/ls -g ...` (in the interactive terminal the corresponding command is `npma`).- **A single global package must only be managed by one package manager**: never use pnpm to uninstall/update npm-installed packages, and never use npm to uninstall/update pnpm-installed packages (mixing managers on the same package causes shim overwrites / accidental deletions).- Global directories are unified: npm prefix and pnpm global are both `Z:\Code\packages\npm` (pnpm store at `Z:\.pnpm-store\v11`, pnpm/yarn versions managed by corepack).Why the Agent Needs Separate Rules
| Environment | What npm Is | Does npma Exist? |
|---|---|---|
| Interactive terminal (loads profile) | Intercepted by function, errors out | Yes, forwards to real npm |
Agent shell (-NoProfile) | Real npm, no interception | No |
Conclusion: running npm in the agent runs the real npm — which is exactly the behavior needed when managing npm’s own global packages. So AGENTS.md simply states “default to pnpm for package management, use npm directly when managing npm’s own packages”, matching the semantics of npma in the interactive terminal.
Appendix: Unified Global Directories
This setup also unifies global directories, preventing packages from being scattered across the C: drive and tool drives:
- npm prefix:
Z:\Code\packages\npm - pnpm global (
global-dir/global-bin-dir): alsoZ:\Code\packages\npm - pnpm store:
Z:\.pnpm-store\v11 - pnpm / yarn versions managed by corepack
pnpm’s global config lives in its own config.yaml (%LOCALAPPDATA%\pnpm\config\config.yaml), not in ~/.npmrc, to avoid npm emitting warnings.