lpm install
Install dependencies from package.json, or add new ones.
lpm install [packages...] # alias: lpm iWith 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.
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, 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 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 --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 --force # full re-install, bypass every fast path
lpm install --strict-peer-dependencies # fail on peer warnings/conflicts
lpm install --verbose # append per-phase timings + lockfile size--verbose footer
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 symlinkedWithout --verbose, only the ✓ Done line is shown — keeps the slim output focused on what changed (+ pkg@version entries) and the success terminus.
How it works
- Resolve. LPM walks
package.json, applies the lockfile, and fetches whatever metadata is missing from the appropriate registry (see Registries for routing). - 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). - Link.
node_modules/<pkg>is created as a symlink into the global store at~/.lpm/store/v2/links/<graph-key>/. Single packages start hoisted (npm-style flat); workspaces auto-default to isolated (pnpm-style strict-deps). If resolution detects incompatible peer requirements and no explicit linker was set, LPM switches that install to isolated and records the decision inlpm.lockso warm installs keep the same layout. Override per-invocation with--linker=isolatedor--linker=hoisted. - Skip scripts. Lifecycle scripts do not run by default. When policy permits them to run (
--policy=allow/--yolo, or a triage-green tier), onlypreinstall,install, andpostinstallfire — other recognized 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.
For registry packages, any tarball URL recorded in lpm.lock is only a cache hint. Before fetching it, LPM 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.
Peer dependency strict mode
By default, LPM 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-dependenciesStrict 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 saves ^resolvedVersion to package.json. If you typed an explicit version or range, LPM preserves what you typed. Prereleases are saved exact for safety.
| You ran | package.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):
save-prefix = "^" # one of "^", "~", or "" (exact, no prefix)
save-exact = false # bool; true forces exact regardless of prefixRe-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.
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:
| Mode | Behavior 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.
Lifecycle scripts
LPM does not execute lifecycle scripts during lpm install by default — the install pipeline materializes the closure, then stops. To run scripts, choose a policy:
| Policy | Behavior |
|---|---|
deny (default) | Scripts blocked. lpm install lists what wanted to run; approve them with lpm approve-scripts |
allow (--yolo) | Run every lifecycle script during install — matches npm/pnpm/bun default |
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. 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=triageOr 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 setupOr in package.json: { "lpm": { "triageAdvisor": "claude-cli" } } · or ~/.lpm/config.toml: triage-advisor = "claude-cli". The advisor is opt-in — triage-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-outPrecedence: --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 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.
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--triagewhen it weakens the current approved machine floor--allow-newor a lower--min-release-age--no-sandboxLPM_PROVENANCE_ENFORCE=warn|offfor the current install run- raw
~/.lpm/config.toml > [sigstore].verify = "warn"|"off" --unverified-provenance*and--ignore-provenance-drift*
Interactive TTY behavior:
lpm install --no-sandboxIn an interactive shell, LPM can ask inline for confirmation and continue the install if you approve.
Automation behavior:
lpm install --no-sandbox --jsonWith --json, in CI, or in any non-TTY shell, LPM 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 10mPackage-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.scriptPolicypackage.json > lpm.minimumReleaseAgelpm.toml > [sandbox]
install or rebuild does not prompt inline. LPM 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 prompts for confirmation; pass -y to skip the prompt.
See Workspaces for filter grammar.
Reproducible installs (CI)
lpm install --offline --strict-integrity--offline— never touches the network. Resolves 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,--no-editor-setup,--no-security-summary— skip the soft-side-effects to shave CI time.
Recently published packages
LPM blocks installs of packages published in the last 24 hours by default — the minimum release age gate.
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 commandSet persistent defaults via package.json (lpm.minimumReleaseAge) or ~/.lpm/config.toml (minimum-release-age-secs). --allow-new skips the 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 — --allow-new proceeds with the install but a recent-publish package whose script body matches an identity-Green shape stays Amber. Use --policy=allow to opt out of the script-tier review as well, or set minimum-release-age=0 to disable cooldown universally.
Audit after install
Optional opt-in: after a successful install, LPM 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.
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=1env (accepts1/true/yes/onand0/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:
| Flag | Behavior on -g |
|---|---|
--allow-new | Bypasses the cooldown |
--min-release-age=<DUR> | Overrides the cooldown window |
--ignore-provenance-drift <PKG> / --ignore-provenance-drift-all | Waives the drift check |
--policy=<deny|allow|triage>, --yolo, --triage | Sets the script-policy |
--strict-peer-dependencies / --no-strict-peer-dependencies | Overrides the peer-dependency strictness setting for the synthesized install |
--auto-build | Auto-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 -gresolves a single package into~/.lpm/global/installs/<pkg>@<ver>/and tracks it in~/.lpm/global/manifest.toml. The project'spackage.jsonis never read or mutated. Approvals fromlpm approve-scripts --globalland in~/.lpm/global/trusted-dependencies.jsoninstead of any project'spackage.json. - Project config is skipped. With no project-level
package.json > lpmblock to read, the script-policy and strict-peer-dependency chains on-gcollapse to CLI flag >~/.lpm/config.toml> default. The~/.lpm/config.toml > minimum-release-age-secschain works the same way.
A global install commits only after LPM 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 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 reads the workspace root package.json > engines block and aborts when a constraint isn't satisfied. Two keys are checked:
| Key | Compared against |
|---|---|
engines.lpm | The running CLI version (env!("CARGO_PKG_VERSION")) |
engines.node | The 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)Other engines.<pm> keys (npm, pnpm, yarn, bun) are recognized and surfaced as a one-line warning that LPM 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.
The check runs before any resolver work, so failures exit cheaply.
lpm install --no-engine-strict # skip enforcement for this invocationPersistent 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). Under engineStrict = false the constraints are still parsed and any mismatches print as stderr warnings (suppressed under --json).
The same gate runs for lpm rebuild and lpm add (preflight, before any manifest mutation).
Flags
| Flag | Effect |
|---|---|
-D, --save-dev | Save under devDependencies |
-g, --global | Install into ~/.lpm/global/ instead of the project (exposes bins on PATH) |
--offline | Never touch the network |
--force | Bypass fast-exit hash check, skip the lockfile, re-download, re-link from scratch |
--allow-new | Skip the minimum-release-age cooldown |
--min-release-age <DUR> | Override the cooldown (<N>h, <N>d, or seconds; 0 disables) |
--strict-integrity | Require manifest-declared SRI for tarball-URL deps |
--strict-peer-dependencies | Fail when required peers are missing, peer ranges mismatch, or peer requirements conflict |
--no-strict-peer-dependencies | Disable strict peer failures for this invocation, overriding project or user config |
--linker <isolated|hoisted> | Linking layout (default starts hoisted; workspaces and default peer-conflict installs use isolated) |
--policy <deny|allow|triage> | Lifecycle-script policy for this invocation |
--yolo | Alias for --policy=allow |
--triage | Alias for --policy=triage |
--advisor <none|claude-cli|codex|ollama> | Triage advisor override (only consulted under --policy=triage) |
--auto-build | Auto-run lpm rebuild for trusted packages after install |
--strict-sandbox | Engage strict sandbox for this install's lifecycle scripts (filesystem containment + env scrubbing + outbound network denial) |
--paranoid | Alias for --strict-sandbox |
--no-sandbox | Drop all sandbox containment for this install's lifecycle scripts (debug only — also drops env scrubbing) |
--exact | Save exact version (no prefix) |
--tilde | Save 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-root | Target the root package.json |
--fail-if-no-match | Exit non-zero if filters match nothing |
-y, --yes | Skip the interactive multi-member confirm |
--no-skills | Skip skills auto-install |
--no-editor-setup | Skip editor auto-integration |
--no-security-summary | Skip the post-install security report (faster CI) |
--ignore-provenance-drift <PKG> | Skip provenance-drift check for one package (repeatable) |
--ignore-provenance-drift-all | Skip the check for every package |
--no-engine-strict | Skip engines.lpm / engines.node enforcement |
--audit-after-install | Run audit after install for this run (informational only — never fails the install) |
--no-audit-after-install | Skip 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 dependencylpm upgrade— bump eligible LPM and npm deps to their latest matching rangelpm rebuild— run lifecycle scripts after installlpm approve-scripts— approve packages to run scripts- Save policy — full details of the save-prefix system
- Resolver — how versions are picked
- Lockfile —
lpm.lockandlpm.lockb