Add A Global pnpm Package
Global pnpm packages are managed via a plain list. The sync command installs missing packages, removes unmanaged packages, then runs pnpm update -g --latest for every unpinned package so installed packages are refreshed instead of staying within the semver range recorded at install time.
Preconditions
- Node.js and pnpm are installed (both through mise in this setup;
~/.config/mise/config.tomlpins the latest pnpm release for global use). ~/.local/share/pnpm/binis onPATH(the shell configs add it; the installer adds it for itself).- You verified the package name.
Steps
Example packages managed this way include @earendil-works/pi-coding-agent.
-
Add the package name to:
This file is installed as
~/.default-pnpm-pkgs.name@x.y.zpins an exact version; bare names track latest. -
Apply dotfiles (which triggers the hook):
chezmoi apply
Or run the installer command directly:
Verification
pnpm ls -g --depth 0 | rg '<package-name>'
What It Does
The installer reads ~/.default-pnpm-pkgs, resolves pnpm root -g, and merges every exact desired package name into <pnpm root -g>/pnpm-workspace.yaml with allowBuilds[<name>] set to true before reading inventory or changing packages. Pins reduce to the bare name. Unrelated approval, denial, and placeholder entries remain unchanged, and this only approves listed top-level packages; it does not approve third-party transitive packages. This durable workspace setting means direct pnpm -g update commands inherit the same build decisions after a sync.
It then compares the desired list with pnpm ls -g --json, installs missing packages (pnpm add -g), re-pins packages whose installed version differs from an exact pin, uninstalls global packages not on the list (pnpm remove -g), then runs pnpm update -g --latest for each unpinned package.
pnpm 11+ installs every global package into its own hashed project directory under ~/.local/share/pnpm/global/v11/, and that directory moves on every add or update. Consumers that need a stable module path (Pi's packages setting loads pi-mcp-adapter and pi-subagents by path) read ~/.local/share/pnpm-global-links/node_modules/<package> instead; the installer rebuilds that symlink tree after every sync.
Package operations pass --yes and disconnect stdin so pnpm does not prompt during a chezmoi run, including when launched from a terminal. Unapproved dependency build scripts remain ignored.
For installs and updates of listed packages, the installer passes PNPM_CONFIG_BLOCK_EXOTIC_SUBDEPS=false to that pnpm subprocess. This permits URL/git dependencies throughout those packages' dependency trees, including the pkg.pr.new dependencies used by pi-mcp-adapter. The override is not written to pnpm configuration and does not affect removal, inventory, build approvals, release-age rules, or ordinary pnpm commands outside the installer.
If a package operation fails, the installer prints pnpm's error and stops with a nonzero exit status. It refreshes links from the installed state even after a partial sync. If that state cannot be read or parsed, it preserves the existing link tree and reports the failure.
Release-Age Quarantine
pnpm 11+ ships a built-in minimumReleaseAge of 1440 minutes: pnpm update -g --latest and pnpm outdated -g silently skip any version published less than a day ago, so ,update can report success while omp update (which reads the latest dist-tag directly) already sees a newer release.
home/dot_config/pnpm/readonly_config.yaml.tmpl renders ~/.config/pnpm/config.yaml with minimumReleaseAgeExclude covering every package in ~/.default-pnpm-pkgs (pins reduce to the bare name), so listed packages update as soon as they publish. Scoped packages are listed as @scope/*, not by exact name: pnpm's non-strict default auto-approves same-day transitive versions into the global project's own pnpm-workspace.yaml, and that project list replaces the global one, which would re-quarantine the top-level package on the next sync. Same-scope companions (for example the 17 @oh-my-pi/* packages behind omp) are the case that triggers this on every release, so the scope glob keeps the global list in effect. Unscoped packages keep exact names, so a same-day third-party transitive release can still trigger that write. The 05 hook hashes the config template, so changing exclusions re-runs the sync.
Check the effective list with:
pnpm config get --global minimum-release-age-exclude
Rollback / Undo
- Remove the package from
home/readonly_dot_default-pnpm-pkgs. - Re-apply:
chezmoi apply
(The package will be automatically uninstalled because it is no longer in the desired list).