LPM-cli

Save policy

How LPM writes version ranges to package.json — caret default, explicit preservation, prerelease safety.

When lpm install <pkg> mutates package.json, the save policy decides what string lands in dependencies (or devDependencies). The decision is a pure function of: what you typed, the resolved version, your CLI flags, and your config.

This page is the reference for the rules. For the CLI flag surface, see lpm install. For the file format, see lpm.toml and ~/.lpm/config.toml.

The decision table

You ranpackage.json ends up withWhy
lpm install zod"zod": "^4.3.6"Bare install — caret default
lpm install zod@4.3.6"zod": "4.3.6"Explicit exact — preserved verbatim
lpm install zod@^4.3.0"zod": "^4.3.0"Explicit range — preserved verbatim
lpm install zod@~4.3.6"zod": "~4.3.6"Explicit tilde — preserved verbatim
lpm install zod@latest"zod": "^4.3.6"Dist-tag → caret default
lpm install zod@beta"zod": "4.4.0-beta.2"Prerelease — saved exact for safety
lpm install zod@*"zod": "*"Explicit wildcard — preserved (only allowed when explicit)
lpm install zod@workspace:*"zod": "workspace:*"Workspace protocol — preserved verbatim
lpm install --catalog zod"zod": "catalog:"Explicit default-catalog save when the root default catalog entry matches the resolved version
lpm install --catalog=ui zod"zod": "catalog:ui"Explicit named-catalog save when catalogs.ui.zod matches the resolved version

The throughline: what you typed wins. The default kicks in only when you didn't say anything.

Precedence

Seven layers, highest first:

  1. Explicit catalog save. --catalog or --catalog=<name> writes catalog: or catalog:<name> when the selected root catalog entry exists and satisfies the resolved version.
  2. Explicit user spec in pkg@spec. Exact, range, wildcard, workspace — preserved verbatim, never reinterpreted.
  3. CLI flag override. --exact, --tilde, --save-prefix '<p>'. Mutually exclusive (clap enforces).
  4. Prerelease-exact safety. If the resolved version is a prerelease and no CLI flag forced something else, save the exact resolved version. Prereleases shouldn't auto-widen under a forgotten save-prefix config setting.
  5. Persistent config. save-exact = true, then save-prefix = "^|~|". Reads from ./lpm.toml first, then ~/.lpm/config.toml.
  6. Catalog mode. package.json > lpm.catalogMode = "prefer" or "strict" can save matching default-catalog entries as catalog:; strict mode fails on missing or mismatched default entries.
  7. Default. ^resolvedVersion.

Each tier feeds the next only when the higher one is silent. Direct save-prefix controls do not override what you typed; --catalog is the deliberate exception because it explicitly asks LPM to save a catalog reference after verifying the selected catalog matches the resolved version.

CLI flags

lpm install zod --exact                 # save the exact resolved version, no prefix
lpm install zod --tilde                 # save with ~ prefix
lpm install zod --save-prefix ''        # same as --exact (empty prefix)
lpm install zod --save-prefix '~'       # same as --tilde
lpm install zod --save-prefix '^'       # explicit caret (the default)
lpm install --catalog zod               # save as catalog:
lpm install --catalog=ui zod            # save as catalog:ui

--exact, --tilde, --save-prefix, and --catalog[=<name>] are mutually exclusive save-policy flags. * is not a valid --save-prefix value — wildcards must be requested per-package via pkg@*.

Setting any of these flags forces a rewrite (see Bare reinstall below).

--catalog is a save policy, not a catalog editor. The target entry must already exist in the root catalog set (package.json > catalogs or pnpm-workspace.yaml > catalog / catalogs), and the resolved version must satisfy that catalog range. On mismatch or missing entry, LPM rolls package.json back and exits with an error.

Persistent config

Pin a default for one project:

lpm.toml
save-prefix = "~"
save-exact  = false

Or for every project on this machine:

~/.lpm/config.toml
save-prefix = "^"
save-exact  = false
KeyTypeDefaultNotes
save-prefix"^" | "~" | """^"Prefix used when nothing else applies
save-exactboolfalseForce exact saves regardless of save-prefix

Manage ~/.lpm/config.toml via the CLI:

lpm config set save-prefix '~'
lpm config set save-exact true

Invalid values (save-prefix = "*", save-prefix = ">=", etc.) are rejected at load time with a clear error pointing at the file.

Prerelease safety

lpm install zod@next          # resolves to e.g. 4.4.0-beta.2
# package.json now has: "zod": "4.4.0-beta.2"

Prereleases are saved exact even when your config says save-prefix = "^". The reasoning: a ^4.4.0-beta.2 range matches 4.4.0-beta.3 (and every other 4.x prerelease + stable), which is almost never what you want. The next prerelease could fix nothing or break everything; you should opt into the bump explicitly.

Override per-invocation if you want the caret behavior anyway:

lpm install zod@next --save-prefix '^'   # → "^4.4.0-beta.2"

Bare reinstall doesn't rewrite

lpm install zod         # already in package.json as "~4.3.6"
# → still "~4.3.6", refresh-only

Re-installing an existing dep without a version or override flag refreshes the lockfile + store entry but doesn't rewrite the existing range. Your team's intentional ~4.3.6 pin survives a routine reinstall.

To force a rewrite, set any flag (--exact, --tilde, --save-prefix) — even one that matches the existing prefix.

Why no * default

* matches every version, including future major bumps that break your code. There is no save policy under which the default is "*". The only way to get "*" is to type it: lpm install zod@*. Wildcards are a deliberate per-package opt-in.

This is enforced at every layer:

  • --save-prefix '*' is rejected at the CLI parser
  • save-prefix = "*" is rejected at the TOML config loader
  • The bare-install default never produces "*", regardless of resolved version

Workspace protocol

lpm install @my-co/shared@workspace:*
# package.json: "@my-co/shared": "workspace:*"

workspace:*, workspace:^, workspace:~, and workspace:<range> are always preserved verbatim. Save policy doesn't touch them — workspace protocol semantics are out of scope.

Why this is in lpm.toml, not package.json

The save-policy config keys (save-prefix, save-exact) deliberately don't live under package.json > lpm. Reasons:

  • Save policy is tool behavior, not publishable metadata. It doesn't ship to consumers of your package.
  • It changes more often. Tweaking a personal preference shouldn't dirty the manifest.
  • Teams disagree about whether to commit it. lpm.toml lets each repo decide — commit it for project-shared policy, .gitignore it for individual preference.

See also