LPM CLI

lpm install

Install dependencies from package.json, or add new ones.

lpm install [packages...]   # alias: lpm i

With no arguments, installs every dependency declared in package.json against the lockfile. With one or more package names, resolves and adds them to package.json and installs them.

Before a new direct dependency enters the project, LPM CLI checks the package name for common typosquatting patterns against popular packages. A likely typo such as axois fails in --json, CI, non-TTY, and --yes runs before package.json is changed; in an interactive terminal LPM CLI defaults to cancel and only switches to the suggested package or writes a committed allow-list entry to lpm.toml when you choose that action. Existing lockfile replays are not blocked by this check.

Project discovery

lpm install (and lpm i) discovers the project root the same way npm, pnpm, yarn, and bun do: it walks up from the current directory looking for the nearest ancestor package.json and treats that directory as the project root. All install side-effects — manifest edits, lpm.lock, lpm.lockb when representable, node_modules/ — land at the discovered root, not in the subdirectory you happened to run the command from.

If lpm i <pkg> runs in a directory with no package.json anywhere up the tree, LPM CLI auto-creates a minimal {"dependencies": {}} manifest in the current directory before installing — matching npm i <pkg>'s fresh-dir behavior. Bare lpm install (no package args) still errors when nothing can be found, since it has nothing to install against.

Examples

lpm install                   # install everything in package.json
lpm install zod               # add latest, save as ^x.y.z
lpm install zod@4.3.6         # add exact, save as 4.3.6
lpm install zod@^4.3.0        # add with explicit range
lpm install -D vitest         # save under devDependencies
lpm install --prod            # install production deps only
lpm install --omit=optional   # skip optionalDependencies
lpm install --catalog react   # save as "catalog:" when the default catalog matches
lpm install --catalog=ui react # save as "catalog:ui" when the named catalog matches
lpm install -g typescript     # install into ~/.lpm/global/
lpm install --offline         # never touch the network
lpm install --frozen-lockfile # fail if package.json and lpm.lock differ
lpm fetch                     # warm the store from lpm.lock, no node_modules
lpm tidy                      # find unused deps and undeclared imports
lpm install --force           # full re-install, bypass every fast path
lpm install --strict-peer-dependencies # fail on peer warnings/conflicts
lpm install --no-skills        # skip @lpm.dev package skills for this run
lpm install --verbose         # append per-phase timings + lockfile size
lpm install --json --timing   # include JSON timing diagnostics

The global --verbose flag (no short form — -v is --version, matching npm / pnpm / yarn) appends a per-phase timing breakdown and the lockfile size beneath the ✓ Done line. Useful for profiling install runs without dropping to JSON.

✓ Done · installed 247 packages in 1.31s
  resolve: 383ms  fetch: 165ms  link: 2ms
  lpm.lock (247 packages) + lpm.lockb (4.2 KB)
  120 linked, 127 symlinked

Without --verbose, only the ✓ Done line is shown — keeps the slim output focused on what changed (+ pkg@version entries) and the success terminus.

JSON timing diagnostics

lpm install --json keeps the default success envelope lean and omits the timing object. Opt in when a tool or benchmark needs install timing diagnostics:

lpm install --json --timing
LPM_TIMING=1 lpm install --json
LPM_TIMING_DETAIL=1 lpm install --json
LPM_TIMING_DETAIL=trace lpm install --json

--timing and LPM_TIMING=1 include the coarse timing.waterfall object for setup, resolve, fetch, link, and tail attribution without the heavier detail tree.

LPM_TIMING_DETAIL=1 adds timing.detail with metadata request attribution by purpose, resolver wall/metadata/scheduler/CPU/policy counters, fetch-stage plan/cache/download attribution, cache-classification branch timing, v2 reusable-object validation counters, registry-signature/provenance timing, v2 link-task timing, link finalize substages, and tail writes such as lockfile and build-state persistence. LPM_TIMING_DETAIL=trace also adds timing.detail.trace.slow_packages buckets for the slowest tarball HTTP, extract, security, finalize, v2 link task, and provenance verification work, plus per-purpose duplicate metadata request rankings.

LPM_TIMING_DETAIL=1 and LPM_TIMING_DETAIL=trace imply timing, so they work without also passing --timing. Use detail mode for local performance investigations and cold/warm install comparisons, not as a stable production API.

How it works

  1. Resolve. LPM CLI walks package.json, applies the lockfile, and fetches whatever metadata is missing from the appropriate registry (see Registries for routing).
  2. Download. Tarballs go into the global content-addressable store at ~/.lpm/store/. Anything already present is reused — installs across projects share on-disk copies via clonefile (macOS) or hardlinks (Linux). Automatically followed redirects are checked at every hop and cannot return to HTTP after the chain has used HTTPS; see Redirect transport policy.
  3. Link. node_modules/<pkg> is created as a symlink into the global store at ~/.lpm/store/v2/links/<graph-key>/. Single packages start hoisted in LPM CLI's v2 virtual-store layout: root direct dependencies are surfaced at the project root, while package-local dependency links live inside shared link entries. Workspaces auto-default to isolated (pnpm-style strict-deps). If resolution detects incompatible peer requirements and no explicit linker was set, LPM CLI switches that install to isolated and records the decision in lpm.lock so warm installs keep the same layout. Override per-invocation with --linker=isolated or --linker=hoisted.
  4. Run root lifecycle, gate dependency scripts. Bare installs run the root project's install lifecycle (pnpm:devPreinstall before dependency work, then preinstall / install / postinstall / preprepare / prepare / postprepare after a successful install). Dependency package scripts stay blocked by default; when policy permits them to run (--policy=allow / --yolo, or a triage-green tier), only dependency preinstall, install, and postinstall fire. Other recognized dependency phase names (prepare, prepublishOnly, preuninstall / uninstall / postuninstall) are surfaced in detection but never executed by install. See lifecycle scripts below.

Source identity is preserved during linking. A registry package, tarball, file: package, and link: package may share the same name@version; each dependency edge points at the source declared by that edge instead of guessing from name and version alone. Local file: and link: source snapshots refresh when you run lpm install again.

Package-published LPM.dev skills

Installed @lpm.dev/* packages can publish package-specific agent guidance. LPM CLI reconciles that content into .lpm/skills/<package>/ by default. This automatic step is specific to LPM.dev Registry packages; npm and custom-registry packages do not use it.

lpm install @lpm.dev/owner.package --no-skills # skip once
lpm config lpm-skills --set false              # persist the opt-out
lpm install @lpm.dev/owner.package --skills    # override config once

The persistent setting does not delete existing package skill files. Use lpm skills clean to remove them. Explicit lpm skills add @lpm.dev/owner.package commands remain available regardless of the setting. See AI agent skills.

For registry packages, any tarball URL recorded in lpm.lock is only a cache hint. Before fetching it, LPM CLI checks the registry metadata for that exact name@version and refuses the install if the hint does not match dist.tarball. Explicit tarball, file:, link:, and git sources keep their own source identity and are not rebound to registry metadata.

JSR dependencies

package.json can declare JSR packages with the jsr: protocol:

package.json
{
  "dependencies": {
    "@std/path": "jsr:@std/path@^1.1.0",
    "@std/fs": "jsr:^1.0.0"
  }
}

LPM CLI installs those dependencies under the JSR import name (node_modules/@std/path) and fetches the folded npm package (@jsr/std__path) from https://npm.jsr.io. Version-only specs such as "jsr:^1.0.0" borrow the package name from the dependency key. Malformed JSR package names are rejected before any registry request.

Use .npmrc to point JSR fetches at a mirror:

.npmrc
@jsr:registry=https://npm-jsr-mirror.example.com

See Registries for the routing details.

Peer dependency strict mode

By default, LPM CLI follows pnpm's non-strict install posture: missing required peers, peer version mismatches, and cross-consumer peer conflicts print warnings, then install continues.

When those warnings include a cross-consumer peer conflict, the default linker auto-switches to isolated for that project. Explicit linker choices (--linker, ~/.lpm/config.toml > linker, LPM_LINKER, or package.json > lpm.linker) are respected and do not auto-switch.

Use strict mode when peer diagnostics should gate CI:

lpm install --strict-peer-dependencies

Strict mode fails after resolution if a required peer is missing, an installed peer does not satisfy the declared range, or the resolver found incompatible peer requirements across consumers. Optional peers that are absent still do not fail.

Precedence: --strict-peer-dependencies / --no-strict-peer-dependencies > package.json > lpm.strictPeerDependencies > ~/.lpm/config.toml > strict-peer-dependencies > default (false). On -g, the package.json tier is not present, so CLI flags override ~/.lpm/config.toml.

Under --json, peer diagnostics are always attached to the success envelope as peer_issues. Missing required peers land in missing, installed peers that do not satisfy the declared range land in bad, and cross-consumer resolver conflicts land in conflicts. Each list has a matching *_count, and total_count is the combined issue count. The legacy peer_conflicts array is still emitted and matches peer_issues.conflicts.

{
  "success": true,
  "peer_issues": {
    "missing": [
      {
        "type": "missing",
        "package": "required-peer-host",
        "version": "1.0.0",
        "peer": "missing-peer",
        "required_range": "^1.0.0",
        "resolved_version": null
      }
    ],
    "bad": [
      {
        "type": "bad",
        "package": "peer-consumer-a",
        "version": "1.0.0",
        "peer": "shared-peer",
        "required_range": "^1.0.0",
        "resolved_version": "2.0.0"
      }
    ],
    "conflicts": [
      {
        "canonical": "shared-peer",
        "chosen_version": "2.0.0",
        "unsatisfied_consumers": [{ "consumer": "peer-consumer-a", "range": "^1.0.0" }]
      }
    ],
    "intersections": [],
    "missing_count": 1,
    "bad_count": 1,
    "conflicts_count": 1,
    "intersections_count": 0,
    "total_count": 3
  }
}

Save policy

When you run lpm install <pkg> without an explicit version, LPM CLI saves ^resolvedVersion to package.json. If you typed an explicit version or range, LPM CLI preserves what you typed. Prereleases are saved exact for safety.

You ranpackage.json ends up with
lpm install zod"zod": "^4.3.6" (caret default)
lpm install zod@4.3.6"zod": "4.3.6" (preserved)
lpm install zod@^4.3.0"zod": "^4.3.0" (preserved)
lpm install zod@~4.3.6"zod": "~4.3.6" (preserved)
lpm install zod@latest"zod": "^4.3.6" (caret default)
lpm install zod@beta"zod": "4.4.0-beta.2" (prerelease → exact)
lpm install zod@*"zod": "*" (explicit wildcard)

Override per-invocation with --exact, --tilde, or --save-prefix '<p>'. Set persistent defaults in ./lpm.toml (project) or ~/.lpm/config.toml (global):

~/.lpm/config.toml
save-prefix = "^"   # one of "^", "~", or "" (exact, no prefix)
save-exact = false  # bool; true forces exact regardless of prefix

Re-installing an existing dep without a version or override flag refreshes the lockfile and store but does not rewrite the existing range — "zod": "~4.3.6" stays put.

Typosquat guard

lpm install <pkg> and bare lpm install both guard new direct dependency names before resolution. The detector covers low-noise cases such as adjacent transpositions (axoisaxios), delimiter variants (crossenvcross-env), and flag-shaped package names after -- (--legacy-peer-deps). Transitive dependencies are not blocked by this guard.

In interactive terminals, a single suspicious CLI package can be replaced with the suggested package or committed to the project allow-list. In CI, --json, non-TTY, and --yes runs, LPM CLI exits 1 with error_code: "typosquat_suspected" and leaves the manifest untouched.

Intentional names belong in lpm.toml:

lpm.toml
[[policy.typosquat.allow]]
package = "axois"
similar-to = "axios"
reason = "Intentional internal compatibility package"

The guard only fires when a name is newly entering the project. If lpm.lock already records the direct dependency, CI=true lpm install and lpm install --frozen-lockfile replay the lockfile normally.

Machine-wide guard mode lives in ~/.lpm/config.toml > typosquat-guard and is managed by lpm config typosquat --set default|on|off. default removes the explicit override and keeps the current product default (enabled); on keeps the guard enabled even if the diagnostic env toggle is set; off is a security-approval-gated machine-wide weakening. The diagnostic env toggle is also ignored when managed policy owns the typosquat floor. Use project allow-list entries for legitimate false positives instead of turning the guard off globally.

npm firewall

When ~/.lpm/config.toml > [firewall] mode is set to monitor or enforce, LPM CLI sends selected public npm package versions to LPM Firewall at firewall.lpm.dev in batches. Metadata and tarballs still use the direct npm route; the firewall request only asks for block/warn verdicts. LPM Firewall is an LPM.dev Registry Pro/Org feature, so active modes send LPM.dev Registry auth; run lpm login locally, or use LPM_TOKEN / registry-audience OIDC in CI.

When human output is active and the install includes public npm packages that need verdict checks, the install phase line shows 🔥 LPM Firewall active.

monitor prints would-block and warning verdicts and lets the install continue. If LPM Firewall denies entitlement or is unreachable, monitor mode warns and continues. enforce blocks packages whose effective firewall action is block before their tarballs are allowed through, and fails before package bytes are materialized when entitlement is denied. Configure it with:

lpm config firewall --set monitor
lpm config firewall --set enforce

The legacy string report is still accepted as an alias for monitor. Disabling or downshifting the firewall after it becomes part of the approved machine posture is guarded by lpm security. See the Firewall for npm guide for custom policy groups.

Policy extensions

Install can run local policy extensions from ~/.lpm/config.toml > [policy.extensions.<name>]. Extensions receive the resolved package candidates as JSON on stdin and return verdicts as JSON on stdout. LPM CLI spawns the configured command directly; it does not use sh -c or interpolate package data into a command string. The command's first entry must be an absolute path or a program name found on an absolute PATH directory; relative executable paths are rejected.

Policy extensions run after resolution, platform filtering, and --omit filtering, but before registry tarballs are fetched or packages are linked. Warm lockfile and offline installs run the same check before linking. When an extension is active, install-time registry tarball prefetch waits until policy verdicts pass. Direct remote tarball URL dependencies are rejected while policy extensions are active because V1 cannot identify the package candidate without downloading the tarball first.

~/.lpm/config.toml
[policy.extensions.local-feed]
command = ["/usr/local/bin/lpm-policy-feed", "--deny-list", "/etc/lpm/deny.json"]
mode = "enforce"       # report | enforce
on-error = "block"     # warn | block
timeout-ms = 5000
events = ["package.candidate"]

report mode warns and continues. enforce mode fails the install when the extension returns a block decision. With --json --timing, the success envelope includes timing.policy_extensions and security.policy_extensions counters.

Use lpm policy to inspect and test configured extensions:

lpm policy list
lpm policy status
lpm policy doctor
lpm policy test local-feed --package react@19.0.0

Full request/response schema: ~/.lpm/config.toml policy extensions.

Use --catalog to force a catalog reference for this invocation. --catalog <pkg> writes "catalog:"; --catalog=<name> <pkg> writes "catalog:<name>". The entry must already exist in the selected root catalog and the resolved version must satisfy that catalog range, otherwise the install fails before committing package.json. This flag is mutually exclusive with --exact, --tilde, and --save-prefix.

Catalogs can override the saved spec for packages that exist in the root default catalog. Set package.json > lpm > catalogMode:

ModeBehavior for lpm install <pkg>
"manual" (default)Keep the raw save policy above. Existing catalog: entries still resolve, but new installs do not auto-save catalog:.
"prefer"Save "catalog:" when the resolved version satisfies the root default catalog entry; warn and keep the direct spec on mismatch.
"strict"Save "catalog:" when the resolved version satisfies the root default catalog entry; fail before committing package.json on mismatch or missing default-catalog entry.

Set package.json > lpm.cleanupUnusedCatalogs = true or pnpm-workspace.yaml > cleanupUnusedCatalogs: true to prune unused root catalog entries after successful installs. The default is to preserve catalog entries exactly as written.

Full details in Save policy.

Production and omitted deps

Use --prod (alias: --production) to install the production dependency closure only. It omits devDependencies from node_modules while keeping the lockfile graph reproducible.

lpm install --prod
lpm install --omit=dev          # same dev-dep omission
lpm install --omit=optional     # skip optionalDependencies
lpm install --omit=dev,optional # compose both

--omit accepts dev and optional, comma-separated or repeated. Production filtering keeps peer packages needed by retained production packages, so production installs do not accidentally drop a peer that a kept package can resolve.

Lifecycle scripts

Bare lpm install runs the root project's install lifecycle:

TimingRoot project scripts
Before dependency installpnpm:devPreinstall
After successful dependency installpreinstall, install, postinstall, preprepare, prepare, postprepare

This root lifecycle applies to bare installs only. lpm install <pkg> adds the requested packages and does not auto-run the root prepare lifecycle.

Dependency package lifecycle scripts are separate and deny-by-default. To run dependency scripts, choose a policy:

PolicyBehavior
deny (default)Dependency scripts blocked. lpm install lists what wanted to run; approve them with lpm approve-scripts
allow (--yolo)Run every dependency lifecycle script during install, including fresh package adds and lockfile/offline replays
triage (--triage)Tiered gate: greens auto-run in a sandbox; ambers and reds require manual review

Triage auto-runs only when every unbuilt scripted package classifies green. If any amber or red remains, scripts defer to lpm approve-scripts review unless an explicit auto-build signal is set: --auto-build on the command line, or — on project installs — package.json > lpm > scripts.autoBuild = true. Global installs (-g) only honor --auto-build; the synthesized package.json doesn't project per-project script knobs.

Auto-build runs after dependency resolution/linking. The same build tail is used for fresh resolution, lpm install <pkg>, and lockfile/offline replays when the effective policy permits dependency scripts. If any trusted lifecycle script exits non-zero, lpm install exits non-zero and surfaces the failing package/script instead of treating the install as successful.

Set per-invocation:

lpm install --policy=allow      # equivalent to --yolo
lpm install --yolo              # alias for --policy=allow
lpm install --triage            # alias for --policy=triage

Or pin in package.json:

{ "lpm": { "scriptPolicy": "allow" } }

Or globally in ~/.lpm/config.toml:

script-policy = "deny"

Precedence: CLI flag > package.json > ~/.lpm/config.toml > default (deny). On -g the package.json tier is N/A; the chain collapses to CLI > ~/.lpm/config.toml > default.

Optional LLM advisor (under triage)

If you have a local LLM available — Claude CLI, Codex, or Ollama — the triage gate can ask it to review Amber-tier scripts during install. If the advisor returns Approve for every amber phase a package presents, that package's scripts run via an ephemeral trust path for the current install only — verdicts are never persisted. See security overview for the full contract.

lpm config triage --set claude-cli          # one-time setup

Or in package.json: { "lpm": { "triageAdvisor": "claude-cli" } } · or ~/.lpm/config.toml: triage-advisor = "claude-cli". The advisor is opt-intriage-advisor: "none" is the default, and script-policy: "triage" alone gives you the portable layers 1-4.

Override per-invocation with --advisor:

lpm install --triage --advisor=claude-cli   # one-off uplift
lpm install --triage --advisor=none         # one-off opt-out

Precedence: --advisor flag > package.json > lpm > triageAdvisor > ~/.lpm/config.toml > triage-advisor > default (none). The flag is only consulted when the effective script-policy is triage; under deny / allow the advisor never runs.

See lpm rebuild and lpm approve-scripts for the manual-approval flow.

Sandbox

When a dependency lifecycle script does run (greens under triage, or anything under allow), it executes inside the filesystem sandbox by default — Seatbelt on macOS, landlock on Linux, AppContainer on Windows. Default mode allows the project read, the package's own directory write, and outbound network. Strict mode adds env scrubbing and denies outbound network.

Linux masks conventional project-secret files through a private namespace overlay. If protected files exist and the host blocks any required namespace, ID-map, propagation, or bind-mount step, LPM CLI refuses to execute the lifecycle script. Projects with no protected files, or only files explicitly authorized through sandboxReadAllow / script-read-allow, continue without that overlay. See Project secret files for the exact policy and remaining limits.

lpm install --strict-sandbox          # engage strict mode for this install
lpm install --paranoid                # alias for --strict-sandbox
lpm install --no-sandbox              # drop ALL containment for this install (single flag — drops env scrubbing too)

Persistent strict mode: ~/.lpm/config.toml > [sandbox] mode = "strict" (or LPM_STRICT_SANDBOX=1). Persistent off: lpm config sandbox --set none. Per-invocation flags override the persistent mode.

--no-sandbox is reserved for debugging a sandbox false-positive — scripts run with full host access including credential-bearing env (LPM_TOKEN, NPM_TOKEN, GITHUB_TOKEN, etc.). The three flags are mutually exclusive. See the filesystem sandbox reference for declaring extra write directories per-package.

Guarded weakeners and approvals

Some install-time weakeners are approval-gated:

  • --yolo / --policy=allow
  • --triage when it weakens the current approved machine floor
  • --allow-new or a lower --min-release-age
  • --no-sandbox
  • LPM_PROVENANCE_ENFORCE=warn|off for the current install run
  • raw ~/.lpm/config.toml > [sigstore].verify = "warn"|"off"
  • --unverified-provenance* and --ignore-provenance-drift*

Interactive TTY behavior:

lpm install --no-sandbox

In an interactive shell, LPM CLI can ask inline for confirmation and continue the install if you approve.

Automation behavior:

lpm install --no-sandbox --json

With --json, in CI, or in any non-TTY shell, LPM CLI does not prompt. It fails with error_code: "security_approval_required" and includes a suggested_command, for example:

lpm security unlock sandbox-none --project . --ttl 10m

Package-scoped unlocks only cover the package names listed with --package. A package-scoped unlock does not authorize blanket all-package provenance flags such as --unverified-provenance-all or --ignore-provenance-drift-all.

Repo-file weakeners are treated differently. If a repo asks for a weaker posture through:

  • package.json > lpm.scriptPolicy
  • package.json > lpm.minimumReleaseAge
  • package.json > lpm.minimumReleaseAgePolicy
  • lpm.toml > [sandbox]

install or rebuild does not prompt inline. LPM CLI treats those file values as proposals, fails the command, and points you at lpm security unlock ... for a temporary project exception.

Use lpm security to create the unlock explicitly or to inspect the current floor with lpm security status.

Workspaces

In a monorepo, target a specific member or the workspace root:

lpm install react --filter web    # add react to packages/web/
lpm install -w typescript -D      # add to the root package.json
lpm install --filter './apps/*'   # any glob the workspace filter accepts
lpm install react --filter-prod ...web # prod dependency closure only

--filter / --filter-prod and -w are mutually exclusive. --filter-prod uses the same grammar but ignores devDependencies during closure expansion. --changed-files-ignore-pattern <glob> and --test-pattern <glob> apply when a filter contains a [git-ref] atom. --fail-if-no-match makes a typo'd filter exit non-zero (recommended in CI). When a filtered install would mutate more than one member's package.json, LPM CLI prompts for confirmation; pass -y to skip the prompt.

See Workspaces for filter grammar.

Frozen Lockfile And CI

lpm ci --offline --strict-integrity
  • lpm ci — frozen install. Requires lpm.lock, validates that the lockfile's importer snapshot matches package.json, and never rewrites lpm.lock or lpm.lockb.
  • --offline — never touches the network. Replays entirely from the lockfile + global store. Errors out if anything is missing.
  • --strict-integrity — fail on tarball-URL deps that don't declare an inline SRI hash. Disables trust-on-first-use for fresh installs.
  • --no-skills and --no-security-summary skip optional work to shave CI time. Persist the package-skill opt-out with lpm config lpm-skills --set false; use --skills to override it for one CI run. --no-editor-setup remains accepted but has no effect; package skills never create editor links or configuration.

For reused expanded-store object validation, use lpm config integrity. integrity = "tree" rehashes expanded files before reuse; --strict-integrity only controls whether tarball URL dependencies must declare SRI before first use.

lpm install --frozen-lockfile gives the same frozen behavior on the regular install command. On CI providers that set CI=true (or a provider-specific CI env var), plain lpm install automatically becomes frozen when lpm.lock exists. Pass --no-frozen-lockfile only when you intentionally want a mutable install in CI.

Frozen means frozen: package specs are rejected, --force is rejected, missing lockfiles fail, stale manifest ranges or dependency sections fail, and drift in overrides, catalogs, patches, peer rules, or autoInstallPeers fails before install work proceeds.

For Docker layers keyed only by the lockfile, warm the store before copying the rest of the project:

COPY lpm.lock ./
RUN lpm fetch --platform linux/x64/glibc

COPY package.json ./
RUN lpm install --offline --frozen-lockfile

For full image patterns, see Docker deploys.

Recently published packages

LPM CLI applies a 24-hour minimum release age by default. On project installs, the default policy checks direct/root dependencies: the package specs declared by the project, plus packages you pass to lpm install <pkg>. For ranges on checked packages, the resolver skips candidates that are still inside the cooldown window and picks the newest older candidate that still satisfies the range. When the request is latest, fallback candidates are also capped at the registry's authoritative dist-tags.latest target. A maintainer rollback to a lower SemVer therefore cannot select an older-by-date but SemVer-greater release. Exact pins to a too-new version still fail, because there is no older version that can satisfy the exact request.

Transitive dependencies of an allowed direct package are not separately halted by the default policy. That keeps a mature direct package on its normal dependency graph instead of downgrading it only because one of its children was published recently.

For stricter supply-chain posture, opt into transitive enforcement:

package.json
{ "lpm": { "minimumReleaseAge": 86400, "minimumReleaseAgePolicy": "strict" } }
~/.lpm/config.toml
minimum-release-age-secs = 86400
release-age-policy = "strict"

Strict mode applies the cooldown to direct and transitive dependencies. Lockfile replays are revalidated from the registry-published-at timestamps persisted in lpm.lock, so a frozen replay cannot silently reintroduce a package that is still inside the configured cooldown window.

lpm install foo --allow-new                # bypass the cooldown for this command
lpm install foo --min-release-age=1h       # tighten or loosen the window
lpm install foo --min-release-age=0        # disable the cooldown for this command
lpm install foo --min-release-age-exclude foo

Set persistent defaults via package.json (lpm.minimumReleaseAge, lpm.minimumReleaseAgeExclude) or ~/.lpm/config.toml (minimum-release-age-secs, minimum-release-age-exclude). Excludes are exact canonical package names in the direct/root cooldown scope: for "local": "npm:real-pkg@1.0.0", exclude real-pkg, not local. CLI, package, and global exclude lists merge in that order, with duplicates removed.

Set cooldown scope via package.json > lpm.minimumReleaseAgePolicy or ~/.lpm/config.toml > release-age-policy. Use lpm config release-age-policy --set strict for the global wizard, or lpm config set release-age-policy strict for the generic setter.

--allow-new and --min-release-age-exclude <pkg> skip the install cooldown only — the provenance-drift check still applies unless you also pass --ignore-provenance-drift <pkg> or --ignore-provenance-drift-all. Under --policy=triage, the identity-match widening is also gated by the cooldown — bypassing the install halt does not make a recent-publish package's scripts trusted. Use --policy=allow to opt out of the script-tier review as well, or set minimum-release-age=0 to disable cooldown universally.

Registry signatures and trust policy

Registry package signatures are off on the install path by default so the hot path stays lean. Audit them on demand:

lpm audit signatures
lpm audit signatures --json

Enable install-time verification when you want the install itself to fail closed on unsigned or unverifiable npm registry packages:

lpm config signatures --set true
# or, for one process:
LPM_VERIFY_REGISTRY_SIGNATURES=1 lpm install

The verifier checks npm-compatible dist.signatures against registry signing keys and the package integrity hash. @lpm.dev/* packages and non-registry sources are skipped by this specific check; they still pass through the normal integrity, provenance, script, and behavioral layers.

trust-policy = "no-downgrade" is a separate verified-history policy:

lpm config trust-policy --set no-downgrade

When enabled, LPM CLI first rejects releases whose npm trusted-publisher or staged-publish evidence is weaker than an earlier published release. It also remembers verified provenance in lpm.lock: after any locked version of a package has verified evidence, a later version without verified evidence fails with the trust-policy reason. verify=warn, verify=off, per-package verification skips, and best-effort availability cannot bypass the lockfile-history floor; disable trust-policy itself to permit an intentional downgrade. Registry attestation pointers alone are not treated as evidence. The default is off.

Sigstore verification has two additional opt-in axes:

lpm config sigstore --set scope=all
lpm config sigstore --set availability=strict

scope=all verifies every resolved package and locks successful evidence; the default approved scope checks packages with previously approved provenance identities. availability=strict requires evidence; the default best-effort mode keeps attestation absence or transient unavailability non-blocking. Invalid supplied bundles still fail under the default verify=deny posture. See Security audit for artifact binding, cache, and frozen-replay details.

Audit after install

Optional opt-in: after a successful install, LPM CLI can run lpm audit silently and emit a one-line advisory:

✓ Done · installed 247 packages in 1.31s
! Audited 247 packages, 1 vulnerability, 67 suspicious in 412ms — run `lpm audit`

The advisory is informational only — vulnerabilities found here NEVER fail the install. Run lpm audit --fail-on=<level> explicitly if you want a gating audit. The feature is disabled by default.

The vulnerability count includes both OSV.dev findings for npm packages and exact-version LPM.dev Registry advisories. If either advisory lookup cannot complete, LPM CLI suppresses the ! Audited line (and the JSON audit_summary) instead of reporting zero vulnerabilities; the install still succeeds and the audit failure remains visible in logs.

Enable per-invocation:

lpm install --audit-after-install        # opt in for this run
lpm install --no-audit-after-install     # opt out for this run (beats env + config)

Or persistently:

  • LPM_AUDIT_AFTER_INSTALL=1 env (accepts 1/true/yes/on and 0/false/no/off)
  • ~/.lpm/config.toml > audit-after-install = true

Precedence: --audit-after-install / --no-audit-after-install > LPM_AUDIT_AFTER_INSTALL > ~/.lpm/config.toml > default (false).

Under --json, the human line is suppressed and the same counts are attached to the install envelope as audit_summary:

{
  "success": true,
  "audit_summary": {
    "packages_audited": 247,
    "vulnerabilities": 1,
    "suspicious": 67,
    "elapsed_ms": 412
  }
}

The ! Audited advisory is hidden when the install short-circuits on the up-to-date fast path — re-auditing an unchanged tree on every lpm install would be noise. Run lpm audit directly if you want to scan without re-installing.

Global installs

lpm install -g shares the project install pipeline. The same security gates fire end-to-end on -g:

FlagBehavior on -g
--allow-newBypasses the cooldown
--min-release-age=<DUR>Overrides the cooldown window
--min-release-age-exclude <PKG>Exempts one exact package name from the cooldown; repeatable
--ignore-provenance-drift <PKG> / --ignore-provenance-drift-allWaives the drift check
--policy=<deny|allow|triage>, --yolo, --triageSets the dependency script-policy
--strict-peer-dependencies / --no-strict-peer-dependenciesOverrides the peer-dependency strictness setting for the synthesized install
--no-engine-strictUses warning-only dependency engine checks for the synthesized install
--auto-buildAuto-runs lpm rebuild for trusted packages immediately after install. On -g under triage with mixed-trust trees, this is the only way to trigger the rebuild — package.json > lpm > scripts.autoBuild is not consulted for global installs. Also useful under deny with an established global trust set.

Two things differ from project installs:

  • Globals don't write to package.json. lpm install -g resolves a single package into ~/.lpm/global/installs/<pkg>@<ver>/ and tracks it in ~/.lpm/global/manifest.toml. The project's package.json is never read or mutated. Approvals from lpm approve-scripts --global land in ~/.lpm/global/trusted-dependencies.json instead of any project's package.json.
  • Project config is skipped. With no project-level package.json > lpm block to read, the script-policy and strict-peer-dependency chains on -g collapse to CLI flag > ~/.lpm/config.toml > default. The ~/.lpm/config.toml > minimum-release-age-secs and release-age-policy chains work the same way.

A global install commits only after LPM CLI has materialized at least one safe, executable bin shim for the package. If the package exposes no usable bins, declares unsafe bin names, or the install cannot write its ready marker, LPM CLI rolls the pending global entry back instead of leaving a half-installed package in ~/.lpm/global/.

Re-running scripts after lpm approve-scripts --global requires lpm uninstall -g <pkg> && lpm install -g <pkg> for each affected top-level global. lpm rebuild --global is a planned follow-up.

Engines enforcement

lpm install enforces engine compatibility at two boundaries:

  • Project preflight: reads the workspace root package.json > engines block before install work begins.
  • Resolved dependencies: checks every selected package version's engines.node constraint before materialization. The constraint is persisted in lpm.lock, so warm, frozen, and offline installs revalidate it without relying on cached registry metadata.

The workspace-root keys are:

KeyCompared against
engines.lpmThe running CLI version (env!("CARGO_PKG_VERSION"))
engines.nodeThe effective Node version — managed runtime under ~/.lpm/runtimes/node/ if one matches the project's pin, else system node --version
$ lpm install
Error: lpm::engine_mismatch
  × lpm version 0.32.0 does not satisfy required >=0.40.0 (from package.json
  │ > engines.lpm)

For dependencies, an incompatible required package aborts with engine_mismatch. An incompatible package reachable only through optional dependency edges is skipped. When the same package is also reachable through a required path, the required path wins and the mismatch remains fatal. Optional reachability is computed from the resolver's final selected graph, so dependency traversal order and discarded backtracking candidates do not change that result.

Workspace members may omit version. LPM keeps the workspace discovery fallback of 0.0.0 for those members and reads engines.node independently, so enabling dependency engine enforcement does not make version mandatory.

Other workspace-root engines.<pm> keys (npm, pnpm, yarn, bun) are recognized and surfaced as a one-line warning that LPM CLI doesn't enforce them. Use engines.lpm for the LPM CLI version, and use lpm.json > runtime.bun when scripts need a managed Bun binary on PATH.

Workspace-root failures exit during preflight. Dependency constraints are evaluated after resolution identifies exact versions. If no effective Node version can be determined, Node constraints are left unenforced until a runtime is available.

lpm install --no-engine-strict            # warning-only for this invocation

Persistent opt-out:

  • package.json > lpm > engineStrict = false (per-project)
  • ~/.lpm/config.toml > engine-strict = false (per-user)

Precedence: CLI flag > package.json > lpm.engineStrict > ~/.lpm/config.toml > default (true). The same resolved policy applies to the workspace root and dependencies. Under engineStrict = false, incompatible dependencies remain installed and mismatches print as stderr warnings (suppressed under --json).

The workspace-root preflight also runs for lpm rebuild and lpm add. Add runs it before manifest mutation, then applies dependency checks during its install phase.

Flags

FlagEffect
-D, --save-devSave under devDependencies
-g, --globalInstall into ~/.lpm/global/ instead of the project (exposes bins on PATH)
--omit <dev|optional>Omit dependency types from node_modules (comma-separated or repeatable)
--prod, --productionProduction install; equivalent to omitting dev dependencies
--offlineNever touch the network
--frozen-lockfileRefuse to update lockfiles; fail if package.json and lpm.lock disagree
--no-frozen-lockfileDisable the CI auto-frozen default for this invocation
--forceBypass fast-exit hash check, skip the lockfile, re-download, re-link from scratch
--allow-newSkip the minimum-release-age cooldown
--min-release-age <DUR>Override the cooldown (<N>h, <N>d, or seconds; 0 disables)
--min-release-age-exclude <PKG>Exempt one exact package name from the cooldown; repeatable
--strict-integrityRequire manifest-declared SRI for tarball-URL deps
--strict-peer-dependenciesFail when required peers are missing, peer ranges mismatch, or peer requirements conflict
--no-strict-peer-dependenciesDisable strict peer failures for this invocation, overriding project or user config
--linker <isolated|hoisted>Linking layout (default starts hoisted in the v2 virtual-store layout; workspaces and default peer-conflict installs use isolated)
--policy <deny|allow|triage>Lifecycle-script policy for this invocation
--yoloAlias for --policy=allow
--triageAlias for --policy=triage
--advisor <none|claude-cli|codex|ollama>Triage advisor override (only consulted under --policy=triage)
--auto-buildAuto-run lpm rebuild for trusted packages after install
--strict-sandboxEngage strict sandbox for this install's dependency lifecycle scripts (filesystem containment + env scrubbing + outbound network denial)
--paranoidAlias for --strict-sandbox
--no-sandboxDrop all sandbox containment for this install's dependency lifecycle scripts (debug only — also drops env scrubbing)
--exactSave exact version (no prefix)
--tildeSave with ~ prefix
--save-prefix <P>Override save prefix (^, ~, or "")
--catalog[=<NAME>]Save through the default or named root catalog when the catalog entry matches
--filter <EXPR>Workspace filter (mutually exclusive with -w)
--filter-prod <EXPR>Workspace filter with production-only dependency closures (mutually exclusive with -w)
--changed-files-ignore-pattern <glob>Ignore matching git-diff paths for [git-ref] filters
--test-pattern <glob>Treat matching git-diff paths as test-only for [git-ref] fan-out decisions
-w, --workspace-rootTarget the root package.json
--fail-if-no-matchExit non-zero if filters match nothing
-y, --yesSkip the interactive multi-member confirm
--skillsInstall package-published LPM.dev skills for this invocation, overriding user config
--no-skillsSkip package-published LPM.dev skill auto-install for this invocation
--no-editor-setupAccepted with no effect. Package skills do not create editor integrations.
--no-security-summarySkip the post-install security report (faster CI)
--timingInclude install timing diagnostics in --json output
--ignore-provenance-drift <PKG>Skip provenance-drift check for one package (repeatable)
--ignore-provenance-drift-allSkip the check for every package
--no-engine-strictInstall with warnings instead of enforcing workspace-root and dependency engine mismatches
--audit-after-installRun audit after install for this run (informational only — never fails the install)
--no-audit-after-installSkip audit after install for this run, overriding env + config
--replace-bin <CMD>(-g only) Take ownership of a colliding bin name (repeatable)
--alias <ORIG=ALIAS>(-g only) Install a declared bin under a different PATH name

Plus the global flags: --token, --registry, --json, --verbose, --insecure.

See also

  • lpm uninstall — remove a dependency
  • lpm tidy — find unused dependency declarations and phantom imports
  • lpm upgrade — bump eligible LPM.dev Registry and npm deps to their latest matching range
  • lpm rebuild — run dependency lifecycle scripts after install
  • lpm approve-scripts — approve packages to run scripts
  • Save policy — full details of the save-prefix system
  • Resolver — how versions are picked
  • Lockfilelpm.lock and lpm.lockb