# Commands (/docs/commands)



A flat reference of every top-level command. Each row links to the command's detail page; aliases are noted in parentheses.

## Package management [#package-management]

| Command                                                      | What it does                                                                         |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| [`lpm install`](/docs/packages/install) (`i`)                | Install dependencies from `package.json`, or add new ones                            |
| [`lpm fetch`](/docs/packages/fetch)                          | Download lockfile packages into the store without installing                         |
| [`lpm tidy`](/docs/packages/tidy)                            | Find unused dependency declarations and undeclared imports                           |
| [`lpm add`](/docs/packages/add)                              | Source delivery — copy package source into your project (any registry)               |
| [`lpm remove`](/docs/packages/remove) (`rm`)                 | Remove a package previously brought in via `lpm add`                                 |
| [`lpm uninstall`](/docs/packages/uninstall) (`un`, `unlink`) | Remove from `dependencies` and `node_modules`                                        |
| [`lpm version`](/docs/packages/version)                      | Bump `package.json` version, optionally commit and tag                               |
| [`lpm release`](/docs/packages/release)                      | Plan, apply, and publish workspace releases in dependency order                      |
| [`lpm publish`](/docs/packages/publish) (`p`)                | Publish to LPM.dev Registry (or `--npm` / `--github`)                                |
| [`lpm stage`](/docs/packages/stage)                          | Stage, inspect, approve, or reject npm staged publishes                              |
| [`lpm upgrade`](/docs/packages/upgrade)                      | Upgrade eligible LPM.dev Registry and npm dependencies, optionally scoped by package |
| [`lpm outdated`](/docs/packages/outdated)                    | Show dependencies with newer versions available                                      |
| [`lpm audit`](/docs/packages/audit)                          | Security + quality audit, with direct-dependency fixes                               |
| [`lpm search`](/docs/packages/search)                        | Search npm, custom registries, or LPM.dev Registry by query route                    |
| [`lpm info`](/docs/packages/info)                            | Metadata for a package from npm, custom registries, or LPM.dev Registry              |
| [`lpm quality`](/docs/packages/quality)                      | Quality report for a package                                                         |
| [`lpm query`](/docs/packages/query)                          | CSS-like selector queries on installed packages                                      |
| [`lpm graph`](/docs/packages/graph) (`ls`)                   | Dependency tree (`--format dot`, `mermaid`, `json`, `stats`, `html`)                 |
| [`lpm why`](/docs/packages/graph#lpm-why-package)            | Explain why a package is installed                                                   |
| [`lpm rebuild`](/docs/packages/rebuild)                      | Run or rerun dependency lifecycle scripts                                            |
| [`lpm approve-scripts`](/docs/packages/approve-scripts)      | Approve packages blocked by the default-deny script policy                           |
| [`lpm trust`](/docs/packages/trust)                          | Manage project package trust and release-age exclusions                              |
| [`lpm patch`](/docs/packages/patch)                          | Generate a local patch (`patch-package` style)                                       |
| [`lpm patch-commit`](/docs/packages/patch)                   | Finalize a patch staging directory                                                   |
| [`lpm patch-remove`](/docs/packages/patch)                   | Remove registered local patches                                                      |
| [`lpm sbom`](/docs/packages/sbom)                            | Export a CycloneDX or SPDX Software Bill of Materials                                |
| [`lpm licenses`](/docs/packages/licenses)                    | Inventory dependency licenses and enforce compliance policy                          |
| [`lpm migrate`](/docs/packages/migrate)                      | Migrate from npm / yarn / pnpm / bun                                                 |

## Scripts and execution [#scripts-and-execution]

| Command                                     | What it does                                                 |
| ------------------------------------------- | ------------------------------------------------------------ |
| [`lpm run`](/docs/dev/run)                  | Run scripts (parallel `-p`, cached, `--watch`)               |
| [`lpm dev`](/docs/dev/dev)                  | Zero-config dev server with HTTPS, tunnel, and orchestration |
| [`lpm <file>` / `lpm exec`](/docs/dev/exec) | Run JS/TS files directly, or run project-local binaries      |
| [`lpm dlx`](/docs/dev/dlx) (`lpx`)          | Run a package without installing it                          |
| [`lpm test`](/docs/dev/test)                | Auto-detect and run the project's test runner                |
| [`lpm bench`](/docs/dev/bench)              | Auto-detect and run the project's benchmark runner           |

## Built-in tools and managed engines [#built-in-tools-and-managed-engines]

| Command                                   | Backed by                                             |
| ----------------------------------------- | ----------------------------------------------------- |
| [`lpm lint`](/docs/dev/lint)              | Oxlint                                                |
| [`lpm fmt`](/docs/dev/fmt)                | Biome                                                 |
| [`lpm bundle`](/docs/dev/bundle)          | Rolldown (managed plugin)                             |
| [`lpm pack`](/docs/dev/pack)              | Project-local tsdown                                  |
| [`lpm check`](/docs/dev/check)            | TypeScript (`tsc --noEmit`) / `tsgo` (managed engine) |
| [`lpm plugin list`](/docs/dev/plugin)     | Show installed tools                                  |
| [`lpm plugin outdated`](/docs/dev/plugin) | Check managed tool updates                            |
| [`lpm plugin update`](/docs/dev/plugin)   | Update managed tools                                  |

## Runtime and environment [#runtime-and-environment]

| Command                               | What it does                                                                                             |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [`lpm use`](/docs/dev/use)            | Install and pin managed Node.js or Bun versions                                                          |
| [`lpm env`](/docs/dev/env)            | Per-project secrets — set / get / list / push / pull / pair / OIDC, with end-to-end-encrypted cloud sync |
| [`lpm global`](/docs/packages/global) | Manage globally-installed CLIs (`~/.lpm/global/`)                                                        |

## Dev infrastructure [#dev-infrastructure]

| Command                            | What it does                                               |
| ---------------------------------- | ---------------------------------------------------------- |
| [`lpm tunnel`](/docs/infra/tunnel) | Expose localhost; claim domains; inspect & replay webhooks |
| [`lpm cert`](/docs/infra/cert)     | Local HTTPS certificate management                         |
| [`lpm proxy`](/docs/infra/proxy)   | Inspect and run the local-domain proxy daemon              |
| [`lpm hosts`](/docs/infra/hosts)   | Remove LPM CLI-managed local-domain hosts-file entries     |
| [`lpm ports`](/docs/infra/ports)   | Dev-service port management                                |

## Workspaces and deployment [#workspaces-and-deployment]

| Command                                   | What it does                                                   |
| ----------------------------------------- | -------------------------------------------------------------- |
| [`lpm filter`](/docs/packages/workspaces) | Preview the workspace set a `--filter` expression selects      |
| [`lpm catalog`](/docs/packages/catalog)   | Inspect catalog declarations and resolved lockfile snapshots   |
| [`lpm deploy`](/docs/packages/workspaces) | Materialize a member's deploy closure for `COPY --from=pruned` |

## Project health [#project-health]

| Command                                        | What it does                                                             |
| ---------------------------------------------- | ------------------------------------------------------------------------ |
| [`lpm doctor`](/docs/infra/doctor)             | Project + environment health check (`--fix` to auto-repair)              |
| [`lpm health`](/docs/infra/doctor)             | Check registry health                                                    |
| [`lpm policy`](/docs/infra/policy)             | List, diagnose, and test local install policy extensions                 |
| [`lpm store verify`](/docs/infra/store)        | Verify package store integrity                                           |
| [`lpm cache`](/docs/packages/cache)            | Manage ephemeral caches and prune the global package store               |
| [`lpm self-update`](/docs/infra/self-update)   | Update LPM CLI on stable or nightly release channels                     |
| [`lpm completions`](/docs/dev/completions)     | Emit a shell completion script (bash, zsh, fish, powershell, elvish)     |
| [`lpm schema <kind>`](/docs/reference/schemas) | Emit the JSON Schema for a config file (`lpm.json` or `lpm.config.json`) |

## Debug and inspection [#debug-and-inspection]

| Command                                   | What it does                                             |
| ----------------------------------------- | -------------------------------------------------------- |
| [`lpm download`](/docs/packages/download) | Download and extract a tarball (no install side-effects) |
| [`lpm resolve`](/docs/packages/resolve)   | Print a resolved dependency tree without installing      |

## Auth and config [#auth-and-config]

| Command                                                     | What it does                                          |
| ----------------------------------------------------------- | ----------------------------------------------------- |
| [`lpm login`](/docs/infra/login) (`l`)                      | Authenticate against a registry                       |
| [`lpm logout`](/docs/infra/login) (`lo`)                    | Clear stored auth state                               |
| [`lpm whoami`](/docs/infra/login)                           | Show the current user                                 |
| [`lpm token-rotate`](/docs/infra/token-rotate)              | Rotate your auth token                                |
| [`lpm setup ci npmrc`](/docs/infra/setup)                   | Generate `.npmrc` for npm-compatible CI clients       |
| [`lpm setup local`](/docs/infra/setup#local-authentication) | Generate a protected read-only project `.npmrc` token |
| [`lpm ci`](/docs/packages/install)                          | Frozen lockfile install for CI                        |
| [`lpm init`](/docs/packages/init)                           | Create a new package manifest                         |
| [`lpm config`](/docs/infra/config)                          | CLI configuration                                     |
| [`lpm security`](/docs/infra/security)                      | Temporary approvals: unlock / lock / status / repair  |
| [`lpm pool`](/docs/packages/pool)                           | Pool revenue stats                                    |
| [`lpm skills`](/docs/packages/skills)                       | Package-published and standalone AI agent skills      |
| [`lpm swift-registry`](/docs/packages/swift-registry)       | Configure SPM integration (SE-0292)                   |
| [`lpm mcp setup`](/docs/reference/mcp-servers)              | Configure MCP server for AI editors                   |

## Global flags [#global-flags]

These work on every command:

| Flag                    | Effect                                                                                                           |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `-V`, `-v`, `--version` | Print the LPM CLI version and exit                                                                               |
| `--token <TOKEN>`       | Use a specific auth token instead of the stored one (env: `LPM_TOKEN`)                                           |
| `--registry <URL>`      | Override the registry URL for this command (env: `LPM_REGISTRY_URL`)                                             |
| `--json`                | Emit JSON output (for CI / scripting)                                                                            |
| `--verbose`             | Debug logging                                                                                                    |
| `--insecure`            | Allow an explicitly configured plain-HTTP non-localhost registry; HTTPS redirects still cannot downgrade to HTTP |


# Compared to npm, pnpm, bun (/docs/comparison)





A factual comparison of default behavior across **npm**, **pnpm**, **bun**, and **lpm**.

## Benchmarks [#benchmarks]

Install benchmarks use the tracked VitePress docs fixture, a real-world workspace graph with 535 packages.

| Benchmark                     |      npm |    pnpm |     bun |   **lpm** | **lpm + LPM Firewall monitor** |
| ----------------------------- | -------: | ------: | ------: | --------: | -----------------------------: |
| Cold install, equal footing ¹ | 17,354ms | 6,125ms | 2,455ms |   2,945ms |                        3,043ms |
| Warm install ¹                |  3,819ms | 3,301ms |   451ms | **387ms** |                      **324ms** |
| Up-to-date install ¹          |    282ms |   522ms |    77ms |  **14ms** |                       **14ms** |

Dev command benchmarks measure already-installed local scripts, local bins, and built-in tools.

| Benchmark                        | npm / npx / tsx |  pnpm | bun / bunx |  **lpm** |
| -------------------------------- | --------------: | ----: | ---------: | -------: |
| Trivial echo script ²            |            74ms | 161ms |       15ms | **10ms** |
| Node startup + tiny script ²     |           101ms | 185ms |       33ms | **27ms** |
| Local bin: `esbuild --version` ² |           144ms | 174ms |       27ms | **21ms** |
| Run TSX app, warm cache ³        |           112ms |     — |       19ms | **41ms** |
| `lpm lint` vs `npx oxlint` ⁴     |           273ms |     — |          — |  **3ms** |
| `lpm fmt` vs `npx biome` ⁴       |           340ms |     — |          — |  **3ms** |

<Accordions>
  <Accordion title="What each row measures">
    All displayed values are medians across 10 measured runs. The dated raw result artifacts are linked with each benchmark group below.

    **Cold install** — a fresh project with no package-manager cache, lockfile, or `node_modules/`; cleanup happens outside the timer. [Install results — July 10, 2026](https://github.com/lpm-dev/rust-client/blob/main/bench/perf-results/readme-install-vitepress-20260710T140139.md).

    **Warm install** — cache and lockfile already exist, so the package manager rebuilds `node_modules/`.

    **Up-to-date install** — nothing changed, so the package manager checks state and exits.

    **Trivial echo script** — the cost of invoking an already-installed `package.json` script that runs `echo hi` through each package manager. [Script and local-bin results — July 10, 2026](https://github.com/lpm-dev/rust-client/blob/main/bench/perf-results/readme-run-bin-20260710T214708Z.md).

    **Node startup + tiny script** — the cost of invoking an already-installed `package.json` script that starts Node and performs a tiny stdout write.

    **Local bin row** — the cost of invoking an already-installed `node_modules/.bin` entry.

    **Run TSX app** — warm-cache execution of a generated 10-module TSX ESM app. [TSX results — July 10, 2026](https://github.com/lpm-dev/rust-client/blob/main/bench/perf-results/readme-exec-runtime-20260710T141510.md).

    **Managed tools** — `lpm lint` and `lpm fmt` use cached managed tools instead of paying `npx` resolution overhead. [Built-in tool results — July 11, 2026](https://github.com/lpm-dev/rust-client/blob/main/bench/perf-results/readme-builtin-tools-20260711T205110Z.md).
  </Accordion>

  <Accordion title="Methodology">
    **¹ Install benchmarks** — Generated by `bench/scripts/run-install-readiness.mjs` with `--samples 10 --fixtures vitepress --managers lpm,bun,pnpm,npm --modes cold,warm,up-to-date --lpm-firewall-modes off,report`. Each run uses an isolated temporary project, `HOME`, package-manager cache, and `LPM_HOME`; samples are round-robin interleaved to reduce live-network bias. Dependency lifecycle scripts are disabled for all package managers.

    **LPM Firewall monitor mode** — The LPM Firewall monitor column runs the same LPM CLI install with `LPM_NPM_FIREWALL=report`. In the cold install row, LPM CLI checked 535 package versions before materializing package bytes.

    **² Script and local-bin benchmarks** — Generated by `bench/scripts/run-bin-benchmark.mjs` with 10 measured iterations after 2 warmups. Dependency installation is setup-only and not timed. Tool versions: `lpm 0.67.0`, `npm 10.9.4`, `npx 10.9.4`, `pnpm 11.3.0`, `bun 1.3.14`, `bunx 1.3.14`.

    **³ TSX benchmark** — Generated by `bench/scripts/exec-runtime-benchmark.mjs` against a generated 10-module TSX ESM app with 10 measured iterations after 2 warmups. The `npm / npx / tsx` column is `tsx`; the `bun / bunx` column is `bun`.

    **⁴ Built-in tool benchmarks** — Generated with `RUNS=10 LPM_BIN=target/release/lpm-rs BENCH_WORK_DIR=/tmp/lpm-readme-builtin-tools-work-20260711T205110Z ./bench/run.sh builtin-tools`. [Raw artifact — July 11, 2026](https://github.com/lpm-dev/rust-client/blob/main/bench/perf-results/readme-builtin-tools-20260711T205110Z.md).
  </Accordion>
</Accordions>

## Security defaults [#security-defaults]

| Axis                                                                                                                | npm                                    | pnpm                                                                             | bun                                                                                                                                    | lpm                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Dependency lifecycle-script sandbox](/docs/packages/install#sandbox)                                               | —                                      | —                                                                                | —                                                                                                                                      | **Dependency lifecycle-script sandbox:** approved or triage-selected dependency scripts run under macOS Seatbelt, Linux Landlock, or Windows AppContainer. Default mode contains filesystem access while allowing network. Strict mode denies outbound network on macOS and AppContainer; Linux 6.7+ uses Landlock V4 plus seccomp, with AF\_UNIX retained for IPC. Older Linux kernels and Windows installs missing the AppContainer helper refuse strict mode unless `allow-degraded = true`. Root-project lifecycle scripts are not sandboxed. |
| [Dependency lifecycle scripts (`preinstall` / `install` / `postinstall`)](/docs/packages/approve-scripts)           | All scripts run                        | Blocked until approved (`approve-builds` flow)                                   | Blocked unless in `trustedDependencies`; bun ships a built-in 367-package default trusted list that also runs scripts without approval | Dependencies are blocked by default. Under the opt-in `triage` policy, green verdicts are eligible to auto-run in the sandbox; an optional advisor can approve amber verdicts for the current run; red verdicts remain blocked. The root project's lifecycle runs on bare `lpm install` without the dependency sandbox.                                                                                                                                                                                                                           |
| [Registry package signatures](/docs/packages/audit#registry-signatures)                                             | Manual via `npm audit signatures`      | Manual via `pnpm audit signatures`                                               | —                                                                                                                                      | `lpm audit signatures`; optional install-time fail-closed verification with `lpm config signatures --set true`                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| [Sigstore provenance verification](/docs/packages/security-audit#layer-4-provenance--cooldown)                      | Publish-time provenance support        | Publish-time provenance support                                                  | —                                                                                                                                      | DSSE + Rekor body + Rekor SET + SCT + X.509 chain verified end-to-end on install for provenance bundles served by npmjs                                                                                                                                                                                                                                                                                                                                                                                                                           |
| [Minimum release age (cooldown)](/docs/packages/install#recently-published-packages)                                | `min-release-age` flag, off by default | 1-day cooldown by default                                                        | Off by default                                                                                                                         | Off by default; configurable direct/root or strict transitive cooldown                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| [`engines.node` enforcement](/docs/packages/install#engines-enforcement)                                            | Warn only                              | Warn only                                                                        | —                                                                                                                                      | Validates the selected or `PATH` Node without installing or switching runtimes; hard-fails missing/incompatible required constraints, skips optional-only incompatible packages, persists dependency constraints for warm/frozen/offline replay, and supports warning-only opt-out                                                                                                                                                                                                                                                                |
| [Built-in vulnerability audit](/docs/packages/audit)                                                                | npm security database                  | Registry-side data                                                               | Proxies to npm audit endpoint                                                                                                          | OSV + behavioral analysis + verdict cache                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [Approval-gated weakening of security posture](/docs/infra/security)                                                | —                                      | —                                                                                | —                                                                                                                                      | Weaker CLI flags can require native approval; pulled repo or config-file weakenings are treated as proposals until a short-lived unlock or persistent signed machine approval exists                                                                                                                                                                                                                                                                                                                                                              |
| [Provenance drift detection on republish](/docs/packages/security-audit#layer-4-provenance--cooldown)               | —                                      | Resolve-time trust-rank downgrade refusal (trustedPublisher → provenance → none) | —                                                                                                                                      | Detects publisher-identity drift for stored rich trust bindings; optional `trust-policy = "no-downgrade"` refuses weaker npm publish trust during resolution and requires continued artifact verification after verified lockfile history                                                                                                                                                                                                                                                                                                         |
| Pluggable third-party security scanner protocol (register an npm package as a scanner, run it against the lockfile) | —                                      | —                                                                                | `bunfig.toml > [install.security] scanner = "<pkg>"`, invoked by `bun pm scan`                                                         | — (built-in triage + audit cover a different surface; not pluggable)                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

## Resolver, store, and layout [#resolver-store-and-layout]

| Axis                                                                                | npm                                | pnpm                                               | bun                                                                 | lpm                                                                                                                                                                                                           |
| ----------------------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Resolver](/docs/packages/resolver)                                                 | Arborist backtracking              | Highest-version + workspace-rooted peer resolution | Tree resolution + backtracking                                      | Greedy-fusion (PubGrub legacy arm available)                                                                                                                                                                  |
| [Package store](/docs/packages/content-addressable-store)                           | Content-addressable cache          | Content-addressable, versioned virtual store       | Content-addressable, hardlinked                                     | Content-addressable virtual store; copy-on-write clone/reflink where available, independent-copy fallback                                                                                                     |
| [`node_modules` layout](/docs/packages/install#how-it-works)                        | Hoisted                            | **Isolated** (symlinked virtual store)             | Auto: hoisted for single-package, isolated when workspaces detected | Auto: v2 hoisted virtual-store for single packages, isolated for workspaces or default peer-conflict installs                                                                                                 |
| [Phantom-dependency prevention](/docs/reference/package-json-lpm#strictdeps)        | Hoisted layout leaks unlisted deps | Isolated by default                                | In isolated mode                                                    | In isolated mode (default for workspaces and peer-conflict installs)                                                                                                                                          |
| [Lockfile](/docs/packages/lockfile)                                                 | `package-lock.json` v3             | `pnpm-lock.yaml` v9                                | `bun.lock` (JSONC)                                                  | `lpm.lock` (TOML, authoritative) plus `lpm.lockb` binary companion when the graph fits the binary format                                                                                                      |
| [Patched dependencies](/docs/packages/patch)                                        | —                                  | `pnpm patch` workflow                              | `bun patch` workflow                                                | Native `patchedDependencies`; reads pnpm's shape for migration                                                                                                                                                |
| [Dependency overrides](/docs/reference/package-json-lpm#overrides--resolutions)     | Yes (`overrides` field, npm 8+)    | Yes                                                | Yes                                                                 | Honors top-level npm `overrides`, Yarn `resolutions`, and `lpm.overrides`; `lpm migrate` translates `pnpm.overrides`                                                                                          |
| [Native dependency build-artifact cache](/docs/packages/rebuild#native-build-cache) | —                                  | Side-effects cache, enabled by default             | —                                                                   | Automatic for eligible strict-sandbox builds on macOS and Linux; content-addressed artifacts are reused only when package graph, platform, Node runtime/ABI, environment, sandbox, and toolchain inputs match |

## Developer orchestration and local infrastructure [#developer-orchestration-and-local-infrastructure]

This is the most lopsided table — three of the four columns are blank for several rows.

| Axis                                                                                           | npm                    | pnpm                    | bun                                                      | lpm                                                                                                                                                                                                         |
| ---------------------------------------------------------------------------------------------- | ---------------------- | ----------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Secrets vault (E2E-encrypted, secure-store preferred, cloud sync)](/docs/infra/secrets-vault) | —                      | —                       | Auto-loads `.env`, no vault                              | Per-project and per-environment; prefers the OS secure store with encrypted-file fallback; org sharing, OIDC CI escrow, and client-direct Vercel, Coolify, Fly.io, Railway, and GitHub Actions integrations |
| [Zero-config dev server orchestrator](/docs/guides/zero-config-dev-server)                     | —                      | —                       | —                                                        | Auto-installs stale deps, loads env, starts services, opens the browser when ready, and starts LPM CLI's browser webhook inspector when tunneling                                                           |
| [Remote task cache](/docs/dev/task-runner#remote-cache)                                        | Use Turborepo / Nx     | Use Turborepo / Nx      | Use Turborepo / Nx                                       | Local task cache + hosted remote cache; content hashes always, optional HMAC signing via `remoteCache.signature`, read-only mode, quota / usage status, and env-policy upload guards                        |
| [Local HTTPS with dev-only certificate authority](/docs/infra/local-https)                     | Use `mkcert`           | Use `mkcert`            | Use `mkcert`                                             | Built-in CA: status / trust / uninstall / generate. Picked up automatically by the dev server.                                                                                                              |
| [Local tunneling (public URL to localhost)](/docs/infra/tunneling)                             | Use `ngrok`            | Use `ngrok`             | Use `ngrok`                                              | Stable claimable domains, webhook capture, replay, and audit log                                                                                                                                            |
| [Managed runtime availability](/docs/dev/node-version-pinning)                                 | —                      | Node version pinning    | Bun is its own runtime                                   | Auto-installed Node and Bun via `lpm.json > runtime`; Node also falls back through `.nvmrc` and `.node-version`                                                                                             |
| [Managed linter / formatter](/docs/dev/builtin-tools)                                          | —                      | —                       | Formatter exists in the runtime, not the install surface | oxlint + biome as lazy-downloaded plugins, pinned via `lpm.json > tools`                                                                                                                                    |
| [One-off package execution (npx-equivalent)](/docs/dev/dlx)                                    | `npx`, permanent cache | `pnpm dlx`, 1-day cache | `bunx`                                                   | `lpm dlx` / `lpx`, 1-day cache                                                                                                                                                                              |
| [Built-in bundler](/docs/dev/bundle)                                                           | —                      | —                       | Yes                                                      | No native bundler; `lpm bundle` uses managed Rolldown, while `lpm pack` wraps project-local tsdown                                                                                                          |
| Built-in test runner                                                                           | —                      | —                       | Yes                                                      | No native test engine; `lpm test` orchestrates Vitest / Jest / Mocha                                                                                                                                        |

## Code and package operations [#code-and-package-operations]

| Axis                                                                                                                        | npm                    | pnpm                            | bun                               | lpm                                                                                                                                                               |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Source-code delivery (copy package files into the project, shadcn-style)](/docs/packages/add)                              | —                      | —                               | —                                 | `lpm add` — works for any tarball-shipped package on any reachable registry (lpm.dev, npm, private via `.npmrc`)                                                  |
| [Dependency selector queries (CSS-like syntax over behavioral tags)](/docs/packages/query)                                  | —                      | —                               | —                                 | `lpm query` — `:eval`, `:scripts:not(:built)`, `:root > :network`, `#express`, `--assert-none` CI gate                                                            |
| [Dependency-graph visualization](/docs/packages/graph)                                                                      | `npm ls` (text)        | `pnpm list` (text)              | `bun pm ls` (text)                | `lpm graph` / `lpm ls` — HTML / JSON / dot output, depth filter, `lpm why <pkg>` reverse-lookup                                                                   |
| [Interactive upgrade picker](/docs/packages/upgrade)                                                                        | —                      | `pnpm update -i`                | `bun update --interactive`        | `lpm upgrade [pkg...]` (interactive by default on TTY) — targeted upgrades, peer-impact analysis                                                                  |
| [License inventory and policy gate](/docs/packages/licenses)                                                                | third-party tooling    | third-party tooling             | third-party tooling               | `lpm licenses` — local inventory, `--fail-on copyleft,missing`, exact `--deny <LICENSE>` gates                                                                    |
| Programmable resolution hooks (intercept `readPackage` / `afterAllResolved` at resolve time)                                | —                      | `.pnpmfile.cjs`                 | —                                 | —                                                                                                                                                                 |
| [Install-time policy extensions](/docs/infra/policy)                                                                        | —                      | —                               | —                                 | Local `package.candidate` extensions evaluate resolved packages before fetch/link and return `allow`, `warn`, or `block`; they do not mutate resolution           |
| [Workspaces (monorepo)](/docs/packages/workspaces)                                                                          | `--workspace` selector | Rich `--filter` pattern syntax  | `--workspace`, `--all-workspaces` | pnpm-style filter grammar                                                                                                                                         |
| [Catalog version pins for monorepos (one declaration, every member references it)](/docs/packages/workspaces#catalogs)      | —                      | `pnpm-workspace.yaml > catalog` | Catalogs in lockfile              | `package.json > catalogs`, plus pnpm-style `pnpm-workspace.yaml` catalogs and cleanup                                                                             |
| [Workspace-package deploy (extract one member + transitive deps to a deployable dir)](/docs/packages/workspaces#lpm-deploy) | —                      | `pnpm deploy`                   | —                                 | `lpm deploy`                                                                                                                                                      |
| [Migration from other package managers](/docs/migrating)                                                                    | —                      | —                               | Reads npm + yarn lockfiles        | One-command lockfile migration from npm / pnpm / Yarn / Bun; pnpm migration also translates overrides, patches, and peer rules                                    |
| [Swift Package Manager (SE-0292)](/docs/packages/swift-package-registry)                                                    | —                      | —                               | —                                 | SE-0292-compatible after LPM CLI configures the scope; certificate/trust setup occurs on the first Swift install; CMS signing is available on a best-effort basis |

## Publishing and registry [#publishing-and-registry]

| Axis                                                                                   | npm                                     | pnpm                           | bun                                 | lpm                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [First-party hosted registry](/docs/registries)                                        | npmjs.org (public and private packages) | —                              | —                                   | LPM.dev Registry: private by default, with Pool and Marketplace as explicit opt-ins. Every upload gets validation, quality scoring, OSV vulnerability scanning where dependency data is available, and distribution checks. Pool and Marketplace versions stay pending until mandatory AI publication review approves them, then queue API-doc extraction and optional AI enrichment; private versions are excluded from AI/enrichment processing. |
| [Sigstore provenance signing on publish](/docs/packages/publish#provenance)            | `--provenance` flag                     | Auto in CI when OIDC available | —                                   | `--provenance` flag + auto-OIDC token exchange                                                                                                                                                                                                                                                                                                                                                                                                     |
| [OIDC trusted-publisher token exchange](/docs/packages/publish#npm-trusted-publishing) | GitHub Actions                          | GitHub Actions + GitLab CI     | —                                   | GitHub Actions + GitLab CI                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [2FA on publish](/docs/packages/publish#authentication-and-otp)                        | OTP                                     | OTP                            | `--otp` flag                        | OTP                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [`.npmrc` scope-to-registry routing](/docs/registries#pointing-at-a-private-registry)  | Yes                                     | Yes                            | Yes                                 | Yes — mappings are honored for npm-compatible private registries and JSR mirrors; per-origin authentication is supported                                                                                                                                                                                                                                                                                                                           |
| [Auth-token storage](/docs/infra/authentication#token-storage)                         | `.npmrc` plain text                     | `.npmrc` plain text            | `.npmrc` / `bunfig.toml` plain text | OS keychain preferred; AES-256-GCM encrypted-file fallback; `.npmrc` honored for compatibility                                                                                                                                                                                                                                                                                                                                                     |
| [Default registry routing](/docs/registries#default-routing)                           | npmjs.org                               | npmjs.org                      | npmjs.org                           | npm packages go directly to npmjs.org; native `jsr:` dependencies go to npm.jsr.io; only `@lpm.dev/*` always routes through lpm.dev                                                                                                                                                                                                                                                                                                                |

## Platform [#platform]

| Axis                                   | npm                  | pnpm                            | bun                      | lpm                                                                                                                  |
| -------------------------------------- | -------------------- | ------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| macOS (Intel + Apple Silicon)          | Yes                  | Yes                             | Yes                      | Yes                                                                                                                  |
| Linux (glibc)                          | Yes                  | Yes                             | Yes                      | Yes                                                                                                                  |
| Linux (musl / Alpine)                  | Yes                  | Yes                             | Yes                      | Yes — official x64 binary through npm, the standalone installer, and GitHub Releases; ARM64 requires a source build  |
| Windows x64                            | Yes                  | Yes                             | Yes                      | Yes                                                                                                                  |
| FreeBSD                                | Yes                  | Yes                             | —                        | —                                                                                                                    |
| [Self-update](/docs/infra/self-update) | `npm install -g npm` | `pnpm self-update`              | `bun upgrade`            | `lpm self-update` — install-method-aware (npm / Homebrew / Cargo / standalone GitHub Releases); 24-hour banner check |
| Implementation language                | JavaScript (Node.js) | TypeScript (Rust port underway) | Zig (Rust port underway) | Rust                                                                                                                 |

## See also [#see-also]

* [Migrating to LPM CLI](/docs/migrating) — one-command path from any of the above
* [Secrets vault](/docs/infra/secrets-vault) — the storage and encryption design
* [Zero-config dev server](/docs/guides/zero-config-dev-server) — what `lpm dev` auto-detects
* [Tunneling](/docs/infra/tunneling) — public URLs to localhost
* [Local HTTPS](/docs/infra/local-https) — the dev certificate authority
* [Package audit](/docs/packages/security-audit) — the audit pipeline


# LPM CLI developer resources (/docs/developer-resources)



Use this page to find developer and agent entry points for LPM CLI and `cli.lpm.dev`.

LPM CLI is a local executable, not an HTTP API. The OpenAPI file on this site describes only the public documentation search API. It does not describe the LPM.dev Registry API.

```bash
curl -sS https://cli.lpm.dev/openapi.json
curl -sS https://cli.lpm.dev/.well-known/api-catalog
curl -sS https://cli.lpm.dev/llms.txt
```

## Machine-readable entry points [#machine-readable-entry-points]

| Resource           | URL                                                                            | Use                                                                                                     |
| ------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| OpenAPI 3.1        | [`/openapi.json`](/openapi.json)                                               | Discover the public `cli.lpm.dev` documentation search API.                                             |
| API catalog        | [`/.well-known/api-catalog`](/.well-known/api-catalog)                         | Discover APIs through an RFC 9727 linkset.                                                              |
| Agent index        | [`/llms.txt`](/llms.txt)                                                       | Find documentation pages and primary machine-readable resources.                                        |
| Full documentation | [`/llms-full.txt`](/llms-full.txt)                                             | Load the complete LPM CLI documentation corpus as markdown.                                             |
| Markdown page      | Append `.mdx` to a documentation URL                                           | Load one page as markdown. For example, use [`/docs/packages/install.mdx`](/docs/packages/install.mdx). |
| Agent Skills       | [`/.well-known/agent-skills/index.json`](/.well-known/agent-skills/index.json) | Discover installable LPM CLI instructions for agents.                                                   |
| Sitemap            | [`/sitemap.xml`](/sitemap.xml)                                                 | Find canonical HTML documentation pages.                                                                |

HTML documentation pages also support content negotiation. Send `Accept: text/markdown` to receive the markdown version.

```bash
curl -sS -H 'Accept: text/markdown' https://cli.lpm.dev/docs/packages/install
```

## LPM CLI documentation search API [#lpm-cli-documentation-search-api]

The search endpoint returns public documentation pages, headings, and text fragments. It does not require authentication.

```bash
curl -sS --get https://cli.lpm.dev/api/v1/search \
  --data-urlencode 'query=install' \
  --data-urlencode 'limit=10'
```

Each result contains an `id`, `url`, `type`, and `content` value. A result can also contain `breadcrumbs`. The `type` value is `page`, `heading`, or `text`.

Use [`/openapi.json`](/openapi.json) as the authoritative HTTP contract for this endpoint.

## Versioning and deprecation [#versioning-and-deprecation]

The API version is part of each stable URL. The current stable version is `v1`.

Compatible changes can add response fields or optional parameters to `v1`. An incompatible change uses a new `/api/vN` path.

The unversioned `/api/search` alias was deprecated on August 24, 2026. Use `/api/v1/search` for new integrations.

The alias returns the [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745) deprecation date and a link to this policy.

```http
Deprecation: @1787529600
Link: <https://cli.lpm.dev/docs/developer-resources#versioning-and-deprecation>; rel="deprecation"; type="text/html"
```

A deprecated version remains available for at least 12 months after its `Deprecation` date.

If removal is scheduled, responses include an [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594) `Sunset` header at least six months before removal. This page will include migration steps.

## Rate limits [#rate-limits]

The search API uses a fixed quota of 60 `GET` requests per 60 seconds. The quota applies to each client network source.

The versioned route and the deprecated alias share the same quota. Each accepted `GET` request consumes one quota unit.

Each running site process tracks its own quota. Use the fields on each response as the current throttling guidance.

Responses include the current `RateLimit` and `RateLimit-Policy` fields from `draft-ietf-httpapi-ratelimit-headers-11`. The IETF document is an active Internet-Draft.

```http
RateLimit-Policy: "docs-search";q=60;w=60
RateLimit: "docs-search";r=42;t=30
```

The `r` parameter is the available quota. The `t` parameter is the effective window in seconds.

Responses also include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` for clients that use the legacy fields.

If the quota is exhausted, the API returns HTTP `429` with an RFC 9457 response. The response includes `Retry-After` and recovery details.

```json
{
  "type": "https://iana.org/assignments/http-problem-types#quota-exceeded",
  "title": "Too many requests",
  "status": 429,
  "detail": "The documentation search quota is exhausted.",
  "instance": "/api/v1/search",
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "The documentation search quota is exhausted.",
  "resolution": "Wait 30 seconds, then retry the request.",
  "documentation_url": "https://cli.lpm.dev/docs/developer-resources",
  "violated-policies": ["docs-search"]
}
```

## JSON errors [#json-errors]

API errors use [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457). Each error also contains a stable `code`, a `message`, a `resolution`, and a `documentation_url`.

```json
{
  "type": "about:blank",
  "title": "API route not found",
  "status": 404,
  "detail": "No API route exists at /api/not-a-route.",
  "instance": "/api/not-a-route",
  "code": "API_ROUTE_NOT_FOUND",
  "message": "No API route exists at /api/not-a-route.",
  "resolution": "Read https://cli.lpm.dev/openapi.json and use a documented path.",
  "documentation_url": "https://cli.lpm.dev/docs/developer-resources"
}
```

## Authentication [#authentication]

The documentation search API is public. It does not accept an LPM CLI token.

For package and registry authentication, read [LPM CLI authentication](/docs/infra/authentication). For local credentials and token rotation, read [`lpm login`](/docs/infra/login) and [`lpm token-rotate`](/docs/infra/token-rotate).

## MCP and Agent Skills [#mcp-and-agent-skills]

Use [`lpm mcp`](/docs/reference/mcp-servers) to connect supported editors to the LPM.dev Registry MCP server. This MCP server gives an agent registry search, package details, install hints, and audit tools.

Use [LPM CLI Agent Skills](/docs/reference/ai-agent-skills) for reusable instructions. LPM CLI can install skills from packages, Git repositories, URLs, and local files.

## Webhook development [#webhook-development]

Use [`lpm tunnel`](/docs/infra/tunnel#receive-webhooks-on-localhost) to expose a local callback URL. The tunnel can capture and replay webhook requests during development.

The `cli.lpm.dev` documentation site does not receive product webhooks.

## See also [#see-also]

* [LPM CLI command index](/docs/commands)
* [LPM CLI configuration reference](/docs/reference)
* [LPM.dev Registry and Pro](/docs/lpm-dev-and-pro)


# Your first install (/docs/first-install)





This page assumes you have [LPM CLI installed](/docs/installation). It will walk through installing a real package and getting a feel for how LPM CLI lays things out.

## A new project from scratch [#a-new-project-from-scratch]

```bash
mkdir hello-lpm && cd hello-lpm
lpm init -y
```

`lpm init -y` creates a minimal `package.json`. Without `-y` it prompts for package target, name, version, and a few defaults.
The default target is an LPM.dev Registry package; use `lpm init --npm` when the package itself should be npm-compatible.

## Install a dependency [#install-a-dependency]

```bash
lpm install zod
```

```text title="Output"
› Resolving dependencies from npmjs.org
› Installing 1 package

+ zod@4.4.3

✓ Done · added 1 package in 1.32s
```

Open `package.json` — `zod` is now under `dependencies` with a caret range:

```json
{
  "dependencies": {
    "zod": "^4.4.3"
  }
}
```

The resolved version and timing will change as new releases are published and your local store warms up.

LPM CLI's [save policy](/docs/packages/save-policy) writes `^resolvedVersion` by default. To save exact, append `--exact` (or set `save-exact = true` in `~/.lpm/config.toml`).

A new lockfile appears next to your `package.json`, with an optional binary companion:

* **`lpm.lock`** — human-readable, git-diffable lockfile. Commit this.
* **`lpm.lockb`** — generated binary companion when the graph fits the binary format. Commit this too when it appears.

`node_modules/` starts **hoisted by default** in LPM CLI's v2 virtual-store layout. Root direct deps are exposed at project `node_modules/<dep>`; transitive/package-local links live inside shared store entries. Workspaces auto-flip to isolated (pnpm-style symlinks), and default installs with incompatible peer requirements auto-switch to isolated. Override per-invocation with `--linker=isolated|hoisted`.

## Install everything from a manifest [#install-everything-from-a-manifest]

If you cloned a project that already has a `package.json`:

```bash
lpm install
```

Resolves and installs every dep declared in `package.json`. At a workspace root, this also installs every member in dependency order before installing the root. If `lpm.lock` exists and is consistent with the manifest, the install is reproducible — the lockfile pins every transitive version and integrity hash.

## Run a script [#run-a-script]

Add a script to `package.json`:

```json
{
  "scripts": {
    "hello": "echo 'hi from lpm'"
  }
}
```

```bash
lpm run hello
# hi from lpm
```

Or just `lpm hello` — `lpm` falls through to scripts as a top-level command, like `npm run` shorthand.

## Look around the global store [#look-around-the-global-store]

Every package LPM CLI downloads goes into a single content-addressable store at `~/.lpm/store/`. Subsequent installs across projects reuse canonical content and graph-keyed link entries. Package files exposed through `node_modules` use copy-on-write clones/reflinks where available and independent copies elsewhere, so project writes cannot alter the canonical store bytes.

```bash
lpm store verify        # fast structural and CAS metadata check
lpm store verify --deep # also rehash experimental v3 CAS blobs
lpm cache prune         # preview orphan entries no project references
lpm cache prune --apply # actually remove them
```

See [Content-addressable store](/docs/packages/content-addressable-store) for the design.

## What now [#what-now]

<Cards>
  <Card title="Project setup" href="/docs/project-setup" description="package.json, lpm.json, and lpm.toml — what each file is for." />

  <Card title="lpm install" href="/docs/packages/install" description="The full install command, every flag." />

  <Card title="Migrating" href="/docs/migrating" description="Bring an existing npm/pnpm/yarn/bun project over." />

  <Card title="Commands" href="/docs/commands" description="One-page index of every LPM CLI command." />
</Cards>


# LPM CLI (/docs)





LPM CLI is a package manager and development toolkit for modern software. One binary, written in Rust, drop-in compatible with the npm ecosystem.

```bash
npm install -g @lpm-registry/cli
```

## Works with any registry [#works-with-any-registry]

LPM CLI is **not tied to LPM.dev Registry**. By default it talks straight to `registry.npmjs.org` for everything you'd `npm install` today. Point it at a private registry through `.npmrc`. Use LPM.dev Registry when you want private, Pool, or Marketplace packages — see [Registries](/docs/registries) for the full picture.

## Why LPM CLI [#why-lpm-cli]

* **Fast.** Cold install on the VitePress docs fixture is **2,945 ms** (vs npm 17,354 ms / pnpm 6,125 ms / bun 2,455 ms). Warm install is **387 ms**. Up-to-date install is **14 ms**. See the [benchmarks](https://github.com/lpm-dev/rust-client#benchmarks).
* **Safe by default.** Lifecycle scripts are blocked until you approve them. Installs apply integrity, provenance, and script policy gates. Typosquat checks, source analysis, and a release cooldown are opt-in. Audits are available on demand.
* **All-in-one.** Package management, task runner, dev server, test runner, lint, format, secrets vault, dev tunnels, local HTTPS — one tool, no wrapper churn.
* **Compatible.** Reads `package.json`. Resolves npm semver. Materializes `node_modules` (hoisted by default, isolated for workspaces or peer conflicts). `lpm install`, `lpm add`, `lpm run` work the way you expect.

## Explore [#explore]

<Cards>
  <Card title="Packages" href="/docs/packages" description="Install, publish, audit, and manage dependencies. Resolver, lockfile, store, and security." />

  <Card title="Dev" href="/docs/dev" description="Zero-config dev server, task runner, test + bench auto-detection, built-in linter and formatter." />

  <Card title="Infra" href="/docs/infra" description="Tunneling, local HTTPS, secrets vault, port management, dependency graph, health checks." />

  <Card title="Guides" href="/docs/guides" description="Task-oriented walkthroughs: publishing, monorepos, CI/CD, migrating from npm/pnpm." />

  <Card title="Commands" href="/docs/commands" description="Every command on one page, grouped by category." />

  <Card title="Reference" href="/docs/reference" description="Configuration files, authentication, editor integrations, file formats, exit codes, glossary." />

  <Card title="Developer resources" href="/docs/developer-resources" description="OpenAPI, agent indexes, search API, authentication, MCP, Agent Skills, and webhooks." />
</Cards>

## Next steps [#next-steps]

<Cards>
  <Card title="Installation" href="/docs/installation" description="Install LPM CLI via npm, Homebrew, curl, or cargo." />

  <Card title="Your first install" href="/docs/first-install" description="Install your first package and look around." />

  <Card title="Registries" href="/docs/registries" description="How LPM CLI routes to npm, LPM.dev Registry, and private registries." />

  <Card title="LPM.dev Registry and Pro" href="/docs/lpm-dev-and-pro" description="What the hosted registry, Pool, Marketplace, and Pro plan add to LPM CLI." />

  <Card title="Migrating" href="/docs/migrating" description="Move an existing project from npm, pnpm, yarn, or bun." />
</Cards>


# Installation (/docs/installation)





LPM CLI ships as a single binary through npm, Homebrew, a standalone installer, and Cargo. Stable is the default release channel. Nightly snapshots are also available through npm and the standalone installer.

## Do not use sudo [#do-not-use-sudo]

Run each installer and all LPM CLI user commands without `sudo`.

LPM CLI stores user state and project files for the current user. A full command under `sudo` can select root's state or create root-owned files in your project. LPM CLI rejects this privilege transition before it reads user state or changes project files.

Some commands need one operating-system change. Run the normal command as your user. LPM CLI elevates only the restricted helper for that change.

If a global npm install reports a permission error, configure a user-writable npm global prefix. You can also use Homebrew or the standalone installer.

An intentional root session is different from a user command through `sudo`. Root can manage root's own LPM CLI state when `SUDO_USER` is not set.

## npm [#npm]

```bash
npm install -g @lpm-registry/cli
```

Works on every currently supported platform, including Windows and x64 Alpine Linux. The npm package is a dependency-free launcher; npm installs the matching platform-native Rust package through `optionalDependencies`, then runs a small `postinstall` verifier that checks the native binary and wires the global command to it.

GNU Linux x64 and ARM64 binaries require glibc 2.28 or newer (Debian 10+, Ubuntu 20.04+, and RHEL-compatible 8+). Linux x64 musl distributions use a separate binary.

Install the latest nightly instead:

```bash
npm install -g @lpm-registry/cli@nightly
```

`latest` always points to stable; `nightly` points to the newest nightly snapshot.

If your npm config omits optional dependencies (`--omit=optional`, `--no-optional`, or a package-manager policy), reinstall with optional dependencies enabled. If install scripts are disabled (`--ignore-scripts`), the JS launcher stays in place and resolves the native package at runtime, but first-run validation and zero-overhead Unix linking are skipped.

## Homebrew (macOS, Linux) [#homebrew-macos-linux]

```bash
brew tap lpm-dev/lpm
brew install lpm
```

The formula auto-updates with every stable release.

## Standalone installer (macOS, Linux) [#standalone-installer-macos-linux]

```bash
curl -fsSL https://cli.lpm.dev/install | sh
```

Downloads the latest binary into `~/.lpm/bin` and adds it to your `PATH` (`.zshrc`, `.bashrc`, or `config.fish` depending on your shell). On Linux x64, the installer distinguishes glibc from musl and selects the matching binary automatically. Open a new terminal — or `source` the rc file — and you're done.

The installer always verifies the downloaded binary's SHA-256 digest against the release manifest. When `cosign` is available, it also authenticates that manifest against the release workflow's Sigstore identity; without `cosign`, the check proves consistency with the downloaded manifest but does not authenticate who produced it. The installer also refuses releases below its minimum security floor. Two recovery overrides exist for release incidents and broken bootstrap environments: `LPM_INSTALL_INSECURE=1` skips all integrity checks, while `LPM_INSTALL_MIN_VERSION_OVERRIDE=1` permits an older release but keeps the normal verification behavior.

For Windows, use the npm installer above.

Install the latest nightly:

```bash
curl -fsSL https://cli.lpm.dev/install | LPM_INSTALL_CHANNEL=nightly sh
```

Pin an exact stable or nightly GitHub release tag with `LPM_INSTALL_VERSION`:

```bash
curl -fsSL https://cli.lpm.dev/install | LPM_INSTALL_VERSION=v0.71.0-nightly.20260728.42.d82ceea sh
```

## From source (Cargo) [#from-source-cargo]

```bash
cargo install --git https://github.com/lpm-dev/rust-client lpm-cli
```

Builds from the latest `main`. Requires Rust 1.94 or newer.

On ARM64 Alpine Linux, install the native build prerequisites first:

```bash
apk add --no-cache build-base cmake perl pkgconf
```

ARM64 musl does not currently have an official prebuilt binary, so source installation is the supported fallback on that platform.

## Verify [#verify]

```bash
lpm --version
# lpm 0.41.0
```

```bash
lpm doctor
```

`lpm doctor` runs a project-and-environment health check (Node detection, registry reachability, store integrity, sandbox availability). Pass `--fix` to auto-repair what it can.

## Updating [#updating]

```bash
lpm self-update
```

The release channel is sticky: stable installations follow stable, while nightly installations follow nightly.

```bash
lpm self-update                    # next release on the installed channel
lpm self-update --channel nightly  # switch stable → nightly
lpm self-update --channel stable   # switch nightly → stable
```

An explicit nightly-to-stable switch is allowed even when the stable version is semantically lower than the installed nightly. npm users can make the same switches with `@nightly` and `@latest`.

Nightly releases are currently available through npm and standalone installs. Homebrew and Cargo installs stay on stable; `lpm self-update --channel nightly` reports that the channel is unsupported for those install methods.

## Uninstalling [#uninstalling]

```bash
# npm
npm uninstall -g @lpm-registry/cli

# Homebrew
brew uninstall lpm && brew untap lpm-dev/lpm

# Standalone
rm -rf ~/.lpm/bin/lpm
# Optional: also remove the global store and caches
rm -rf ~/.lpm
```

`~/.lpm/` holds the global content-addressable store, caches, and config. Removing it is safe — LPM CLI will recreate what it needs on the next install.

## What got installed [#what-got-installed]

| Path                 | Contains                                                                                                                                                   |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `~/.lpm/bin/lpm`     | The binary itself (or a shim from npm/Homebrew)                                                                                                            |
| `~/.lpm/store/`      | Content-addressable package store — extracted bytes (`objects/`) and per-graph wrapper directories (`links/`), shared across every project on this machine |
| `~/.lpm/cache/`      | Ephemeral metadata and download caches                                                                                                                     |
| `~/.lpm/global/`     | Globally-installed CLIs (`lpm install -g …`)                                                                                                               |
| `~/.lpm/config.toml` | User-level config (created on demand)                                                                                                                      |

See [Reference → Configuration](/docs/reference) for what lives where.

## Next [#next]

<Cards>
  <Card title="Your first install" href="/docs/first-install" description="A two-minute walkthrough." />

  <Card title="Registries" href="/docs/registries" description="How LPM CLI routes to npm, LPM.dev Registry, and private registries." />

  <Card title="Migrating" href="/docs/migrating" description="Move an existing project from npm, pnpm, yarn, or bun." />
</Cards>


# LPM.dev Registry and Pro (/docs/lpm-dev-and-pro)



`lpm` is the LPM CLI command. LPM.dev Registry is the hosted registry and platform layer behind the `@lpm.dev/*` scope.

LPM CLI is registry-agnostic. It installs npm packages from npm, honors private registries in `.npmrc`, and only routes `@lpm.dev/*` packages through LPM.dev Registry by default. Use LPM.dev Registry when you want hosted private packages, Swift packages, Pool distribution, Marketplace sales, or the cloud-backed developer platform features.

## What LPM.dev Registry adds [#what-lpmdev-registry-adds]

```bash
lpm login
lpm publish
```

Publishing to LPM.dev Registry creates a private package by default. From there, you can keep it private for yourself or your team, switch it to Pool, or sell it through the Marketplace.

Every accepted upload receives baseline Registry processing:

* Archive, manifest, and package-configuration validation.
* Quality scoring for JavaScript and Swift packages.
* OSV vulnerability scanning where dependency data is available.
* Dependency and access checks for the package's selected distribution mode.

Pool and Marketplace versions also enter public publication review. Their new versions remain pending while LPM.dev Registry performs the public-only AI review, reviews any bundled Agent Skills, and prepares public package metadata. Private versions do not wait for that public review and remain available only to authorized users.

## Distribution modes [#distribution-modes]

| Mode        | Who can install                            | Best fit                                                 |
| ----------- | ------------------------------------------ | -------------------------------------------------------- |
| Private     | You, or members of the owning organization | Internal packages, paid-client work, early versions      |
| Pool        | Pool subscribers                           | Broadly useful packages that should earn from real usage |
| Marketplace | License owners                             | Commercial packages where you set the price              |

All LPM.dev Registry packages start private. Pool and Marketplace are explicit per-package choices.

## Free vs Pro [#free-vs-pro]

| Capability                          | Free                      | Pro                      |
| ----------------------------------- | ------------------------- | ------------------------ |
| Price                               | $0                        | $8/mo                    |
| Private packages                    | 2                         | Unlimited                |
| Package storage                     | 100 MB                    | 5 GB                     |
| Marketplace platform fee            | 15%                       | 10%                      |
| Firewall for public npm packages    | -                         | Included                 |
| Local env storage                   | Included                  | Included                 |
| Cloud env sync                      | -                         | Included                 |
| Platform env push and OIDC CI pulls | -                         | Included                 |
| Remote task cache                   | -                         | 25 GB                    |
| Ephemeral tunnel URLs               | Included, 1-hour sessions | Included                 |
| Concurrent tunnels                  | 1                         | 3                        |
| Included tunnel requests            | 20,000/UTC month          | 100,000/billing period   |
| Account-wide tunnel rate            | 4,000/min                 | 20,000/min               |
| Per visitor IP within the account   | 600/min                   | 600/min                  |
| Tunnel requests beyond allowance    | Hard stop                 | $1/100,000, proportional |
| Claimed tunnel domains              | -                         | 3                        |
| Tunnel auth                         | -                         | Included                 |
| Webhooks, analytics, audit logs     | -                         | Included                 |

Pro tunnel overage is enabled by default. Turn it off from the personal tunnel dashboard when you prefer a hard stop at 100,000 requests. There is no retroactive billing; paid usage follows the active Stripe subscription period.

Organizations use separate org billing: $8 per seat, unlimited private packages, 5 GB package storage per seat, 25 GB remote cache per seat, 10 concurrent tunnels, and 10 claimed tunnel domains. When a tunnel usage period is first opened, each billed Stripe seat in that snapshot adds 100,000 included requests and 20,000 requests/minute to the organization account for the rest of the period. Organization overage uses the same proportional $1/100,000 rate and default-on toggle.

Tunnel request limits are account-wide. Three Pro tunnels share one 100,000-request allowance; the allowance is not multiplied by the number of tunnels or domains. See [Tunneling](/docs/infra/tunneling#request-usage-and-overage) for exactly what counts.

## The Pool [#the-pool]

The Pool is LPM.dev Registry's usage-based funding model for packages that should be broadly available but still economically sustainable. Subscribers pay one flat monthly fee for access to Pool packages. LPM.dev Registry distributes the author share to the packages those subscribers actually install, then follows the dependency tree with depth weighting, so maintainers of low-level transitive dependencies can earn when real projects depend on their work.

That matters more in the AI era: packages are increasingly discovered and installed by tools, agents, and automated workflows, not by humans clicking donation links. The Pool moves funding closer to the place where value is actually used: the install graph. It is not a complete answer to open source funding, but it is a more structural one than donations, sponsorship buttons, or one-off marketplace sales alone.

Pool access is separate from Pro. Pro unlocks creator and platform features. Pool is a $12/mo subscription that unlocks access to Pool packages and funds authors through usage-based revenue sharing.

## Marketplace [#marketplace]

Marketplace packages are for direct sales. You set the price, buyers receive a license, and only license owners can install the package. Free publishers pay a 15% platform fee; Pro and Org publishers pay 10%.

Use Marketplace for commercial or proprietary packages. Use Pool when the package benefits from broad access and usage-based revenue.

## See also [#see-also]

* [Registries](/docs/registries) - how LPM CLI routes npm, private registries, and LPM.dev Registry.
* [`lpm publish`](/docs/packages/publish) - publish JavaScript and Swift packages.
* [`lpm pool`](/docs/packages/pool) - view Pool revenue stats from the CLI.
* [Environment variables and secrets](/docs/infra/secrets-vault) - local secrets, cloud sync, and platform pushes.
* [Tunneling](/docs/infra/tunnel) - ephemeral URLs, claimed domains, auth, and webhook capture.
* [lpm.dev pricing](https://lpm.dev/pricing) - current billing and plan details.
* [The Pool](https://lpm.dev/features/the-pool) - the full Pool model.


# Migrating to LPM CLI (/docs/migrating)





[`lpm migrate`](/docs/packages/migrate) is the one-command path from any npm-ecosystem project to LPM CLI. It auto-detects the source package manager, converts its lockfile to LPM CLI's TOML lockfile (`lpm.lock`) plus `lpm.lockb` when the graph fits the binary format, preserves your config, and runs a fresh install to verify nothing broke. Every file it touches is backed up first — `lpm migrate --rollback` reverts everything.

The migration shape is the same for every source. What differs is the lockfile parser and any source-specific config (most notably `pnpm.*` blocks → `lpm.*`).

## Sources supported today [#sources-supported-today]

| From                   | Source files                                                                  | Walkthrough                                             |
| ---------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------- |
| npm                    | `package-lock.json`                                                           | [Migrating from npm](/docs/guides/migrating-from-npm)   |
| pnpm                   | `pnpm-lock.yaml` (+ `pnpm-workspace.yaml`, `pnpm.*` blocks in `package.json`) | [Migrating from pnpm](/docs/guides/migrating-from-pnpm) |
| Yarn (classic + Berry) | `yarn.lock`                                                                   | Same flow — `lpm migrate` auto-detects                  |
| Bun                    | `bun.lock` / `bun.lockb`                                                      | Same flow — `lpm migrate` auto-detects                  |

## The 30-second version [#the-30-second-version]

```bash
lpm migrate --dry-run   # preview: parse the source lockfile, report what'll convert
lpm migrate             # do it: convert, install, verify
```

That's the whole flow. Each step is backed up; if anything goes wrong, `lpm migrate --rollback` walks the `.backup` files and restores the pre-migration state.

## What carries over without changes [#what-carries-over-without-changes]

* `package.json > dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies` — read identically.
* `package.json > scripts` — `lpm run <name>` and the bare-name shorthand (`lpm <name>`) work the same as `npm run` / `pnpm run`.
* `package.json > workspaces` — both array and object forms accepted. (`pnpm-workspace.yaml` is also read as a fallback.)
* `.npmrc` — scope-to-registry mappings and per-origin auth tokens carry over verbatim. LPM CLI honors `.npmrc` for routing across npm, LPM.dev Registry, and private registries.
* `package.json > overrides` (npm-style) and `resolutions` (yarn-style) — honored as-is.

## What changes — be deliberate [#what-changes--be-deliberate]

These behave differently than your previous PM:

| Difference                                                                                                                                                                                                                                    | What to do                                                                                                                                                                                                                                                                       |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Dependency lifecycle scripts deny-by-default.** Bare `lpm install` runs the root project's lifecycle, including `prepare`, but dependency `postinstall` scripts stay blocked.                                                               | Run [`lpm rebuild`](/docs/packages/rebuild) once after migration, then [`lpm approve-scripts`](/docs/packages/approve-scripts) to whitelist packages that need to run scripts. Or set `package.json > lpm > scriptPolicy = "allow"` for npm-classic dependency-script semantics. |
| **`node_modules` layout differs.** Single packages start in LPM CLI's v2 **hoisted** virtual-store layout. Workspaces auto-flip to **isolated** (pnpm-style symlinks), and default installs with peer conflicts also auto-switch to isolated. | Phantom-dep code (importing a package you didn't declare in `dependencies`) breaks under isolated. Add the missing entries; or override per-project with `package.json > lpm > linker = "hoisted"`.                                                                              |
| **Optional release cooldown.** The cooldown is off by default. You can enable it for direct dependencies or select strict scope for transitives.                                                                                              | `lpm config release-age --set 1d`, or `package.json > lpm > minimumReleaseAge` / `minimumReleaseAgePolicy` per-project.                                                                                                                                                          |
| **`engines` enforced by default.** npm reads `engines` but only enforces with `engine-strict=true` in `.npmrc`; LPM CLI also checks selected dependencies' `engines.node`.                                                                    | Set `package.json > lpm > engineStrict = false` for warning-only behavior.                                                                                                                                                                                                       |
| **`pnpm.overrides` / `pnpm.patchedDependencies` / `pnpm.peerDependencyRules`**                                                                                                                                                                | Auto-translated by `lpm migrate` to `lpm.overrides` / `lpm.patchedDependencies` / `lpm.peerDependencyRules`. The original `pnpm.*` blocks stay in place so a parallel `pnpm install` keeps working during transition.                                                            |

## After the migration [#after-the-migration]

```bash
lpm install --offline       # confirms reproducibility from the new lockfile
lpm test                    # confirms nothing broke under isolated layout / deny-by-default
lpm lint
lpm fmt --check
```

Commit `lpm.lock`, `lpm.lockb` when present, the updated `.npmrc`, and the (possibly mutated) `package.json`. Discard the source lockfile (`package-lock.json` / `pnpm-lock.yaml` / `yarn.lock` / `bun.lock*`) only after everyone on the team has switched over — leaving it in place during transition lets developers keep using the old PM until they cut over.

## Walkthroughs [#walkthroughs]

<Cards>
  <Card title="Migrating from npm" href="/docs/guides/migrating-from-npm" description="package-lock.json → lpm.lock, plus lpm.lockb when representable." />

  <Card title="Migrating from pnpm" href="/docs/guides/migrating-from-pnpm" description="pnpm-lock.yaml + pnpm.* blocks → LPM CLI equivalents. Isolated layout preserved." />

  <Card title="Migrating from Yarn" href="/docs/guides/migrating-from-yarn" description="Yarn Classic or Berry yarn.lock → lpm.lock, with rollback." />

  <Card title="Migrating from Bun" href="/docs/guides/migrating-from-bun" description="bun.lock or bun.lockb → lpm.lock, with rollback." />
</Cards>

All paths use the same `lpm migrate` command and produce the same LPM CLI end state. The per-source guides call out parser-specific details.

## See also [#see-also]

* [`lpm migrate`](/docs/packages/migrate) — full flag reference (`--dry-run`, `--force`, `--rollback`, `--no-install`, `--skip-verify`, `--no-npmrc`, `--ci`, `--no-ci`)
* [npm compatibility](/docs/packages/npm-compatibility) — what carries over, what differs, what LPM CLI adds
* [Lockfile](/docs/packages/lockfile) — what the lockfile actually pins
* [Project setup](/docs/project-setup) — where post-migration config lives


# Project setup (/docs/project-setup)





LPM CLI splits project configuration across three files. Each owns a different kind of setting, by design:

| File                           | What it owns                                                                                                                                                          | Commit?     |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| [`package.json`](#packagejson) | Standard npm fields (dependencies, scripts, engines) **plus** an `"lpm"` block for project-shared LPM CLI behavior (script policy, trust, linker, sandbox, overrides) | Yes         |
| [`lpm.json`](#lpmjson)         | Dev-server, task-runner, env-file mapping, env schema, services, publish targets — runtime infrastructure that isn't publishable metadata                             | Yes         |
| [`lpm.toml`](#lpmtoml)         | Per-project CLI defaults and reviewable local policy exceptions — tool behavior, not publishable                                                                      | Team's call |

The split is deliberate: anything that ships to consumers of your package lives in `package.json`. Anything that's about running the project locally lives in `lpm.json`. Anything that's about which way you personally (or the team) prefer the CLI to behave lives in `lpm.toml`.

## `package.json` [#packagejson]

Most LPM CLI behavior keys live under the `"lpm"` block — committed alongside dependencies so every contributor picks them up:

```json title="package.json"
{
  "name": "my-app",
  "version": "1.0.0",
  "engines": { "node": ">=22.0.0", "lpm": ">=0.40.0" },
  "workspaces": ["packages/*"],

  "dependencies": { "react": "^19.0.0" },

  "lpm": {
    "linker": "isolated",
    "scriptPolicy": "deny",
    "trustedDependencies": ["esbuild", "sharp"],
    "scripts": {
      "autoBuild": false,
      "sandboxWriteDirs": ["build/"]
    }
  }
}
```

Full field reference: [`package.json` "lpm" key](/docs/reference/package-json-lpm).

## `lpm.json` [#lpmjson]

Optional. Sits next to `package.json` and configures runtime / dev infrastructure:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "runtime": { "node": ">=22.0.0", "bun": "1.3.14" },
  "tools": { "oxlint": "1.57.0", "biome": "2.4.8" },

  "env": {
    "dev":  ".env.development",
    "prod": ".env.production"
  },
  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url" }
    }
  },

  "tasks": {
    "build": {
      "command": "tsup",
      "dependsOn": ["^build"],
      "cache": true,
      "outputs": ["dist/**"]
    }
  },

  "services": {
    "db":  { "command": "docker compose up postgres", "readyPort": 5432 },
    "web": { "command": "next dev", "port": 3000, "primary": true }
  },

  "publish": {
    "registries": ["lpm", "npm"]
  }
}
```

Full field reference: [`lpm.json`](/docs/reference/lpm-json).

The `$schema` line is optional but enables inline validation and field completion in any JSON-schema-aware editor.

## `lpm.toml` [#lpmtoml]

Optional. Sits next to `package.json` and pins per-project CLI defaults. It owns the [save policy](/docs/packages/save-policy), workspace/test defaults, tidy ignores, sandbox defaults, and reviewable local policy exceptions:

```toml title="lpm.toml"
save-prefix = "~"     # team prefers tilde over caret
save-exact  = false

[[policy.typosquat.allow]]
package = "crossenv"
similar-to = "cross-env"
reason = "Internal migration package kept for compatibility"
```

Commit it for team-shared CLI defaults; `.gitignore` it if save policy should stay per-developer. The keys here override `~/.lpm/config.toml` (user-level) but lose to CLI flags.

Full field reference: [`lpm.toml`](/docs/reference/lpm-toml).

## Local configuration size limits [#local-configuration-size-limits]

LPM CLI bounds local configuration before UTF-8 decoding or parsing. The limit applies to the bytes read from the opened file, including the target of a supported symlink.

| Input                                                                                                                                                                                                                                                          | Limit                         |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| JSON, TOML, YAML, and dotenv configuration or manifests — including `package.json`, `lpm.toml`, `~/.lpm/config.toml`, `lpm.json`, `lpm.config.json`, `pnpm-workspace.yaml`, `.env*`, security/trust policy, and CLI-managed MCP, proxy, or Swift registry JSON | 16 MiB per file               |
| `.npmrc`                                                                                                                                                                                                                                                       | 1 MiB per configuration layer |
| File-referenced CA bundles, client certificates, and private keys                                                                                                                                                                                              | 1 MiB per file                |

A missing optional file keeps its normal default or fallback behavior. A file that exists but exceeds its limit is not treated as missing: LPM CLI returns an error naming the path and byte limit. Install-time configuration fails before dependency resolution or registry access, and run-time configuration fails before a script or child process is spawned. This prevents an oversized higher-precedence file from silently changing registry, source, or security-policy selection.

Malformed or semantically invalid `lpm.json` fails before any project-bound environment operation can mutate local or remote state. That includes local secret operations, named `--env` resolution, validation, aliases, inheritance, schema operations, OIDC policy environment resolution, and `lpm env push`, which needs the complete alias, inheritance, and schema metadata to preserve environment boundaries. `lpm env pull` and `lpm env rotate-key` validate before network access or local vault replacement; `lpm env share` validates before sharing-key classification or registration. Only account-level operations that do not consult project configuration, such as browser pairing and sharing-key rotation, are unaffected by an unrelated malformed file. Oversized or unreadable configuration remains a hard error whenever the command needs it.

These small-file limits do not apply to `lpm.lock` / `lpm.lockb`, package tarballs and downloaded archives, package source or build outputs, patch and skill source payloads, README content, growing task/audit/webhook logs, or operating-system pseudo-files such as `/proc`. Those inputs retain their own streaming, integrity, rotation, or format-specific controls.

## Safe atomic rewrites [#safe-atomic-rewrites]

LPM-managed atomic rewrites of project manifests, configuration, and state use an exclusively created, collision-resistant file in the destination directory. They do not follow an attacker-preplanted temporary symlink, hardlink, junction, or reparse entry. The final destination is replaced as a path entry rather than dereferenced, while existing regular-file modes and restricted `0600` outputs retain their intended Unix permissions.

The boundary is the audited atomic-rewrite paths. This does not provide arbitrary parent-directory confinement, cover every file written by LPM CLI or a package script, or imply crash durability unless a particular operation also synchronizes its file and parent directory.

## Which file owns what [#which-file-owns-what]

Quick decision table when you're not sure where a setting belongs:

| You want to…                                                  | Goes in                                                             |
| ------------------------------------------------------------- | ------------------------------------------------------------------- |
| Pin a runtime dep with a version range                        | `package.json > dependencies`                                       |
| Pin Node version for the project                              | `lpm.json > runtime.node` (or `.nvmrc` / `.node-version`)           |
| Declare compatible Node versions without selecting one        | `package.json > engines.node`                                       |
| Make Bun available to scripts                                 | `lpm.json > runtime.bun`                                            |
| Block dependency lifecycle scripts for everyone on the team   | `package.json > lpm.scriptPolicy = "deny"`                          |
| Approve a specific package to run scripts                     | `package.json > lpm.trustedDependencies` (or `lpm approve-scripts`) |
| Define a custom build task with caching                       | `lpm.json > tasks.<name>`                                           |
| Map `lpm run dev` to a specific `.env` file                   | `lpm.json > env.dev`                                                |
| Configure dev services with readiness checks                  | `lpm.json > services`                                               |
| Set a save-prefix that overrides every developer's preference | `lpm.toml > save-prefix`                                            |
| Allow an intentional suspicious package name                  | `lpm.toml > policy.typosquat.allow`                                 |
| Set your personal default save prefix machine-wide            | `~/.lpm/config.toml > save-prefix`                                  |
| Publish to multiple registries from one `lpm publish`         | `lpm.json > publish.registries`                                     |

## `.gitignore` essentials [#gitignore-essentials]

`lpm init` creates a `.gitattributes` entry for the binary lockfile automatically:

```text title=".gitattributes"
lpm.lockb binary
```

You probably want to ignore a few LPM CLI artifacts:

```text title=".gitignore"
node_modules/
.lpm/                  # local install hash, security caches, skills, tunnel state
.env*.local            # local-only env overrides
```

`.lpm/` holds project-local install state (`install-hash`, captured tunnel webhooks, agent skill markdown, etc.). The global store lives in `~/.lpm/`, completely separate — nothing project-specific belongs there.

## See also [#see-also]

* [`package.json` "lpm" key](/docs/reference/package-json-lpm) — every field under the `"lpm"` block
* [`lpm.json`](/docs/reference/lpm-json) — runtime / task / publish config
* [`lpm.toml`](/docs/reference/lpm-toml) — project-level CLI defaults
* [`~/.lpm/config.toml`](/docs/reference/config-toml) — user-level CLI defaults
* [Save policy](/docs/packages/save-policy) — full precedence chain across these files


# Registries (/docs/registries)



LPM CLI is registry-agnostic. It is **not** a client only for LPM.dev Registry — it is a general-purpose package manager with LPM.dev Registry support.

If you have an existing `package.json` full of `react`, `lodash`, `typescript`, and friends, LPM CLI installs them from `registry.npmjs.org` exactly like npm, pnpm, yarn, or bun would. Nothing has to change.

## Default routing [#default-routing]

LPM CLI resolves each package by name and routes accordingly:

| Package                                              | Goes to                                                   |
| ---------------------------------------------------- | --------------------------------------------------------- |
| `react`, `lodash`, `typescript`, …                   | `registry.npmjs.org`                                      |
| `@scope/anything` (except built-in scopes below)     | `registry.npmjs.org`, unless `.npmrc` overrides the scope |
| `jsr:@std/path` manifest specs (`@jsr/*` internally) | `https://npm.jsr.io`, unless `.npmrc` maps `@jsr`         |
| `@lpm.dev/*`                                         | LPM.dev Registry (`https://lpm.dev`; auth + monetization) |
| Anything that matches a registry mapping in `.npmrc` | The registry you mapped                                   |

The `@lpm.dev/*` scope is the **only** scope that always goes to LPM.dev Registry. JSR packages use JSR's npm-compatible registry. Everything else is npm by default.

## One dependency, one source [#one-dependency-one-source]

LPM CLI chooses a package's registry route before resolving it. A dependency does not fall through from a configured private index to npm, or from npm to LPM.dev Registry, when lookup fails. Missing, unauthorized, or invalid metadata from the selected registry is a hard error. This prevents dependency-confusion behavior where the same name silently resolves from a lower-priority index.

The selected logical source is recorded on the resolved package in `lpm.lock`, for example `registry+https://npm.my-company.com`. Scoped `.npmrc` mappings, the configured npm registry, and the active LPM.dev Registry origin are preserved exactly. The package identity used by lockfile evidence includes this source, so evidence from one registry cannot be replayed for a same-name package from another.

LPM CLI may use the LPM.dev Registry Worker as a transport proxy for public npm metadata and fall back to a direct npm request when that transport is unavailable. Both paths represent the same configured logical npm origin, so this is transport failover—not index fallback—and the lockfile records the npm origin rather than the Worker URL.

## JSR packages [#jsr-packages]

LPM CLI accepts `jsr:` dependency specs in `package.json` and resolves them through JSR's npm-compatible registry. The JSR package name stays as the local import name, while the metadata fetch uses JSR's folded npm package name under `@jsr`.

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

`"@std/path": "jsr:@std/path@^1.1.0"` fetches `@jsr/std__path` from `https://npm.jsr.io` and links it at `node_modules/@std/path`. Version-only specs such as `"@std/fs": "jsr:^1.0.0"` borrow the package name from the dependency key.

To mirror or intercept JSR traffic, map the `@jsr` scope explicitly:

```ini title=".npmrc"
@jsr:registry=https://npm-jsr-mirror.example.com
```

## Pointing at a private registry [#pointing-at-a-private-registry]

LPM CLI reads `.npmrc` project-local first, then user-level (`~/.npmrc`). Keep committed project `.npmrc` files to literal routing data, and put env-backed secrets in user or CI-owned config.

```ini title=".npmrc"
# A private registry for one scope
@my-company:registry=https://npm.my-company.com

# Override the default registry for everything else (optional)
registry=https://my-mirror.example.com
```

```ini title="~/.npmrc"
//npm.my-company.com/:_authToken=${NPM_TOKEN}
//my-mirror.example.com/:_authToken=${MIRROR_TOKEN}
```

`@my-company/internal-tool` will be fetched from `npm.my-company.com` with the configured auth token. Public packages still flow through the default registry. No CLI-specific config is required.

For per-environment tokens, prefer `${VAR}` expansion in trusted config over hardcoded secrets. LPM CLI refuses `${VAR}` expansion for registry URLs, auth values, proxy URLs, and TLS paths in project-local `.npmrc` because those files can be controlled by the repo you cloned. Move those entries to `~/.npmrc`, have CI generate literal-token config at runtime, or use the matching login command.

## Registry configuration size limits [#registry-configuration-size-limits]

Each `.npmrc` layer has a 1 MiB limit, and each file referenced by `cafile`, `certfile`, or `keyfile` has a 1 MiB limit. LPM CLI enforces these limits before parsing or building an HTTP client. Missing optional layers are still skipped, but an oversized or invalid UTF-8 `.npmrc` is a fatal configuration error: it is not ignored in favor of a lower-precedence registry. An oversized TLS file likewise fails with its path and byte limit, without logging tokens, certificates, keys, or file contents.

For installs, these errors surface before dependency resolution or any registry request. Inline `ca` / `cert` / `key` values are part of the containing `.npmrc` and therefore covered by that file's 1 MiB layer limit. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits) for non-registry config and explicit exclusions.

## Redirect transport policy [#redirect-transport-policy]

LPM CLI checks every automatically followed redirect before sending the next request. Once a request chain has used HTTPS, a redirect to HTTP is refused — including redirects to `localhost`, `127.0.0.1`, or `[::1]`. The command fails with a network or registry error containing `refused HTTPS-to-HTTP redirect`.

HTTPS-to-HTTPS redirects and HTTP-to-HTTPS upgrades remain allowed. HTTP-to-HTTP development flows can still begin from a directly configured loopback URL. For registry operations, `--insecure` can admit an explicitly configured HTTP endpoint outside loopback, but it never permits an HTTPS request to downgrade through a redirect.

`strict-ssl=false` changes certificate verification only; it does not weaken the redirect rule. This policy covers outbound HTTP clients used by LPM CLI itself. It does not make claims about arbitrary application URLs opened by project code or installed packages.

## TLS, custom CAs, and mTLS [#tls-custom-cas-and-mtls]

LPM CLI honors the same `.npmrc` TLS keys npm does. All settings can be set globally or per-origin (`//host/:key=value`); per-origin scoping wins for the matching host.

### Trusting a corporate CA [#trusting-a-corporate-ca]

```ini title=".npmrc"
# Add a CA to the trust store (file path or inline PEM).
cafile=/etc/ssl/corp-ca.pem
ca="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"

# Per-origin form: only this host trusts the extra CA.
//npm.internal/:cafile=/etc/ssl/corp-ca.pem
```

`cafile` and `ca` are **additive** — they extend the system trust store, never replace it. A globally-trusted CA still validates `registry.npmjs.org` normally.

### Disabling cert verification (escape hatch) [#disabling-cert-verification-escape-hatch]

```ini title=".npmrc"
strict-ssl=false
```

Disables hostname, expiry, and CA validation **process-wide**. It does not allow an HTTPS redirect to downgrade to HTTP. LPM CLI logs a loud warning at install start whenever `strict-ssl=false` is in effect — this is the kind of setting that should never accidentally land in CI.

Per-origin `//host/:strict-ssl=false` is **not** supported in v1; the parser warns and ignores it. Use `cafile` to scope a custom-CA trust addition instead.

### Mutual TLS (client certificates) [#mutual-tls-client-certificates]

For private registries that require client-cert authentication, point LPM CLI at a PEM cert and key:

```ini title=".npmrc"
# Global mTLS identity — applies to every request from the default client.
certfile=/etc/ssl/client.pem
keyfile=/etc/ssl/client.key

# Per-origin mTLS — only this host gets the client cert.
//npm.internal/:certfile=/etc/ssl/client.pem
//npm.internal/:keyfile=/etc/ssl/client.key
```

`certfile` and `keyfile` must always be set together — half-configured global identities abort the install with a cited error. Per-origin half-configurations only fail when that origin is actually used.

#### Path resolution [#path-resolution]

Relative paths in `certfile=` / `keyfile=` / `cafile=` resolve against the `.npmrc` file that contained them, not against the working directory you ran `lpm install` from. So in `~/.npmrc`:

```ini title="~/.npmrc"
certfile=client.pem      # → ~/client.pem
keyfile=client.key       # → ~/client.key
```

You can drop secrets next to the `.npmrc` and reference them with bare names.

#### Encrypted private keys [#encrypted-private-keys]

LPM CLI accepts encrypted PKCS#8 keys. The passphrase is resolved in this order:

1. `LPM_KEY_PASSPHRASE` environment variable (the CI-friendly path).
2. Interactive TTY prompt — only when both stdin AND stdout are a terminal.
3. Otherwise, hard error citing the keyfile line.

```bash
LPM_KEY_PASSPHRASE='hunter2' lpm install
```

The passphrase is cached in memory for the lifetime of the install — multiple per-origin clients sharing the same key file prompt at most once.

If your key uses the legacy PKCS#1 `Proc-Type: 4,ENCRYPTED` format, convert it first:

```bash
openssl pkcs8 -topk8 -in legacy.key -out key.pem
```

#### PKCS#12 (.p12 / .pfx) is not supported [#pkcs12-p12--pfx-is-not-supported]

LPM CLI uses rustls, which doesn't ingest PKCS#12 archives directly. Convert with openssl and point `certfile=` / `keyfile=` at the resulting PEMs:

```bash
openssl pkcs12 -in identity.pfx -clcerts -nokeys -out cert.pem
openssl pkcs12 -in identity.pfx -nocerts  -nodes  -out key.pem
```

Then:

```ini title=".npmrc"
certfile=/path/to/cert.pem
keyfile=/path/to/key.pem
```

If you point `certfile=` or `keyfile=` directly at a `.p12`/`.pfx` file, LPM CLI emits a cited error with this recipe inline.

## When does LPM.dev Registry come in [#when-does-lpmdev-registry-come-in]

LPM.dev Registry is a registry **plus** an auth and monetization layer. Use it when:

* You want **private packages** without standing up your own registry. Every published package starts private — only the publisher can install it. (Same shape as npm private packages.)
* You publish under your scope and want to flip a package to **pool** distribution. Metadata becomes public, but installs are gated to pool subscribers — you earn a share of the pool's revenue.
* You publish a paid package via **marketplace** distribution. Installs require a license purchase.

Setting `distribution` is a per-package opt-in on the publisher's side. As an installer, you simply `lpm install @lpm.dev/owner.package` — LPM CLI handles the auth, license, and download through LPM.dev Registry.

If you're not publishing, ignore LPM.dev Registry entirely — LPM CLI is a strict superset of npm-the-client and will never send your `react` install through it.

## Private packages on LPM.dev Registry [#private-packages-on-lpmdev-registry]

```bash
lpm login
lpm install @lpm.dev/acme.internal
```

`lpm login` stores a token in your OS keychain (Keychain on macOS, libsecret on Linux, Credential Manager on Windows). The token is scoped to LPM.dev Registry — no other registry sees it.

To generate `.npmrc` for an npm-compatible client in CI, provide `LPM_TOKEN`, an eligible stored login, or explicit OIDC:

```bash
lpm setup ci npmrc # generates an .npmrc for CI/CD use
```

For a protected project-local file, run `lpm setup local`. It creates a read-only project token for 30 days by default (1–90 days), preserves other `.npmrc` routes, writes only the scoped `@lpm.dev/*` route, and ensures `.npmrc` is ignored by git. See [`lpm setup`](/docs/infra/setup#local-authentication).

## Mixing registries in one project [#mixing-registries-in-one-project]

A `package.json` can pull from npm, LPM.dev Registry, and a private registry simultaneously — LPM CLI resolves each dep against the correct origin and writes the resolution to the lockfile.

```json title="package.json"
{
  "dependencies": {
    "react": "^19.0.0",
    "@my-company/internal-tool": "^2.1.0",
    "@lpm.dev/acme.private-utils": "^1.0.0"
  }
}
```

```ini title=".npmrc"
@my-company:registry=https://npm.my-company.com
```

```ini title="~/.npmrc"
//npm.my-company.com/:_authToken=${NPM_TOKEN}
```

```bash
lpm install
# react                          → registry.npmjs.org
# @my-company/internal-tool      → npm.my-company.com  (via .npmrc)
# @lpm.dev/acme.private-utils    → lpm.dev             (via stored token)
```

## See also [#see-also]

* [Installation](/docs/installation) — get the CLI on your machine
* [Authentication](/docs/infra/authentication) — token storage, scopes, rotation
* [`lpm install`](/docs/packages/install) — the install command
* [`lpm publish`](/docs/packages/publish) — publishing to LPM.dev Registry or npm


# lpm bench (/docs/dev/bench)



```bash
lpm bench [-- args...]
```

Detects whether the project uses `vitest bench` and runs it, forwarding any trailing arguments verbatim.

## Examples [#examples]

```bash
lpm bench
lpm bench src/parser.bench.ts
lpm bench -- --reporter=verbose
```

## Detection [#detection]

If `vitest` is in `dependencies` or `devDependencies`, `lpm bench` runs `vitest bench` with whatever args you pass. Otherwise, if `package.json` has a `bench` script, `lpm bench` runs that script via the shell. The script fallback is only consulted when vitest is not declared. Use it for non-vitest benchmark frameworks (mitata, tinybench standalone, hyperfine).

## Argument forwarding [#argument-forwarding]

Anything after the command (or after `--`) is forwarded to the runner:

```bash
lpm bench -- --reporter=json --outputFile=bench.json
```

## Workspaces [#workspaces]

```bash
lpm bench --all                            # every member
lpm bench --filter web                     # exact name
lpm bench --filter '@scope/*'              # glob
lpm bench --filter-prod ...shared          # prod graph closure
lpm bench --affected --base develop
lpm bench --filter './packages/*' --workspace-concurrency 2
```

Detection runs per member. A workspace member with no installed bench runner and no `scripts.bench` becomes a per-member detection failure in the JSON envelope rather than aborting the whole run. `--all` is mutually exclusive with filters and `--affected`.

`--filter` and `--filter-prod` compose with `--affected` the same way as [`lpm test`](/docs/dev/test): the affected set is unioned with the filter result. `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

`--workspace-concurrency <N>` caps how many selected workspace members run at once within each topological level.

### Forwarding runner flags with the same names [#forwarding-runner-flags-with-the-same-names]

The workspace flags (`--all`, `--filter`, `--filter-prod`, `--affected`, `--base`, `--fail-if-no-match`, `--workspace-concurrency`) are claimed by LPM CLI. To pass any of them through to the bench runner itself, put them after `--`:

```bash
lpm bench -- --filter pattern             # forwards --filter to vitest bench
lpm bench --filter web -- --reporter=json # workspace + forwarded args
```

### Watch with a workspace selector [#watch-with-a-workspace-selector]

Same gating as `lpm test`: watch is allowed when the selection resolves to exactly one member, rejected when it resolves to two or more (would start N watchers) or zero (nothing to watch).

```bash
lpm bench --filter web --watch            # ✓ one member, one watcher
lpm bench --all --watch                   # ✗ rejected: N watchers
lpm bench --filter typo --watch           # ✗ rejected: nothing to watch
```

When the filter resolves to exactly one member, `lpm bench` runs against that member's directory as if you'd `cd`-ed in.

## Flags [#flags]

| Flag                                    | Effect                                                                                                  |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `--all`                                 | Run in every workspace member                                                                           |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                       |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                     |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                         |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                   |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions             |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set                                                       |
| `--workspace-concurrency <N>`           | Limit concurrent workspace members for `--all`, `--filter`, or `--affected` runs                        |

Anything after `--` (or trailing) is forwarded to the bench runner.

Plus the [global flags](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

Single-package mode (`lpm bench`) preserves the runner's stdout. Workspace mode emits a single LPM CLI envelope on stdout; per-member stdout/stderr is captured and surfaced inside the envelope only on failure. See [`lpm lint`](/docs/dev/lint#json-in-workspace-mode) for the exact envelope shape.

## See also [#see-also]

* [`lpm test`](/docs/dev/test) — same auto-detection, for tests
* [Test & bench runners](/docs/dev/test-bench-runners) — how detection works


# Built-in tools (/docs/dev/builtin-tools)



LPM CLI ships a small set of built-in tools — [Oxlint](https://oxc.rs) for [`lpm lint`](/docs/dev/lint), [Biome](https://biomejs.dev) for [`lpm fmt`](/docs/dev/fmt), a type-check wrapper for [`lpm check`](/docs/dev/check), a package-build wrapper for [`lpm pack`](/docs/dev/pack), and a bundler wrapper for [`lpm bundle`](/docs/dev/bundle). Oxlint, Biome, and Rolldown are managed through [`lpm plugin`](/docs/dev/plugin): lazy-downloaded on first use, cached, and pinned per-project. `lpm check --engine tsgo` uses a separate internal managed engine cache under `~/.lpm/engines`.

This page covers the design — how the plugin system works, where binaries land, how versions are resolved, and the security model.

## Why lazy-download [#why-lazy-download]

Bundling 50+ MB of toolchains into the LPM CLI binary would bloat every install and waste disk for users who don't lint. Bundling nothing and using `npx` adds hundreds of milliseconds of resolution overhead per invocation. The middle path: download tools on first use, cache them globally, hash-verify, share across every project.

Measured cost: `lpm lint` is **3 ms** (vs `npx oxlint` 273 ms). `lpm fmt` is **3 ms** (vs `npx biome` 340 ms). The win is not the tool itself — it's the resolution overhead `npx` pays per invocation.

## What ships as a managed plugin today [#what-ships-as-a-managed-plugin-today]

| Plugin     | Backs                            | Source                            | Format                 |
| ---------- | -------------------------------- | --------------------------------- | ---------------------- |
| `oxlint`   | [`lpm lint`](/docs/dev/lint)     | `oxc-project/oxc` GitHub Releases | `.tar.gz` archive      |
| `biome`    | [`lpm fmt`](/docs/dev/fmt)       | `biomejs/biome` GitHub Releases   | Direct binary          |
| `rolldown` | [`lpm bundle`](/docs/dev/bundle) | npm registry                      | Verified package graph |

Other tool backends sit beside the plugin system:

* `tsc` shells out through the same PATH-injection mechanism `lpm run` uses. PATH-injection prepends `node_modules/.bin` and falls back to the system `PATH`, so a globally-installed `tsc` can run too — but the project-local install is the right answer. [`lpm doctor`](/docs/infra/doctor) flags the system-only case as `typescript_missing_for_tsconfig` (warn).
* `tsdown` stays project-local. [`lpm pack`](/docs/dev/pack) walks `node_modules/.bin` from the current package up to the workspace root and requires a reachable `tsdown` there. LPM CLI owns the workspace selection, `--json` envelope, and stable top-level flags; the backend version still comes from the repo's own `package.json`.
* `tsgo` is an internal managed engine. LPM CLI downloads the pinned platform tarball on first use, verifies its SRI integrity, preserves the full extracted layout, and reuses it from `~/.lpm/engines/tsgo/<version>/<platform>/` on later runs.
* `rolldown` is user-facing as a managed plugin, even though its internal cache uses the managed engine layout. [`lpm bundle`](/docs/dev/bundle) installs the root `rolldown` package, `@rolldown/pluginutils`, `@oxc-project/types`, and the current platform binding into one verified cached layout under `~/.lpm/engines/rolldown/<version>/<platform>/`.

That split is intentional: `tsc` and `tsdown` preserve repo-local version ownership, `tsgo` remains an internal engine choice, and Rolldown gets the public plugin update UX while keeping the package-graph layout it needs internally.

## Managed engine layout [#managed-engine-layout]

```text
~/.lpm/engines/
├── tsgo/
│ └── 7.0.0-dev.20260525.1/
│   └── darwin-arm64/
│     ├── lib/tsgo
│     ├── lib/lib.d.ts
│     └── .lpm-engine.json
└── rolldown/
  └── 1.0.2/
    └── darwin-arm64/
      ├── bin/cli.mjs
      ├── node_modules/@rolldown/pluginutils/
      ├── node_modules/@oxc-project/types/
      ├── node_modules/@rolldown/binding-darwin-arm64/
      └── .lpm-engine.json
```

The engine sidecar records the engine identity, platform, entry path, every package's install subdir and tarball metadata, per-package SHA-256s, and a hash of the extracted layout. Reuse re-hashes the installed layout before the engine runs, so a stray binary without a sidecar, a tampered `lib/*.d.ts` file, or a stale Rolldown package set is treated as a cache miss instead of trusted state.

## On-disk layout [#on-disk-layout]

```text
~/.lpm/plugins/
├── oxlint/
│   └── 1.58.0/
│       └── darwin-arm64/
│           ├── oxlint              ← downloaded binary
│           └── .lpm-plugin.json    ← sidecar (verification metadata)
├── biome/
│   └── 2.4.10/
│       └── linux-x64/
│           ├── biome
│           └── .lpm-plugin.json
└── .version-cache.json             ← cached "latest version" map
```

Each plugin gets a directory keyed by `{version}/{platform}`. The platform segment is required: a `$LPM_HOME` shared across architectures (cross-arch CI runners, NFS, Docker bind mounts) would otherwise let one host install a binary the next host can't exec.

Multiple versions can coexist; multiple platforms within a version can coexist too. The plugin `.version-cache.json` is the **install-selection cache**: each entry is the highest version that has been successfully verified-and-installed via `lpm plugin update`. Sticky by design — entries never expire by time, and the file is only written after a successful verified install (so a transient missing upstream `.sha256`, parse failure, or download failure cannot poison it and route every future invocation down a permanently-failing install path).

The `.lpm-plugin.json` sidecar records the plugin name, version, platform, asset URL, asset and on-disk binary checksums, and how the install was verified (`bundled`, `upstream`, or `unverified-override`). It is the source of truth for whether a cached binary may be reused — a bare binary with no sidecar, or a sidecar that fails any check, is treated as a cache miss and re-verified.

## Version resolution [#version-resolution]

The lookup order, when [`lpm lint`](/docs/dev/lint), [`lpm fmt`](/docs/dev/fmt), or [`lpm bundle`](/docs/dev/bundle) starts:

1. **Pinned version** in `lpm.json > tools.<name>` — exact match.
2. **`max(hardcoded, approved-cache)`** — whichever is newer, the version baked into the LPM CLI binary or one a previous successful `lpm plugin update` approved.
3. **Auto-download** if the resolved version isn't installed for the current platform yet.

Normal tool execution never touches the network for upstream-latest discovery — it only reads the bundled floor and sticky approved cache. To check upstream for newer releases, run `lpm plugin outdated`. To approve a newer version, run `lpm plugin update <name>`; that command runs the discovery probe, prints the download and verification phases for human runs, and only then approves the verified new version into the cache.

Pin a version per-project:

```json title="lpm.json"
{
  "tools": {
    "oxlint": "1.57.0",
    "biome": "2.4.8",
    "rolldown": "1.1.3"
  }
}
```

Update to the latest (sticky — the new "latest" is cached for future invocations):

```bash
lpm plugin update                # installed managed plugins
lpm plugin update oxlint         # one plugin
lpm plugin update rolldown       # Rolldown package graph
```

Force a fresh download of an existing version (useful for corrupted installs):

```bash
LPM_FORCE_TOOL_INSTALL=1 lpm lint
```

## Checksum verification [#checksum-verification]

Every install — whether the registry's pinned `latest_version`, a `lpm.json`-pinned version, or a freshly-fetched `lpm plugin update` result — must pass verification before it can be reused. Oxlint and Biome pass a SHA-256 checksum gate. Rolldown verifies npm SRI integrity for each package tarball, records per-package SHA-256s, and hashes the extracted layout.

For Oxlint and Biome, the checksum gate tries three sources in order:

1. **Bundled checksum** — SHA-256 baked into the LPM CLI binary at build time. Used when the requested version equals the registry's hardcoded `latest_version`. The fast path; no network round-trip.
2. **Upstream sidecar** — SHA-256 fetched from `<asset_url>.sha256` (oxlint via cargo-dist, biome publishes per-asset sidecars). Used for pinned versions and `lpm plugin update` results.
3. **Unverified override** — only if `LPM_ALLOW_UNVERIFIED_PLUGINS=1` is set. Records `verification-source: unverified-override` in the sidecar; reuse requires the same env var on every subsequent invocation.

A mismatch at any stage is a hard error and the partial download is removed.

If neither bundled nor upstream verification is available — typically when the upstream sidecar is unreachable for an old or yanked release — and `LPM_ALLOW_UNVERIFIED_PLUGINS` is not set, the install fails with an actionable message:

```text
refusing to install oxlint 1.42.0 for darwin-arm64: no bundled checksum and
upstream sidecar https://.../oxlint-aarch64-apple-darwin.tar.gz.sha256 is
unavailable. Update LPM CLI (newer releases ship pinned checksums for newer plugin
versions), pin a different version, or set LPM_ALLOW_UNVERIFIED_PLUGINS=1
to install without verification.
```

The bundled-checksum table is declared inline in the registry:

```rust
PluginDef {
    name: "oxlint",
    latest_version: "1.58.0",
    // ...
    checksums: &[
        ("darwin-arm64", "422756416c840b77212c673ae4aa88c8ef27e0e09b8ae51aeed21a2cef6b7191"),
        ("darwin-x64",   "f4d49bb4a636c8a0810e4c5a56adb02be9cf448570292a102d8a8835f7ba1980"),
        // ...
    ],
}
```

### Reuse from cache [#reuse-from-cache]

The sidecar — not bare file existence — gates reuse. Every cache hit re-validates:

* `schema_version` matches the current LPM CLI binary,
* `plugin_name` and `version` match the request,
* `platform` matches the current host,
* `verification_source` is compatible with the current trust posture (`unverified-override` is only honored when `LPM_ALLOW_UNVERIFIED_PLUGINS=1` is set in the consuming process),
* the binary's on-disk SHA-256 matches `binary_sha256` recorded at install time (tamper detection).

Anything that fails gets treated as a cache miss and re-verified through the full download pipeline.

## Supported platforms [#supported-platforms]

Each plugin definition declares which platforms get a binary:

| Plugin   | Platforms                                                           |
| -------- | ------------------------------------------------------------------- |
| `oxlint` | `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win-x64` |
| `biome`  | `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win-x64` |

LPM CLI detects the current platform and downloads the matching asset. Unsupported platforms get an immediate error rather than a download attempt.

## Manage installed plugins [#manage-installed-plugins]

```bash
lpm plugin list              # installed plugins and latest locally approved versions
lpm plugin outdated          # live upstream current vs latest check
lpm plugin update            # pull latest for every installed managed plugin
lpm plugin update oxlint     # one plugin
lpm plugin update rolldown   # Rolldown package graph
lpm plugin remove biome      # delete cached binaries
```

See [`lpm plugin`](/docs/dev/plugin) for the full subcommand surface.

## Why the registry is hardcoded [#why-the-registry-is-hardcoded]

The plugin registry ships **inside the LPM CLI binary**, not as a runtime-fetched config. New plugins land in an LPM CLI release — there's no add-your-own-plugin path. This is deliberately conservative: tool distribution bugs ship with the LPM CLI binary and pass through the team's QA, instead of a registry config that could change between two `lpm lint` invocations on the same machine.

If you want to use a tool that isn't in the plugin set today, install it as a normal dep and run it through your `package.json` scripts:

```bash
lpm install -D eslint
lpm run lint           # runs the package.json `lint` script
```

## CI behavior [#ci-behavior]

In CI, plugins behave the same way as locally — first invocation downloads, subsequent invocations reuse. Cache `~/.lpm/plugins/` in your CI to skip the download step:

```yaml
- name: Cache LPM CLI plugins
  uses: actions/cache@v4
  with:
    path: ~/.lpm/plugins
    key: lpm-plugins-${{ hashFiles('lpm.json') }}
```

The cache key is the `lpm.json` hash — when `tools` versions change, the cache invalidates and re-downloads.

## See also [#see-also]

* [`lpm lint`](/docs/dev/lint) — backed by oxlint
* [`lpm fmt`](/docs/dev/fmt) — backed by biome
* [`lpm check`](/docs/dev/check) — project-local `tsc` by default, managed `tsgo` when selected
* [`lpm pack`](/docs/dev/pack) — project-local tsdown with LPM CLI workspace orchestration
* [`lpm bundle`](/docs/dev/bundle) — managed Rolldown plugin
* [`lpm plugin`](/docs/dev/plugin) — manage installed plugins
* [`lpm.json` tools](/docs/reference/lpm-json#tools) — version pinning per project


# lpm bundle (/docs/dev/bundle)



```bash
lpm bundle [--entry <path>] [--out-dir <dir>] [--config <file>] [-- args...]
```

Runs [Rolldown](https://rolldown.rs) through an LPM CLI-owned command surface. Common bundle flags are exposed directly, and anything after `--` is forwarded to Rolldown unchanged.

`--entry` maps to Rolldown's `--input`, and `--out-dir` maps to Rolldown's `--dir`. If you omit both, Rolldown falls back to its config file or upstream defaults.

## Examples [#examples]

```bash
lpm bundle --entry src/index.js --out-dir dist
lpm bundle --config rolldown.config.mjs
lpm bundle --entry src/index.js --format esm --platform browser --minify --sourcemap
lpm bundle -- --watch
lpm bundle --all --config rolldown.config.mjs
lpm bundle --filter web --entry src/index.js --out-dir dist
lpm bundle --affected --base develop --config rolldown.config.mjs
```

## Managed Rolldown plugin [#managed-rolldown-plugin]

`lpm bundle` does not rely on a project-local `rolldown` dependency. On first use, LPM CLI installs its approved Rolldown version into `~/.lpm/engines/rolldown/<version>/<platform>/`, verifies every tarball's SRI integrity, preserves the full extracted package layout, and reuses that verified cache on later runs.

Rolldown's managed layout is a composed npm package tree: the root `rolldown` package, `@rolldown/pluginutils`, `@oxc-project/types`, and the platform-specific native binding.

```text
~/.lpm/engines/
└── rolldown/
  └── 1.0.2/
    └── darwin-arm64/
      ├── bin/cli.mjs
      ├── node_modules/@rolldown/pluginutils/
      ├── node_modules/@oxc-project/types/
      ├── node_modules/@rolldown/binding-darwin-arm64/
      └── .lpm-engine.json
```

The engine sidecar records the engine identity, platform, entry path, every package's install subdir and tarball metadata, per-package SHA-256s, and a hash of the extracted layout. Reuse re-hashes the installed layout before Rolldown runs, so a tampered tree or mismatched package definition becomes a cache miss instead of trusted state.

Rolldown is managed through the public plugin command family:

```bash
lpm plugin outdated
lpm plugin update rolldown
```

`lpm bundle` itself never checks the network for the upstream latest version. It uses the bundled verified floor or a newer version already approved by `lpm plugin update rolldown`.

## Pinning Rolldown [#pinning-rolldown]

Pin Rolldown per project in `lpm.json`:

```json title="lpm.json"
{
  "tools": {
    "rolldown": "1.1.3"
  }
}
```

The pin is exact. The version must be either the bundled verified floor or a version already approved in the sticky cache by `lpm plugin update rolldown`; otherwise `lpm bundle` exits before launching Node and tells you to run the update command.

## Node runtime [#node-runtime]

LPM CLI manages Rolldown, not Node itself. `lpm bundle` still needs `node` available through the normal PATH chain. The usual path is:

```bash
lpm use node@22
```

That installs a managed Node runtime and makes it available to LPM CLI commands. A system `node` on PATH also works.

## Forwarding Rolldown flags [#forwarding-rolldown-flags]

Anything after `--` is passed straight to Rolldown:

```bash
lpm bundle --entry src/index.js --out-dir dist -- --watch
lpm bundle --config rolldown.config.mjs -- --external react --treeshake
```

This is the escape hatch for upstream flags LPM CLI does not surface directly.

## Workspaces [#workspaces]

```bash
lpm bundle --all                               # every member
lpm bundle --filter web                        # exact name
lpm bundle --filter '@scope/*'                 # glob
lpm bundle --filter './apps/*'                 # path glob
lpm bundle --filter-prod ...shared             # prod graph closure
lpm bundle --affected                          # members changed vs main
lpm bundle --affected --base develop           # change base branch
lpm bundle --filter web --fail-if-no-match     # exit non-zero on typo'd filter
```

Members run in topological levels, with packages inside each level executing in parallel up to the available CPU count. The managed Rolldown plugin is resolved once before workspace fan-out, so a cold install happens once per workspace run, not once per member.

`--all` and `--affected` are mutually exclusive; filters compose with `--affected` (the affected set is unioned with the filter result). `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

Filter grammar is documented in [Workspaces](/docs/packages/workspaces#filter-grammar).

## Watch mode [#watch-mode]

`lpm bundle -- --watch` works in single-package mode.

In workspace mode, watch is only supported when the selection resolves to exactly one member. If `--all`, `--affected`, or a broad filter resolves to multiple members, LPM CLI exits with an error instead of starting one watcher per package.

## Flags [#flags]

| Flag                                    | Effect                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--entry <PATH>`                        | Entry file to bundle                                                                                                   |
| `--out-dir <DIR>`                       | Output directory for bundle artifacts                                                                                  |
| `--config <PATH>`                       | Explicit Rolldown config file                                                                                          |
| `--format <esm\|cjs\|iife>`             | Output format                                                                                                          |
| `--platform <node\|browser\|neutral>`   | Target platform                                                                                                        |
| `--minify`                              | Minify the bundle output                                                                                               |
| `--sourcemap`                           | Emit a sourcemap                                                                                                       |
| `--all`                                 | Run in every workspace member                                                                                          |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable; entries union) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                                      |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                                    |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                                        |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                                  |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions                            |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set (recommended in CI)                                                  |

Anything after `--` (or trailing) is forwarded to Rolldown.

Plus the [global flags](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

Single-package mode preserves Rolldown's stdout — LPM CLI does not wrap it. Workspace mode emits one LPM CLI envelope on stdout, with per-member stdout/stderr captured only on failure:

```json
{
  "success": false,
  "packages": 3,
  "succeeded": 2,
  "failed": 1,
  "duration_ms": 1840,
  "members": [
    { "name": "web", "success": true, "exit_code": 0, "duration_ms": 420 },
    {
      "name": "api",
      "success": false,
      "exit_code": 1,
      "duration_ms": 180,
      "stdout": "...",
      "stderr": "..."
    }
  ]
}
```

Spawn/config/engine failures surface as `exit_code: null` paired with an `error` string, distinguishing “ran and exited non-zero” from “could not even launch.”

## See also [#see-also]

* [`lpm check`](/docs/dev/check) — type-checking, with project-local `tsc` or managed `tsgo`
* [`lpm lint`](/docs/dev/lint) — Oxlint
* [Built-in tools](/docs/dev/builtin-tools) — managed plugins, project-local tools, and internal engines


# lpm check (/docs/dev/check)



```bash
lpm check [--engine tsc|tsgo] [-- args...]
```

Runs `tsc --noEmit` against the project by default. Pass `--engine tsgo` to run `tsgo --noEmit` instead. LPM CLI forwards any trailing arguments to the selected engine.

`lpm check` now has two engine paths:

* `tsc` stays project-local. LPM CLI resolves it from `node_modules/.bin` via the same PATH-injection it uses for `lpm run`, then falls back to the system `PATH`.
* `tsgo` is a managed engine. LPM CLI downloads the pinned platform package on first use, verifies the tarball integrity, preserves the full extracted layout, and caches it under `~/.lpm/engines/tsgo/<version>/<platform>/`.

## Examples [#examples]

```bash
lpm check                          # type-check the whole project
lpm check --engine tsgo            # opt into tsgo for this run
lpm check -- --pretty              # forward tsc flags
lpm check -- -p tsconfig.test.json # type-check a different tsconfig
lpm check --all                    # workspace: every member
lpm check --filter web             # workspace: one member
lpm check --filter './apps/*'      # workspace: path glob
lpm check --affected               # workspace: only affected members
```

## Engine installation [#engine-installation]

```bash
lpm install -D typescript
```

Use `typescript` for the default `tsc` engine.

For `tsc`, the project-local install is the right answer: editor IntelliSense (`tsserver` from `node_modules/typescript`) and CI run the same version, and your `tsconfig.json` references resolve against the same `@types/*` packages.

For `tsgo`, no project dependency is required. The first `lpm check --engine tsgo` run installs the managed engine into `~/.lpm/engines`, and later runs reuse that verified cache.

### Preflight [#preflight]

Before spawning the selected engine, `lpm check` verifies setup and surfaces specific errors:

| Condition                                                                                | Error                                                                                  |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `tsconfig.json` is missing                                                               | `no tsconfig.json found in <dir>. Add one (or pass -p <path>) to use a different one.` |
| `typescript` declared in `package.json` but not installed and `--engine tsc` is selected | `typescript declared in package.json but not installed. Run: lpm install`              |
| `typescript` not declared and no `tsc` on `PATH` while `--engine tsc` is selected        | `typescript not installed. Run: lpm install -D typescript`                             |
| Managed `tsgo` install or reuse fails                                                    | `engine error: ...` or a network/HTTP error naming the managed engine download         |

The preflight is **argument-aware**: when you pass `-p <path>`, `--project <path>`, or a positional file argument, the tsconfig check is skipped and the selected engine decides — `lpm check -- -p tsconfig.test.json` and `lpm check src/foo.ts` both work without a root `tsconfig.json`.

### `lpm doctor` reports the same signal [#lpm-doctor-reports-the-same-signal]

`lpm doctor` emits one TypeScript check per `tsconfig.json` in the workspace, with stable codes:

| Code                              | Severity | Meaning                                                                                                                                                            |
| --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `typescript_healthy`              | pass     | `tsc` resolves through the project-local `node_modules/.bin` chain. Editor + CI parity holds.                                                                      |
| `typescript_missing_for_tsconfig` | warn     | `tsc` runs only via the system `PATH`. The project lacks a local install — editor and CI may use a different version. Run `lpm install -D typescript` to converge. |
| `typescript_unavailable`          | fail     | `tsc` cannot run at all. Run `lpm install` if `typescript` is already declared, or `lpm install -D typescript` to add it.                                          |

Doctor performs only the cheap reachability + dep-declaration check; the type-check itself belongs to `lpm check`.

## Workspaces [#workspaces]

```bash
lpm check --all                              # every member
lpm check --filter web                       # exact name
lpm check --filter '@scope/*'                # glob
lpm check --filter './apps/*'                # path glob
lpm check --filter-prod ...shared            # prod graph closure
lpm check --affected --base develop          # affected vs a non-main branch
lpm check --filter web --fail-if-no-match    # exit non-zero on typo'd filter
```

Members run in topological levels, with packages inside each level executing in parallel up to the available CPU count.

Filter grammar is documented in [Workspaces](/docs/packages/workspaces#filter-grammar).

`--all` and `--affected` are mutually exclusive; filters compose with `--affected` (the affected set is unioned with the filter result). `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

## Flags [#flags]

| Flag                                    | Effect                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--engine <name>`                       | Select the type-check engine for this run: `tsc` or `tsgo` (default: `tsc`)                                            |
| `--all`                                 | Run in every workspace member                                                                                          |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable; entries union) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                                      |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                                    |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                                        |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                                  |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions                            |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set (recommended in CI)                                                  |

Anything after `--` (or trailing) is forwarded to the selected engine. `--noEmit` is always passed.

Plus the [global flags](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

Single-package mode (`lpm check`) preserves the selected engine's stdout — LPM CLI does not wrap it. Workspace mode emits a single LPM CLI envelope on stdout; per-member stdout/stderr is captured and surfaced inside the envelope only on failure. See [`lpm lint`](/docs/dev/lint#json-in-workspace-mode) for the exact envelope shape.

## See also [#see-also]

* [`lpm lint`](/docs/dev/lint) — Oxlint
* [`lpm fmt`](/docs/dev/fmt) — Biome formatter
* [`lpm plugin`](/docs/dev/plugin) — for tools that ARE lazy-downloaded


# lpm completions (/docs/dev/completions)



```bash
lpm completions <shell>
```

Emits a clap-driven completion script for the given shell to stdout. Pipe the output into your shell's completion-load path and `lpm <TAB>` will start completing subcommands, flags, and value-enums.

The script is generated from the live CLI definition, so it stays in sync with the binary at every release — no separately maintained completion files to drift out of date.

## Supported shells [#supported-shells]

| Shell      | Argument     | Typical install path                                                                             |
| ---------- | ------------ | ------------------------------------------------------------------------------------------------ |
| Bash       | `bash`       | `/etc/bash_completion.d/lpm` (system) or `~/.local/share/bash-completion/completions/lpm` (user) |
| Zsh        | `zsh`        | A directory in `$fpath`, e.g. `${fpath[1]}/_lpm` or `~/.zfunc/_lpm`                              |
| Fish       | `fish`       | `~/.config/fish/completions/lpm.fish`                                                            |
| PowerShell | `powershell` | Sourced from `$PROFILE` via `Invoke-Expression`                                                  |
| Elvish     | `elvish`     | Source from `~/.config/elvish/rc.elv`                                                            |

## Examples [#examples]

```bash
# Zsh — point fpath at a user dir, generate, restart the shell
mkdir -p ~/.zfunc && lpm completions zsh > ~/.zfunc/_lpm
echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc
echo 'autoload -Uz compinit && compinit' >> ~/.zshrc

# Bash (system-wide, requires sudo)
lpm completions bash | sudo tee /etc/bash_completion.d/lpm > /dev/null

# Bash (per-user)
mkdir -p ~/.local/share/bash-completion/completions
lpm completions bash > ~/.local/share/bash-completion/completions/lpm

# Fish
lpm completions fish > ~/.config/fish/completions/lpm.fish

# PowerShell — append to your profile
lpm completions powershell | Out-String | Invoke-Expression
# Or, persistently:
lpm completions powershell >> $PROFILE
```

## Flags [#flags]

`lpm completions` takes the target shell as a positional argument and otherwise has no specific flags. The [global flags](/docs/commands#global-flags) (`--json`, `--verbose`, etc.) do not affect the generated script.

## See also [#see-also]

* [`lpm self-update`](/docs/infra/self-update) — keep the binary current so completions reflect the latest commands
* [Installation](/docs/installation) — get LPM CLI installed before generating completions


# lpm dev (/docs/dev/dev)





```bash
lpm dev
```

`lpm dev` runs a development server with optional configuration. Without `lpm.json`, it runs the `dev` script from `package.json`. With `lpm.json`, it can also manage runtimes, environments, HTTPS, tunnels, and multiple services.

```text
$ lpm dev

  ● Node     22.12.0 (from .nvmrc)
  ● Deps     up to date (2ms)
  ● Env      .env loaded
  ● HTTPS    certificate valid
  ● Tunnel   https://acme-api.lpm.llc

  [db]  ✔ ready (0.8s)
  [web] ✔ ready (1.2s)
  [api] ✔ ready (3.4s)

```

## What it does, in order [#what-it-does-in-order]

1. **Managed runtimes** — uses versions pinned in `lpm.json`. Node can also use `.nvmrc` or `.node-version`. A service can override one root runtime and inherit the other. LPM CLI installs missing runtimes before service startup (see [`lpm use`](/docs/dev/use)).
2. **Dependencies** — re-runs the install pipeline if `package.json` / lockfile / install hash drift. Skip with `--no-install`.
3. **Env** — if `.env` is missing but `.env.example` exists, copies it once. The order is `--env`, `tasks.dev.env`, `env.dev`, then the default files. Mode files load after `.env` and `.env.local`.
4. **Endpoint discovery** — runs the child server, reads local URLs from its output, and verifies that the launched process tree owns the listener before publishing a URL. Vite can use `5173`, `5174`, or another free port; LPM CLI does not assume `3000`.
5. **HTTPS** — if `lpm.json` has `https: true` (top-level) or you pass `--https`, starts an LPM CLI-owned TLS frontend over the verified child endpoint. The framework can stay on plain HTTP. When `lpm.json` declares local-domain proxy hosts, `lpm dev` also prepares constrained project certificate-chain coverage and writes managed hosts-file entries for non-`.localhost` hosts when needed. See [`lpm cert`](/docs/infra/cert) for the trust store install.
6. **Tunnel** — if `lpm.json` has `tunnel.domain` set or you pass `--tunnel`, exposes the same verified child endpoint through an LPM CLI tunnel.
7. **Services** — if `lpm.json` declares `services`, assigns managed ports, starts each service with prefixed log output, and waits for owned listeners plus any explicit ready-checks. Otherwise runs the `dev` script from `package.json` and automatically prepares project-local compatibility for the dev-tool binary when it needs normal `node_modules` ancestry. Next.js/Turbopack, Vite, Astro, Webpack, Remix, Nuxt, SvelteKit, and Storybook-style entrypoints work with LPM CLI's install layout without extra config.
8. **Browser** — opens the verified primary service URL when ready. Skip with `--no-open`.

## Examples [#examples]

```bash
lpm dev                                # auto-detect everything
lpm dev --https                        # force HTTPS this run
lpm dev --tunnel                       # expose to the public internet
lpm dev --network                      # show LAN URLs + QR for mobile
lpm dev --port 4000                    # override default port
lpm dev --env=staging                  # load .env.staging + .env.staging.local after the base env files
lpm dev --dashboard                    # TUI dashboard for multi-service
lpm dev -- --inspect                   # forward args to the dev script
```

## HTTPS [#https]

```bash
lpm dev --https
```

Serves over `https://localhost` (or whichever host you pass with `--host`) using a certificate signed by LPM CLI's local CA. First-time setup needs the CA installed into your OS trust store; `lpm dev --https` can prompt for that step, or you can run `lpm cert trust` ahead of time. See [Local HTTPS](/docs/infra/local-https).

If the CA is not trusted yet, `lpm dev --https` prompts before installing it into the system trust store. Pass `-y` / `--yes` to pre-approve that step in non-interactive runs. Add `--allow-ca-bootstrap` to serve the root CA over plain HTTP on an OS-assigned port for mobile-device trust bootstrap on the local network.

The framework process remains on its verified HTTP endpoint; LPM CLI terminates TLS and forwards HTTP and WebSocket/HMR traffic to it. With `--https --port 4000`, `4000` is the browser-facing HTTPS port, while the child gets a separate internal port. This avoids framework-specific certificate switches and prevents the HTTPS frontend from colliding with the app listener.

Configured `proxy.host` and `services.<name>.host` entries use the same project certificate path for the HTTPS proxy. When the proxy daemon has an HTTPS listener, `lpm dev` prepares a leaf-first project certificate chain for those hosts; custom-host chains include a constrained project intermediate. LPM CLI does not inject certificate paths or framework HTTPS variables into the app process.

Hosts under `localhost` / `*.localhost` usually resolve without a hosts-file entry. Other local TLDs such as `.test`, `.local`, `.internal`, and `.home.arpa` make `lpm dev` ask for consent, add a project-scoped managed block to the system hosts file, and remove that block when the dev session exits. On Unix, LPM CLI uses `sudo` for the system hosts file when the current process lacks permission; on Windows, it asks for Administrator elevation through UAC. If a session is interrupted, [`lpm hosts clean`](/docs/infra/hosts) removes orphaned LPM CLI-managed blocks.

The startup banner lists configured proxy hosts. For host-only services it may show `auto port` before orchestration assigns the final port; the exact `host -> localhost:<port>` route line prints after registration. In dashboard mode, service rows include their local-domain host URLs.

`--no-https` disables HTTPS even if it's enabled in `lpm.json`.

## Tunnel [#tunnel]

```bash
lpm dev --tunnel --domain my-api.lpm.llc
```

Exposes the dev server through the LPM CLI tunnel network. Webhooks land on the public URL and persist in the project's `.lpm/inspector.db`; `lpm tunnel inspect`, `lpm tunnel replay`, and the browser inspector all read that same history. See [Tunneling](/docs/infra/tunneling).

The tunnel waits for endpoint discovery; it never connects to a guessed fallback port. The browser inspector and tunnel session display the verified child port, including framework-selected ports such as Vite's `5173` / `5174`.

The tunnel startup row includes the account-wide request usage advertised by the relay. `lpm dev --tunnel` uses the same shared allowance as [`lpm tunnel`](/docs/infra/tunnel): Free gets 20,000 requests per UTC month, Pro gets 100,000 per billing period, and Organization gets 100,000 per billed seat. Starting the tunnel through `lpm dev` does not create a separate allowance. Live allowance and overage notices appear as warnings; if the tunnel ends with a permanent billing, quota, concurrency, or authentication error, LPM CLI reports the failure while the local dev server continues running.

A browser inspector auto-starts alongside `--tunnel` on a free ephemeral port; the dashboard's `o` key opens it. Pass `--inspect-port <N>` to bind a specific port (strict — fails if `N` is in use), or `--no-inspect` to skip the browser server. Capture still persists to `.lpm/inspector.db` with `--no-inspect`. Both flags are no-ops without `--tunnel`.

`--tunnel-auth` requires a per-session auth token to access the tunnel URL (Pro/Org feature).

`--no-tunnel` disables the tunnel even if configured in `lpm.json`.

## Multi-service orchestration [#multi-service-orchestration]

If `lpm.json` declares services (`db`, `api`, `web`, …), `lpm dev` starts each one, waits for ready-check, and prefixes logs with the service name. Failed services exit the run.

Each service resolves runtimes from its `cwd`. A local Node or Bun selector replaces the same root runtime. The service inherits each unselected runtime.

Before startup, LPM CLI validates the selected Node against applicable root and service `engines.node` constraints.

An explicit `--env=<MODE>` applies to all services. Without this flag, `tasks.dev.env` takes precedence over `env.dev`. Service-specific `services.<name>.env` values apply after the shared project environment.

LPM CLI removes inherited credential and runtime-hook variables before service startup. Explicit project environment values still use the normal environment policy.

For every other service with a resolved port, LPM CLI also injects `{SERVICE}_URL` and `{SERVICE}_PORT` into the child environment. A resolved port can come from `port`, conflict reassignment, or host-only auto-assignment. For example, an `api` service can read `DB_URL=http://localhost:5432` and `DB_PORT=5432`, while `web` can read `API_URL` and `API_PORT`.

The primary service always receives a managed port, even when its `port` field is omitted. LPM CLI passes framework-native flags where needed: Vite receives `--port <N> --strictPort`; Next.js, Nuxt, SvelteKit, Remix, and Astro receive `--port <N>`; generic servers continue to receive `PORT=<N>`. A service that ignores its assigned port fails with an explicit diagnostic instead of allowing browser, proxy, certificate, or tunnel setup to target the wrong listener.

When the dev session stops, LPM CLI stops the tracked process trees. This cleanup also applies after a readiness failure.

A targeted termination signal also starts this cleanup.

Service log processing has byte limits. A newline-free service line is limited to 64 KiB. Dashboard logs retain at most 16 MiB per service.

`--dashboard` swaps the prefixed-log output for a TUI dashboard with per-service log panels, local-domain host URLs, and webhook inspection. `--no-dashboard` forces the prefixed-log output even if the dashboard is configured.

## Flags [#flags]

| Flag                       | Effect                                                                                                       |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--https` / `--no-https`   | Force-on or force-off HTTPS for this run                                                                     |
| `--tunnel` / `--no-tunnel` | Force-on or force-off the tunnel                                                                             |
| `--tunnel-auth`            | Require an auth token to access the tunnel URL (Pro/Org)                                                     |
| `--network`                | Show LAN URLs and a QR code for mobile testing                                                               |
| `--port <N>`               | Use this non-zero public dev port; passed through framework-native args when supported                       |
| `--host <NAME>`            | Custom hostname for the HTTPS certificate                                                                    |
| `--domain <NAME>`          | Tunnel domain (overrides `lpm.json > tunnel.domain`)                                                         |
| `--env=<MODE>`             | Load `.env.<MODE>` and `.env.<MODE>.local` after `.env` and `.env.local`                                     |
| `--no-open`                | Skip auto-opening the browser                                                                                |
| `--no-install`             | Skip dependency auto-install even if stale                                                                   |
| `--no-env-check`           | Skip env-var schema validation                                                                               |
| `-q`, `--quiet`            | Suppress inline webhook output (still persisted to `.lpm/inspector.db`)                                      |
| `--dashboard`              | Launch the TUI dashboard                                                                                     |
| `--no-dashboard`           | Force prefixed log output                                                                                    |
| `--no-inspect`             | Skip the auto-started webhook inspector (no-op without `--tunnel`)                                           |
| `--inspect-port <N>`       | Bind the inspector to exactly this port (default: auto-pick a free ephemeral port; no-op without `--tunnel`) |
| `-y`, `--yes`              | Pre-approve the trust-store install and local-domain hosts-file update prompts                               |
| `--allow-ca-bootstrap`     | Serve the root CA over `http://<lan-ip>:<auto-port>` for mobile trust bootstrap                              |

Anything after `--` is forwarded verbatim to the dev script.

## Configuration: `lpm.json` [#configuration-lpmjson]

`lpm.json` sits next to `package.json` and is where the dev server, task runner, and publish pipeline pick up their config. The file is optional — every field has a sensible default and falls back to `package.json` semantics when absent.

```json title="lpm.json (full surface)"
{
  "runtime": { "node": ">=22.0.0" },
  "tools": { "oxlint": "1.57.0", "biome": "2.4.8" },

  "https": true,
  "cert": { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false },
  "tunnel": { "domain": "acme-api.lpm.llc" },
  "proxy": { "host": "app.localhost", "port": 443, "httpRedirect": true },

  "env": {
    "dev": ".env.development",
    "staging": ".env.staging",
    "prod": ".env.production"
  },

  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url" }
    }
  },

  "environments": {
    "staging": { "extends": "base", "file": ".env.staging" }
  },

  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true,
      "outputs": ["dist/**"],
      "inputs": ["src/**", "package.json"]
    },
    "test": { "cache": true },
    "dev": { "command": "vite dev", "env": "development" }
  },

  "services": {
    "db": {
      "command": "docker compose up postgres",
      "readyPort": 5432,
      "readyTimeout": 60
    },
    "api": {
      "command": "node server.js",
      "port": 4000,
      "dependsOn": ["db"],
      "env": { "DATABASE_URL": "postgres://localhost:5432/myapp" }
    },
    "web": { "command": "next dev", "port": 3000, "primary": true, "host": "web.app.localhost" }
  },

  "publish": {
    "registries": ["lpm", "npm"],
    "lpm": { "name": "@lpm.dev/owner.pkg" },
    "npm": {
      "name": "@scope/pkg",
      "access": "public",
      "tag": "latest",
      "otpRequired": true
    },
    "github": { "name": "@owner/pkg", "access": "public" },
    "gitlab": { "name": "pkg", "access": "public", "projectId": "12345" }
  }
}
```

### Top-level fields [#top-level-fields]

| Field          | Type   | Purpose                                                                                                                                                                                                                                                               |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runtime`      | object | Pinned runtime versions, e.g. `{"node": ">=22.0.0", "bun": "1.3.14"}`. Auto-installed if missing — see [`lpm use`](/docs/dev/use).                                                                                                                                    |
| `tools`        | object | Pinned built-in tool versions: `{"oxlint": "1.57.0", "biome": "2.4.8"}`. Honored by `lpm lint`, `lpm fmt`, etc.                                                                                                                                                       |
| `https`        | bool   | If `true`, `lpm dev` serves over HTTPS by default. CLI `--no-https` overrides.                                                                                                                                                                                        |
| `cert`         | object | Certificate policy for local HTTPS and local-domain proxy hosts.                                                                                                                                                                                                      |
| `tunnel`       | object | Tunnel config — see below.                                                                                                                                                                                                                                            |
| `proxy`        | object | Friendly local-domain proxy config. `lpm dev` prepares constrained project cert-chain coverage, updates the hosts file when needed, auto-starts a detached [`lpm proxy`](/docs/infra/proxy) daemon when absent, and registers routes against the final service ports. |
| `env`          | object | Map of script name → env file. `{"dev": ".env.development"}` makes `lpm run dev` load `.env.development`.                                                                                                                                                             |
| `envSchema`    | object | Required-var declarations + format / pattern / default / secret flags. Drives env validation; `--no-env-check` skips it.                                                                                                                                              |
| `environments` | object | Named environments with `extends` inheritance — useful when you have many `.env.*` variants.                                                                                                                                                                          |
| `tasks`        | object | Task config — see below. Drives caching, dep ordering, watched inputs.                                                                                                                                                                                                |
| `services`     | object | Multi-service orchestration for `lpm dev` — see below.                                                                                                                                                                                                                |
| `publish`      | object | Multi-registry publish settings — see below.                                                                                                                                                                                                                          |

### `cert` [#cert]

| Field               | Type      | Default | Notes                                                                                                                                                      |
| ------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `extraPermittedDns` | string\[] | `[]`    | Extra validated DNS subtrees to permit on the constrained project intermediate. These are policy constraints, not extra browser SANs and not proxy routes. |
| `allowPublicDns`    | bool      | `false` | Allows public DNS names in `extraPermittedDns` and local-domain `host` fields.                                                                             |

Use `--host`, `proxy.host`, or `services.<name>.host` for actual browser hostnames. `extraPermittedDns` only shapes the project intermediate's NameConstraints.

### `tunnel` [#tunnel-1]

| Field    | Type   | Default | Notes                                                                                                                   |
| -------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `domain` | string | —       | Stable tunnel domain (e.g., `acme-api.lpm.llc`). Pro/Org only — free users get an ephemeral random domain on every run. |

### `proxy` [#proxy]

| Field          | Type   | Default | Notes                                                                                                                                                                                                                                       |
| -------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`         | string | —       | Friendly top-level local hostname registered by `lpm dev` when the proxy daemon has an HTTPS listener. `lpm dev` also prepares constrained project cert-chain coverage and updates the hosts file when this host is not under `.localhost`. |
| `port`         | number | `443`   | HTTPS proxy listen port used by `lpm proxy start` when no explicit listener flags are passed. `0` lets the OS pick a free port.                                                                                                             |
| `httpRedirect` | bool   | `true`  | Whether `lpm proxy start` also binds an HTTP-to-HTTPS redirect listener on port 80 when no explicit listener flags are passed.                                                                                                              |

`proxy.host` and `services.<name>.host` are normalized to lowercase, must use a local TLD (`.localhost`, `.test`, `.local`, `.internal`, `.home.arpa`) unless `cert.allowPublicDns` is true, and cannot duplicate another local-domain host in the same `lpm.json`.

Use [`lpm proxy status`](/docs/infra/proxy) to inspect the local proxy route table. When local-domain config is present and no daemon is running, `lpm dev` starts `lpm proxy start --detach` automatically. With no explicit listener flags, that start path uses `proxy.port` and `proxy.httpRedirect`. A control-only daemon, or one started only with `--http-port`, is not enough for local-domain HTTPS routes; if you start the daemon manually, include `--tls-port`, for example `lpm proxy start --tls-port 9443`. `lpm dev` prepares the project certificate chain for configured hosts through the normal trust consent flow and manages hosts-file entries for hosts that are not `localhost` / `*.localhost`; use [`lpm hosts clean`](/docs/infra/hosts) for orphan cleanup after interrupted sessions. The TLS daemon also prepares or refreshes project certificate chains during route registration, but it does not install the root CA into the trust store.

### `tasks.<name>` [#tasksname]

| Field       | Type      | Default                         | Notes                                                                                                                |
| ----------- | --------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `command`   | string    | (uses `package.json > scripts`) | Override the script body                                                                                             |
| `dependsOn` | string\[] | `[]`                            | Other tasks that must finish first. `"build"` = same package; `"^build"` = the same task in upstream workspace deps. |
| `cache`     | bool      | `false`                         | Enable task caching. Requires `outputs` to define what to cache.                                                     |
| `outputs`   | string\[] | `[]`                            | Globs of files this task produces (e.g., `["dist/**"]`). Required when `cache: true`.                                |
| `inputs`    | string\[] | sensible defaults\*             | Globs that invalidate the cache.                                                                                     |
| `env`       | string    | —                               | Env mode for this task — `"development"` loads `.env.development`.                                                   |

\*Default inputs include `src/**`, `lib/**`, `app/**`, `pages/**`, `components/**`, `package.json`, `tsconfig.json`, `tsconfig.*.json`, `*.config.{js,ts,mjs}`.

### `services.<name>` [#servicesname]

| Field          | Type      | Default              | Notes                                                                                                                                                                                                                                                                                                                                                                                  |
| -------------- | --------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command`      | string    | **required**         | Shell command to run                                                                                                                                                                                                                                                                                                                                                                   |
| `port`         | number    | —                    | Port the service listens on. The primary service and host-routed services receive a managed port when omitted                                                                                                                                                                                                                                                                          |
| `dependsOn`    | string\[] | `[]`                 | Services that must be ready before this one starts                                                                                                                                                                                                                                                                                                                                     |
| `readyPort`    | number    | falls back to `port` | TCP port to poll for readiness                                                                                                                                                                                                                                                                                                                                                         |
| `readyUrl`     | string    | —                    | HTTP URL to poll for readiness — first 2xx wins                                                                                                                                                                                                                                                                                                                                        |
| `readyTimeout` | number    | `30`                 | Seconds before a readiness failure stops the initial startup                                                                                                                                                                                                                                                                                                                           |
| `env`          | object    | `{}`                 | Extra env vars injected into this service only                                                                                                                                                                                                                                                                                                                                         |
| `restart`      | bool      | `false`              | Auto-restart on crash, exponential backoff                                                                                                                                                                                                                                                                                                                                             |
| `primary`      | bool      | `false`              | Its verified endpoint receives HTTPS, tunnel, network, and browser-open handling                                                                                                                                                                                                                                                                                                       |
| `host`         | string    | —                    | Friendly local hostname registered by `lpm dev` against the final assigned service port when the proxy daemon has an HTTPS listener. `lpm dev` also prepares constrained project cert-chain coverage and updates the hosts file when the host is not under `.localhost`. With `port`, `localhost:<port>` keeps working; without `port`, `lpm dev` injects the assigned port as `PORT`. |
| `cwd`          | string    | project root         | Working directory, relative to project root                                                                                                                                                                                                                                                                                                                                            |

### `publish` [#publish]

```json
"publish": {
  "registries": ["lpm", "npm"],
  "lpm":    { "name": "@lpm.dev/owner.pkg" },
  "npm":    { "name": "@scope/pkg", "access": "public", "tag": "latest", "otpRequired": true },
  "github": { "name": "@owner/pkg", "access": "public" },
  "gitlab": { "name": "pkg", "access": "public", "projectId": "12345", "registry": "https://gitlab.com" }
}
```

| Block    | Fields                                                                                                                                              |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| (top)    | `registries` (string\[]) — defaults to `["lpm"]`                                                                                                    |
| `lpm`    | `name` (must be `@lpm.dev/owner.pkg`)                                                                                                               |
| `npm`    | `name`, `access` (`"public"` / `"restricted"`, default `"public"`), `tag` (default `"latest"`), `registry`, `otpRequired` (prompt for OTP up front) |
| `github` | `name` (`@owner/pkg`), `access`                                                                                                                     |
| `gitlab` | `name`, `access`, `projectId` (required), `registry` (default `https://gitlab.com`)                                                                 |

CLI flags on `lpm publish` (`--npm`, `--lpm`, `--github`, `--gitlab`, `--publish-registry`) override the `registries` list.

## More in this section [#more-in-this-section]

<Cards>
  <Card title="lpm run" href="/docs/dev/run" description="Run package.json scripts. Parallel, cached, watchable, workspace-aware." />

  <Card title="lpm <file> / exec" href="/docs/dev/exec" description="Run JS/TS files directly, or run project-local binaries." />

  <Card title="lpm dlx / lpx" href="/docs/dev/dlx" description="Run a package binary without installing it into the project." />

  <Card title="lpm test" href="/docs/dev/test" description="Auto-detect and run vitest / jest / mocha." />

  <Card title="lpm bench" href="/docs/dev/bench" description="Auto-detect and run vitest bench." />

  <Card title="lpm lint" href="/docs/dev/lint" description="Built-in Oxlint, lazy-downloaded, no npx round-trip." />

  <Card title="lpm fmt" href="/docs/dev/fmt" description="Built-in Biome formatter." />

  <Card title="lpm check" href="/docs/dev/check" description="TypeScript check (tsc --noEmit)." />

  <Card title="lpm use" href="/docs/dev/use" description="Pin managed Node.js or Bun versions, with auto-install on use." />

  <Card title="lpm plugin" href="/docs/dev/plugin" description="Manage built-in tool plugins (list, update, remove)." />
</Cards>

## See also [#see-also]

* [Tunneling](/docs/infra/tunneling) — how the tunnel works, claim a stable domain
* [Local HTTPS](/docs/infra/local-https) — CA install, custom hostnames
* [Task runner](/docs/dev/task-runner) — what the runner does behind `run` and `dev`


# lpx / lpm dlx (/docs/dev/dlx)



```bash
lpx [--refresh] [--allow-new] [--min-release-age=<dur>] [--min-release-age-exclude <selector>] <package> [-- args...]
lpm dlx [--refresh] [--allow-new] [--min-release-age=<dur>] [--min-release-age-exclude <selector>] <package> [-- args...]
```

Fetches a package's binary, caches it under `~/.lpm/cache/dlx/`, and runs it without touching your project's `package.json` or `node_modules`. The LPM CLI equivalent of `npx`, `pnpm dlx`, and `bunx`.

`lpx` is the short form for `lpm dlx`; both run the same command.

## Examples [#examples]

```bash
lpx cowsay "hello"
lpx dlx cowsay "hello"
lpx create-next-app@latest my-app
lpx prettier --check .
lpx http-server -p 8080
lpx --refresh create-next-app   # bypass the dlx cache
lpx --min-release-age=0 create-next-app@latest
lpx --min-release-age-exclude create-next-app create-next-app@latest
```

## How it works [#how-it-works]

1. Resolve the spec. If the package already exists in the caller project's `lpm.lock` and the locked version satisfies the requested spec, `lpx` uses that exact version first. Otherwise it resolves against the appropriate registry. `lpx` calls into the full install pipeline, so routing follows the same rules as [`lpm install`](/docs/packages/install): `@lpm.dev/*` packages go to LPM.dev Registry, `.npmrc`-declared scopes go to the registry that scope points at, and everything else goes to `registry.npmjs.org`.
2. Materialize the package into a cache directory under `~/.lpm/cache/dlx/`. Lockfile-selected entries are keyed by resolved `name@version` plus integrity; registry-resolved entries keep the requested spec as their cache key. Every run prints the resolved `name@version`, integrity, and source before executing the binary.
3. Read the installed package's `bin` metadata and choose the default executable. If the package exposes exactly one bin, LPM CLI uses it. If it exposes multiple bins, LPM CLI prefers the one matching the package's short name (`eslint` for `eslint`, `foo` for `@scope/foo`). If there is still no unambiguous default, `lpx` errors instead of guessing.

`lpx <pkg>@<version>` pins the requested version. `lpx <pkg>` prefers the project lockfile when available, then reuses the dlx cache while the cache entry is fresh and auditable. Once the TTL expires, the next run reinstalls against the current resolution of that spec and reports that it is refreshing the expired cache entry.

## Cache TTL [#cache-ttl]

dlx cache entries live for **24 hours** from install or explicit refresh. After that, the next invocation reinstalls so you don't keep running an old binary forever. Within the TTL window, repeat invocations are essentially free — the cache entry already has `node_modules/.bin/` populated and the binary spawns directly.

Cache hits do **not** extend the TTL. A frequently used dlx entry is still revalidated after 24 hours.

`--refresh` forces an immediate reinstall regardless of the TTL.

## Trust model [#trust-model]

Dependency lifecycle scripts under `lpx` go through the same install policy chain as [`lpm install`](/docs/packages/install): release-age cooldown for the requested package, provenance checks, script policy, trusted dependencies, and sandbox policy are all evaluated by the install pipeline. `lpx` also carries the caller project's `package.json > lpm` policy into the temporary install root.

The package binary itself still runs as a third-party command in your project directory. LPM CLI strips common secret-bearing env vars and runtime-hijack env vars before spawn, but you should still treat `lpx <pkg>` as running arbitrary code from that package.

## Argument forwarding [#argument-forwarding]

Anything after the package name is forwarded to the binary:

```bash
lpx prettier --write src/
# runs: prettier --write src/
```

## Cache management [#cache-management]

The dlx cache lives at `~/.lpm/cache/dlx/`. Manage it with [`lpm cache`](/docs/packages/cache):

```bash
lpm cache clean dlx     # drop only the dlx cache
lpm cache path dlx      # print the cache root
```

Bypass the cache for one invocation with `--refresh`:

```bash
lpx --refresh create-next-app
```

## Flags [#flags]

| Flag                                   | Effect                                                                                                                                                  |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--refresh`                            | Force a fresh download, ignoring the dlx cache                                                                                                          |
| `--allow-new`                          | Bypass the minimum-release-age cooldown for this run, subject to the same security approval boundary as install                                         |
| `--min-release-age=<DUR>`              | Override the cooldown window for this run (`<N>h`, `<N>d`, `<N>m`, or seconds; `0` disables), subject to the same security approval boundary as install |
| `--min-release-age-exclude <SELECTOR>` | Exempt a package name, exact version, or `@scope/*` for this run. Repeat the flag for more selectors.                                                   |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm exec`](/docs/dev/exec) — run a project-local binary
* [`lpm <file>`](/docs/dev/exec) — run a JS/TS source file directly
* [`lpm install -g`](/docs/packages/install) — keep a CLI around permanently instead
* [`lpm cache`](/docs/packages/cache) — manage the dlx cache


# lpm env (/docs/dev/env)



```bash
lpm env set KEY=VALUE              # set one or many vars
lpm env get KEY                    # read one (masked unless --reveal)
lpm env list                       # list all (masked unless --reveal)
lpm env delete KEY                 # remove
lpm env push                       # encrypt + push to lpm.dev
lpm env pull                       # pull + decrypt
lpm env push --to vercel           # push to a connected platform
lpm env push --to coolify          # push to a connected Coolify app
lpm env push --to fly              # push write-only secrets to a Fly.io app
lpm env push --to railway          # push to a connected Railway target
lpm env push --to github-actions   # push variables + sealed secrets to GitHub
```

`lpm env` manages encrypted environment variables for the project. Local values use OS-keychain-backed storage, cloud sync persists only ciphertext to lpm.dev, and the Vercel, Coolify, Fly.io, Railway, and GitHub Actions integrations send application env values directly from the CLI to the selected platform.

For the underlying storage and encryption model, see [env encryption architecture](/docs/infra/secrets-vault).

## Local-file management [#local-file-management]

```bash
lpm env list                              # list all vars (values masked)
lpm env list --reveal                     # list with values shown
lpm env get DATABASE_URL                  # get one var
lpm env get DATABASE_URL --reveal         # get with value shown
lpm env set DATABASE_URL=postgres://...   # set one
lpm env set FOO=bar BAZ=qux               # set many at once
lpm env delete FOO BAZ                    # delete one or more
lpm env import .env.production            # import from a file
lpm env import .env --overwrite           # overwrite existing values
lpm env export .env.backup                # export to a file
```

Dotenv inputs are limited to 16 MiB per file before parsing. Missing optional `.env*` files retain their existing fallback behavior; an oversized import or run-time env file fails with its path and byte limit, before any configured script or child process is spawned. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

Project-bound environment commands validate `lpm.json` with the canonical schema before using aliases, inheritance, or environment mappings. A syntactically valid but semantically invalid file fails closed: `lpm env pull` and `lpm env rotate-key` stop before network access or local vault replacement, and `lpm env share` stops before sharing-key classification or registration. Malformed, oversized, and unreadable files follow the same fail-closed path. Account-level operations that do not consult project configuration remain independent.

### Per-environment scoping [#per-environment-scoping]

Pass `--env=<name>` to scope reads and writes to a named environment (e.g., `staging`, `production`). Environments are stored independently — `staging` and `production` keep separate value sets for the same key.

```bash
lpm env set --env=staging API_URL=https://staging.example.com
lpm env set --env=production API_URL=https://api.example.com
lpm env list --env=staging
```

### Other local commands [#other-local-commands]

```bash
lpm env init                              # interactive environment setup
lpm env ls                                # environment overview table with sync/update status
lpm env copy <src> <dst>                  # copy all secrets between envs
lpm env print                             # print resolved env (for `eval`)
lpm env check                             # check every env against lpm.json > envSchema
lpm env validate                          # compare the default vault with .env.example
lpm env example                           # generate `.env.example` from the schema
```

### Validate local environments [#validate-local-environments]

`lpm env check` loads `lpm.json > envSchema`, discovers the project's environments from its configuration and local vault, then validates each resolved environment. It exits non-zero when any environment is invalid in both human and JSON modes.

```bash
lpm env check
lpm --json env check | jq -e '.success'
```

JSON output retains the per-environment validation details and sets `"success": false` for an invalid result, so automation can use either the process status or the structured field.

`lpm env validate` is a separate, narrower check. It compares the key names in the **default local vault** with the required names in `.env.example`; it does not inspect named environments, cloud state, or `envSchema`. Values assigned in `.env.example` are ignored.

```bash
lpm env validate
lpm env validate --strict
lpm --json env validate | jq -e '.valid'
```

Without `--strict`, the result is valid when every required key is present; extra keys in the default local vault are allowed. With `--strict`, those extra keys also make the result invalid. Invalid results exit non-zero in both human and JSON modes. In JSON, `"success"` and `"valid"` always agree while the `required`, `present`, `missing`, and `extra` fields remain available for structured inspection.

Resolved env surfaces such as `lpm env print`, [`lpm run`](/docs/dev/run), [`lpm <file>` / `lpm exec`](/docs/dev/exec), and platform pushes ignore runtime-hook names such as `NODE_OPTIONS`, `LD_PRELOAD`, `LD_AUDIT`, `BASH_ENV`, and `DYLD_INSERT_LIBRARIES` when they come from env files, env secrets, or env-schema defaults. LPM CLI also strips inherited values for these names before spawning scripts.

## Cloud sync [#cloud-sync]

`lpm env pull` and `lpm env push` round-trip the local env project to lpm.dev's encrypted storage. The server stores ciphertext and wrapped key material; it never sees plaintext values.

```bash
lpm env push                              # encrypt + push the local env project
lpm env pull                              # pull + decrypt remote → local
lpm env diff                              # diff local vs remote (default env)
lpm env diff staging                      # diff local staging vs remote staging
lpm env diff staging production           # diff two local environments
lpm env share --org <org-slug>            # share the env project with an org
lpm env pair <CODE>                       # pair this device via dashboard code (interactive confirmation)
lpm env pair <CODE> --yes                 # skip the confirmation prompt (NOT recommended)
lpm env unpair                            # revoke this device's pairing
lpm env log                               # last 50 audit log entries
lpm env rotate-key                        # rotate the complete personal cloud payload
lpm env rotate-key --org <org-slug>       # rotate an organization content key
lpm env rotate-sharing-key                # rotate your X25519 sharing key (interactive, step-up reauth)
```

Cloud sync is plan-gated. Free accounts get local-only storage. Pro plans unlock personal sync, and active Organization plans unlock organization sync and sharing. Every push uses fresh randomized encryption and increments the server version, even when the plaintext did not change.

### Pair a dashboard browser [#pair-a-dashboard-browser]

Create a pairing code on the dashboard Secrets page. Then run this command on the CLI machine that holds your env key:

```bash
lpm env pair <CODE>
```

The browser and CLI exchange temporary P-256 public keys. Both clients derive an eight-digit comparison number from the shared ECDH secret.

The number has two groups of four digits. Make sure that the number in the terminal equals the dashboard number.

Type `y` only when the numbers are equal. The Registry relays the public keys, but it cannot calculate the shared secret.

The pairing code expires after five minutes. Use `lpm env unpair` to revoke all paired browsers.

Start pairing from an organization project for organization access. Personal and organization pairings use separate keys and browser storage.

Organization pairing requires the CLI machine with your registered account sharing key. It does not register or rotate a sharing key.

The browser can decrypt projects shared with your account. Owners, administrators, and maintainers can save changes; other members have read access.

The server checks current membership, subscription access, and key versions before a save. If rotation is required, an owner or administrator must run `lpm env rotate --org <slug>` first.

After a sharing-key change, pair the browser again and ask an owner or administrator to refresh your environment access.

> CAUTION: The `--yes` flag skips the comparison. Use this flag only for a command that you copied from your trusted dashboard.

### Re-encrypt personal cloud data [#re-encrypt-personal-cloud-data]

`lpm env rotate-key` pulls the authoritative personal cloud payload. It validates every environment boundary and re-encrypts the complete payload with a fresh data key.

The command uses the current remote version for a compare-and-swap write. It preserves named, empty, and remote-only environments and project metadata.

A conflict or an error before upload leaves the previous remote payload unchanged. A timeout after upload has an ambiguous result.

After a timeout, the CLI keeps its local version metadata unchanged. Run `lpm env rotate-key` again to use the authoritative remote version.

### Rotate an organization content key [#rotate-an-organization-content-key]

An organization owner or administrator can rotate the content key:

```bash
lpm env rotate-key --org <org-slug>
```

The command pulls and decrypts the authoritative organization payload. It validates the complete payload and creates a fresh content key.

The CLI re-encrypts the payload and wraps the new key for each current member who has a registered sharing key.

The server locks the current member and public-key rows. It rejects a missing, extra, stale, or incorrectly fingerprinted recipient.

The server then uses the remote version for a compare-and-swap write. One transaction replaces the ciphertext, advances the content-key version, and replaces all wraps.

If membership or a public key changes during rotation, the server returns a conflict. The previous ciphertext and wraps remain unchanged.

After you remove a member, run this command from a current owner or administrator machine. The machine must have current organization access.

Rotation prevents the removed key from decrypting future ciphertext. It cannot erase content keys or plaintext that a former member already copied.

Each organization wrap records three bindings: the recipient key version, the full SHA-256 fingerprint, and the organization content-key version.

If a binding is absent or stale, the server rejects the pull. The dashboard shows **Needs share** for that member.

### Rotating your sharing key [#rotating-your-sharing-key]

`lpm env rotate-sharing-key` generates a fresh X25519 keypair, uploads the new public key after step-up authentication, and invalidates every organization env-project wrapped-key entry the server held for you. The CLI keeps the new private key in a pending slot until the server acknowledges the rotation, so a crash mid-flight is safe; re-running the command resumes the pending slot if the server already moved on. The flow:

1. Prints the blast radius — every organization env project you can access needs an owner or admin to run [`lpm env share --org <slug>`](#cloud-sync) before pulls resume.
2. Asks you to type `ROTATE` (uppercase) to confirm.
3. Prompts for your password (and authenticator code, if MFA is enrolled) to mint the step-up proof.
4. Uploads the new public key; the server invalidates the old wrapped-key rows and sends an out-of-band security email to your account plus an impact email to every affected org's owners and admins.
5. Promotes the pending slot to the live keychain entry. The dashboard's env-project Member Access view shows the affected rows as **Needs share**.

`rotate-sharing-key` refuses to run without a TTY — there's no `--yes` / non-interactive path, by design. On every machine where you want to keep pulling, you re-register the rotated key the next time the CLI needs it: any org-touching verb (`env share`, `env pull --org`, etc.) detects a missing server-side key and walks you through a `vault:public-key:set` step-up to register it.

The pre-rotation key never re-establishes itself. If a teammate's machine still has the old private key it cannot decrypt anything pushed under the new AES key, and the dashboard's audit log records the rotation with both fingerprints so cross-team forensics are trivial.

## Deployment platform integrations [#deployment-platform-integrations]

Vercel, Coolify, Fly.io, Railway, and GitHub Actions are the supported env platforms. Application env values travel directly between the Rust CLI and the selected platform; they never pass through lpm.dev. lpm.dev stores the platform credential encrypted at rest, non-secret connection configuration, authorization scope, and bounded operation counts.

```bash
# Vercel
lpm env connect vercel --project=<id> --linked-env=production --target=production
lpm env push --to vercel                           # merge current env into Vercel
lpm env push --to vercel --env=production --clean  # remove Vercel-only values too
lpm env pull --from vercel --env=production        # import readable values into the local env

# Coolify
lpm env connect coolify --url=https://coolify.example.com \
  --application=<application-uuid> --linked-env=production
lpm env push --to coolify                          # merge into production values
lpm env push --to coolify --clean                  # remove Coolify-only production values
lpm env pull --from coolify --env=production

# Fly.io app secrets
lpm env connect fly --app=<fly-app-name> --linked-env=production
lpm env push --to fly                              # refresh every expected secret
lpm env push --to fly --clean                      # remove Fly-only secret names
lpm env pull --from fly --env=production           # reports names skipped; values are write-only

# Railway service values
lpm env connect railway --project=<project-id> \
  --environment=<environment-id> --service=<service-id> \
  --linked-env=production
lpm env push --to railway                          # merge into the service
lpm env push --to railway --clean                  # exact service user-values
lpm env pull --from railway --env=production

# Railway shared environment values
lpm env connect railway --project=<project-id> \
  --environment=<environment-id>

# GitHub repository or environment values
lpm env connect github-actions --repository=<owner/repository> \
  --environment=production --linked-env=production
lpm env push --to github-actions
lpm env push --to github-actions --clean
lpm env pull --from github-actions

lpm env status                                     # compare without mutation
```

The connect command prompts for the platform token and verifies it directly with the platform before saving the encrypted credential to lpm.dev.

For Vercel, optional connection flags are `--team`, `--label`, `--linked-env`, and comma-separated `--target` values (`production`, `preview`, `development`). Omitting `--target` selects all three. Targetless values, duplicate relevant keys, and variables shared with an unselected target fail closed before mutation.

For Coolify, create the token as a team administrator. Use either a `root`
token or a token with `read`, `write`, and `read:sensitive`. Coolify hides
application values from non-administrators and from tokens without
`read:sensitive`; the CLI fails closed instead of importing a hidden value as
an empty string.

`--url` must be the HTTPS origin of the Coolify instance, without credentials,
a path, query parameters, or a fragment. `--application` selects the
application UUID. Add `--preview` to target preview values instead of
production values. `--label` and `--linked-env` have the same meaning as on
Vercel. The CLI disables redirects for every Coolify request so the API token
and env values cannot be forwarded to another origin.

The CLI syncs Coolify's raw stored value, not its deployment-rendered value,
and preserves the existing literal, multiline, and shown-once flags during an
update. Shared-variable references and shown-once values cannot be read
authoritatively and are rejected before mutation. Convert shared references to
application values, or delete and recreate shown-once values as readable
values, before syncing.

For a new production key, the CLI first reserves the preview key with a unique
non-secret ownership value. It creates production with that same non-secret
value, then updates only the owned production row with the real value. A
`409 Conflict` is accepted only after Coolify confirms exactly one existing
preview value; that pre-existing value is never changed or deleted. If a
create response is lost, malformed, or too large to read safely, the CLI
relists the application and recovers only an exact key, target, and unique
operation-value match. Cleanup deletes only rows proven to belong to that
operation by UUID or unique value; if recovery or cleanup cannot establish the
final state, synchronization fails closed.

For Fly.io, `--app` selects one canonical app. Connect discovers and stores the immutable app ID, organization ID, and organization slug. Every later operation verifies the canonical name, app ID, and organization ID before mutation, so an app rename, transfer, deletion, or wrong target fails closed.

Fly.io app secrets are write-only. Every selected local key is sent as a Fly secret, regardless of `envSchema`. A push refreshes all expected values in one `setSecrets` release; `--clean` can create a second `unsetSecrets` release for extra names. If that removal response fails or disconnects, the CLI rereads the authoritative names and counts only keys confirmed absent; if the reread fails, no exact failed-operation audit is sent. Status is `names_only` when expected names exist, without claiming value equality. Pull cannot recover secret values and reports `no_readable_values` plus `skippedSecrets`. Fly-managed runtime names such as `FLY_APP_NAME`, `FLY_REGION`, and `FLY_MACHINE_ID` are excluded.

For Railway, `--project` and `--environment` are required. `--service` selects service variables; omit it to select shared environment variables. Account, workspace, and OAuth tokens use Bearer authentication. Add `--project-token` when the credential is a Railway project token so the CLI uses `Project-Access-Token`. Reads request unrendered values so `${{Service.VARIABLE}}` references round-trip intact. The adapter disables redirects, treats GraphQL `errors` as failures even with HTTP 200, performs one bulk upsert, then rereads the target and reports success only after exact verification.

For GitHub Actions, `--repository=<owner/name>` is required. Add `--environment=<name>` to target environment-level values; omit it for repository-level values. The CLI stores the canonical repository name plus immutable numeric repository ID and verifies both before every operation. A rename, transfer, wrong repository, or unconfigured environment fails closed.

GitHub Actions variables are readable, but Actions secrets are write-only. Only a key explicitly declared as `client: true` and not `secret: true` in `lpm.json > envSchema` becomes an Actions variable. Undeclared keys, secret-marked keys, and `client: true, secret: true` keys become Actions secrets. Without an env schema, every key is a secret.

```json title="lpm.json"
{
  "envSchema": {
    "vars": {
      "PUBLIC_ORIGIN": { "client": true },
      "API_TOKEN": { "secret": true },
      "AMBIGUOUS_TOKEN": { "client": true, "secret": true }
    }
  }
}
```

Each secret is sealed locally with GitHub's Actions public key; only ciphertext reaches GitHub. Status returns `names_only` when readable variables match and every expected secret name exists—it never claims that secret values are equal. Pull imports readable variables, preserves local-only values, reports skipped secret names, and never overwrites local secrets with unavailable values. Push refreshes every expected secret. `--clean` removes extra variable and secret names. After an ambiguous mutation response, the CLI rereads comparable variable or secret-name state before deciding whether the operation committed.

Fine-grained GitHub tokens need Metadata read access. Repository-scoped connections additionally need Variables read/write and Secrets read/write for full sync. Environment-scoped connections need Environments read/write. Read-only permission is sufficient only for status and readable pulls.

Normal pushes preserve values that exist only on the platform. `--clean` removes platform-only values within the configured target. Platform-managed variables are always excluded. Fly runtime names and Railway-provided runtime and Git variables are managed; user-configurable Railway names such as `RAILWAY_DOCKERFILE_PATH` remain syncable. `lpm env status` performs an exact name comparison for Fly.io and an exact value comparison where platforms return plaintext, so preserved platform-only values are reported as drift until they are removed or added locally.

Direct platform pushes record exact failed-operation counts only when acknowledged responses or authoritative final-state evidence prove them. Readable values can be reconciled by value, while Fly.io removals and GitHub secret creation or removal can be reconciled by name. An ambiguous existing-secret update remains unprovable because Fly.io and GitHub never return the value; in that case, or when reconciliation itself fails, the CLI returns the provider error without sending a falsely exact `push_failed` audit.

Encrypted cloud storage and platform deployment remain two explicit operations:

```bash
lpm env push                  # ciphertext + wrapped key to lpm.dev
lpm env push --to fly         # plaintext directly from the CLI into write-only Fly secrets
lpm env push --to railway     # plaintext directly from the CLI to Railway
lpm env push --to github-actions # values and sealed secrets directly to GitHub
```

The two systems cannot participate in an atomic transaction. A failure is reported for the operation that failed and does not claim to roll back a successful operation against the other system. After `lpm env pull` recovers the encrypted state on another paired machine, `lpm env status` verifies that state directly against the platform.

## OIDC policies (CI) [#oidc-policies-ci]

OIDC pulls support personal Pro projects and organization projects on an active Organization or Enterprise plan. GitHub Actions and GitLab.com CI are supported. Self-managed GitLab issuers are not supported. CI does not store a long-lived Registry token.

```bash
lpm env push
lpm env oidc allow \
  --provider=github \
  --repo=owner/repo \
  --workflow=.github/workflows/deploy.yml \
  --branch=main \
  --env=production
lpm env oidc list
```

Run those commands from a logged-in development machine. `lpm env push` must create the personal cloud vault before `oidc allow` can enable CI escrow. The workflow path is mandatory and must be a file directly under `.github/workflows/` ending in `.yml` or `.yaml`. The default allowed event is `push`. Use `--events=push,workflow_dispatch` to allow more than one.

For an organization project, an owner or admin must first share or pull its current encrypted revision. Then explicitly enable server decryption:

```bash
lpm env pull --org=acme
lpm env oidc allow \
  --org=acme \
  --allow-server-decryption \
  --provider=github \
  --repo=owner/repo \
  --workflow=.github/workflows/deploy.yml \
  --branch=main \
  --env=production
lpm env oidc list --org=acme
```

This opt-in gives the Registry this project's current content key. It does not transfer your organization sharing private key or personal wrapping key. The Registry can decrypt the project's environments for CI jobs that match the policy.

The organization owns these policies and CI credentials. They do not depend on the original policy creator remaining an admin. The Registry checks current billing, policy restrictions, token validity, and content-key state at each pull.

Content-key rotation or membership key invalidation disables CI decryption and revokes issued credentials. After the new key is shared, an owner or admin must run the complete `oidc allow` command again.

Owners and admins can add or replace policies, delete policies, and disable CI decryption from the project's **Access** page. To disable it from the CLI:

```bash
lpm env oidc disable --org=acme
```

Disabling decryption also revokes existing CI credentials. Enabling it again does not restore those credentials. Disabling remains available when organization billing lapses.

The CI pull command is the same for personal and organization projects. Its policy ID selects the ownership scope, so the CI job does not need `--org`.

After a successful `oidc allow`, LPM CLI prints the server-issued policy ID. Copy this UUID into the CI configuration as `LPM_OIDC_POLICY_ID`.

The policy ID is a selector, not a bearer credential. It cannot authorize a pull without a valid CI identity token. Protect its integrity because a changed value can select the wrong policy.

Each `lpm env pull --oidc` command requires this selector. You can pass `--policy-id=<uuid>` instead. The command flag takes precedence over `LPM_OIDC_POLICY_ID`.

By default, LPM CLI gets the immutable numeric repository ID from the GitHub API. Public repository requests do not need GitHub authentication.

If the repository is private, set `GITHUB_TOKEN` or `GH_TOKEN` before you run the command. Alternatively, add `--repository-id=<numeric-id>`.

The policy binds the repository name and its numeric ID. Both values must match during the OIDC exchange and the CI pull.

A repository rename or transfer stops CI access. Run `oidc allow` again with the current repository name.

A different repository cannot use the policy if it reuses the old name. The new repository has a different numeric ID.

Existing GitHub policies without a repository ID fail closed. If `lpm env oidc list` reports a missing ID, run `oidc allow` again.

GitLab.com uses the stable numeric `project_id` claim and has no GitHub workflow/event/fork fields:

```bash
lpm env oidc allow \
  --provider=gitlab \
  --project-id=12345 \
  --branch=main \
  --env=production
```

`--repo`, `--repository-id`, `--workflow`, `--events`, and `--allow-forks` are rejected for GitLab.com policies. The canonical subject is `project:12345`. Exchanges require `ref_type: "branch"`. A tag whose name matches an allowed branch is still rejected.

`lpm env oidc allow` replaces the policy's complete allowlists rather than merging an individual branch or environment. Before changing an existing policy, inspect it with `lpm env oidc list` and review the full replacement surface with `lpm env oidc allow --help`.

The policy command succeeds only after it creates or updates the policy, retrieves the local wrapping key, and uploads the escrow material needed for server-side CI decryption. If wrapping-key retrieval or escrow upload fails, the command exits non-zero without reporting policy success. The policy may already exist, but CI pulls are not ready; fix the escrow problem and rerun the same command. Rerunning safely updates the matching policy.

For `lpm env pull --oidc`, a non-empty [`LPM_VAULT_ID`](/docs/reference/env-vars#auth-and-routing) takes precedence over `lpm.json > vault` and can bootstrap a checkout that has no local vault field. If the variable is unset, empty, or whitespace-only, LPM CLI falls back to the local field. Commit the generated local value or pass the same vault ID through the workflow environment.

```yaml title=".github/workflows/deploy.yml"
name: Deploy

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @lpm-registry/cli
      - name: Load production environment
        run: lpm env pull --oidc --env=production --output=.env
        env:
          LPM_VAULT_ID: 7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b
          LPM_OIDC_POLICY_ID: ${{ vars.LPM_OIDC_POLICY_ID }}
      - run: ./deploy.sh
```

GitHub exposes its OIDC runtime endpoint when the job has `id-token: write`. LPM CLI requests the `https://lpm.dev` audience automatically. The Registry checks the repository name, numeric ID, workflow, event, branch, and environment before it returns values.

Create `LPM_OIDC_POLICY_ID` as a GitHub repository variable for this job. If you use an environment variable, add `environment: <name>` to the job. GitHub does not expose an environment variable to a job that does not target that environment.

GitLab.com CI supplies `LPM_OIDC_TOKEN` with audience `https://lpm.dev`; LPM CLI binds it to the exact numeric project ID. Self-managed GitLab issuers are not supported. Organization projects use the same CI token exchange after explicit decryption setup.

```yaml title=".gitlab-ci.yml"
deploy:
  id_tokens:
    LPM_OIDC_TOKEN:
      aud: https://lpm.dev
  variables:
    LPM_VAULT_ID: 7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b
  script:
    - npm install -g @lpm-registry/cli
    - lpm env pull --oidc --env=production --output=.env
    - ./deploy.sh
```

Create `LPM_OIDC_POLICY_ID` as a GitLab CI/CD variable. Mark it protected only when every branch or tag allowed by the OIDC policy is also protected. GitLab does not expose protected variables to unprotected refs.

## CI output [#ci-output]

```bash
lpm env print --ci --env=staging
lpm env export --ci --env=staging ci.env
```

`lpm env print --ci` emits resolved environment variables in a CI-native format. GitHub Actions runners get GitHub's masked-output format, Vercel gets dotenv, and generic CI gets shell-export lines.

`lpm env export --ci <file>` writes a dotenv-formatted copy to the given path. `--env=<mode>` selects which environment file is loaded. Secret-marked vars from `lpm.json > envSchema` are masked in CI-native output where the platform supports it.

`lpm ci` is still the frozen-lockfile install command. See [`lpm install`](/docs/packages/install#frozen-lockfile-and-ci).

## Flags [#flags]

| Flag                                                     | Effect                                                                                            |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `--env=<name>`                                           | Scope to a named environment (default: `default`)                                                 |
| `--reveal`                                               | Show values in `get` / `list` (default is masked)                                                 |
| `--overwrite`                                            | `import` overwrites existing keys instead of skipping                                             |
| `--to <vercel\|coolify\|fly\|railway\|github-actions>`   | Push directly from the CLI to the connected platform                                              |
| `--from <vercel\|coolify\|fly\|railway\|github-actions>` | Pull readable values directly from the platform into the selected local env                       |
| `--clean`                                                | Replace platform values instead of merging                                                        |
| `--yes`                                                  | Skip confirmation prompts                                                                         |
| `--ci`                                                   | Emit CI-native output for `print` or dotenv output for `export`                                   |
| `--oidc`                                                 | Pull a personal Pro or approved organization project through GitHub Actions or GitLab.com CI OIDC |
| `--policy-id=<uuid>`                                     | Select the exact OIDC policy. This flag takes precedence over `LPM_OIDC_POLICY_ID`                |
| `--output <file>`                                        | Write `pull` results to a file instead of local env storage                                       |
| `--org <slug>`                                           | Org-scoped sync target                                                                            |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [Env encryption architecture](/docs/infra/secrets-vault) — storage and encryption design, sharing-key lifecycle, CLI step-up reauth
* [CI escrow and OIDC pulls](/docs/infra/secrets-vault#ci-escrow--oidc-pulls) — trust and decryption model
* [`lpm.json`](/docs/reference/lpm-json) — `envSchema` and environment definitions


# lpm <file> and lpm exec (/docs/dev/exec)



```bash
lpm <file> [--env <mode>] [--watch] [--plain-node] [args...]
lpm exec [--env <mode>] [--no-env-check] <bin> [args...]
```

LPM CLI has two execution lanes:

* `lpm <file>` runs a JavaScript or TypeScript source file directly.
* `lpm exec <bin>` runs a project-local binary from `node_modules/.bin`, matching npm/pnpm muscle memory.

`lpm exec` does not run source files. Use `lpm scripts/seed.ts`, not `lpm exec scripts/seed.ts`.

There is also a shorthand: `lpm <bin>` tries a project-local binary only after the name does not match a built-in command, a source-file/path invocation, a `package.json` script, or an `lpm.json` task. Use `lpm exec <bin>` when you want the local-bin lane explicitly.

## Source Files [#source-files]

```bash
lpm scripts/seed.ts
lpm scripts/view.tsx
lpm scripts/migrate.js
lpm scripts/seed.ts --env staging
lpm scripts/seed.ts --watch
lpm scripts/seed.ts --plain-node
lpm scripts/seed.ts -- --env=staging
```

Runs a single JavaScript or TypeScript file without adding a `scripts` entry to `package.json`. `.js` / `.mjs` / `.cjs` go through plain `node`; `.ts` / `.tsx` / `.mts` / `.cts` use LPM CLI's OXC-backed TypeScript runtime on supported Node versions. If no safe runtime is available, LPM CLI fails with a fix-it message instead of downloading `tsx` through `npx`.

Human progress goes to stderr and names the selected runtime before the file starts:

```bash
› Executing scripts/seed.ts with Node.js v22.18.0 + LPM CLI TS runtime
✓ Done · exited 0 in 412ms
```

## Local Binaries [#local-binaries]

```bash
lpm exec eslint --fix src
lpm exec vitest run
lpm exec --env staging prisma migrate deploy
lpm jest --watch  # shorthand when no script/task named "jest" exists
```

`lpm exec <bin>` resolves `<bin>` only from project `node_modules/.bin` directories, walking upward through the workspace. It loads project env, injects the same local-bin PATH used by `lpm run`, strips inherited runtime-hook env like `NODE_OPTIONS` and `LD_PRELOAD`, then spawns the binary directly without a shell.

It does not fetch packages and does not fall back to system PATH. The bare shorthand uses the same local-bin lookup, but only after scripts/tasks miss. Use [`lpm dlx`](/docs/dev/dlx) when you want to run a package binary without adding it to the project.

## Runtime Selection [#runtime-selection]

| Extension             | Runtime                                                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `.js`, `.mjs`, `.cjs` | `node` - direct spawn, no shell, no LPM CLI TS preload                                                                                                 |
| `.ts`, `.mts`, `.cts` | Node 22.18+ / 23.6+ / 24+ with the LPM CLI TS runtime -> Node 22.6-22.17 / 23.0-23.5 with `--experimental-strip-types` -> project-local `tsx`          |
| `.tsx`                | Node 22.18+ / 23.6+ / 24+ with the LPM CLI TS runtime's OXC JSX transform -> project-local `tsx` when the Node version cannot load the LPM CLI runtime |

The effective PATH includes project `node_modules/.bin`, any matching managed Node that LPM CLI resolved for the project, and then your existing system PATH.

LPM CLI source-file execution does not fall back to `npx tsx`. That fallback can download and execute npm code outside LPM CLI's install-policy/security model. If TypeScript execution cannot use the LPM CLI runtime, Node's built-in strip-types path, or a project-local `tsx`, use `lpm use node@22.18+` or add and pin `tsx` in the project explicitly.

The LPM CLI TS runtime is LPM CLI-owned: it writes LPM CLI's bundled loader into the LPM CLI cache, calls LPM CLI's Rust/OXC transformer without fetching npm packages, executes `.ts`, `.tsx`, `.mts`, and `.cts`, reads `tsconfig.json` `baseUrl` / `paths` mappings for project-local imports, emits source maps, and caches transforms by source content, runtime version, Node version, platform, architecture, file path, tsconfig fingerprint, and transform options. Cache entries are validated before reuse.

TSX uses OXC's JSX transform. By default it emits the React automatic JSX runtime (`react/jsx-runtime`). `jsx: "react"` or explicit `jsxFactory` / `jsxFragmentFactory` selects the classic runtime, `jsx: "react-jsxdev"` enables development JSX output, and `jsxImportSource` changes the automatic-runtime import source. JSX runtime packages still need to be present in the project or otherwise resolvable by Node; LPM CLI does not download them during execution.

## Plain Node Mode [#plain-node-mode]

`--plain-node` disables the LPM CLI TS runtime preload and child Node propagation for `lpm <file>`. `--no-augment` is an alias.

Use it when you need to check what Node itself can run without LPM CLI augmentation:

```bash
lpm scripts/seed.ts --plain-node
```

In plain mode, `.ts` / `.mts` / `.cts` use Node's built-in TypeScript support when available. `.tsx` is refused because plain Node does not support TSX.

## Child Node Processes [#child-node-processes]

When a TypeScript entrypoint runs with the LPM CLI TS runtime, child processes spawned as `node child.ts` inherit a controlled LPM CLI `NODE_OPTIONS` preload. That lets TypeScript child scripts use the same loader and `tsconfig.json` path mappings without invoking `npx` or a project-local wrapper.

Inherited parent `NODE_OPTIONS` is stripped before execution, so a developer shell or CI environment cannot silently replace the LPM CLI loader. `--plain-node` / `--no-augment` disables this child propagation.

## Environment Loading [#environment-loading]

Both execution lanes load the project's env files and env secrets before spawn. Pass `--env <mode>` to load the same mode-specific files as [`lpm run`](/docs/dev/run), such as `.env.staging` and `.env.staging.local`.

`--no-env-check` skips env-schema validation only; it does not disable env loading.

Before spawning, LPM CLI strips inherited runtime-hook env like `NODE_OPTIONS`, `LD_PRELOAD`, `LD_AUDIT`, `BASH_ENV`, and `DYLD_INSERT_LIBRARIES`. Project env files, env secrets, and env-schema defaults are filtered for the same runtime-hook names before they are applied, so those keys are ignored when they come from project env. LPM CLI-controlled runtime env is added after that filtering.

## Watch Mode [#watch-mode]

`--watch` re-runs the same source-file execution plan when the target file changes:

```bash
lpm scripts/seed.ts --watch
```

It watches the requested file only; dependency-graph watching is not part of direct source-file execution yet.

## Argument Forwarding [#argument-forwarding]

For source files, put LPM CLI flags after the file and use `--` when the script arg starts with `-`:

```bash
lpm scripts/seed.ts --env staging -- --verbose --env=prod
```

For local binaries, flags for LPM CLI belong before the binary name. Everything after the binary name is passed to the binary:

```bash
lpm exec --env staging eslint --fix src/index.ts
```

## Flags [#flags]

| Lane         | Flag             | Effect                                                            |
| ------------ | ---------------- | ----------------------------------------------------------------- |
| Both         | `--env <mode>`   | Load env files and secrets for the selected mode                  |
| Both         | `--no-env-check` | Skip env-var schema validation before execution                   |
| Source files | `--watch`        | Re-run the same plan when the target file changes                 |
| Source files | `--plain-node`   | Disable the LPM CLI TS runtime preload and child Node propagation |
| Source files | `--no-augment`   | Alias for `--plain-node`                                          |

Plus the [global flags](/docs/commands#global-flags).

## See Also [#see-also]

* [`lpm run`](/docs/dev/run) - run named scripts from `package.json`
* [`lpm dlx`](/docs/dev/dlx) - run a binary from a registry package without installing it
* [`lpm dev`](/docs/dev/dev) - full dev server with HTTPS, tunnel, services


# lpm fmt (/docs/dev/fmt)



```bash
lpm fmt [path...]              # format and write
lpm fmt --check [path...]      # check only, exit 1 if unformatted
```

Runs [Biome](https://biomejs.dev) `format` across the project. The binary is downloaded the first time you run `lpm fmt`, cached under `~/.lpm/cache/`, and reused thereafter.

Faster than `npx biome` (`3 ms` vs `340 ms` on the [reference fixture](https://github.com/lpm-dev/rust-client#benchmarks)) because there's no per-invocation `npx` resolution overhead.

## Examples [#examples]

```bash
lpm fmt                       # format the whole project, write changes
lpm fmt src/                  # format one directory
lpm fmt --check               # check without writing — CI mode
lpm fmt --check src/          # check one directory
lpm fmt --all                 # workspace: format every member
lpm fmt --filter web          # workspace: format one member
lpm fmt --filter './apps/*'   # workspace: path glob
lpm fmt --affected            # workspace: only members affected vs main
```

## Default vs `--check` [#default-vs---check]

| Mode                | What happens                                                                                        |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| `lpm fmt` (default) | Runs `biome format <path> --write` — actually formats files                                         |
| `lpm fmt --check`   | Runs `biome format <path>` (no `--write`) — exits non-zero if any file is unformatted, useful in CI |

Without an explicit path argument, both modes operate on `.` (the project root).

## Pinning a version [#pinning-a-version]

By default, `lpm fmt` uses the version baked into LPM CLI's plugin registry. Override per-project in `lpm.json`:

```json title="lpm.json"
{
  "tools": {
    "biome": "2.4.8"
  }
}
```

Run `lpm plugin update biome` to pull the latest release into the cache.

## Workspaces [#workspaces]

```bash
lpm fmt --all                              # every member
lpm fmt --filter web                       # exact name
lpm fmt --filter '@scope/*'                # glob
lpm fmt --filter './apps/*' --check        # path glob, CI mode
lpm fmt --filter-prod ...shared            # prod graph closure
lpm fmt --affected --base develop          # affected vs a non-main branch
lpm fmt --filter web --fail-if-no-match    # exit non-zero on typo'd filter
```

Members run in topological levels, with packages inside each level executing in parallel up to the available CPU count. The Biome binary is resolved once at the workspace root before fan-out, so the homogeneous-version case never races on cold-cache install.

Filter grammar is documented in [Workspaces](/docs/packages/workspaces#filter-grammar).

`--all` and `--affected` are mutually exclusive; filters compose with `--affected` (the affected set is unioned with the filter result). `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

## Flags [#flags]

| Flag                                    | Effect                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--check`                               | Check only, don't write — exits non-zero on unformatted files                                                          |
| `--all`                                 | Run in every workspace member                                                                                          |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable; entries union) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                                      |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                                    |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                                        |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                                  |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions                            |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set (recommended in CI)                                                  |

Anything after `--` (or trailing path) is forwarded to Biome.

Plus the [global flags](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

Single-package mode (`lpm fmt`) preserves Biome's stdout — LPM CLI does not wrap it. Workspace mode emits a single LPM CLI envelope on stdout; per-member stdout/stderr is captured and surfaced inside the envelope only on failure. See [`lpm lint`](/docs/dev/lint#json-in-workspace-mode) for the exact envelope shape.

## See also [#see-also]

* [`lpm lint`](/docs/dev/lint) — Oxlint, same lazy-download mechanism
* [`lpm check`](/docs/dev/check) — TypeScript type-check
* [`lpm plugin`](/docs/dev/plugin) — manage tool versions and updates
* [Built-in tools](/docs/dev/builtin-tools) — how the plugin system works


# Dev (/docs/dev)





LPM CLI's development surface. The marquee command is [`lpm dev`](/docs/dev/dev) — one command that picks up managed runtimes, syncs deps, loads `.env`, optionally serves over HTTPS, optionally exposes a public tunnel, and orchestrates multi-service projects. The rest of this section is the supporting cast: a parallel + cached task runner, runners for test / bench / lint / fmt / type-check / bundle / pack, and the runtime + env management around it.

For a step-by-step walkthrough, see [Zero-config dev server](/docs/guides/zero-config-dev-server).

## Run scripts + tasks [#run-scripts--tasks]

<Cards>
  <Card title="lpm dev" href="/docs/dev/dev" description="Zero-config dev server with HTTPS, tunnel, env loading, and multi-service orchestration." />

  <Card title="lpm run" href="/docs/dev/run" description="Run package.json scripts. Parallel, cached, watchable, workspace-aware." />

  <Card title="lpm <file> / exec" href="/docs/dev/exec" description="Run JS/TS files directly, or run project-local binaries." />

  <Card title="lpm dlx / lpx" href="/docs/dev/dlx" description="Run a package binary without installing it into the project." />
</Cards>

## Test + bench [#test--bench]

<Cards>
  <Card title="lpm test" href="/docs/dev/test" description="Auto-detect and run vitest / jest / mocha. Workspace-aware with --filter / --affected." />

  <Card title="lpm bench" href="/docs/dev/bench" description="Auto-detect and run vitest bench, or your scripts.bench fallback." />
</Cards>

## Lint, format, type-check, bundle, pack (built-in tools) [#lint-format-type-check-bundle-pack-built-in-tools]

<Cards>
  <Card title="lpm lint" href="/docs/dev/lint" description="Backed by Oxlint — lazy-downloaded, version-pinned, no npx round-trip." />

  <Card title="lpm fmt" href="/docs/dev/fmt" description="Backed by Biome — same pinning + caching model." />

  <Card title="lpm bundle" href="/docs/dev/bundle" description="Backed by a managed Rolldown plugin with a verified cached package layout." />

  <Card title="lpm pack" href="/docs/dev/pack" description="Backed by the project's tsdown install, wrapped in LPM CLI's workspace and JSON contract." />

  <Card title="lpm check" href="/docs/dev/check" description="TypeScript check (tsc --noEmit) against the project-local install." />

  <Card title="lpm plugin" href="/docs/dev/plugin" description="Manage installed built-in tool plugins (list, update, remove)." />

  <Card title="lpm completions" href="/docs/dev/completions" description="Generate shell completions for bash, zsh, fish, PowerShell, or elvish." />
</Cards>

## Runtime + environment [#runtime--environment]

<Cards>
  <Card title="lpm use" href="/docs/dev/use" description="Install and pin managed Node.js or Bun versions. Auto-installs on first use of a pinned version." />

  <Card title="lpm env" href="/docs/dev/env" description="Per-project secrets — set / get / list / push / pull / pair / OIDC. Encrypted local storage with optional cloud sync." />
</Cards>

## Features (concept pages) [#features-concept-pages]

<Cards>
  <Card title="Task runner" href="/docs/dev/task-runner" description="Parallel + cached execution model behind lpm run and lpm dev's services." />

  <Card title="Test + bench auto-detection" href="/docs/dev/test-bench-runners" description="How lpm test and lpm bench pick a runner — vitest, jest, mocha, or scripts fallback." />

  <Card title="Built-in tools" href="/docs/dev/builtin-tools" description="How plugin-backed tools, project-local wrappers, and managed engines fit together." />

  <Card title="Managed runtimes" href="/docs/dev/node-version-pinning" description="Node and Bun detection order, auto-install, storage, and script PATH behavior." />
</Cards>


# lpm lint (/docs/dev/lint)



```bash
lpm lint [-- args...]
```

Runs [Oxlint](https://oxc.rs/docs/guide/usage/linter.html) — a fast Rust-based linter — across the project. The binary is downloaded the first time you run `lpm lint`, cached under `~/.lpm/cache/`, and reused thereafter.

Faster than `npx oxlint` (`3 ms` vs `273 ms` on the [reference fixture](https://github.com/lpm-dev/rust-client#benchmarks)) because there's no per-invocation `npx` resolution overhead.

## Examples [#examples]

```bash
lpm lint                       # lint the whole project
lpm lint src/                  # lint a directory
lpm lint -- --fix              # auto-fix where Oxlint can
lpm lint -- --rules typescript # forward Oxlint flags
lpm lint --all                 # workspace: lint every member
lpm lint --filter web          # workspace: lint one member
lpm lint --filter './apps/*'   # workspace: lint a path glob
lpm lint --affected            # workspace: only members affected vs main
lpm lint --affected --base develop
```

## Pinning a version [#pinning-a-version]

By default, `lpm lint` uses the version baked into LPM CLI's plugin registry. Override per-project in `lpm.json`:

```json title="lpm.json"
{
  "tools": {
    "oxlint": "1.57.0"
  }
}
```

Run `lpm plugin update oxlint` to pull the latest release into the cache.

## Workspaces [#workspaces]

```bash
lpm lint --all                            # every member
lpm lint --filter web                     # exact name
lpm lint --filter '@scope/*'              # glob
lpm lint --filter './apps/*'              # path glob
lpm lint --filter web --filter api        # union
lpm lint --filter-prod ...shared          # prod graph closure
lpm lint --filter '!web-tests'            # exclusion
lpm lint --affected                       # only members touched since the base ref
lpm lint --affected --base main           # change the base (default: main)
lpm lint --filter web --fail-if-no-match  # exit non-zero on typo'd filter
```

Members run in topological levels, with packages inside each level executing in parallel up to the available CPU count. The Oxlint binary is resolved once at the workspace root before fan-out, so the homogeneous-version case never races on cold-cache install.

Filter grammar is documented in [Workspaces](/docs/packages/workspaces#filter-grammar).

`--all` and `--affected` are mutually exclusive; filters compose with `--affected` (the affected set is unioned with the filter result). `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

## Flags [#flags]

| Flag                                    | Effect                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--all`                                 | Run in every workspace member                                                                                          |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable; entries union) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                                      |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                                    |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                                        |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                                  |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions                            |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set (recommended in CI)                                                  |

Anything after `--` (or trailing) is forwarded to Oxlint.

Plus the [global flags](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

Single-package mode (`lpm lint`) preserves Oxlint's stdout — LPM CLI does not wrap it. Workspace mode (`--all` / `--filter` / `--affected`) emits a single LPM CLI envelope on stdout, with each member's stdout/stderr captured per-member and surfaced inside the envelope only on failure:

```json
{
  "success": false,
  "packages": 5, "succeeded": 4, "failed": 1,
  "duration_ms": 12340,
  "members": [
    { "name": "web", "success": true,  "exit_code": 0, "duration_ms": 2100 },
    { "name": "api", "success": false, "exit_code": 1, "duration_ms": 800,
      "stdout": "...", "stderr": "..." }
  ]
}
```

Spawn / config / plugin failures surface as `exit_code: null` paired with an `error` string, distinguishing "ran and exited non-zero" from "couldn't even launch."

## See also [#see-also]

* [`lpm fmt`](/docs/dev/fmt) — Biome formatter, same lazy-download mechanism
* [`lpm check`](/docs/dev/check) — TypeScript type-check
* [`lpm plugin`](/docs/dev/plugin) — manage tool versions and updates
* [Built-in tools](/docs/dev/builtin-tools) — how the plugin system works


# Managed runtimes (/docs/dev/node-version-pinning)



LPM CLI manages project runtimes itself. It detects the versions your project asks for, downloads missing installs on demand, stores them under `~/.lpm/runtimes/`, and prepends the right `bin/` directories to `PATH` for every script you run.

Node and Bun are supported managed runtimes. Deno is still unsupported.

## Detection Order [#detection-order]

When [`lpm run`](/docs/dev/run), [`lpm dev`](/docs/dev/dev), [`lpm <file>` / `lpm exec`](/docs/dev/exec), [`lpm test`](/docs/dev/test), or [`lpm bench`](/docs/dev/bench) starts, LPM CLI resolves managed runtime pins in deterministic order: Node first, then Bun.

### Node [#node]

| Priority   | Source                    | Format                                                                   |
| ---------- | ------------------------- | ------------------------------------------------------------------------ |
| 1          | `lpm.json > runtime.node` | Semver: `">=22.0.0"`, `"22.5.0"`, `"22"`                                 |
| 2          | `.nvmrc`                  | Plain version: `22.5.0`, `lts/iron`, `v22`                               |
| 3          | `.node-version`           | Plain version                                                            |
| (fallback) | None of the above         | Use the first Node on the constructed script `PATH` — no managed runtime |

`.nvmrc` lives in this list intentionally — projects migrating from `nvm` get Node pinning for free without writing `lpm.json`. New projects should prefer `lpm.json > runtime.node` because it sits next to the rest of the LPM CLI config.

### Bun [#bun]

| Priority   | Source                    | Format                                                      |
| ---------- | ------------------------- | ----------------------------------------------------------- |
| 1          | `lpm.json > runtime.bun`  | Exact, `latest`, `v1.3.14`, `bun-v1.3.14`, prefix, or range |
| (fallback) | No `runtime.bun` declared | Do not prepend managed Bun                                  |

`package.json > engines.bun` is recognized as a compatibility warning, but LPM CLI does not enforce it and does not use it as a managed-runtime pin. Use `lpm.json > runtime.bun` when scripts need Bun available.

## Auto-Install [#auto-install]

If a runtime selector is not already installed, LPM CLI downloads it before running the script unless auto-install is disabled:

```bash
LPM_NO_AUTO_INSTALL=true lpm dev
```

`LPM_NO_AUTO_INSTALL=true` applies to Node and Bun selectors. When auto-install is off and a managed runtime is missing, LPM CLI warns and leaves the system `PATH` in place for that runtime. It has no effect on `package.json > engines.node`, because an engine constraint never triggers installation.

Node releases come from `https://nodejs.org/dist/index.json`, cached for 1 hour at `~/.lpm/runtimes/index-cache.json`. Downloads are SHA-256 verified against the matching `SHASUMS256.txt` entry.

Bun releases come from the GitHub releases API for `oven-sh/bun`, cached for 1 hour at `~/.lpm/runtimes/bun-index-cache.json`. Downloads use Bun's platform asset names such as `darwin-aarch64`, `linux-x64-musl`, `linux-x64-baseline`, and `windows-x64`. Downloads are SHA-256 verified against GitHub asset digests and/or `SHASUMS256.txt`; when both are present, both must verify.

## Storage Layout [#storage-layout]

```text
~/.lpm/runtimes/
├── node/
│   └── 22.12.0/
│       └── bin/
│           ├── node
│           ├── npm
│           └── npx
├── bun/
│   └── 1.3.14/
│       └── bin/
│           └── bun
├── index-cache.json
└── bun-index-cache.json
```

Each version is installed once and reused by every project on the machine. `LPM_HOME=<path>` moves the entire LPM CLI root, including `runtimes/`, which is useful for hermetic CI.

Concurrent projects can request the same missing runtime. LPM CLI serializes that version's installation. It publishes only a complete runtime directory.

Other projects then reuse that installation.

## Script PATH Order [#script-path-order]

When a script runs, LPM CLI builds `PATH` from:

1. Project-local `node_modules/.bin/`
2. Managed Node `bin/`, if a Node pin resolves to an installed version
3. Managed Bun `bin/`, if `runtime.bun` resolves to an installed version
4. The inherited `PATH`

This means locally-installed CLIs win over managed runtime tools, and managed Node wins over managed Bun when both provide a command with the same name.

`runtime.bun` only exposes `bun` to scripts. It does **not** switch LPM CLI script execution to `bun run`.

For workspace runs and multi-service dev sessions, each member or service resolves selectors from its directory. A local selector replaces the same root runtime.

The member or service inherits each unselected root runtime. For example, a service can inherit root Node and select its own Bun version.

## Node Engine Enforcement [#node-engine-enforcement]

The workspace root's `engines.node` is a compatibility constraint, never a runtime selector. LPM CLI first applies `lpm.json > runtime.node`, `.nvmrc`, or `.node-version`. It then constructs the same `PATH` scripts receive—project and workspace `node_modules/.bin` directories, the selected managed runtime, then inherited `PATH`—and validates the first Node resolved from it against `engines.node`.

For workspace runs, LPM CLI resolves the local Node selector for each runnable member. It validates the first Node on that member's script `PATH`.

The validation uses the root and member constraints. Members without the requested task are skipped without runtime preparation.

For multi-service dev sessions, LPM CLI applies the same rule to each service `cwd`. Runtime validation finishes before LPM CLI starts the service set.

An engine range never selects another installed managed runtime, downloads Node, or prepends a different Node to `PATH`. This preserves externally selected runtimes in CI matrices. If no Node is available, the engine check fails with the declared constraint and an explicit `lpm use node@<version>` hint. `package.json > lpm > engineStrict: false` and `~/.lpm/config.toml > engine-strict = false` retain warning-only behavior.

Root constraints gate `lpm run`, `lpm install`, `lpm rebuild`, and `lpm add`. Selected dependencies' `engines.node` ranges are install-time enforcement only: required mismatches abort, optional-only incompatible packages are skipped, and the ranges are persisted for lockfile replay.

```text
$ lpm install
Error: lpm::engine_mismatch
  × node version 18.20.4 does not satisfy required >=22.0.0 (from package.json
  │ > engines.node (compared against script PATH))
```

Resolution: select a matching managed runtime (`lpm use node@22`), provide a compatible Node on `PATH`, or relax the constraint.

`engines.bun` is not enforced in this first Bun runtime pass. Put the desired Bun version in `lpm.json > runtime.bun` to manage Bun availability.

## `lpm use` [#lpm-use]

```bash
lpm use node@22                 # install the latest 22.x, pin in lpm.json
lpm use node@lts                # install latest Node LTS
lpm use bun@1.3.14              # install + pin Bun
lpm use bun@latest              # install + pin latest Bun
lpm use --list bun              # list installed Bun versions
lpm use remove bun@1.3          # remove all installed 1.3.x Bun runtimes
```

`lpm use <runtime>@<spec>` writes the resolved version into `lpm.json > runtime.<runtime>`.
During a fresh install, the human output shows the resolved version, downloaded size, SHA-256 verification, extraction/linking, final runtime, and PATH hint.

`lpm use <runtime>@<spec> --pin` skips the download step. If the spec already matches an installed runtime, LPM CLI writes that exact installed version; otherwise it records the requested spec unchanged so a later `lpm dev` / `lpm run` can resolve or auto-install it.

`lpm use remove <runtime>@<spec>` deletes installed managed runtimes matching the spec without changing `lpm.json`. If the project still pins a matching runtime, LPM CLI warns because later commands can auto-install it again.

## Compatibility With `nvm` / `fnm` [#compatibility-with-nvm--fnm]

LPM CLI's Node management is independent. If you have `nvm` or `fnm` installed:

* They modify `PATH` in your shell, affecting `node` lookup outside LPM CLI.
* LPM CLI's Node detection respects `.nvmrc`, so a project pinned via `nvm` works under LPM CLI without converting.
* When LPM CLI runs a script with a managed Node pin, LPM CLI's runtime takes precedence over whatever `nvm` set in the shell.

You do not have to remove `nvm` to adopt LPM CLI. If a project has both `.nvmrc` and `lpm.json > runtime.node`, the `lpm.json` value wins.

## See Also [#see-also]

* [`lpm use`](/docs/dev/use) — install + pin command
* [`lpm.json` runtime](/docs/reference/lpm-json#runtime) — config field reference
* [Environment variables](/docs/reference/env-vars#install-pipeline) — `LPM_NO_AUTO_INSTALL`
* [`lpm dev`](/docs/dev/dev) — picks up runtime pins automatically
* [`lpm doctor`](/docs/infra/doctor) — checks managed runtime state


# lpm pack (/docs/dev/pack)



```bash
lpm pack [--entry <path>] [--out-dir <dir>] [--dts] [-- args...]
```

Runs [tsdown](https://tsdown.dev) through an LPM CLI-owned command surface aimed at package builds. Common pack flags are exposed directly, and anything after `--` is forwarded to tsdown unchanged.

`--entry` is forwarded as tsdown's positional input file. `--out-dir` maps to tsdown's `--out-dir`. If you omit both, tsdown falls back to its config file or upstream defaults.

## Examples [#examples]

```bash
lpm install -D tsdown
lpm pack --entry src/index.ts --out-dir dist --format esm --dts
lpm pack --config tsdown.config.ts
lpm pack --entry src/index.ts --target es2022 --platform node --minify --sourcemap
lpm pack -- --watch
lpm pack --all --config tsdown.config.ts
lpm pack --filter web --entry src/index.ts --out-dir dist
lpm pack --affected --base develop --config tsdown.config.ts
```

## Local tsdown resolution [#local-tsdown-resolution]

`lpm pack` does not install or manage tsdown for you. LPM CLI walks the local `node_modules/.bin` chain from the current package upward to the workspace root and requires a reachable `tsdown` binary there.

That is deliberate: the tsdown version stays pinned in the repo's own `package.json`, so local runs, CI, and editor tooling all see the same backend.

If nothing resolves, `lpm pack` fails fast with:

```text
tsdown not installed. Run: lpm install -D tsdown
```

## Node runtime [#node-runtime]

LPM CLI wraps tsdown, not Node itself. `lpm pack` still needs `node` available through the normal PATH chain. The straightforward path is:

```bash
lpm use node@22
```

That installs a managed Node runtime and makes it available to LPM CLI commands. A system `node` on PATH also works.

## Forwarding tsdown flags [#forwarding-tsdown-flags]

Anything after `--` is passed straight to tsdown:

```bash
lpm pack --entry src/index.ts --out-dir dist -- --watch
lpm pack --config tsdown.config.ts -- --clean --exports
```

Use this for upstream flags LPM CLI does not surface directly.

## Workspaces [#workspaces]

```bash
lpm pack --all                               # every member
lpm pack --filter web                        # exact name
lpm pack --filter '@scope/*'                 # glob
lpm pack --filter './apps/*'                 # path glob
lpm pack --filter-prod ...shared             # prod graph closure
lpm pack --affected                          # members changed vs main
lpm pack --affected --base develop           # change base branch
lpm pack --filter web --fail-if-no-match     # exit non-zero on typo'd filter
```

Members run in topological levels, with packages inside each level executing in parallel up to the available CPU count. Each member resolves `tsdown` through its own local bin chain, so a root-level workspace install is reused automatically by every member.

`--all` and `--affected` are mutually exclusive; filters compose with `--affected` (the affected set is unioned with the filter result). `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

Filter grammar is documented in [Workspaces](/docs/packages/workspaces#filter-grammar).

## Watch mode [#watch-mode]

`lpm pack -- --watch` works in single-package mode.

In workspace mode, watch is only supported when the selection resolves to exactly one member. If `--all`, `--affected`, or a broad filter resolves to multiple members, LPM CLI exits with an error instead of starting one watcher per package.

## Flags [#flags]

| Flag                                    | Effect                                                                                                                 |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--entry <PATH>`                        | Entry file to pack                                                                                                     |
| `--out-dir <DIR>`                       | Output directory for package artifacts                                                                                 |
| `--config <PATH>`                       | Explicit tsdown config file                                                                                            |
| `--tsconfig <PATH>`                     | Explicit tsconfig file                                                                                                 |
| `--target <VALUE>`                      | Target runtime passed to tsdown                                                                                        |
| `--format <esm\|cjs\|iife>`             | Output format                                                                                                          |
| `--platform <node\|browser\|neutral>`   | Target platform                                                                                                        |
| `--dts`                                 | Emit declaration files                                                                                                 |
| `--minify`                              | Minify the packed output                                                                                               |
| `--sourcemap`                           | Emit a sourcemap                                                                                                       |
| `--all`                                 | Run in every workspace member                                                                                          |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable; entries union) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                                      |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                                    |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                                        |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                                  |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions                            |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set (recommended in CI)                                                  |

Anything after `--` (or trailing) is forwarded to tsdown.

Plus the [global flags](/docs/commands#global-flags).

## `--json` output [#--json-output]

`lpm pack --json` emits one LPM CLI envelope on stdout in both single-package and workspace mode. Successful member stdout/stderr stays suppressed; per-member stdout/stderr is included only when that member fails:

```json
{
  "success": false,
  "packages": 3,
  "succeeded": 2,
  "failed": 1,
  "duration_ms": 1840,
  "members": [
    { "name": "pkg-a", "success": true, "exit_code": 0, "duration_ms": 420 },
    {
      "name": "pkg-b",
      "success": false,
      "exit_code": 1,
      "duration_ms": 180,
      "stdout": "...",
      "stderr": "..."
    }
  ]
}
```

In single-package mode the envelope has `packages: 1` and one `members[]` row named from `package.json#name` when present. Spawn/config failures surface as `exit_code: null` paired with an `error` string, distinguishing “ran and exited non-zero” from “could not even launch.”

## See also [#see-also]

* [`lpm bundle`](/docs/dev/bundle) — managed Rolldown plugin for direct bundling
* [`lpm check`](/docs/dev/check) — type-checking, with project-local `tsc` or managed `tsgo`
* [Built-in tools](/docs/dev/builtin-tools) — plugin-backed tools, project-local wrappers, and managed engines


# lpm plugin (/docs/dev/plugin)



```bash
lpm plugin <action> [name]
```

Manages the lazy-downloaded tools that back [`lpm lint`](/docs/dev/lint) (Oxlint), [`lpm fmt`](/docs/dev/fmt) (Biome), and [`lpm bundle`](/docs/dev/bundle) (Rolldown). Oxlint and Biome are single-binary plugins cached under `~/.lpm/plugins/{name}/{version}/{platform}/`. Rolldown is user-facing as a plugin, but its verified npm package layout is stored under `~/.lpm/engines/rolldown/{version}/{platform}/`.

## Examples [#examples]

```bash
lpm plugin list                # show installed managed plugins
lpm plugin outdated            # check upstream for newer managed plugin versions
lpm plugin update              # update every installed managed plugin
lpm plugin update oxlint       # update one plugin
lpm plugin update rolldown     # update Rolldown's verified package graph
lpm plugin remove biome        # delete cached binaries for one plugin
```

## Available plugins [#available-plugins]

| Plugin     | Backs                            | Notes                                                  |
| ---------- | -------------------------------- | ------------------------------------------------------ |
| `oxlint`   | [`lpm lint`](/docs/dev/lint)     | Distributed as `.tar.gz` archives by `oxc-project/oxc` |
| `biome`    | [`lpm fmt`](/docs/dev/fmt)       | Distributed as direct binaries by `biomejs/biome`      |
| `rolldown` | [`lpm bundle`](/docs/dev/bundle) | Distributed as a verified npm package graph            |

`tsdown` is project-owned: [`lpm pack`](/docs/dev/pack) uses the repo's local `tsdown` dependency and `lpm plugin update tsdown` does not update it. `lpm check` is also not a plugin — it shells out to project-local `tsc` unless `--engine tsgo` is selected.

## Pinning per-project [#pinning-per-project]

```json title="lpm.json"
{
  "tools": {
    "oxlint": "1.57.0",
    "biome": "2.4.8",
    "rolldown": "1.1.3"
  }
}
```

For Oxlint and Biome, a pinned version downloads on first use and verifies against the upstream `<asset_url>.sha256` sidecar. For Rolldown, the pinned version must already be an approved managed plugin version from `lpm plugin update rolldown`; otherwise `lpm bundle` fails with a clear repair command. Normal `lpm lint`, `lpm fmt`, and `lpm bundle` runs do not check upstream for newer versions.

See [Built-in tools — Checksum verification](/docs/dev/builtin-tools#checksum-verification) for the full verification pipeline.

## Subcommands [#subcommands]

| Subcommand | Aliases           | Effect                                                                                                                                                                   |
| ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list`     | `ls`              | Print installed managed plugins and the latest locally approved version                                                                                                  |
| `outdated` | —                 | Check upstream and print current vs latest for Oxlint, Biome, and Rolldown; notes that tsdown is project-owned                                                           |
| `update`   | `upgrade`         | Pull the latest release for one plugin (or installed plugins if no name given); human output shows the download and verification phases before the final updated version |
| `remove`   | `rm`, `uninstall` | Delete cached binaries for one plugin                                                                                                                                    |

Plus the [global flags](/docs/commands#global-flags) — `--json` returns a structured listing.

## See also [#see-also]

* [`lpm lint`](/docs/dev/lint), [`lpm fmt`](/docs/dev/fmt) — the user-facing commands plugins back
* [Built-in tools](/docs/dev/builtin-tools) — design overview, security model, cache layout


# lpm run (/docs/dev/run)



```bash
lpm run <scripts...> [-- script-args...]
```

Runs one or more `scripts` entries from `package.json`. With multiple independent
scripts, sequential mode preserves the command-line order. Dependencies always
run before their dependents. Pass `-p` to run independent tasks in parallel.

`lpm.json` is optional. Without it, the command uses `package.json` scripts and standard environment files.

It also uses a Node selector in `.nvmrc` or `.node-version`. Task dependencies and task caching require `lpm.json`.

## Shorthand: `lpm <script-or-bin>` without `run` [#shorthand-lpm-script-or-bin-without-run]

`lpm` falls through to the same task runner as `lpm run <name>` when the first argument isn't a known subcommand. That means plain shorthand script invocations pick up `lpm.json` meta-tasks, `dependsOn` expansion, caching, and the normal script/task resolution rules.

```bash
lpm build              # equivalent to lpm run build
lpm typecheck          # equivalent to lpm run typecheck
lpm verify             # equivalent to lpm run verify (including lpm.json meta-tasks)
lpm jest --watch       # runs node_modules/.bin/jest --watch if no script/task named "jest" exists
```

The lookup only kicks in for unknown subcommand names — built-in commands always win. So `lpm test` runs the Vitest/Jest forwarder (built-in `lpm test`), not your `"test"` script entry. To force the script entry when a built-in conflicts, use `lpm run <name>` explicitly.

If no `package.json` script or `lpm.json` task matches the shorthand name, LPM CLI tries a project-local binary with the same resolver as [`lpm exec`](/docs/dev/exec). That fallback searches only `node_modules/.bin` directories for the project/workspace; it does not run arbitrary system PATH binaries and it does not use a shell. Scripts and tasks still win over same-named local binaries.

Path-like invocations such as `lpm scripts/seed.ts` use the source-file runner instead of the shorthand lookup.

Use `lpm run` when you need runner flags. `lpm build --filter web` forwards `--filter web` to your script; `lpm run build --filter web` applies LPM CLI's workspace selection.

## Hidden package.json scripts [#hidden-packagejson-scripts]

Package scripts whose name starts with `.` are hidden helper scripts:

```json title="package.json"
{
  "scripts": {
    "build": "lpm run .build",
    ".build": "tsc -p tsconfig.build.json"
  }
}
```

`lpm run .build` and the shorthand `lpm .build` are rejected when invoked directly from your shell. Hidden scripts are also omitted from missing-script suggestions and script listings.

Visible scripts may call hidden scripts, and `lpm.json` task dependencies may reference them:

```json title="lpm.json"
{
  "tasks": {
    "build": {
      "dependsOn": [".build"]
    }
  }
}
```

Use this for internal building blocks you do not want to expose as top-level project commands.

## Examples [#examples]

```bash
lpm run build
lpm run lint test                 # sequential
lpm run -p lint test typecheck    # parallel
lpm run dev -- --port 4000        # forward args to the script
lpm run build --filter web        # workspaces: build only the web member
lpm run build --all               # build every workspace member
lpm run test --affected           # only members affected by recent changes
lpm run test --filter web --no-bail  # keep running selected members after a failure
lpm run build --filter './packages/*' --workspace-concurrency 2
lpm run test --watch              # re-run on file changes
lpm run --env=staging start       # load .env.staging before running
```

## Forwarding arguments [#forwarding-arguments]

Anything after `--` is passed to the script as argument data:

```bash
lpm run dev -- --port 4000 --host 0.0.0.0
# runs: <whatever "dev" expands to> --port 4000 --host 0.0.0.0
```

Shell metacharacters in an argument do not become another shell command. On Windows, LPM CLI rejects arguments that contain line breaks.

`cmd.exe` cannot receive these arguments safely.

## Parallel execution [#parallel-execution]

```bash
lpm run -p lint test typecheck
```

Runs all three scripts concurrently, respecting any task dependencies declared in `lpm.json`. Output is buffered per task by default — pass `--stream` for live prefixed output. With `--no-bail`, the runner keeps going past failed tasks and reports the failures at the end.

## Workspaces [#workspaces]

```bash
lpm run build --all                    # every member, topological order
lpm run build --filter web             # one member
lpm run build --filter web --filter api # two members (union)
lpm run build --filter-prod ...shared  # prod graph closure only
lpm run test --affected                # members touched since base branch
lpm run test --affected --base develop # change base branch (default: main)
lpm run test --affected --changed-files-ignore-pattern '**/README.md'
lpm run test --affected --test-pattern '**/*.test.js'
```

`--filter` accepts the [filter grammar](/docs/packages/workspaces): exact name (`foo`), glob (`@scope/*`), combined name/path (`@scope/*{./apps/web}`), path glob (`./apps/*`), forward closure (`foo...`), reverse closure (`...foo`), exclusion (`!foo`).

Full grammar also includes path exact (`{./apps/web}`), git ref (`[origin/main]`), and the dependency/dependent-closure variants `foo^...` and `...^foo`.

`--filter-prod <EXPR>` accepts the same grammar, but closure operators ignore `devDependencies` edges. Use it when a production deploy/build/test selection should include runtime workspace dependents without pulling in test-only packages.

`--all` is mutually exclusive with `--filter`, `--filter-prod`, and `--affected`; filters compose with `--affected` (the affected set is unioned with the filter result).

`--all`, filters, and `--affected` require a workspace.

`--filter core` does **not** match `@babel/core` — substring matching is not supported. Use `--filter '*/core'` for that.

`--fail-if-no-match` makes a typo'd filter exit non-zero — recommended in CI.

`--no-bail` continues independent selected members after a member fails. A
member does not run if its explicit `^task` dependency fails. The command exits
non-zero if a selected member fails.

If a selected task uses `^task`, LPM CLI also runs that task in the selected
member's upstream workspace dependencies. This expansion covers the complete
dependency chain. It can include packages outside the original filter. The
command checks the expanded graph before it starts a task process.

`--workspace-concurrency <N>` caps how many selected workspace members run at once within each topological level. The command reads persistent defaults from `lpm.toml > [workspace].concurrency`, then `~/.lpm/config.toml > workspace-concurrency`, then falls back to the host's available parallelism.

`--changed-files-ignore-pattern <glob>` drops matching paths before `[git-ref]` filters or `--affected` map changed files to workspace members. It also reads project defaults from `lpm.toml > [workspace].changed-files-ignore-pattern`.

`--test-pattern <glob>` marks matching changed files as test-only. Directly changed packages still run, but those packages do not seed dependent fan-out when all their changes match the test pattern. It also reads project defaults from `lpm.toml > [workspace].test-pattern`.

## Caching [#caching]

Task results are cached only when the task opts in through `lpm.json`. The task needs `cache: true` and non-empty `outputs`.

The key includes script arguments, the child environment, runtime identities, and selected Node and Bun executable fingerprints.

A cached task with unchanged inputs replays without execution. Bypass the cache with `--no-cache`.

Single-script human output prints a compact stderr header before the script's own output:

```bash
› Running build
    cache    miss
    command  next build

✓ build · success in 4.82s
```

On a cache hit, LPM CLI replays the captured stdout/stderr and reports that the task was restored from cache.

Buffered, streamed-with-prefix, and cache-replayed task output is sanitized before LPM renders it. A child process that receives stdout/stderr directly retains raw terminal ownership so interactive programs and terminal-aware tools keep their normal behavior. See [Terminal output security](/docs/reference/terminal-output) for the full boundary, including JSON semantics.

The local cache lives under `~/.lpm/cache/tasks/`. With `lpm.json > remoteCache.enabled`, LPM CLI checks the hosted cache after the local cache and uploads successful task outputs after the local cache write. Remote outages, 404s, bad signatures, and corrupt artifacts are misses — they do not fail a successful local build.

Manage cache state with [`lpm cache`](/docs/packages/cache). See [Task runner — Remote cache](/docs/dev/task-runner#remote-cache) for setup.

## JSON output [#json-output]

```bash
lpm run build --json
lpm run build --filter web --json
```

`lpm run --json` emits an LPM CLI metadata envelope, even for a single script in a single package. Single-package task output is captured into the task result instead of being passed through as raw stdout. The envelope includes `success`, task counts (`total`, `passed`, `failed`), and `tasks[]` entries with each task name, status, exit code, duration, cache-hit state, and skip reason.

Workspace selections emit `success`, `packages`, `succeeded`, and `duration_ms`.
`packages` counts each scheduled member after `^task` expansion. This count
excludes selected members that do not define the task. `succeeded` counts the
members that completed successfully.

If a workspace filter matches nothing and `--fail-if-no-match` is not set, JSON
mode emits a successful zero-package envelope.

## Environment files [#environment-files]

```bash
lpm run start --env=staging
```

With `--env=staging`, LPM CLI loads `.env`, `.env.local`, `.env.staging`, then `.env.staging.local`. Without `--env`, it loads `.env`, then `.env.local`.

The environment order is `--env`, `tasks.<name>.env`, `env.<name>`, then the default files. Thus, the command-line mode overrides all `lpm.json` mappings.

Vault-backed env secrets override file values after dotenv loading.

An environment file must resolve inside the project directory. LPM CLI rejects a configured path or symlink that resolves outside the project.

Runtime-hook env names such as `NODE_OPTIONS`, `LD_PRELOAD`, `LD_AUDIT`, `BASH_ENV`, and `DYLD_INSERT_LIBRARIES` are ignored when they come from env files, env secrets, or env-schema defaults. LPM CLI also strips inherited values for these names before spawning the script.

LPM CLI also strips inherited credential names such as `LPM_TOKEN` and `NPM_TOKEN`.

It strips names that end in `_TOKEN`, `_SECRET`, or `_PASSWORD`. Project-resolved values still use the environment policy.

`--no-env-check` skips environment-variable schema validation if you've declared a schema.

## Watch mode [#watch-mode]

```bash
lpm run dev --watch
```

Re-runs one script in one package on file changes.

If the task declares `inputs` globs in `lpm.json`, only matching file changes retrigger the run. Otherwise, relevant project-file changes retrigger it.

`--watch` is single-package today: it does not support multiple script names or workspace-selection flags like `--all`, `--filter`, `--filter-prod`, or `--affected`. Watch runs fresh on every change; the task cache is not used.

## Flags [#flags]

| Flag                                    | Effect                                                                                            |
| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `-p`, `--parallel`                      | Run scripts concurrently (respects task deps from `lpm.json`)                                     |
| `--no-bail`                             | Keep running remaining tasks or selected workspace members after a failure                        |
| `--stream`                              | Live prefixed output instead of buffered per-task in task-graph / parallel runs                   |
| `--all`                                 | Run in every workspace member (topological order); mutually exclusive with filters / `--affected` |
| `--filter <EXPR>`                       | Filter workspace members (repeatable; unions)                                                     |
| `--filter-prod <EXPR>`                  | Filter workspace members with production-only dependency closures                                 |
| `--fail-if-no-match`                    | Exit non-zero if the selected workspace set is empty                                              |
| `--workspace-concurrency <N>`           | Limit concurrent workspace members for `--all`, `--filter`, or `--affected` runs                  |
| `--affected`                            | Only members affected by changes vs `--base`                                                      |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                   |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                             |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions       |
| `--no-cache`                            | Bypass the task cache for cache-enabled tasks; ignored in watch mode                              |
| `--no-env-check`                        | Skip env-var schema validation                                                                    |
| `--env=<MODE>`                          | Load `.env`, `.env.local`, `.env.<MODE>`, and `.env.<MODE>.local`                                 |
| `--watch`                               | Re-run one script in one package on file changes                                                  |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm <file>`](/docs/dev/exec) — run a JS/TS file directly (no script needed)
* [`lpm exec <bin>`](/docs/dev/exec) — explicitly run a project-local binary
* [`lpm dlx`](/docs/dev/dlx) — run a package binary without installing
* [Task runner](/docs/dev/task-runner) — caching, dependencies, `lpm.json` task graph
* [Workspaces](/docs/packages/workspaces) — filter grammar


# Task runner (/docs/dev/task-runner)



The task runner is what powers [`lpm run`](/docs/dev/run) and the `services` orchestration in [`lpm dev`](/docs/dev/dev). It reads scripts from `package.json` (and per-task config from `lpm.json > tasks`), builds a topological execution graph, runs tasks in parallel where the graph allows, caches results when configured, and re-executes on file changes when `--watch` is set.

This page covers the design — what the cache key includes, how `--affected` is computed, how parallelism interacts with task dependencies, and where state lives. For the CLI surface, see [`lpm run`](/docs/dev/run).

## Execution model [#execution-model]

```text
[scripts] ──┐
            ├─→ [task graph DAG] ─→ [parallel executor] ─→ [stdout / cache]
[lpm.json] ─┘                            │
                                         ↓
                                   [.bin/ PATH injection]
                                   [pre/post hooks]
                                   [env file loading]
                                   [readiness checks]
```

The runner is built as a DAG over tasks. Each task's `dependsOn` edges become predecessors; the executor processes tasks level by level, running independent tasks concurrently within each level.

## Task definition [#task-definition]

A task can come from two places:

```json title="package.json (the npm-shaped path)"
{
  "scripts": {
    "build": "tsup",
    "test": "vitest run"
  }
}
```

```json title="lpm.json (the cached + DAG-aware path)"
{
  "tasks": {
    "build": {
      "command": "tsup",
      "dependsOn": ["^build"],
      "cache": true,
      "outputs": ["dist/**"],
      "inputs": ["src/**", "package.json"]
    }
  }
}
```

`lpm.json > tasks.<name>` overrides `package.json > scripts.<name>`. The `command` field is optional — when absent, the runner falls back to the package.json script.

For full per-task field reference, see [`lpm.json` tasks](/docs/reference/lpm-json#tasksname).

## `dependsOn` semantics [#dependson-semantics]

Two flavors of dependency:

| Form       | Meaning                                                                                                           |
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
| `"build"`  | Same-package dependency. Wait for *this* package's `build` task to finish first.                                  |
| `"^build"` | Upstream-workspace dependency. Wait for `build` to finish in **every workspace member that this one depends on**. |

With `workspace:*`, `^build` means: "build this package after each internal dependency finishes its `build` task."

LPM CLI expands `^task` recursively. If you filter one application, its
required upstream tasks still run through the complete workspace dependency
chain. Each upstream workspace member must define the requested task.

Before LPM CLI starts a task process, it checks the complete expanded graph.
The command stops for a missing task, a cycle, `^`, or `^^task`. No task process
starts after one of these graph errors.

## Caching [#caching]

Caching is opt-in per task:

```json
{
  "tasks": {
    "build": {
      "command": "tsup",
      "cache": true,
      "outputs": ["dist/**"]
    }
  }
}
```

`cache: true` requires `outputs` to know what to cache. Without `outputs`, the
runner disables caching because it has nothing to record.

All `inputs` and `outputs` values must be valid, project-relative globs. LPM
rejects invalid syntax, absolute paths, and parent traversal before it starts
the task.

### Cache key [#cache-key]

The cache key is a SHA-256 hash of:

* The `command` string
* Each forwarded script argument, with length boundaries
* The per-file SHA-256 of every file matching `inputs` globs
* The cache identities of transitive same-project task dependencies
* The declared outputs of direct same-project task dependencies
* The complete `lpm.json` file, including dependency edges and output globs
* The inherited environment after credential and runtime-hook removal, or the variables selected by `cacheEnv`
* All environment values loaded from project files, secrets, and schema defaults
* Managed Node and Bun versions, plus fingerprints of the selected executables
* The complete `package.json` contract, including scripts and resolution fields
* Supported lockfiles in the project root
* The workspace-root configuration and lockfiles during workspace task runs
* The task identity of each reachable `^task` dependency

Any change to any of those flips the key. Same key = cache hit.

To reuse output across machines or CI jobs, declare the inherited variables that affect a task:

```json title="lpm.json"
{
  "tasks": {
    "build": {
      "cache": true,
      "cacheEnv": ["NODE_ENV", "BUILD_TARGET"],
      "outputs": ["dist/**"]
    }
  },
  "remoteCache": { "enabled": true }
}
```

With this configuration, changes to `LPM_HOME`, `CI`, or `GITHUB_RUN_ID` do not change the cache key.
If your build uses `CI` to change behavior, include `CI`. Include every inherited variable that can affect the result.
Names are exact and case-sensitive. Wildcards are not supported.

If you omit `cacheEnv`, all inherited values remain cache inputs. An empty list excludes all inherited values from the key.
Project-loaded environment values always remain cache inputs. `cacheEnv` does not change the task's environment or bypass remote-upload secret checks.

A local dependency identity includes its cache key and each reachable
dependency identity. LPM also validates each declared dependency output before
a cache hit or cache publication. If an output changes, LPM does not use or
publish the downstream cache entry.

LPM canonicalizes `package.json` before it hashes the manifest. Changes to
scripts, module settings, dependencies, peer metadata, overrides, resolutions,
catalogs, engine and platform selectors, and LPM controls invalidate the cache.

LPM always hashes `lpm.lock`. If the text lockfile is absent, LPM hashes
`lpm.lockb` instead. When these ecosystem lockfiles exist, LPM also hashes
them:

* `package-lock.json`
* `npm-shrinkwrap.json`
* `yarn.lock`
* `pnpm-lock.yaml`
* `bun.lock`
* `bun.lockb`
* `deno.lock`

For workspace task runs, LPM also hashes the root `package.json`, `lpm.json`,
and `pnpm-workspace.yaml` files. The root lockfiles use the same rules as the
member lockfiles.

Each `^task` identity includes its command, arguments, environment, runtime,
inputs, configuration, manifest, lockfiles, and upstream task identities. An
upstream identity change invalidates the cache of each reachable downstream
task. If LPM cannot calculate an upstream identity safely, it disables caching
for the dependent workspace member.

### Cache layout [#cache-layout]

```text
~/.lpm/cache/tasks/
  {sha256-hex-key}/
    meta.json       ← timing, command, key, output count
    stdout.log      ← captured stdout for replay
    stderr.log      ← captured stderr for replay
    outputs.tar.gz  ← archived output files
```

On hit:

1. Validate the metadata, logs, archive paths, file types, sizes, and output count.
2. Stage all outputs without changing the project.
3. Remove declared output files that are absent from the cached result.
4. Replace project outputs in one rollback-protected transaction.
5. Preserve each archived file's mode and modification time.
6. Replay `stdout.log` and `stderr.log` to the current process.
7. Exit successfully.

LPM treats an incomplete or corrupt local entry as a cache miss. It runs the
task and replaces the bad entry. Concurrent writers for one key publish one
complete entry.

LPM writes a recovery journal before it replaces an output. If a process stops
during restore, the next restore rolls back the incomplete operation first. If
an immediate rollback fails, LPM keeps the recovery files and reports their
path.

The restore rejects output paths that use an existing symlink or junction.
The cache cannot write through that link to a location outside the project.
When LPM creates an archive, it also rejects symlink and junction outputs.
This rule prevents a restored output from changing its file type.

The replay shows the same logs as a real `lpm run build`. The runner's
`(cached)` status tells you why the result was immediate.

### Bypass [#bypass]

```bash
lpm run build --no-cache       # bypass for one invocation
```

Or remove the `cache: true` from `lpm.json` to disable persistently.

### Remote cache [#remote-cache]

```json title="lpm.json"
{
  "remoteCache": {
    "enabled": true,
    "team": "acme",
    "signature": true
  },
  "tasks": {
    "build": {
      "cache": true,
      "outputs": ["dist/**"]
    }
  }
}
```

Remote cache is opt-in on top of the local task cache. A run checks:

1. Local task cache under `~/.lpm/cache/tasks/`
2. Hosted cache at `remoteCache.url` (default: configured registry + `/v8`)
3. The actual script

Successful scripts always write the local cache first. If remote cache is
enabled and not `readOnly`, LPM CLI uploads a portable artifact. The artifact
contains task metadata, stdout, stderr, and `outputs/`. LPM validates the
complete download before it changes project files. A rejected download leaves
the project unchanged. The restore uses the current project's `outputs` globs.
It does not trust output declarations from the remote artifact.

Use `LPM_REMOTE_CACHE_TOKEN` in CI, or rely on the token from `lpm login` when the cache endpoint is on the configured LPM.dev Registry origin. Third-party cache hosts never receive the registry login token; they require both `LPM_REMOTE_CACHE_TOKEN` and `LPM_REMOTE_CACHE_SIGNATURE_KEY`. When `signature: true`, set `LPM_REMOTE_CACHE_SIGNATURE_KEY` on every machine that reads or writes the cache. A missing or invalid signature is a cache miss.

Loaded or inherited env vars with secret-looking names block remote uploads by default.

Use `remoteCache.env.include` only for values that are safe to store in a shared build cache.

### Manage [#manage]

```bash
lpm cache path tasks           # print cache root
lpm cache clean tasks          # drop the task cache
lpm cache status --json        # local usage + hosted cache status
```

See [`lpm cache`](/docs/packages/cache).

## Parallelism [#parallelism]

```bash
lpm run -p lint test typecheck
```

`-p` / `--parallel` runs scripts concurrently — the runner enforces `dependsOn` edges but otherwise lets independent tasks proceed in parallel. Output is buffered per-task by default; `--stream` prefixes live output instead.

`--no-bail` continues independent tasks after a failure. It does not run a
workspace member if that member's explicit `^task` dependency fails. Without
this flag, the first failure stops the run.

Workspace fan-out is separate from per-package task parallelism. `lpm run`, `lpm test`, and `lpm bench` accept `--workspace-concurrency <N>` in workspace mode to cap how many selected members run at once within a topological level. The persistent chain is CLI flag, then `lpm.toml > [workspace].concurrency`, then `~/.lpm/config.toml > workspace-concurrency`, then available host parallelism.

## Workspaces [#workspaces]

Workspace-aware commands accept `--filter`, `--filter-prod`, `--all`, and `--affected`. The set today:

| Command                        | `--all` | `--filter` | `--filter-prod` | `--affected` |
| ------------------------------ | ------- | ---------- | --------------- | ------------ |
| [`lpm run`](/docs/dev/run)     | ✓       | ✓          | ✓               | ✓            |
| [`lpm lint`](/docs/dev/lint)   | ✓       | ✓          | ✓               | ✓            |
| [`lpm fmt`](/docs/dev/fmt)     | ✓       | ✓          | ✓               | ✓            |
| [`lpm check`](/docs/dev/check) | ✓       | ✓          | ✓               | ✓            |
| [`lpm test`](/docs/dev/test)   | ✓       | ✓          | ✓               | ✓            |
| [`lpm bench`](/docs/dev/bench) | ✓       | ✓          | ✓               | ✓            |

`lpm test` and `lpm bench` claim the same flag names from any underlying runner that uses them (notably bun's `--filter`); to forward those to the runner instead of LPM CLI, prefix with `--` — see [Test & bench runners](/docs/dev/test-bench-runners#forwarding-the-same-flag-names-to-the-runner).

Across workspace-aware commands, `--all` is the broad selector and is mutually exclusive with filters and `--affected`. Filters may compose with `--affected`.

The runner walks the workspace graph and selects matching members in topological order:

```bash
lpm run build --all                         # every member, deps-first
lpm run test --filter web                   # one member
lpm run test --filter './apps/*'            # path glob
lpm run test --filter '@scope/*{./apps/web}' # combined name + exact path
lpm run test --filter web --filter api      # union
lpm run test --filter-prod ...shared        # prod graph closure
lpm run test --filter '!web-tests'          # exclusion
lpm run test --filter './apps/*' --workspace-concurrency 2

lpm lint --filter './apps/*' --fail-if-no-match
lpm fmt --filter '@scope/*' --check
lpm check --affected --base develop
```

`lpm lint`, `lpm fmt`, and `lpm check` add `--fail-if-no-match` for CI pipelines that want to catch typo'd filters early. They don't expose `--parallel` or `--no-bail` — workspace fan-out is parallel by default within each topological level. The run continues across all levels even if individual members fail; the overall command exits non-zero after aggregation when any member failed.

Filter grammar is documented in [Workspaces](/docs/packages/workspaces#filter-grammar).

## `--affected` [#--affected]

```bash
lpm run test --affected                     # default base = main
lpm run test --affected --base develop
lpm run test --affected --changed-files-ignore-pattern '**/README.md'
lpm run test --affected --test-pattern '**/*.test.js'
```

Computed by:

1. `git diff --name-only $base...HEAD` against the base ref.
2. Map each changed file to a workspace member (with proper directory boundary checks — `packages/api-client/x.ts` does **not** match the member at `packages/api`).
3. Root-level changes (files outside any member, e.g. workspace `package.json`, `tsconfig.json`) are treated as affecting **every** member.
4. Expand the directly-changed set through dep edges to include transitive dependents.

That last step is what gives you "the API package changed, so the web package that imports it also runs." Use `--filter '[origin/main]'` (the git-ref filter atom) for the directly-changed-only set without the dependents expansion — see [Workspaces](/docs/packages/workspaces#filter-grammar).

Use `--changed-files-ignore-pattern <glob>` to drop noise paths from the git diff before this mapping step. The same setting can be persisted as `lpm.toml > [workspace].changed-files-ignore-pattern`.

Use `--test-pattern <glob>` to mark matching changed files as test-only. The directly changed package still runs, but dependents are not added when a package only changed in test files. Persist defaults as `lpm.toml > [workspace].test-pattern`.

## Watch mode [#watch-mode]

```bash
lpm run dev --watch
```

The runner sets up a file watcher over the task's effective inputs (same globs as the cache key) and re-runs on change. Pairs nicely with `--filter` for "rebuild only the affected member" loops during development.

## PATH injection [#path-injection]

Every script runs with `node_modules/.bin/` prepended to `PATH`, so locally-installed binaries (`tsc`, `vitest`, etc.) resolve. Pre and post hooks (npm convention — `prebuild`, `postbuild`) run automatically when the corresponding scripts exist.

## Env loading [#env-loading]

Before each task starts, the runner:

1. Selects the environment: `--env` first, then `tasks.<name>.env`, then `env.<name>`, then the default files.
2. Loads the resolved file(s).
3. Validates against `lpm.json > envSchema` (skip with `--no-env-check`).
4. Injects into the script's environment.

CLI override: `--env=<mode>` forces a specific mode for one invocation, regardless of the `lpm.json` mappings.

Existing environment files must resolve inside the project directory. The runner rejects configured paths and symlinks that resolve outside the project.

## Multi-service orchestration [#multi-service-orchestration]

When `lpm.json > services` is non-empty, [`lpm dev`](/docs/dev/dev) starts each service via the runner and waits for readiness:

* Ownership-verified listener on each managed service port
* Optional additional TCP poll on `readyPort`
* Optional additional HTTP poll on `readyUrl`
* `readyTimeout` seconds before failure

Services with `dependsOn` start in topological order. A readiness failure stops the initial startup. Dependent services do not start.

Each service receives the root managed runtimes, except for runtimes that it selects in its own `cwd`.

Shutdown stops the complete tracked service process trees.

The resolved primary endpoint receives LPM CLI-owned HTTPS, tunnel, and LAN frontends plus the browser-open.

One service is the implicit primary service. A multi-service configuration must mark one service as `primary: true` to use these features.

See [`lpm dev`](/docs/dev/dev) and [`lpm.json` services](/docs/reference/lpm-json#servicesname).

## See also [#see-also]

* [`lpm run`](/docs/dev/run) — CLI surface
* [`lpm dev`](/docs/dev/dev) — services + dev-server orchestration
* [`lpm cache`](/docs/packages/cache) — task cache management
* [`lpm.json` tasks](/docs/reference/lpm-json#tasksname) — full task config reference
* [Workspaces](/docs/packages/workspaces) — `--filter` grammar


# Test & bench auto-detection (/docs/dev/test-bench-runners)



[`lpm test`](/docs/dev/test) and [`lpm bench`](/docs/dev/bench) both work the same way: scan your installed deps, pick a runner, exec it, forward arguments verbatim. No config, no per-runner subcommand, no `lpm test --vitest` flag.

This page documents the detection rules, what gets executed, and what's NOT in the auto-detection set today.

## Detection order [#detection-order]

For [`lpm test`](/docs/dev/test):

1. **`vitest`** in `dependencies` or `devDependencies` → runs `vitest run`
2. **`jest`** in `dependencies` or `devDependencies` → runs `jest`
3. **`mocha`** in `dependencies` or `devDependencies` → runs `mocha`
4. **`scripts.test` in `package.json`** → runs the user-defined script via the platform shell (`sh -c` on Unix, `cmd /C` on Windows)
5. **None of the above** → error: "no test runner found. Install vitest/jest/mocha or add a 'test' script to package.json"

First match wins — vitest beats jest, jest beats mocha, and any installed runner takes precedence over `scripts.test`. The script fallback only fires for projects with no recognized runner installed.

For [`lpm bench`](/docs/dev/bench):

1. **`vitest`** in `dependencies` or `devDependencies` → runs `vitest bench`
2. **`scripts.bench` in `package.json`** → runs the user-defined script via the platform shell (`sh -c` on Unix, `cmd /C` on Windows)
3. **None of the above** → error: "no benchmark runner found. Install vitest or add a 'bench' script to package.json"

Vitest is the only auto-detected bench runner today. For other frameworks (mitata, tinybench standalone, hyperfine), define a `bench` script in `package.json` — `lpm bench` runs it directly via the script fallback above.

## Argument forwarding [#argument-forwarding]

Anything after the command (or after `--`) is forwarded verbatim to the runner:

```bash
lpm test                              # → vitest run (or jest, or mocha)
lpm test src/utils.test.ts            # → vitest run src/utils.test.ts
lpm test -- --reporter=verbose        # → vitest run --reporter=verbose
lpm test -- --coverage --watch        # → vitest --coverage --watch     (see Watch mode)

lpm bench
lpm bench src/parser.bench.ts         # → vitest bench src/parser.bench.ts
lpm bench -- --reporter=json
```

`--` is optional but useful for arguments that look like LPM CLI flags (otherwise clap might try to interpret them).

## Watch mode [#watch-mode]

Vitest's `run` subcommand forces single-pass execution — passing `--watch` after `run` is silently dropped. To make `lpm test --watch` actually enter watch mode, LPM CLI rewrites the base command from `vitest run` to `vitest` whenever the forwarded args contain `--watch` (or the `-w` short form).

```bash
lpm test --watch                      # → vitest --watch
lpm test -- -w src/utils.test.ts      # → vitest -w src/utils.test.ts
```

Other runners are unaffected:

* `jest` and `mocha` accept `--watch` natively against their bare command, so the rewrite is vitest-specific.
* `lpm bench --watch` works as-is — vitest's `bench` subcommand respects `--watch` directly.

If your project's `package.json` has a custom `"test"` script (the [scripts.test fallback](#why-no-per-runner-subcommand)), LPM CLI forwards `--watch` to that script verbatim without rewriting; the watch contract is whatever your script defines.

## Why no per-runner subcommand [#why-no-per-runner-subcommand]

Other PMs ship `pnpm test` / `npm test` shortcuts that effectively `npm run test`. LPM CLI's `lpm test` is **not** a script alias — it prefers the runner over the script:

| Command                         | Behavior                                                                                                                |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [`lpm run test`](/docs/dev/run) | Runs the `test` script from `package.json` (whatever you wrote there)                                                   |
| `lpm test`                      | Auto-detects vitest / jest / mocha and runs it directly. Falls back to `scripts.test` only when no runner is installed. |

Why this shape: `lpm test` works in projects that don't have a `test` script — newly-bootstrapped repos, monorepo members where the test config is implicit, etc. If your `test` script does setup or env-injection beyond what the runner does on its own, reach for `lpm run test` explicitly so the script always wins; with `lpm test`, the installed runner wins over your script.

## Subprocess passthrough [#subprocess-passthrough]

Both commands forward the runner's exit code:

```bash
lpm test
echo "exit: $?"      # whatever vitest / jest / mocha exited with
```

Vitest exits 1 on failed tests; LPM CLI exits 1. Vitest exits 0; LPM CLI exits 0. No mangling. CI gates work the way you expect.

## `--json` mode [#--json-mode]

Neither command emits a wrapping JSON envelope. The runner's own stdout (whatever shape it has) IS the result. This preserves the "single JSON result" contract — if you piped `lpm test --json` into a parser expecting LPM CLI's JSON shape, you'd get the runner's output instead, which is what you actually want for test results.

## Runtime [#runtime]

Tests run with whatever managed runtimes [the runtime detection](/docs/dev/node-version-pinning) picks. So:

```json title="lpm.json"
{ "runtime": { "node": ">=22.0.0" } }
```

Then `lpm test` runs vitest on Node 22, regardless of your system Node. Auto-installs the version if missing (unless `LPM_NO_AUTO_INSTALL=true`).

## Workspaces [#workspaces]

`lpm test` and `lpm bench` accept `--all`, `--filter`, `--filter-prod`, `--affected`, `--base`, `--changed-files-ignore-pattern`, `--test-pattern`, `--fail-if-no-match`, and `--workspace-concurrency` — same grammar as [`lpm run`](/docs/dev/run) and the lint/fmt/check tools.

```bash
lpm test --all
lpm test --filter web --filter api
lpm test --filter-prod ...shared
lpm test --filter './apps/*' --fail-if-no-match
lpm bench --affected --base develop --test-pattern '**/*.test.js' --workspace-concurrency 2
```

Detection runs per member, so a workspace can mix runners (vitest in one member, jest in another) and each gets the right command. A member with no installed runner becomes a per-member detection failure in the JSON envelope rather than aborting the run.

`--all` is mutually exclusive with filters and `--affected`. Filters compose with `--affected`.

`--workspace-concurrency <N>` caps how many selected workspace members run at once within each topological level.

### Forwarding the same flag names to the runner [#forwarding-the-same-flag-names-to-the-runner]

Bun's `bun test --filter` and a few other runner flags share names with the LPM CLI workspace flags. To forward them to the runner instead of having LPM CLI claim them, put them after `--`:

```bash
lpm test -- --filter pattern        # → bun test --filter pattern (or vitest, jest)
lpm test -- --all                   # → runner --all (e.g. jest's git-mode flag)
lpm test --filter web -- --filter pattern   # workspace + runner filter compose
```

Anything after `--` is forwarded verbatim regardless of name.

### Watch mode in workspaces [#watch-mode-in-workspaces]

`--watch` interaction with workspace selectors is gated by selection size, not by whether a workspace flag was used at all:

| Selection resolves to | `--watch` behavior                                                                                                                    |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Exactly one member    | Hands off to single-package mode against that member's directory. The vitest `run` → bare `vitest` rewrite documented above kicks in. |
| Two or more members   | Rejected with the actual count, to prevent N parallel watchers.                                                                       |
| Zero members          | Rejected with "nothing to watch."                                                                                                     |

```bash
lpm test --filter web --watch             # ✓ exactly one member matches
lpm test --all --watch                    # ✗ rejected: N watchers
lpm bench --filter '@scope/*' --watch     # ✓ if exactly one member matches; ✗ otherwise
```

If you want to watch a member from outside the workspace selection grammar, `cd <member> && lpm test --watch` works the same as the one-member-filter handoff.

## See also [#see-also]

* [`lpm test`](/docs/dev/test) — CLI command reference
* [`lpm bench`](/docs/dev/bench) — bench equivalent
* [`lpm run`](/docs/dev/run) — for workspace `--filter` / `--affected`
* [Managed runtimes](/docs/dev/node-version-pinning) — which Node and Bun binaries tests see on `PATH`


# lpm test (/docs/dev/test)



```bash
lpm test [-- args...]
```

Detects which test runner is installed in the project (vitest → jest → mocha) and runs it, forwarding any trailing arguments verbatim. No config; no per-runner subcommand.

## Examples [#examples]

```bash
lpm test                              # run the whole suite (single-pass)
lpm test src/utils.test.ts            # run one file
lpm test --watch                      # vitest: enter watch mode (see below)
lpm test -- -w                        # short form, same effect
lpm test -- --reporter=verbose
```

## Detection order [#detection-order]

1. `vitest` (in `dependencies` or `devDependencies`)
2. `jest`
3. `mocha`
4. `scripts.test` in `package.json` (only consulted when no runner above is installed)

Whichever is found first wins. If none of the runners is installed and no `test` script exists, `lpm test` reports it.

## Argument forwarding [#argument-forwarding]

Anything after the command (or after `--`) is forwarded to the runner:

```bash
lpm test -- --coverage --reporter=html
# vitest: vitest run --coverage --reporter=html
# jest:   jest       --coverage --reporter=html
```

The vitest base command is `vitest run` (single-pass), except in [Watch mode](#watch-mode) below.

## Watch mode [#watch-mode]

`lpm test --watch` (or `-w`) enters watch mode. For vitest specifically, LPM CLI rewrites the base command from `vitest run` to `vitest` so the `--watch` flag is honored — `vitest run` would silently force single-pass and drop the flag. jest and mocha accept `--watch` natively against their bare command, so the rewrite is vitest-only.

```bash
lpm test --watch                # vitest: → vitest --watch
                                # jest:   → jest --watch
                                # mocha:  → mocha --watch
```

See [Test & bench runners](/docs/dev/test-bench-runners#watch-mode) for the full rewrite rules.

## Workspaces [#workspaces]

```bash
lpm test --all                            # every member
lpm test --filter web                     # exact name
lpm test --filter '@scope/*'              # glob
lpm test --filter './apps/*'              # path glob
lpm test --filter web --filter api        # union
lpm test --filter-prod ...shared          # prod graph closure
lpm test --affected                       # only members touched since main
lpm test --affected --base develop
lpm test --filter web --fail-if-no-match  # exit non-zero on typo'd filter
lpm test --filter '@scope/*' --workspace-concurrency 2
```

Detection runs per member, so a workspace can mix runners (one member uses vitest, another uses jest) and each gets the right command. A member with no installed runner and no `scripts.test` becomes a per-member detection failure in the JSON envelope rather than aborting the whole run.

`--all` is mutually exclusive with filters and `--affected`. `--filter` and `--filter-prod` compose with `--affected` (the affected set is unioned with the filter result). `--filter-prod` uses the same grammar as `--filter`, but closure operators ignore `devDependencies`.

`--workspace-concurrency <N>` caps how many selected workspace members run at once within each topological level.

### Forwarding runner flags with the same names [#forwarding-runner-flags-with-the-same-names]

The workspace flags (`--all`, `--filter`, `--filter-prod`, `--affected`, `--base`, `--fail-if-no-match`, `--workspace-concurrency`) are claimed by LPM CLI. To pass any of them through to the underlying runner (e.g. bun's `--filter`, jest's `--all`), put them after `--`:

```bash
lpm test -- --filter pattern              # forwards --filter to bun/vitest/etc
lpm test -- --all                         # forwards --all to the runner
lpm test --filter web -- --reporter=verbose  # workspace + forwarded args
```

Anything after `--` is forwarded verbatim regardless of name.

### Watch with a workspace selector [#watch-with-a-workspace-selector]

`lpm test --all --watch` would start one watcher per workspace member — almost always a mistake. So watch mode is gated by selection size:

```bash
lpm test --filter web --watch             # ✓ one member, one watcher (hands off
                                          #   to single-package mode in web/)
lpm test --all --watch                    # ✗ rejected: would start N watchers
lpm test --filter '@scope/*' --watch      # ✓ allowed only if exactly one member matches
                                          # ✗ rejected with the actual count if more
lpm test --filter typo --watch            # ✗ rejected: nothing to watch
```

When the filter resolves to exactly one member, `lpm test` runs against that member's directory as if you'd `cd`-ed in — the existing vitest `run` → bare `vitest` rewrite kicks in, so `--watch` is honored.

## Flags [#flags]

| Flag                                    | Effect                                                                                                  |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `--all`                                 | Run in every workspace member                                                                           |
| `--filter <expr>`                       | Select workspace members by the [filter grammar](/docs/packages/workspaces#filter-grammar) (repeatable) |
| `--filter-prod <expr>`                  | Select workspace members with production-only dependency closures                                       |
| `--affected`                            | Run only in members affected by changes vs `--base`                                                     |
| `--base <REF>`                          | Git base ref for `--affected` (default: `main`)                                                         |
| `--changed-files-ignore-pattern <glob>` | Ignore matching git-diff paths for `--affected` / `[git-ref]` filters                                   |
| `--test-pattern <glob>`                 | Treat matching git-diff paths as test-only for `--affected` / `[git-ref]` fan-out decisions             |
| `--fail-if-no-match`                    | Exit non-zero if no member matches the filter set                                                       |
| `--workspace-concurrency <N>`           | Limit concurrent workspace members for `--all`, `--filter`, or `--affected` runs                        |

Anything after `--` (or trailing) is forwarded to the runner.

Plus the [global flags](/docs/commands#global-flags).

## `--json` in workspace mode [#--json-in-workspace-mode]

Single-package mode (`lpm test`) preserves the runner's stdout — LPM CLI does not wrap it. Workspace mode emits a single LPM CLI envelope on stdout; per-member stdout/stderr is captured and surfaced inside the envelope only on failure. See [`lpm lint`](/docs/dev/lint#json-in-workspace-mode) for the exact envelope shape. Per-member detection failures (no runner installed) appear as `exit_code: null` paired with an `error` string.

## See also [#see-also]

* [`lpm bench`](/docs/dev/bench) — same auto-detection, for benchmarks
* [`lpm run test`](/docs/dev/run) — run the `test` script from `package.json` instead
* [Test & bench runners](/docs/dev/test-bench-runners) — how detection works


# lpm use (/docs/dev/use)



```bash
lpm use <runtime>@<version>      # install + pin
lpm use --list [runtime]         # list installed versions
lpm use remove <runtime>@<spec>  # remove installed versions matching a spec
```

Installs and pins managed runtimes under `~/.lpm/runtimes/`. Supported runtimes are `node` and `bun`.

After `lpm use node@22`, scripts run with the pinned Node version via PATH injection. After `lpm use bun@1.3.14`, scripts can call `bun` from the managed Bun install. `runtime.bun` does **not** make LPM CLI execute scripts through `bun run`; `lpm run` still uses LPM CLI's runner and exposes Bun on `PATH`.

For project environment variables and secrets management, see [`lpm env`](/docs/dev/env).

## Installing Node [#installing-node]

```bash
lpm use node@22                # install + pin Node 22 (resolves to latest 22.x)
lpm use node@lts               # install + pin the latest LTS
lpm use node@22.12.0           # install + pin an exact version
lpm use node@22 --pin          # already installed — pin the concrete version, no download
```

Versions land under `~/.lpm/runtimes/node/`. `lpm use node@22` resolves the spec, downloads the build for your platform if missing, then writes the pin to `lpm.json`:

```json title="lpm.json"
{ "runtime": { "node": "22.12.0" } }
```

When `lpm.json > runtime.node` is absent, detection falls back through `.nvmrc`, then `.node-version`. `package.json > engines.node` validates the resulting managed or `PATH` runtime but never selects or installs one. See [Managed runtimes](/docs/dev/node-version-pinning) for the full ladder.

## Installing Bun [#installing-bun]

```bash
lpm use bun@1.3.14             # install + pin an exact Bun version
lpm use bun@latest             # install + pin the latest Bun release
lpm use bun@1.3 --pin          # pin a matching installed 1.3.x version
```

Versions land under `~/.lpm/runtimes/bun/`. `lpm use bun@1.3.14` writes:

```json title="lpm.json"
{ "runtime": { "bun": "1.3.14" } }
```

Bun specs accept exact versions, `v1.3.14`, `bun-v1.3.14`, `latest`, major/minor prefixes, and semver ranges. Bun does not have an LTS channel, so `bun@lts` is rejected.

`runtime.bun` is read only from `lpm.json`. `package.json > engines.bun` is surfaced as a compatibility warning but is not enforced.

## Listing Installed Versions [#listing-installed-versions]

```bash
lpm use --list                 # list installed Node versions
lpm use --list node            # same
lpm use --list bun             # list installed Bun versions
```

With `--json`, the envelope includes the selected runtime:

```json
{ "success": true, "runtime": "bun", "versions": ["1.3.14"] }
```

## Removing Installed Versions [#removing-installed-versions]

```bash
lpm use remove node@20         # remove all installed 20.x Node runtimes
lpm use remove bun@1.3         # remove all installed 1.3.x Bun runtimes
lpm use remove bun@1.3.14      # remove one exact Bun runtime
```

Removal matches the local managed runtime set only. Exact versions remove one installed runtime. Prefixes and semver ranges remove every installed version that satisfies the spec.

`lpm use remove` accepts explicit versions, prefixes, and semver ranges. It does not accept `node@lts`, `node@latest`, `bun@lts`, or `bun@latest` because removal must be deterministic against the local install set.

Removing a managed runtime does **not** rewrite `lpm.json`. If the project still pins a matching runtime, LPM CLI warns because the next `lpm run` / `lpm dev` / `lpm install` path that needs that pin can auto-install it again.

## Flags [#flags]

| Flag       | Effect                                                                                                                        |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `--list`   | List installed versions for the runtime                                                                                       |
| `--pin`    | Pin only. Exactizes to an installed version when one already matches; otherwise stores the requested spec without downloading |
| `--remove` | Remove installed managed runtimes matching a spec. `lpm use remove bun@1.3` is the preferred explicit command form.           |

Plus the [global flags](/docs/commands#global-flags).

## See Also [#see-also]

* [`lpm env`](/docs/dev/env) — project environment variables and secrets
* [`lpm dev`](/docs/dev/dev) — picks up runtime pins automatically
* [Managed runtimes](/docs/dev/node-version-pinning) — how pins propagate to scripts


# CI/CD setup (/docs/guides/ci-cd-setup)



This guide covers the moving parts of running LPM CLI in CI: pinning the binary, authenticating reproducibly, gating on quality and security signals, and producing reliable artifacts. We'll go through GitHub Actions in detail; GitLab CI follows the same shape.

## 1. Install LPM CLI in CI [#1-install-lpm-cli-in-ci]

The fastest path is npm — it works everywhere:

```yaml
- run: npm install -g @lpm-registry/cli
```

Pin the version explicitly. Don't `@latest` in CI — reproducible builds depend on a fixed toolchain.

For Linux runners that don't have npm bootstrapped, use the standalone installer:

```yaml
- run: curl -fsSL https://cli.lpm.dev/install | sh
- run: echo "$HOME/.lpm/bin" >> $GITHUB_PATH
```

## 2. Authenticate reproducibly [#2-authenticate-reproducibly]

For private packages on lpm.dev, use a read-scoped Registry token.

### Static token [#static-token]

```yaml
- run: npm install -g @lpm-registry/cli
- run: lpm fetch
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
- run: lpm ci --offline --strict-integrity
```

Create a read-scoped token and store it as `LPM_TOKEN` in your CI's secret store. LPM CLI reads it directly and routes `@lpm.dev/*` packages without an `.npmrc`; use [`lpm setup ci npmrc`](/docs/infra/setup) only when another npm-compatible client in the job needs Registry authentication. That command must resolve a bearer first and writes the literal only to an atomically replaced, owner-only project file; its JSON output is redacted.

## 3. Reproducible installs [#3-reproducible-installs]

```bash
lpm ci --offline --strict-integrity --no-skills --no-editor-setup --no-security-summary
```

| Flag                    | What it does in CI                                                                                                   |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `--offline`             | No network. Replays entirely from `lpm.lock` + the global store. Errors if anything is missing.                      |
| `--strict-integrity`    | Refuse to install tarball-URL deps that don't declare an inline SRI. Disables trust-on-first-use for fresh installs. |
| `--no-skills`           | Skip skills auto-install. Saves time.                                                                                |
| `--no-editor-setup`     | Compatibility flag. Package skills do not create editor integrations.                                                |
| `--no-security-summary` | Skip the post-install security report. Pair with `lpm audit` as a separate explicit step.                            |

If `lpm ci` fails, the lockfile is missing, stale, or cannot be replayed from the available store. Run `lpm install` locally, commit the updated `lpm.lock`, and commit `lpm.lockb` only when LPM CLI writes it.

## 4. npm firewall in CI [#4-npm-firewall-in-ci]

The npm firewall is a network verdict check, so the command that runs it cannot be `--offline`. Use `LPM_NPM_FIREWALL=monitor` or `LPM_NPM_FIREWALL=enforce` for a per-job policy, or run `lpm config firewall --set enforce` on runners where the home directory is intentionally persistent.

```yaml
- run: npm install -g @lpm-registry/cli
- run: LPM_NPM_FIREWALL=enforce lpm ci --strict-integrity
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
```

`LPM_TOKEN` is used for the firewall verdict API and any lpm.dev package auth in the same run.

If the workflow deliberately separates an online store warm from an offline replay, put the firewall on the online step and keep the replay offline:

```yaml
- run: LPM_NPM_FIREWALL=enforce lpm fetch
- run: lpm ci --offline --strict-integrity
```

## 5. Gating [#5-gating]

The standard gate sequence:

```yaml
- run: lpm ci --offline --strict-integrity
- run: lpm trust diff --assert-none
- run: lpm audit --fail-on vuln # fail on vulnerabilities (not behavior signals)
- run: lpm licenses --fail-on copyleft --deny GPL-3.0 # optional license policy gate
- run: lpm test # forwards vitest/jest exit code
- run: lpm lint # forwards oxlint exit code
- run: lpm fmt --check # fails if anything is unformatted
- run: lpm check # tsc --noEmit
```

Each step exits non-zero on failure — no special handling needed. See [Exit codes](/docs/reference/exit-codes). `lpm trust diff` is informational by default; add `--assert-none` to gate on `trustedDependencies` drift. Use `lpm trust diff --assert-none --json` if you also want the structured diff envelope in CI logs.

For finer-grained security gates, use [`lpm query`](/docs/packages/query):

```yaml
- run: lpm query ":vulnerable:not(:built)" --assert-none
- run: lpm query ":eval:scripts" --assert-none
```

For compliance gates, use [`lpm licenses`](/docs/packages/licenses):

```yaml
- run: lpm licenses --fail-on copyleft,missing
- run: lpm licenses --deny GPL-3.0 --deny AGPL-3.0
```

## 6. Caching `~/.lpm/` [#6-caching-lpm]

Cache the content LPM CLI can safely reuse. Do **not** cache `node_modules/`; it is a generated layout that LPM CLI recreates from the lockfile and store.

| Path               | Cache key                                                   | Use                                                                                                |
| ------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `~/.lpm/store`     | `lpm-store-${{ runner.os }}-${{ hashFiles('lpm.lock') }}`   | Package store used by `lpm ci --offline` and `lpm install --offline`.                              |
| `~/.lpm/plugins`   | `lpm-plugins-${{ runner.os }}-${{ hashFiles('lpm.json') }}` | Managed Oxlint, Biome, and Rolldown plugin binaries used by built-in tools.                        |
| `~/.lpm/cache/dlx` | `lpm-dlx-${{ runner.os }}-${{ hashFiles('lpm.lock') }}`     | Optional. Speeds up repeated `lpm dlx` / `lpx` calls; entries still expire after their normal TTL. |

The store is the important cache:

```yaml
- name: Cache LPM CLI store
  uses: actions/cache@v4
  with:
    path: ~/.lpm/store
    key: lpm-store-${{ runner.os }}-${{ hashFiles('lpm.lock') }}
    restore-keys: lpm-store-${{ runner.os }}-
```

If your workflow runs managed tools, cache plugins separately so tool downloads do not share the dependency-store key:

```yaml
- name: Cache LPM CLI plugins
  uses: actions/cache@v4
  with:
    path: ~/.lpm/plugins
    key: lpm-plugins-${{ runner.os }}-${{ hashFiles('lpm.json') }}
    restore-keys: lpm-plugins-${{ runner.os }}-
```

`lpm install --offline` is fast in benchmarks when the store + lockfile match. On the VitePress docs fixture, up-to-date checks land around 14 ms and warm reinstalls around 387 ms. Caching `~/.lpm/store` turns a cold CI install into a warm one.

The cache key is the lockfile hash. Typical CI runners are short-lived enough that orphan cleanup isn't needed; if you do persist the store across runs and want to bound its size, run `lpm cache prune --apply` periodically.

For persistent self-hosted runners, also clean short-lived caches on your own schedule:

```bash
lpm cache clean dlx
lpm cache prune --apply
```

## 7. Publishing from CI [#7-publishing-from-ci]

```yaml
- run: lpm publish --provenance --min-score 60 --wait -y
```

`--provenance` requires OIDC and produces a Sigstore attestation. `--min-score 60` blocks a low local quality score.

`--wait` keeps the job active until the LPM.dev Registry version becomes active under its distribution rules. The default wait limit is 20 minutes.

Some quality points remain pending until Registry analysis. Set the threshold from a representative `lpm publish --check` result. `-y` skips the prompt.

For npm-compatible publish provenance, target npm explicitly:

```yaml
- run: lpm publish --npm --provenance -y
```

Generated npm provenance requires public access. LPM CLI's npm default is public; if your config sets restricted access, change it to public or skip generated provenance. A normal npm-only publish does not run the LPM.dev quality scorer, so `--min-score` is not a quality gate for this command; keep the project's lint, test, type-check, and audit jobs explicit in CI.

For multi-registry publishing in one job, configure `lpm.json > publish.registries` and pass nothing — `lpm publish` walks every configured target. Custom npm-compatible registries need an exact registry-scoped token from `lpm login --login-registry <URL> --token <T>`; `NPM_TOKEN` only authenticates `https://registry.npmjs.org`.

## Common pitfalls [#common-pitfalls]

* **`@latest` in CI.** Don't pin the binary to floating tags. Reproducible builds need a fixed toolchain.
* **`lpm install` without `--offline` in CI.** Lets lockfile drift slip through unnoticed.
* **Combining active npm firewall with `--offline`.** Firewall verdicts need network access. Run the firewall-protected install online, or firewall-gate an online `lpm fetch` before the offline replay.
* **Forgetting `permissions: id-token: write`** when publishing with OIDC or generated provenance in GitHub Actions.
* **Caching `node_modules/` instead of `~/.lpm/store/`.** The LPM CLI store is the right cache key — `node_modules` is a layout, the store is content.
* **Skipping `lpm audit` to "save time".** It's a few hundred ms; the time-not-paid is paid back on the next CVE.

## See also [#see-also]

* [`lpm setup ci npmrc`](/docs/infra/setup) — generate `.npmrc` for npm-compatible CI clients
* [`lpm ci`](/docs/packages/install#frozen-lockfile-and-ci) — frozen lockfile install
* [`lpm env print --ci`](/docs/dev/env#ci-output) — CI-native env output
* [`lpm audit`](/docs/packages/audit) — security gating
* [Exit codes](/docs/reference/exit-codes) — what each non-zero code means
* [Environment variables](/docs/reference/env-vars) — `LPM_TOKEN`, `LPM_OIDC_TOKEN`


# Docker Deploys (/docs/guides/docker-deploys)



Use one of two Docker patterns:

* Single package: warm the store from `lpm.lock`, then run an offline frozen install.
* Workspace app: run `lpm deploy` in a build stage, then `COPY --from=pruned` the self-contained output.

Both patterns start from committed lockfiles. `lpm.lock` is authoritative; commit `lpm.lockb` too when LPM CLI writes it, but Docker cache layers can key off `lpm.lock`.

## Single-Package Image [#single-package-image]

```dockerfile
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim

RUN npm install -g @lpm-registry/cli
WORKDIR /app

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

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

COPY . .

CMD ["node", "server.js"]
```

`lpm fetch` only reads `lpm.lock`, so ordinary source changes do not invalidate package downloads. The later `lpm install --offline --frozen-lockfile --prod` links from the warmed store and fails if `package.json` does not match the importer snapshot in `lpm.lock`.

Use a platform that matches the runtime image:

```bash
lpm fetch --platform linux/x64/glibc  # Debian/Ubuntu images
lpm fetch --platform linux/x64/musl   # Alpine x64 images
lpm fetch --platform linux/arm64/musl # Alpine arm64 images
```

If the project has local `file:` or `link:` sources, copy those source directories before the offline install. `lpm fetch` skips local sources because their bytes live in the checkout, not in a registry tarball.

## Layer Cache Notes [#layer-cache-notes]

Keep the LPM CLI store in an image layer when that same stage ships `node_modules`. LPM CLI links installed packages through the store, so a BuildKit cache mount at `/root/.lpm/store` would disappear after the `RUN` step and leave broken links in the final image.

Use ordinary Docker layer caching for the store-warm step, or use [`lpm deploy`](#workspace-deploy-image), whose output carries a deploy-local `.lpm/store/` alongside `node_modules`. Do not cache or copy `node_modules` between builds; LPM CLI recreates it from the lockfile and store.

## Workspace Deploy Image [#workspace-deploy-image]

For monorepos, `lpm deploy` materializes one workspace member into a self-contained output directory. The output includes the selected member source, selected local workspace dependencies under `.lpm/deploy-workspace/`, a pruned `lpm.lock`, a deploy-local store, and a populated `node_modules/`.

```dockerfile
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim AS pruned

RUN npm install -g @lpm-registry/cli
WORKDIR /repo

COPY . .
RUN lpm deploy /prod/api --filter api

FROM node:22-bookworm-slim AS runtime

WORKDIR /app
COPY --from=pruned /prod/api /app

CMD ["node", "server.js"]
```

`lpm deploy` requires `--filter` or `--filter-prod`, and the final selection must match exactly one workspace member. `--prod` is the default. Use `--dev` for a dev-dependency deploy tree and `--no-optional` to omit optional dependencies.

The runtime image does not need LPM CLI unless your own runtime scripts call it.

## Deploy Copy Rules [#deploy-copy-rules]

`lpm deploy` copies publishable files: `package.json > files` when present, otherwise `.npmignore`, otherwise `.gitignore`. It then applies a deny list at every directory level:

* `node_modules`, `.lpm`, `lpm.lock`, `lpm.lockb`
* `.env`, `.env.local`, `.env.development`, `.env.development.local`, `.env.production`, `.env.production.local`, `.env.test`, `.env.test.local`
* `.git`, `.gitignore`, `.npmignore`, `.gitattributes`, `.svn`, `.hg`
* `.DS_Store`, `Thumbs.db`

Still add a `.dockerignore` so secrets and local state never enter the Docker build context:

```text title=".dockerignore"
node_modules
.lpm
.env*
.git
```

## Common Pitfalls [#common-pitfalls]

| Symptom                                                          | Fix                                                                                                 |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `lpm install --offline` says a package is missing from the store | Run `lpm fetch --platform <target>` in an earlier layer or remove `--offline` for that build        |
| Native optional package is missing in Alpine                     | Fetch for `linux/<arch>/musl`, not `glibc`                                                          |
| `lpm deploy` says the filter matched zero or many members        | Narrow the filter and preview it with [`lpm filter`](/docs/packages/workspaces#filter-grammar)      |
| Deploy output path is rejected                                   | Put it outside the workspace tree, such as `/prod/api`                                              |
| Source changes invalidate install cache                          | Copy only `lpm.lock` before `lpm fetch`, then copy `package.json`, then copy the rest of the source |

## See also [#see-also]

* [`lpm fetch`](/docs/packages/fetch) - lockfile-only store warming
* [`lpm install --offline --frozen-lockfile`](/docs/packages/install#frozen-lockfile-and-ci) - reproducible installs
* [Workspaces: `lpm deploy`](/docs/packages/workspaces#lpm-deploy) - deploy command reference
* [Monorepo setup](/docs/guides/monorepo-setup) - workspace filters and deploy flow
* [CI/CD setup](/docs/guides/ci-cd-setup) - caching `~/.lpm/store`


# Firewall for npm (/docs/guides/firewall)



LPM Firewall checks selected public npm package versions with [firewall.lpm.dev](https://firewall.lpm.dev) before LPM CLI materializes package bytes. Metadata and tarballs still come from the normal npm registry path; the firewall request only asks for a package-version verdict.

The hosted verdict service is an LPM.dev Registry Pro/Org feature. Active modes send LPM.dev Registry auth; run `lpm login` locally or provide `LPM_TOKEN` in CI.

## Quickstart [#quickstart]

```bash
lpm login
lpm config firewall --set monitor  # show would-block / warn verdicts
lpm config firewall --set enforce  # block according to the recommended policy profile
```

`off` is the default. `monitor` prints verdicts and lets installs continue. `enforce` blocks packages whose effective firewall action is `block`. The legacy string `report` is still accepted as an alias for `monitor`, but new config should use `monitor`.

For guided setup, run the wizard without `--set`:

```bash
lpm config firewall
```

## What gets checked [#what-gets-checked]

The firewall runs for eligible public npm package-version rows used by:

* `lpm install`
* `lpm install -g`
* `lpm fetch`
* `lpm download`
* the source-package download step of `lpm add`

Private/custom registry packages and LPM.dev Registry packages are not sent to the LPM Firewall verdict API. Integrity-specific evidence can be attached to a verdict, but the fast product path is the package-version lookup.

## Modes [#modes]

| Mode      | Behavior                                                                                                                                      |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `off`     | Do not call firewall.lpm.dev. Use direct npm metadata and tarballs only.                                                                      |
| `monitor` | Call firewall.lpm.dev, print block/warn verdicts, and continue. Entitlement or network failures warn and continue.                            |
| `enforce` | Call firewall.lpm.dev and stop before package bytes are materialized when the effective action is `block`. Entitlement failures are blocking. |

When a human-output materialization command has eligible packages to check, its progress line shows `🔥 LPM Firewall active`.

## CI usage [#ci-usage]

Firewall checks run for `lpm install`, `lpm ci`, `lpm fetch`, `lpm download`, and the source-package download step of `lpm add`. Because the verdict check calls firewall.lpm.dev, an active firewall check cannot run under `--offline`.

CI can enable the firewall for one command with `LPM_NPM_FIREWALL` and a Registry token:

```yaml
- run: LPM_NPM_FIREWALL=enforce lpm ci --strict-integrity
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
```

For offline replay workflows, run the firewall during the online fetch step instead:

```yaml
- run: LPM_NPM_FIREWALL=enforce lpm fetch
- run: lpm ci --offline --strict-integrity
```

## Policy groups [#policy-groups]

LPM CLI receives a default verdict from LPM Firewall, then sends your configured policy profile with the request. LPM Firewall evaluates the profile server-side and returns the user-effective action for each decision.

Choosing `enforce` in the interactive wizard opens a single policy review screen. Press Enter to accept the recommended profile, or use the arrow keys to change individual groups before saving.

```text title="Interactive policy review"
  current: enforce
◇  How should LPM CLI handle LPM Firewall verdicts for npm packages?
│  enforce
│
◆  Review npm firewall enforcement policy
│  Use ↑/↓ to move, ←/→ to change, Enter to save, Esc to cancel.
│
│  › Trusted public malicious advisories
│    OSV / OpenSSF / GHSA
│    ● Block  ○ Warn only  ○ Allow
│
│    LPM Firewall AI-confirmed malware
│    Credential/data exfiltration, RCE, remote payload execution, persistence, dependency confusion
│    ● Block  ○ Warn only  ○ Allow
│
│    LPM Firewall AI-agent control-surface policy
│    Silent install-lifecycle writes into foreign or broad AI-agent control surfaces
│    ○ Block  ● Warn only  ○ Allow
│
│    Critical vulnerabilities
│    Legitimate package risk without malicious author intent
│    ○ Block  ● Warn only  ○ Allow
│
│    Static-only suspicious signals
│    No AI or trusted advisory confirmation yet
│    ● Warn only  ○ Allow
└
```

The equivalent config lives in `~/.lpm/config.toml`:

```toml title="~/.lpm/config.toml"
[firewall]
mode = "enforce"

[firewall.npm.policies]
trusted_public_malicious_advisories = "block"
lpm_ai_confirmed_malware = "block"
lpm_ai_agent_control_surface = "warn"
critical_vulnerability = "warn"
lpm_ai_suspicious = "warn"
```

| Policy group                          | Default | Allowed actions          | Meaning                                                                                                                                                                                                                                                             |
| ------------------------------------- | ------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trusted_public_malicious_advisories` | `block` | `allow`, `warn`, `block` | Public malicious-package advisories from OSV/OpenSSF/GHSA. LPM Firewall-owned decisions are not included in this group.                                                                                                                                             |
| `lpm_ai_confirmed_malware`            | `block` | `allow`, `warn`, `block` | LPM Firewall AI-confirmed malware behavior such as credential or data exfiltration, remote payload execution, persistence/destructive behavior, dependency confusion, and classic malware.                                                                          |
| `lpm_ai_agent_control_surface`        | `warn`  | `allow`, `warn`, `block` | LPM Firewall policy warnings for silent install-lifecycle writes into foreign or broad AI-agent control surfaces. This is a dangerous-capability policy, not necessarily a malware-intent finding.                                                                  |
| `critical_vulnerability`              | `warn`  | `allow`, `warn`, `block` | Critical vulnerability advisories. The wizard offers block, warn, or allow for this warn-default group.                                                                                                                                                             |
| `lpm_ai_suspicious`                   | `warn`  | `allow`, `warn`, `block` | Suspicious signals that LPM Firewall AI does not classify as malware. Trusted public sources also do not identify these signals as malicious. The wizard normally offers warn or allow. If hand-edited config already uses `block`, the review screen preserves it. |

If `lpm_ai_suspicious` is absent, LPM CLI accepts the legacy `static_only_suspicious` key. The firewall editor writes the canonical `lpm_ai_suspicious` key.

Use `lpm config firewall` in an interactive terminal to choose:

* Off
* Monitor only
* Enforce, then review the policy profile

The wizard writes the policy table only when the chosen profile differs from the default profile, or when an existing policy table needs to be updated.

## AI-agent control-surface policy [#ai-agent-control-surface-policy]

LPM Firewall flags silent install-lifecycle writes into foreign or broad AI-agent control surfaces because a transitive npm install should not mutate another agent's skills, commands, hooks, MCP config, or startup surface without an explicit user command. The recommended profile warns by default; teams that want stricter containment can set this group to `block`.

This policy is version-scoped and remediable. A package can move setup into an explicit command such as `<tool> setup` or `<tool> init`, then the changed version can be evaluated under a weaker action. See [firewall.lpm.dev/policy](https://firewall.lpm.dev/policy) for the public policy text.

## Monitor and JSON output [#monitor-and-json-output]

In monitor mode, LPM CLI never blocks installs. If LPM Firewall returns an effective `block`, human output says it would block and the command continues.

When JSON output includes firewall decisions, each decision can include optional policy and authority metadata:

```json
{
  "action": "block",
  "verdict": "malicious",
  "policy": {
    "group": "lpm_ai_agent_control_surface",
    "key": "ai_agent_control_surface",
    "intent": "dangerous_capability",
    "default_action": "warn"
  },
  "authority": {
    "source": "lpm_ai",
    "source_type": "lpm",
    "external_intel": false
  }
}
```

Older firewall records can omit `policy` and `authority`. Those records fall back to the plain verdict behavior.

## See also [#see-also]

* [`lpm config`](/docs/infra/config#npm-firewall) — interactive and non-interactive setup
* [CI/CD setup](/docs/guides/ci-cd-setup#4-npm-firewall-in-ci) — token example
* [`~/.lpm/config.toml`](/docs/reference/config-toml#npm-firewall-policy-profile) — exact config keys
* [`lpm install`](/docs/packages/install#npm-firewall) — install-time behavior
* [Public firewall policy](https://firewall.lpm.dev/policy)


# Guides (/docs/guides)





End-to-end walkthroughs for the workflows you'll do more than once. Each guide stands on its own — start at whichever one matches what you're trying to do.

For per-command flag references, see the [Packages](/docs/packages), [Dev](/docs/dev), and [Infra](/docs/infra) sections. For schema-style configuration reference, see [Reference](/docs/reference).

## Workflows [#workflows]

<Cards>
  <Card title="Publishing your first package" href="/docs/guides/publishing-a-package" description="From lpm init to lpm publish, with quality checks and provenance." />

  <Card title="Zero-config dev server" href="/docs/guides/zero-config-dev-server" description="Configure lpm.json and run lpm dev for HTTPS, tunnel, and multi-service orchestration." />

  <Card title="Firewall for npm" href="/docs/guides/firewall" description="Configure monitor mode, enforcement, and policy-group controls for firewall.lpm.dev." />

  <Card title="Monorepo setup" href="/docs/guides/monorepo-setup" description="Workspaces, --filter grammar, --affected, lpm deploy for production builds." />

  <Card title="CI/CD setup" href="/docs/guides/ci-cd-setup" description="GitHub Actions and GitLab CI — OIDC, .npmrc, gates." />

  <Card title="Docker deploys" href="/docs/guides/docker-deploys" description="Layer-cached installs and lpm deploy multi-stage images." />

  <Card title="Managing secrets" href="/docs/guides/managing-secrets" description="Per-environment secrets with lpm env." />

  <Card title="Using LPM CLI with Swift" href="/docs/guides/using-with-swift" description="SPM integration via SE-0292." />
</Cards>

## Migrating from another package manager [#migrating-from-another-package-manager]

<Cards>
  <Card title="Migrating from npm" href="/docs/guides/migrating-from-npm" description="package-lock.json → lpm.lock, with rollback." />

  <Card title="Migrating from pnpm" href="/docs/guides/migrating-from-pnpm" description="pnpm-lock.yaml → lpm.lock, isolated layout preserved." />

  <Card title="Migrating from Yarn" href="/docs/guides/migrating-from-yarn" description="yarn.lock → lpm.lock, with rollback." />

  <Card title="Migrating from Bun" href="/docs/guides/migrating-from-bun" description="bun.lock or bun.lockb → lpm.lock, with rollback." />
</Cards>


# Managing secrets across environments (/docs/guides/managing-secrets)



`lpm env` is LPM CLI's surface for environment-scoped secrets. It stores values in encrypted form on disk, organizes them per-environment (`dev` / `staging` / `production`), and validates them against an env schema declared in `lpm.json`. This guide walks through the full lifecycle.

## 1. Set a few vars [#1-set-a-few-vars]

```bash
lpm env set DATABASE_URL=postgres://localhost/myapp
lpm env set API_KEY=sk-... LOG_LEVEL=debug          # multiple at once
```

`set` writes to the project's vault. Values are stored encrypted; only LPM CLI can read them back.

```bash
lpm env list                  # values are masked
lpm env list --reveal         # show actual values
lpm env get API_KEY --reveal  # show one
lpm env delete API_KEY        # remove
```

## 2. Per-environment vars [#2-per-environment-vars]

Use `--env=<name>` to scope a var to one environment:

```bash
lpm env set --env=staging API_URL=https://staging.example.com
lpm env set --env=production API_URL=https://api.example.com
lpm env list --env=staging
```

`lpm run start --env=staging` (or `lpm dev --env=staging`) loads only the staging-scoped vars plus any unscoped defaults.

## 3. Bulk import / export [#3-bulk-import--export]

Bring in a `.env`-style file:

```bash
lpm env import .env.production --env=production
lpm env import .env --overwrite       # overwrite any existing keys
```

Export back out (e.g., for a deploy hand-off):

```bash
lpm env export .env.backup
lpm env export .env.staging --env=staging
```

The exporter writes a plain `.env` format — be careful where the file lands. `.env.backup` should be in `.gitignore`.

## 4. Declare a schema [#4-declare-a-schema]

Project-shared validation goes in `lpm.json`:

```json title="lpm.json"
{
  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url" },
      "API_KEY":      { "required": true, "secret": true },
      "LOG_LEVEL":    { "default": "info", "pattern": "^(trace|debug|info|warn|error)$" },
      "PORT":         { "default": "3000", "format": "port" }
    }
  }
}
```

Per-var fields:

| Field      | Effect                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------ |
| `required` | Fail if not set                                                                            |
| `format`   | Built-in validator. One of: `url`, `email`, `port`, `boolean`, `integer`, `hostname`, `ip` |
| `pattern`  | Regex the value must match                                                                 |
| `default`  | Default value if unset                                                                     |
| `secret`   | Treat as sensitive — masked in logs and `lpm env list`                                     |

Check every discovered environment against the schema:

```bash
lpm env check
lpm --json env check | jq -e '.success'
```

The command exits non-zero when any environment is invalid in both human and JSON modes. JSON retains the per-environment details and sets `"success": false`, so automation can inspect the structured result as well as the process status.

The same `lpm.json > envSchema` validation runs for the selected environment before `lpm run`, `lpm dev`, `lpm <file>`, and `lpm exec`. Skip it with `--no-env-check` only when you deliberately want to run with an invalid environment.

### Compare the default vault with `.env.example` [#compare-the-default-vault-with-envexample]

`lpm env validate` does not use the schema. It compares the key names in the default local vault with `.env.example`; it does not inspect named environments or remote cloud state.

```bash
lpm env validate
lpm env validate --strict
lpm --json env validate | jq -e '.valid'
```

Without `--strict`, validation requires every `.env.example` key and allows extra keys in the default local vault. With `--strict`, extra default-vault keys also make the result invalid. Invalid results exit non-zero in both human and JSON modes; JSON sets both `"success"` and `"valid"` to `false` and retains the `required`, `present`, `missing`, and `extra` arrays for structured inspection.

## 5. Named environments with inheritance [#5-named-environments-with-inheritance]

For projects with many `.env.*` variants:

```json title="lpm.json"
{
  "environments": {
    "base":    { "file": ".env" },
    "staging": { "extends": "base", "file": ".env.staging" },
    "preview": { "extends": "staging", "file": ".env.preview" }
  }
}
```

`extends` chains resolve transitively. `lpm dev --env=preview` loads `.env`, then `.env.staging` on top, then `.env.preview` on top of that — last write wins. Useful for "preview is staging plus a few overrides."

## 6. Onboarding a new contributor [#6-onboarding-a-new-contributor]

For a teammate joining a project that uses cloud sync (Pro/Org):

```bash
git clone <repo>
cd <repo>

# 1. Generate a pairing code in the LPM.dev Registry dashboard ("Pair device").
#    The dashboard will prompt you to re-enter your password (or your TOTP
#    code if you have MFA enrolled) before it issues the code — this
#    step-up check is what keeps an unattended unlocked browser from
#    silently pairing a new device on your behalf. Then redeem the code
#    on this machine. One-time per machine.
#
#    The CLI prints the browser-key fingerprint, the device label, and an
#    eight-digit comparison number from the P-256 ECDH exchange. Make sure
#    that the dashboard shows the same number before you answer 'y'.
lpm env pair <code>

# 2. Pull the latest secrets — decrypted locally with the wrapping key
#    that pairing just installed in this machine's OS keychain.
lpm env pull

lpm install
lpm dev
```

The `vault` field in `lpm.json` is what links the working directory to the right cloud vault — see [Secrets vault — Per-project identity](/docs/infra/secrets-vault#per-project-identity). It's a UUID, not a secret; it ships with the repo so every clone resolves to the same vault.

For teams without cloud sync, hand secrets over out-of-band and use the import flow instead:

```bash
lpm env import .env.shared
```

Either way, the schema (`lpm.json > envSchema`) catches missing vars before runtime. The `secret: true` flag prevents accidental leaking into logs.

## 7. Rotate organization encryption after member removal [#7-rotate-organization-encryption-after-member-removal]

If you remove an organization member, rotate each env project that granted access to the member:

```bash
lpm env rotate-key --org <org-slug>
```

Run the command as an organization owner or administrator. Use a machine that can pull the current organization env project.

The command preserves all environments and creates a fresh content key. It wraps that key for the complete current member set.

The server compares the remote version, member set, and sharing-key fingerprints before it commits. A conflict leaves the previous remote state unchanged.

The rotation stops the removed key from decrypting future ciphertext. It cannot erase secrets or keys that the former member already copied.

## What about `lpm vault`? [#what-about-lpm-vault]

The supported CLI surface for secrets is `lpm env`. "Vault" elsewhere in the docs refers to the underlying storage layer — OS keychain + encrypted file fallback + cloud-sync infrastructure. See [Secrets vault](/docs/infra/secrets-vault) for the storage and encryption design.

## Common pitfalls [#common-pitfalls]

* **`.env.backup` and `.env.shared` should be in `.gitignore`.** `lpm env export` writes plaintext — committing it defeats the encryption.
* **`--reveal` shows real values.** Don't pipe `lpm env list --reveal` into a CI log. Use it locally for verification only.
* **Keep validation details in automation.** Exit status is sufficient to fail CI, while `success`, `valid`, and the result arrays explain what needs remediation.
* **Rotation uses remote compare-and-swap.** Personal and organization rotations preserve all named environments. If the remote version changes, resolve the conflict and retry.
* **Organization wraps are state-bound.** A stale key fingerprint or content-key version fails closed. The dashboard shows **Needs share**.

## See also [#see-also]

* [`lpm env`](/docs/dev/env) — full subcommand reference
* [`lpm.json` envSchema](/docs/reference/lpm-json#envschema) — schema field reference
* [`lpm.json` environments](/docs/reference/lpm-json#environments) — named environments with inheritance
* [Secrets vault](/docs/infra/secrets-vault) — storage and encryption design


# Migrating from Bun (/docs/guides/migrating-from-bun)



`lpm migrate` converts a Bun project to LPM CLI. It writes a version 12 staging lockfile.

By default, an online install then writes version 13 with exact package-instance identities.

This guide covers the Bun-specific parts. For the full flag reference, see [`lpm migrate`](/docs/packages/migrate).

## Prerequisites [#prerequisites]

* LPM CLI installed - see [Installation](/docs/installation).
* A `package.json` plus `bun.lock` or `bun.lockb` in the project root.
* A clean or committed working tree.

`bun.lock` is the direct path. When `bun.lockb` is selected, LPM CLI first looks for a sibling `bun.lock`; if it exists, LPM CLI parses that text lockfile instead. If no sibling text lockfile exists, LPM CLI runs `bun bun.lockb` and parses the emitted JSON. If the `bun` binary is unavailable, migration exits with a clear error.

## 1. Preview [#1-preview]

```bash
lpm migrate --dry-run
```

Dry-run detects Bun, parses the selected lockfile, converts the graph in memory, and writes nothing. It reports package counts, workspace counts, and any conversion problems before touching disk.

If the project contains multiple lockfiles, LPM CLI chooses the most recently modified one. On timestamp ties, the priority order is `bun.lockb`, `bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, then `package-lock.json`.

## 2. Run The Migration [#2-run-the-migration]

```bash
lpm migrate
```

What runs:

1. Confirm `package.json` exists and refuse to overwrite an existing `lpm.lock` unless `--force` is set.
2. Parse Bun package entries, exact versions, tarball URLs, integrity strings, dependency edges, and dev/optional metadata.
3. Write a version 12 staging `lpm.lock`. If the graph fits the binary format, write `lpm.lockb`.
4. Back up the Bun lockfile, `.npmrc` when touched, `.gitattributes`, and any pre-existing LPM CLI lockfiles.
5. Add LPM CLI registry routing to `.npmrc` unless `--no-npmrc` is set.
6. If `--no-install` is not set, run an online `lpm install`. This step writes version 13 and removes the staging binary file.
7. Run `build` + `test` scripts unless `--skip-verify` is set.

The flow is non-interactive. `-y` is accepted but reserved for a future interactive mode; it does not imply `--force`.

## 3. Verify [#3-verify]

If you used `--no-install`, run `lpm install` before the offline command.

```bash
lpm install --offline
lpm test
lpm lint
```

`lpm install --offline` confirms the new lockfile and the warmed store can replay without network access. If your Bun project used scripts that shell out through `bun`, keep Bun installed wherever those scripts run; migration changes the package manager, not your project scripts.

## 4. Commit [#4-commit]

```bash
git add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm bun.lock bun.lockb bun.lock.backup bun.lockb.backup .npmrc.backup
git commit -m "Migrate to LPM CLI"
```

Only remove files that exist in your repo. Do not commit `.backup` files unless you intentionally keep migration rollback artifacts in the repository.

## Bun-Specific Notes [#bun-specific-notes]

* `bun.lock` JSON is parsed directly.
* `bun.lockb` needs either a sibling `bun.lock` or the `bun` binary available during migration.
* Bun lockfile metadata carries dev and optional package flags, so the converted lockfile preserves those package classifications.
* The install step rebuilds `node_modules`. Single-package projects default to LPM CLI's hoisted v2 layout; workspaces and peer-conflict installs use isolated layout unless you override the linker.
* Dependency lifecycle scripts remain denied by default. After migration, run [`lpm rebuild`](/docs/packages/rebuild) and [`lpm approve-scripts`](/docs/packages/approve-scripts) for packages that need install-time builds.

## Rollback [#rollback]

```bash
lpm migrate --rollback
```

Restores backups from the previous migration run and removes LPM CLI files created by that run. Safe to run repeatedly.

## See also [#see-also]

* [`lpm migrate`](/docs/packages/migrate) - full flag reference
* [Migrating from npm](/docs/guides/migrating-from-npm) - package-lock migration
* [Migrating from pnpm](/docs/guides/migrating-from-pnpm) - pnpm-specific translations
* [Migrating from Yarn](/docs/guides/migrating-from-yarn) - Yarn lockfile migration
* [Lockfile](/docs/packages/lockfile) - what gets committed and why


# Migrating from npm (/docs/guides/migrating-from-npm)



`lpm migrate` converts an npm project to LPM CLI. It writes a version 12 staging lockfile, then runs an online install by default.

The install writes version 13 with exact package-instance identities. Every changed file has a backup. Use `--rollback` to restore the original state.

This guide is the migration walkthrough. For the full flag reference, see [`lpm migrate`](/docs/packages/migrate).

## Prerequisites [#prerequisites]

* LPM CLI installed — see [Installation](/docs/installation).
* A working `package-lock.json` in the project root.
* A reasonably-clean working tree (commit before migrating, just to be safe).

## 1. Preview [#1-preview]

```bash
lpm migrate --dry-run
```

Detects npm, parses `package-lock.json`, converts to the LPM CLI format, but **writes nothing**. Reports the package count, any packages that couldn't be converted (with reasons), and the workspace member count.

If anything looks wrong, fix it before doing the real run.

## 2. Run the migration [#2-run-the-migration]

```bash
lpm migrate
```

What runs, in order:

1. **Pre-flight** — confirms `package.json` exists, refuses to clobber an existing `lpm.lock`.
2. **Detect, parse, convert** — reads `package-lock.json`, converts entries to LPM CLI's lockfile shape.
3. **Write** — emits a version 12 staging `lpm.lock`. If the graph fits the binary format, it also emits `lpm.lockb`.
4. **`.npmrc`** — adds LPM CLI-aware config to `.npmrc` (or creates one). Backs up the original to `.npmrc.backup`.
5. **Install** — runs an online `lpm install`. This step writes version 13 and removes the staging `lpm.lockb`.
6. **Verify** — runs `build` + `test` if those scripts exist, to confirm nothing broke.
7. **CI hint** — when a CI platform is detected (`.github/workflows`, `.gitlab-ci.yml`, etc.), prints a hint suggesting you re-run with `lpm migrate --ci` to generate a workflow template.
8. **Summary** — what was done, where backups live, and how to roll back.

The migrate flow is non-interactive — every step runs straight through. If `lpm.lock` already exists, pass `--force` to overwrite it.

## 3. Verify [#3-verify]

If you used `--no-install`, run an online install first:

```bash
lpm install
```

Then run your usual checks:

```bash
lpm install --offline       # confirm reproducibility from the new lockfile
lpm test
lpm lint
lpm fmt --check
```

If anything's broken, see [Rollback](#rollback) below.

## 4. Commit [#4-commit]

```bash
git add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm package-lock.json package-lock.json.backup .npmrc.backup     # if you don't want them committed
git commit -m "Migrate to LPM CLI"
```

You probably don't want the `.backup` files in the repo. Add them to `.gitignore`:

```text
*.backup
```

## Rollback [#rollback]

If anything goes wrong (or the migration was a mistake):

```bash
lpm migrate --rollback
```

Walks the backups created by the previous run and restores `package-lock.json`, `.npmrc` (if it was touched), and `.gitattributes` to their pre-migration state. Files the migration newly created — `lpm.lock`, `lpm.lockb`, any new `.gitattributes`, and any patch files copied to `patches/` — are removed. `package.json` is only rolled back if the migration mutated it (i.e., when `pnpm.*` blocks were translated; usually not on the npm path). Safe to run repeatedly.

## Optional flags [#optional-flags]

| Flag            | Effect                                                                                             |
| --------------- | -------------------------------------------------------------------------------------------------- |
| `--dry-run`     | Parse + convert only, write nothing                                                                |
| `--force`       | Overwrite an existing `lpm.lock`                                                                   |
| `-y`, `--yes`   | Reserved. The flow is non-interactive today, so this flag is a no-op. It does NOT imply `--force`. |
| `--no-install`  | Convert lockfile only, skip the install step                                                       |
| `--skip-verify` | Skip the `build` + `test` verification                                                             |
| `--no-npmrc`    | Don't touch `.npmrc`                                                                               |
| `--ci`          | Also generate a CI workflow template for the detected platform                                     |
| `--no-ci`       | Suppress the CI template hint                                                                      |
| `--rollback`    | Restore from `.backup` files                                                                       |

## What stays the same [#what-stays-the-same]

* `package.json` — `dependencies`, `devDependencies`, `scripts`, `workspaces` are read as-is. No renames, no deletions.
* `node_modules/` — gets rebuilt by the install step. The on-disk shape may change from npm's flat tree to LPM CLI's v2 hoisted layout, or to LPM CLI's isolated layout for workspaces and peer-conflict installs; declared imports continue to work because LPM CLI's resolver respects the same semver semantics npm does.
* `.npmrc` — any existing `@scope:registry=` lines for private registries survive the migration. The migration additively appends `@lpm.dev:registry=https://lpm.dev/api/registry/` (unless you pass `--no-npmrc`); if your `.npmrc` already declared that scope, the step is a no-op. LPM CLI honors `.npmrc` for routing — see [Registries](/docs/registries).

## What changes [#what-changes]

* `package-lock.json` → `lpm.lock`. The default install writes version 13 and removes the temporary binary companion.
* `node_modules/` layout starts **hoisted** for single packages in LPM CLI's v2 virtual-store layout. **Workspaces auto-flip to isolated** (pnpm-style symlinks) for phantom-dep catching; force hoisted with `lpm install --linker=hoisted` or `package.json > lpm > linker = "hoisted"`.
* New `.gitattributes` line: `lpm.lockb binary` (so git doesn't try to text-merge the binary lockfile).

## Common pitfalls [#common-pitfalls]

* **The lockfile is now strict.** An offline or frozen install rejects a version 12 staging lockfile. Run `lpm install` once after `lpm migrate --no-install`.
* **Phantom-dep code breaks under isolated layout.** If you imported a package you didn't declare in `dependencies`, npm hoisted it into `node_modules/` and your code worked anyway. LPM CLI's isolated layout doesn't hoist — undeclared imports become hard errors. Add the missing entries to `dependencies`.
* **Dependency lifecycle scripts don't run on `lpm install` by default.** Bare installs run the root project's lifecycle, including `prepare`, but dependency `postinstall` scripts (esbuild, sharp, etc.) stay blocked until you run `lpm rebuild` and approve the script-running packages with [`lpm approve-scripts`](/docs/packages/approve-scripts).

## See also [#see-also]

* [Migrating from pnpm](/docs/guides/migrating-from-pnpm) — same shape, different source lockfile
* [`lpm migrate`](/docs/packages/migrate) — full flag reference
* [Registries](/docs/registries) — how `.npmrc` routing works after migration
* [Lockfile](/docs/packages/lockfile) — what gets committed and why
* [`lpm install --linker`](/docs/packages/install) — keeping the flat layout if you need to


# Migrating from pnpm (/docs/guides/migrating-from-pnpm)



`lpm migrate` converts a pnpm project to LPM CLI. It writes a version 12 staging lockfile, then runs an online install by default.

The install writes version 13 with exact package-instance identities. The command creates backups before it changes files.

The migration shape is the same as [migrating from npm](/docs/guides/migrating-from-npm). This page covers the pnpm-specific differences.

## What carries over cleanly [#what-carries-over-cleanly]

* **Isolated `node_modules` layout for workspaces and peer conflicts.** Workspace projects auto-flip to LPM CLI's isolated layout (symlinks into a virtual store) — same shape pnpm uses, so your imports keep working unchanged and you get the same phantom-dep protection. Single-package projects start hoisted, but default installs auto-switch to isolated when resolution detects incompatible peer requirements. Force isolated with `lpm install --linker=isolated` or `package.json > lpm > linker = "isolated"` if you want pnpm's shape there for every install.
* **`.pnpmfile.cjs` is unaffected.** LPM CLI doesn't use it, but doesn't delete it either. If you have one, decide whether you still need it.
* **Workspaces and catalogs.** LPM CLI reads `package.json > workspaces` first; if it's absent, it falls back to `pnpm-workspace.yaml` for member globs. It also reads pnpm-style `pnpm-workspace.yaml > catalog` and `catalogs` for default and named catalog entries, so `catalog:` dependencies keep resolving without moving catalog data into `package.json`. If `cleanupUnusedCatalogs: true` is set in `pnpm-workspace.yaml`, LPM CLI prunes unused catalog entries after successful installs.
* **`.npmrc`-declared private registries** carry over verbatim. LPM CLI honors `.npmrc` for routing.

## Run the migration [#run-the-migration]

```bash
lpm migrate --dry-run   # preview
lpm migrate             # do it
```

Same flow as the npm path:

1. Detect pnpm, parse `pnpm-lock.yaml`, convert.
2. Write a version 12 staging `lpm.lock`. If the graph fits the binary format, write `lpm.lockb`.
3. Touch `.npmrc` (with `--no-npmrc` to skip), backing up the original.
4. Run an online `lpm install`. This step writes version 13 and removes the staging `lpm.lockb`.
5. Run `build` + `test` to verify (skip with `--skip-verify`).
6. Print summary + CI hint.

## Optional flags [#optional-flags]

| Flag            | Effect                                                                                             |
| --------------- | -------------------------------------------------------------------------------------------------- |
| `--dry-run`     | Parse + convert only, write nothing                                                                |
| `--force`       | Overwrite an existing `lpm.lock`                                                                   |
| `-y`, `--yes`   | Reserved. The flow is non-interactive today, so this flag is a no-op. It does NOT imply `--force`. |
| `--no-install`  | Convert lockfile only, skip install                                                                |
| `--skip-verify` | Skip `build` + `test` verification                                                                 |
| `--no-npmrc`    | Don't touch `.npmrc`                                                                               |
| `--ci`          | Generate a CI workflow template                                                                    |
| `--no-ci`       | Suppress CI hint                                                                                   |
| `--rollback`    | Restore from `.backup` files                                                                       |

## Verify and commit [#verify-and-commit]

If you used `--no-install`, run `lpm install` before the offline command.

```bash
lpm install --offline       # confirm reproducibility
lpm test
lpm lint
lpm fmt --check
```

```bash
git add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm pnpm-lock.yaml pnpm-lock.yaml.backup .npmrc.backup
git commit -m "Migrate to LPM CLI"
```

## What `lpm migrate` translates from `package.json` [#what-lpm-migrate-translates-from-packagejson]

* **`pnpm.overrides`** is auto-translated to `package.json > lpm.overrides` with the same selector grammar. `lpm install` does not use the original `pnpm.overrides` block as resolution input. Successful entries land in `lpm.overrides`, and the original block stays in place for parallel pnpm use. The translation validates every entry before it changes a file. Invalid values and merge conflicts stop the migration without a partial update.

* **`pnpm.patchedDependencies`** is auto-translated to `package.json > lpm.patchedDependencies` with `originalIntegrity` resolved from the migrated lockfile. Each patch file is copied to LPM CLI's canonical `patches/<safe_key>.patch` location (or treated as a validated no-op if the source is already at that path), and `lpm.lock > [patches]` records the patch path + SHA-256 so the first LPM CLI install can verify the bytes it replays. The plan validates everything up front: missing source files, paths outside the project root, paths through symlinked ancestors, directory targets, and any package whose lockfile entry is missing or has no integrity hash all abort the migration with a structured error before any disk mutation. Like the overrides block, the original `pnpm.patchedDependencies` is left in place after migration.

* **`pnpm.peerDependencyRules`** is auto-translated to `package.json > lpm.peerDependencyRules` with the same shape verbatim — `ignoreMissing`, `allowedVersions`, and `allowAny` all carry over. List entries (`ignoreMissing` / `allowAny`) are unioned with any pre-existing LPM CLI-side entries; map entries (`allowedVersions`) merge selector-by-selector. Same selector with the same range is a no-op idempotent merge; same selector with a different range is a hard conflict that aborts the migration before any disk mutation. Range strings that don't parse as valid semver, and selector keys that don't match LPM CLI's grammar, are surfaced up-front as parse errors with the offending entry named. `allowedVersions` selectors mirror `lpm.overrides`: bare peer names (`"react"`), scoped peers (`"@scope/foo"`), and parent-scoped forms (`"foo>react"`, `"foo@^2>react"`, `"@scope/foo@^2>react"`) all carry over verbatim; multi-segment paths and bare-name-with-version forms (`"foo@2"` without `>`) are rejected on both surfaces. Pattern semantics (`@scope/*`, `*-suffix`, etc.) work for `ignoreMissing` and `allowAny`. The same parser runs at install time, so hand-edits to `lpm.peerDependencyRules` after migration fail loud (`LpmError::Script`) instead of silently no-op'ing.

After migration, `lpm install` is silent when the LPM CLI fields cover every pnpm entry. Later drift causes a human-output warning that suggests `lpm migrate`.

The `--json` flag suppresses that stderr warning. Use `lpm doctor --json` to detect drift through `pnpm_overrides_drift`, `pnpm_patches_drift`, and `pnpm_peer_rules_drift`.

## pnpm-specific gotchas [#pnpm-specific-gotchas]

* **`pnpm.overrides` is migration input, not install input.** Run `lpm migrate` after each pnpm-side change, or update `lpm.overrides` directly.
* **`pnpm-workspace.yaml`** is read as a fallback when `package.json > workspaces` is absent, and its `catalog` / `catalogs` blocks are read as root catalogs. If the same catalog package entry exists in both `package.json > catalogs` and `pnpm-workspace.yaml`, LPM CLI uses the `package.json` value. `cleanupUnusedCatalogs: true` is honored unless `package.json > lpm.cleanupUnusedCatalogs` explicitly says otherwise.
* **`shamefully-hoist`** in `.npmrc` is pnpm-specific. LPM CLI's closest equivalent is `lpm install --linker=hoisted` or `package.json > lpm > linker = "hoisted"`; hoisted mode still uses LPM CLI's v2 virtual-store layout instead of flattening every transitive dependency to the project root.

## Rollback [#rollback]

```bash
lpm migrate --rollback
```

Restores `pnpm-lock.yaml`, `.npmrc` (if touched), `.gitattributes`, and `package.json` (rolled back when the `pnpm.*` translations mutated it) to pre-migration state. Files the migration newly created — `lpm.lock`, `lpm.lockb`, any new `.gitattributes`, and any patch files copied to `patches/` — are removed.

## Lifecycle scripts [#lifecycle-scripts]

Bare `lpm install` runs your root project lifecycle, including `prepare`. Dependency lifecycle scripts (`postinstall`, etc.) remain under `script-policy: deny` by default. After the migration's `lpm install` step, run:

```bash
lpm rebuild
```

For packages that pnpm allowed to run scripts but LPM CLI hasn't trusted yet, [`lpm approve-scripts`](/docs/packages/approve-scripts) walks the blocked set interactively.

If your project depends on `postinstall` for a tight inner loop and you're confident about the trust set:

```bash
lpm install --auto-build
```

Auto-runs `lpm rebuild` for trusted packages right after install. If a trusted lifecycle script fails, install exits non-zero. Or set `package.json > lpm > scripts.autoBuild: true` to make it sticky.

## See also [#see-also]

* [Migrating from npm](/docs/guides/migrating-from-npm) — same shape, different source lockfile
* [`lpm migrate`](/docs/packages/migrate) — full flag reference
* [Registries](/docs/registries) — `.npmrc` routing
* [`lpm install --linker`](/docs/packages/install) — keeping a flat npm-style layout if you need it
* [`lpm approve-scripts`](/docs/packages/approve-scripts) — review blocked lifecycle scripts after first install


# Migrating from Yarn (/docs/guides/migrating-from-yarn)



`lpm migrate` converts a Yarn Classic or Yarn Berry project to LPM CLI. It writes a version 12 staging lockfile.

By default, an online install then writes version 13 with exact package-instance identities.

This guide covers the Yarn-specific parts. For the full flag reference, see [`lpm migrate`](/docs/packages/migrate).

## Prerequisites [#prerequisites]

* LPM CLI installed - see [Installation](/docs/installation).
* A `package.json` and `yarn.lock` in the project root.
* A clean or committed working tree.

Run the dry-run first and treat it as the source of truth for what will convert.

## 1. Preview [#1-preview]

```bash
lpm migrate --dry-run
```

Dry-run detects Yarn, parses `yarn.lock`, converts the graph in memory, and writes nothing. It reports package counts, workspace counts, and any conversion problems before touching disk.

If the project contains multiple lockfiles, LPM CLI chooses the most recently modified one. On timestamp ties, the priority order is `bun.lockb`, `bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, then `package-lock.json`. Remove stale lockfiles before migrating so `yarn.lock` is the unambiguous source.

## 2. Run The Migration [#2-run-the-migration]

```bash
lpm migrate
```

What runs:

1. Confirm `package.json` exists and refuse to overwrite an existing `lpm.lock` unless `--force` is set.
2. Parse Yarn lockfile entries: Classic specifier blocks or Berry descriptor entries, exact versions, and dependency edges.
3. Preserve Classic resolved tarball URLs and SRI integrity strings. Berry cache checksums are not npm SRI strings, so they are not written as `integrity`.
4. Mark direct dev and optional dependencies from root `package.json`, because Yarn does not encode those flags in a way LPM CLI can carry over per lockfile entry.
5. Write a version 12 staging `lpm.lock`. If the graph fits the binary format, write `lpm.lockb`.
6. Back up `yarn.lock`, `.npmrc` when touched, `.gitattributes`, and any pre-existing LPM CLI lockfiles.
7. Add LPM CLI registry routing to `.npmrc` unless `--no-npmrc` is set.
8. If `--no-install` is not set, run an online `lpm install`. This step writes version 13 and removes the staging binary file.
9. Run `build` + `test` scripts unless `--skip-verify` is set.

The flow is non-interactive. `-y` is accepted but reserved for a future interactive mode; it does not imply `--force`.

## 3. Verify [#3-verify]

If you used `--no-install`, run `lpm install` before the offline command.

```bash
lpm install --offline
lpm test
lpm lint
```

`lpm install --offline` confirms the new lockfile and the warmed store can replay without network access. If your project does not have `test` or `lint` scripts, run the equivalent checks you normally trust before a package-manager migration.

## 4. Commit [#4-commit]

```bash
git add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm yarn.lock yarn.lock.backup .npmrc.backup
git commit -m "Migrate to LPM CLI"
```

Do not commit `.backup` files unless you intentionally keep migration rollback artifacts in the repository.

## Yarn-Specific Notes [#yarn-specific-notes]

* `yarn.lock` resolved URLs from `registry.yarnpkg.com` are normalized into registry package entries when possible.
* Berry lockfiles are detected through the `__metadata` block. `npm:` descriptors such as `react@npm:^18` are resolved to the exact versions pinned in `yarn.lock`.
* Berry's `.pnp.cjs` / Plug'n'Play runtime is not carried over. LPM CLI materializes `node_modules/` using its hoisted or isolated linker.
* Berry entries with local or non-registry protocols (`workspace:`, `portal:`, `link:`, `file:`, `patch:`, `exec:`, `git:`, `git+`, `github:`) are skipped and reported in the migration output.
* `dependencies` and `optionalDependencies` blocks inside lockfile entries both contribute dependency edges.
* The install step rebuilds `node_modules`. Single-package projects default to LPM CLI's hoisted v2 layout; workspaces and peer-conflict installs use isolated layout unless you override the linker.
* Dependency lifecycle scripts remain denied by default. After migration, run [`lpm rebuild`](/docs/packages/rebuild) and [`lpm approve-scripts`](/docs/packages/approve-scripts) for packages that need install-time builds.

## Rollback [#rollback]

```bash
lpm migrate --rollback
```

Restores backups from the previous migration run and removes LPM CLI files created by that run. Safe to run repeatedly.

## See also [#see-also]

* [`lpm migrate`](/docs/packages/migrate) - full flag reference
* [Migrating from npm](/docs/guides/migrating-from-npm) - same migration shape
* [Migrating from pnpm](/docs/guides/migrating-from-pnpm) - pnpm-specific translations
* [Migrating from Bun](/docs/guides/migrating-from-bun) - Bun text and binary lockfiles
* [Lockfile](/docs/packages/lockfile) - what gets committed and why


# Monorepo setup (/docs/guides/monorepo-setup)



LPM CLI treats workspaces as a first-class concept — declare them in `package.json`, then every command that operates on the dep graph (`install`, `uninstall`, `run`, `lint`, `fmt`, `check`) accepts `--filter`, `--all`, and `--affected` to scope work. This guide covers a real monorepo from scratch.

## 1. Declare workspaces [#1-declare-workspaces]

```json title="package.json"
{
  "name": "my-repo",
  "private": true,
  "workspaces": ["packages/*", "apps/*"]
}
```

Or the object form (yarn-style):

```json
{ "workspaces": { "packages": ["packages/*", "apps/*"] } }
```

LPM CLI accepts both. Add member packages under those globs — each member has its own `package.json`. Run:

```bash
lpm install
```

LPM CLI walks the workspace, resolves eligible importers together, projects the shared result back into isolated member graphs, and links every member's deps (including cross-member `workspace:*` references) into the right `node_modules/`.

The result is one authoritative `lpm.lock` at the workspace root. Commit that file; do not create or keep one lockfile per member. Each importer in the root lockfile points at its own content-addressed package closure, so member-local commands still see only that member's graph. Workspace lockfiles are TOML-only and do not have an `lpm.lockb` companion.

No flag is required at the workspace root: bare `lpm install` installs every member in dependency order and the root last. From inside a member it remains member-local; use `lpm install --recursive` to widen to the owning workspace, or `lpm install --no-recursive` at the root when you intentionally want only the root project.

The first successful recursive install after upgrading migrates legacy member lockfiles into the root union and removes the obsolete member `lpm.lock` / `lpm.lockb` files. The root lockfile is committed only after every selected target succeeds, so a failed migration leaves the previous lockfiles intact.

The workspace root can also be a named local package. A member may depend on that root package with `workspace:*`; LPM CLI resolves and links it locally just like a member-to-member edge.

## 2. Add a dep to one member [#2-add-a-dep-to-one-member]

```bash
lpm install react --filter web
```

Adds `react` to `packages/web/package.json` and runs the install pipeline scoped to `packages/web/`. The other members are untouched.

The command updates `packages/web`'s importer projection in the root lockfile. Manifest and lockfile replacement are atomic as a workspace mutation: if the install fails, LPM CLI restores the prior manifest and lockfile.

For a tooling dep that lives at the workspace root:

```bash
lpm install -D typescript -w
```

`-w` / `--workspace-root` writes to the root `package.json > devDependencies` instead of any member.

## 3. The filter grammar [#3-the-filter-grammar]

`--filter` is the most-used flag in monorepo workflows. `--filter-prod` accepts the same grammar but skips `devDependencies` during closure expansion. Both are available across `install`, `uninstall`, `run`, `lint`, `fmt`, `check`, `deploy`, and the standalone [`lpm filter`](/docs/packages/workspaces) preview command.

| Filter                 | Matches                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `web`                  | The member literally named `web`                                                                 |
| `@scope/*`             | Every member with a name under a scope                                                           |
| `@scope/*{./apps/web}` | Members matching both the name atom and exact path                                               |
| `./apps/*`             | Every member under a path glob                                                                   |
| `{./apps/web}`         | Exact path match (path with literal characters)                                                  |
| `[origin/main]`        | Members changed since a git ref                                                                  |
| `web...`               | `web` and **every member `web` depends on** (transitive deps — downstream closure)               |
| `web^...`              | Same, but excluding `web` itself (deps-only)                                                     |
| `...web`               | `web` and &#x2A;*every member that depends on `web`** (transitive dependents — upstream closure) |
| `...^web`              | Same, but excluding `web` itself (dependents-only)                                               |
| `!web`                 | Exclusion — remove `web` from the resulting set                                                  |

Filters compose: `--filter web --filter api` unions the two sets. `--filter web --filter '!web-tests'` selects `web` minus `web-tests`.

Use `--filter-prod ...shared` for production-only dependent closures: members that reach `shared` only through `devDependencies` are not selected.

> **Substring matching is not supported.** `--filter core` does NOT match `@babel/core`. Write `--filter '*/core'` for that.

Preview what a filter expression would select without running anything:

```bash
lpm filter web...
lpm filter --filter-prod ...shared
lpm filter '[main]' --changed-files-ignore-pattern '**/README.md'
lpm filter '...[main]' --test-pattern '**/*.test.js'
lpm filter web... --explain      # full per-package trace (which filter matched, how)
```

## 4. `--affected` for CI [#4---affected-for-ci]

```bash
lpm run test --affected
```

Selects only members affected by changes vs the base branch (default `main`). Pair with `--base develop` to change the branch:

```bash
lpm run test --affected --base develop
lpm run test --affected --changed-files-ignore-pattern '**/README.md'
lpm run test --affected --test-pattern '**/*.test.js'
```

Saves CI minutes — a one-line README change in `apps/marketing` doesn't trigger a full-monorepo test run.

Persist noisy git-diff paths and test-only globs in `lpm.toml` when every CI run should use them:

```toml
[workspace]
changed-files-ignore-pattern = ["**/README.md", "docs/**"]
test-pattern = ["**/*.test.js", "**/*.spec.ts"]
```

`--test-pattern` keeps the directly changed package selected, but it stops test-only changes from expanding to every dependent package.

`--fail-if-no-match` makes a typo'd filter exit non-zero. Recommended in CI:

```bash
lpm run test --filter web --fail-if-no-match
```

## 5. Cross-member deps [#5-cross-member-deps]

```json title="packages/web/package.json"
{
  "dependencies": {
    "@my-co/shared": "workspace:*"
  }
}
```

`workspace:*` tells the resolver "use the local workspace member, not a published version." LPM CLI links the two via symlink (in isolated layout) and respects topological order in tasks (`dependsOn: ["^build"]` waits for upstream member's `build` task to finish first).

## 6. Deploy builds with `lpm deploy` [#6-deploy-builds-with-lpm-deploy]

For Docker / serverless deploys, you don't want to ship the entire monorepo. [`lpm deploy`](/docs/packages/workspaces) materializes one member's deploy closure into a self-contained directory:

```bash
lpm deploy /prod/api --filter api
lpm deploy /prod/api --filter api --no-optional
lpm deploy /prod/api --filter api --dev
```

The output dir has:

* The targeted member's publishable source files (honors `files`, `.npmignore`, then `.gitignore`, while still excluding `.env*`, `node_modules`, `.git`, etc.)
* Local workspace dependencies copied under `.lpm/deploy-workspace/` and referenced with relative `file:` specs
* A populated `node_modules/`
* A deploy-local `.lpm/store/` and `lpm.lock` for the deploy tree

In a Dockerfile:

```dockerfile
FROM workspace as pruned
RUN lpm deploy /prod/api --filter api

FROM node:22-alpine
COPY --from=pruned /prod/api /app
WORKDIR /app
CMD ["node", "server.js"]
```

Constraints:

* `--filter` or `--filter-prod` is required and must match exactly one member.
* The output directory must be outside the workspace tree.
* `--prod` is the default dependency mode. Use `--dev` for a dev-dependency deploy tree, and `--no-optional` to omit optional dependencies.

## Common pitfalls [#common-pitfalls]

* **`--filter core` doesn't match `@babel/core`.** Substring matching is not supported. Use `--filter '*/core'`.
* **Multi-member install prompts you for confirmation by default.** If a `--filter` expression would mutate more than one member's `package.json`, `lpm install` asks before writing. Use `-y` in scripts.
* **Keep only the root `lpm.lock`.** A successful recursive install removes migrated member lockfiles. `lpm.lockb` is not written for a workspace union.
* **`lpm deploy` targets one member.** If your filter matches more than one workspace package, narrow it before deploying.

## See also [#see-also]

* [Workspaces](/docs/packages/workspaces) — full filter grammar reference
* [`lpm filter`](/docs/packages/workspaces) — preview what a filter selects
* [`lpm install --filter`](/docs/packages/install#workspaces) — install scoped to members
* [`lpm run --filter --affected`](/docs/dev/run#workspaces) — task running with workspace selection
* [`lpm deploy`](/docs/packages/workspaces) — production-closure materialization


# Publishing your first package (/docs/guides/publishing-a-package)



This guide walks through publishing a package from scratch. By the end you'll have a package on the LPM.dev Registry that anyone you grant access to can `lpm install`. We'll cover quality gates, secret scanning, and provenance — the things you can't see from the command name alone.

## 1. Authenticate [#1-authenticate]

```bash
lpm login
```

Opens your browser, you authenticate against the LPM.dev Registry, LPM CLI captures the redirect token and stores it in your OS keychain. You only do this once per machine.

```bash
lpm whoami
```

Confirms the identity, plan tier, and any orgs you belong to.

## 2. Create the package [#2-create-the-package]

```bash
mkdir my-pkg && cd my-pkg
lpm init --lpm
```

`lpm init --lpm` walks you through `owner` / `name` / `version` / `description`. The published name will be `@lpm.dev/<owner>.<name>` — pick an `owner` you've claimed on the LPM.dev Registry (or one your org owns). Plain `lpm init` asks for the target first; choose **LPM.dev Registry package** for this guide.

For a non-interactive bootstrap (CI scripts):

```bash
lpm init --lpm -y --owner <owner> --name <name>
```

Plain `lpm init -y` is still valid and defaults to an LPM.dev Registry package, but the explicit flags avoid leaving placeholder owner/name values in automation.

## 3. Write your code [#3-write-your-code]

Build out the package — `src/` (or wherever), `dist/` (your build output), README, license. The defaults `lpm init` writes to `package.json` (substituting the owner/name you supplied):

```json
{
  "name": "@lpm.dev/<owner>.<name>",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "module",
  "license": "MIT",
  "files": ["dist"],
  "packageManager": "lpm@<version>"
}
```

It also writes an `AGENTS.md` package-manager hint so coding agents in the repo use `lpm install`, `lpm add`, and `lpm run`.

Tighten `files` to whatever you actually want shipped. Anything not listed (README, LICENSE, package.json) is included by default per npm convention. If `files` is absent, LPM CLI excludes project-local `.lpm` state from the implicit package set while preserving valid direct publisher-authored `.lpm/skills/*.md` files. Installed dependency skill sets are excluded whether the file set is implicit or explicit.

## 4. Preview the publish [#4-preview-the-publish]

```bash
lpm publish --dry-run
```

Validates the package configuration and authored skills, packs each target artifact, scans the actual final artifacts for hardcoded secrets, computes the normalized local LPM.dev quality score, resolves real LPM.dev authentication, and performs a non-mutating Registry permission/version preflight. Explicitly included files are scanned even when Git ignores them. It does not reserve or upload a version, create Registry state, generate requested provenance, or claim publication occurred.

```bash
lpm publish --check
```

Runs the network-free preparation path and stops before LPM.dev OIDC, skills staleness, and publish-permission lookups. Both preview modes update only the effective manifest used in memory when authored skills require `.lpm/skills`; they do not modify `package.json`. A real publish may persist the entry.

## 5. Set a quality floor (optional) [#5-set-a-quality-floor-optional]

```bash
lpm publish --min-score 80
```

Aborts an LPM.dev Registry publish if its normalized local score is below 80. The score is an aggregate, so use the failing rows from `lpm publish --check` to decide which improvements matter rather than treating the threshold as a substitute for specific project checks.

The local score is computed from readme presence/length, license declaration, types coverage, test signals, and maintenance heuristics, normalized over locally applicable weights. A package passing every local check reaches 100. Server-only checks are reported as `not_evaluated`, never fabricated as passes or counted as local failures. The Registry computes its separate final server score after upload. See [`lpm quality`](/docs/packages/quality) for the completed report of an already-published package.

If `package.json` declares `bundledDependencies` or `bundleDependencies`, install first so every requested direct and transitive package is available in the verified LPM layout. Bundled content counts toward tarball limits, secret scanning, and provenance. Missing packages or unsafe symlinks stop the publish.

## 6. Publish [#6-publish]

```bash
lpm publish
```

Pack → scan every final target artifact → quality report → confirmation prompt → upload. Skip the prompt with `-y` (good in CI).

When CI must wait for public availability, use `--wait` for a Pool or Marketplace version:

```bash
lpm publish --wait
```

The command keeps upload success separate from the publication wait. If the wait fails, do not publish the same version again.

The package starts in **private** distribution mode — only you (and anyone you grant access to) can install it. To make it discoverable or paid, flip the distribution mode in the LPM.dev Registry dashboard:

| Mode                  | What it means                                                                                    |
| --------------------- | ------------------------------------------------------------------------------------------------ |
| **Private** (default) | Only the publisher and granted users can install. Metadata is not public.                        |
| **Pool**              | Metadata is public. Installs gated to pool subscribers. Publisher earns a share of pool revenue. |
| **Marketplace**       | Installs require a license purchase.                                                             |

Mode changes are **irreversible** — going public is a one-way door. See [Distribution mode](/docs/reference/glossary#distribution-mode).

## 7. (Optional) Provenance [#7-optional-provenance]

In CI with an audience-`sigstore` OIDC token — GitHub Actions or GitLab CI:

```bash
lpm publish --provenance
lpm publish --npm --provenance
```

Generates a Sigstore-signed attestation that proves which workflow built the tarball and which commit it came from. With `--npm`, LPM CLI attaches npm's expected `{name}-{version}.sigstore` bundle in the publish payload. Consumers can verify provenance via the audit pipeline or the registry UI.

`--provenance` fails loud (non-zero exit) if no usable Sigstore-audience token is found or if Sigstore signing fails — it never silently publishes without provenance. Useful as a hard gate in security-critical packages.

On GitHub Actions, enable `permissions: id-token: write` on the job. On GitLab CI, mint `SIGSTORE_ID_TOKEN` via the `id_tokens` block with `aud: sigstore`. Other platforms aren't supported.

For npm-compatible targets, provenance requires public access. LPM CLI's npm default is public for scoped and unscoped packages. If repo config enables provenance but a one-off publish should skip it, pass `--no-provenance`.

## 8. Publishing elsewhere [#8-publishing-elsewhere]

`lpm publish` defaults to the LPM.dev Registry. To target other registries:

```bash
lpm publish --npm                                 # registry.npmjs.org
lpm publish --github                              # GitHub Packages
lpm publish --gitlab                              # GitLab Packages
lpm publish --publish-registry https://r.example.com   # custom npm-compatible
```

You must have auth for the selected target. `lpm login --npm` opens the browser login for npm.

GitHub and GitLab login can use environment tokens or existing `gh` and `glab` sessions. These sources are not copied into LPM storage.

Custom registries use `lpm login --login-registry <URL> --token <T>`. For multiple targets, set `lpm.json > publish.registries`. See [`lpm.json`](/docs/reference/lpm-json#publish).

## Common pitfalls [#common-pitfalls]

* **`init -y` defaults aren't directory-derived.** Under `-y`, the target is the LPM.dev Registry, owner is your `lpm whoami` profile username (or the literal `"username"` if offline / not logged in), and &#x2A;*name is literally `"package"`** (not your directory's name). The published shape becomes `@lpm.dev/<your-username>.package` — pass `--owner` / `--name` or edit before publishing.
* **Quality score below 80 by default = no readme, no license, no types.** Easy fix; usually a 5-minute task.
* **Distribution mode is irreversible.** Pool / marketplace are one-way doors. Stay private until you're sure.
* **Generated provenance only works in OIDC-capable CI.** Don't pass `--provenance` from a developer laptop. For npm-compatible targets with a pre-generated bundle, use `--provenance-file <PATH>`.

## See also [#see-also]

* [`lpm publish`](/docs/packages/publish) — full flag reference
* [`lpm quality`](/docs/packages/quality) — what the score covers
* [`lpm init`](/docs/packages/init) — manifest scaffolding
* [Authentication](/docs/infra/authentication) — token management


# Using LPM CLI with Swift (/docs/guides/using-with-swift)



LPM.dev Registry speaks the [SE-0292 Swift Package Registry API](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md), so Swift Package Manager (SPM) can resolve hosted Swift packages natively. LPM CLI performs the required Registry setup as part of installation.

## Prerequisites [#prerequisites]

* LPM CLI installed — see [Installation](/docs/installation).
* Swift toolchain installed.
* An lpm.dev account, logged in (`lpm login`).

## Installing a Swift package [#installing-a-swift-package]

```bash
lpm install @lpm.dev/owner.swift-pkg
lpm install @lpm.dev/owner.swift-pkg@1.2.0      # specific version
lpm install --yes @lpm.dev/owner.swift-pkg      # choose the first eligible target
```

On the first Swift install in an unconfigured project, LPM CLI:

1. Sets the `lpmdev` scope to the LPM.dev Registry SE-0292 endpoint.
2. Passes the current LPM.dev Registry session to SPM.
3. Installs the Registry CMS signing certificate.
4. Configures the `lpmdev` signing-trust policy.
5. Updates `Package.swift` and runs `swift package resolve`.

Setup failures abort the install instead of leaving a partially configured trust path. One eligible target is selected automatically. With multiple eligible targets, a normal install asks which target should receive the dependency; `-y` / `--yes` skips that selector and chooses its default, the first eligible target.

During later HTTPS installs, LPM CLI signs in to SPM again even when the Registry scope matches. A rejected stored bearer causes one session refresh and one login retry.

The same routing applies in workspaces. Use `-w` for a root `Package.swift`, run from a member directory for that member, or use `--filter` to select Swift members. LPM CLI mutates, configures, reports, and runs `swift package resolve` from the same selected directory. In mixed JavaScript/Swift workspaces, Swift versions go only through SE-0292 while JavaScript versions retain normal `package.json` installation.

Use [`lpm install`](/docs/packages/install) for Swift dependencies. `lpm add` is the legacy source-delivery path and is not the normal SPM dependency workflow.

You do not need to run `lpm swift-registry` before installing. That command remains available for explicit repair and certificate refresh:

```bash
lpm swift-registry --force
```

See the [trust model](/docs/packages/swift-package-registry#trust-model) for what package signing currently attests to and why the `lpmdev` scope uses a dedicated trust policy.

## Publishing a Swift package [#publishing-a-swift-package]

LPM CLI detects a Swift package from `Package.swift` at the repository root.

```bash
lpm publish
```

The publish pipeline:

* Packs the repo into a tarball.
* Generates a `.zip` source archive with the SE-0292-required `{pkgName}-{version}/` top-level wrapper.
* Computes the SHA-256 checksum that SPM expects.
* Generates a CMS-1.0.0 detached signature (ECDSA P-256 / SHA-256) with the LPM.dev Registry signing cert.
* Uploads tarball + zip + signature to lpm.dev.

After publish, `lpm install` configures SPM when needed and resolves the package by its LPM.dev Registry identity.

## Identity mapping [#identity-mapping]

LPM.dev Registry packages are named `@lpm.dev/owner.pkg-name`. SE-0292 scopes can't contain dots, so LPM CLI maps them when surfacing to SPM:

| LPM.dev Registry name     | SPM identity            |
| ------------------------- | ----------------------- |
| `@lpm.dev/owner.pkg-name` | `lpmdev.owner_pkg-name` |

The `_` between owner and package name is the unambiguous separator: LPM.dev Registry forbids `_` in both owner and package name, so the boundary remains clear when either half contains hyphens. You do not need to deal with the SPM identity by hand; `lpm install` performs the translation.

## Verifying signatures [#verifying-signatures]

LPM.dev Registry serves the CMS signing certificate at `https://lpm.dev/api/swift-registry/certificate` in DER form. On every automatic setup check, LPM CLI validates the bounded DER response, compares it with the local certificate, and verifies the complete default and `lpmdev` signing policy—even when the scope URL already matches. Stale, malformed, missing, or mismatched state is atomically repaired before SPM resolves the dependency.

Signatures are surfaced in three places per release:

* The release metadata JSON (base64 CMS).
* The `Digest` HTTP response header on tarball download.
* A comment block in the `Package.swift` manifest response.

If the cert rotates, re-run `lpm swift-registry --force`. Failed `--force` re-downloads are fatal — the CLI will not silently fall back to the stale cert.

For the trust-model rationale (why the cert is self-signed, why SPM uses a `silentAllow` scope override rather than a trust-store entry, what the signature actually attests to), see [Trust model](/docs/packages/swift-package-registry#trust-model).

## Common pitfalls [#common-pitfalls]

* **`lpm add` for Swift is the legacy path.** Use `lpm install` — `add` is source-delivery, not what you want for a runtime SPM dep.
* **`Package.swift` must use the SE-0292 identity** (`lpmdev.owner_pkg-name`), not the LPM.dev Registry-native form. `lpm install` writes it correctly; if you hand-edit, mind the format — the `_` separates owner from package name.
* **Refresh after a documented certificate rotation.** Run `lpm swift-registry --force`. A failed forced refresh is fatal; LPM CLI does not silently keep the stale certificate.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — runtime dependency installer (works for Swift)
* [`lpm swift-registry`](/docs/packages/swift-registry) — repair and certificate-refresh command
* [SE-0292](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md) — the upstream protocol spec


# Zero-config dev server (/docs/guides/zero-config-dev-server)



`lpm dev` is the marquee command of the Dev section. With nothing configured, it runs your `dev` script. With a few lines of `lpm.json`, it picks up the right Node version, syncs deps, loads `.env`, serves over HTTPS, exposes a public tunnel, and orchestrates multiple services with readiness checks. This guide covers the path from "works in my repo" to "works the same for the whole team."

## Step 0: Just run it [#step-0-just-run-it]

```bash
lpm dev
```

If your `package.json` has a `dev` script, `lpm dev` runs it. Stop here if that's all you need.

The rest of this guide is about turning that into something repeatable: a checked-in config that gives every contributor the same setup with no per-machine fiddling.

## Step 1: Pin the Node version [#step-1-pin-the-node-version]

```json title="lpm.json"
{
  "runtime": { "node": ">=22.0.0" }
}
```

`lpm dev` will use the pinned version, auto-installing it via [`lpm use`](/docs/dev/use) if missing. Node's fall-back chain is `lpm.json > runtime.node` → `.nvmrc` → `.node-version` → inherited `PATH`; Bun is read from `lpm.json > runtime.bun`. `package.json > engines.node` validates compatibility but never selects or installs Node. See [Managed runtimes](/docs/dev/node-version-pinning) for the full detection contract.

```bash
lpm use node@22                # install + pin in one step (writes lpm.json)
```

## Step 2: Map env files to scripts [#step-2-map-env-files-to-scripts]

```json title="lpm.json"
{
  "env": {
    "dev":     ".env.development",
    "staging": ".env.staging",
    "prod":    ".env.production"
  }
}
```

`lpm run dev` now loads `.env.development` automatically. `lpm run staging` loads `.env.staging`. CLI override: `lpm dev --env=preview` loads `.env.preview` regardless.

For env-var validation (CI catches missing required vars before they explode at runtime):

```json title="lpm.json"
{
  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url" },
      "API_KEY":      { "required": true, "secret": true }
    }
  }
}
```

The check runs before every `lpm run` / `lpm dev` / `lpm <file>` / `lpm exec`. Skip with `--no-env-check`.

## Step 3: Turn on HTTPS [#step-3-turn-on-https]

```json title="lpm.json"
{ "https": true }
```

Plus a one-time CA install:

```bash
lpm cert trust
```

Now `lpm dev` serves over `https://localhost`. Browsers trust the cert. See [Local HTTPS](/docs/infra/local-https) for the file layout.

## Step 4: Add a tunnel [#step-4-add-a-tunnel]

```json title="lpm.json"
{ "tunnel": { "domain": "acme-api.lpm.llc" } }
```

Plus the claim (Pro/Org only):

```bash
lpm tunnel claim acme-api.lpm.llc
```

Now `lpm dev` exposes the dev server at `https://acme-api.lpm.llc`. Webhooks to the public URL are captured to disk for replay:

```bash
lpm tunnel inspect            # browse captured events
lpm tunnel replay 3           # re-deliver event #3 to localhost
lpm tunnel inspect --ui       # browser-based inspector
```

Free users get an ephemeral random domain on every run. To keep one stable URL, claim it.

## Step 5: Multi-service orchestration [#step-5-multi-service-orchestration]

```json title="lpm.json"
{
  "services": {
    "db": {
      "command": "docker compose up postgres",
      "readyPort": 5432,
      "readyTimeout": 60
    },
    "api": {
      "command": "node server.js",
      "port": 4000,
      "dependsOn": ["db"],
      "env": { "DATABASE_URL": "postgres://localhost:5432/myapp" }
    },
    "web": {
      "command": "next dev",
      "port": 3000,
      "primary": true
    }
  }
}
```

`lpm dev` starts each service, assigns and verifies owned listener ports, runs any additional `readyPort` / `readyUrl` checks, and prefixes each service's logs:

```text
[db]  ✔ ready (0.8s)
[api] ✔ ready (3.4s)
[web] ✔ ready (1.2s)
Local http://localhost:<resolved-port>/
```

The primary service's verified endpoint receives LPM CLI-owned HTTPS, tunnel, and LAN frontends plus the browser-open. Mark exactly one service as `primary: true` in a multi-service config.

For better-than-prefixed-logs viewing:

```bash
lpm dev --dashboard
```

TUI dashboard with per-service log panels and webhook inspection.

## Step 6: Task graph and caching [#step-6-task-graph-and-caching]

For things you build, not run-and-watch — define them in `tasks` for caching and dependency ordering:

```json title="lpm.json"
{
  "tasks": {
    "build": {
      "command": "tsup",
      "dependsOn": ["^build"],
      "cache": true,
      "outputs": ["dist/**"],
      "inputs": ["src/**", "package.json"]
    }
  }
}
```

`^build` means "build me only after every upstream workspace dep has built." `cache: true` caches the result keyed by inputs; a re-run with unchanged inputs replays instantly. See [`lpm run`](/docs/dev/run) and [Task runner](/docs/dev/task-runner).

## Common pitfalls [#common-pitfalls]

* **`lpm cert trust` only needs to run once per machine.** Teammates each run it on their laptop; CI doesn't need it.
* **Tunnel domain `claim` is per-organization or per-user.** Claim it once, commit `lpm.json`, every teammate uses the same URL.
* **Mark exactly one service `primary: true`.** Otherwise `lpm dev`'s flag-routing and browser-open don't have a clear target.
* **`readyPort` defaults to `port` if absent.** If your service listens on a different port for readiness checks (uncommon), set `readyPort` explicitly.

## See also [#see-also]

* [`lpm dev`](/docs/dev/dev) — full command reference
* [`lpm.json` reference](/docs/reference/lpm-json) — every field, every type
* [`lpm cert`](/docs/infra/cert) — local HTTPS detail
* [`lpm tunnel`](/docs/infra/tunnel) — claim, inspect, replay
* [Task runner](/docs/dev/task-runner) — caching internals


# Authentication (/docs/infra/authentication)



Authentication in LPM CLI is registry-specific. LPM CLI first selects the registry that receives a request. Then it selects a credential for that registry.

Credentials for the LPM.dev Registry, npm, GitHub Packages, GitLab Packages, and custom registries remain separate. LPM CLI does not send a token to an unrelated registry.

## How authentication works [#how-authentication-works]

Each authenticated request uses the same model:

1. A package name, publish target, or registry option selects the destination registry.
2. LPM CLI selects the first available credential for that registry.
3. LPM CLI sends the credential only to the selected registry.

This order matters because an environment variable can override a stored credential. A project file can also select a different registry.

## Credential priority [#credential-priority]

LPM CLI uses the first available source in each row.

| Registry or target              | Credential priority                                                            |
| ------------------------------- | ------------------------------------------------------------------------------ |
| LPM.dev Registry                | `--token` → `LPM_TOKEN` → stored access token                                  |
| npm (`registry.npmjs.org`)      | `NPM_TOKEN` → token from `lpm login --npm` → project `.npmrc` → home `.npmrc`  |
| GitHub Packages                 | `GITHUB_TOKEN` → `gh auth token --hostname github.com` → stored fallback token |
| GitLab Packages on `gitlab.com` | `GITLAB_TOKEN` → `CI_JOB_TOKEN` → `glab auth token` → stored fallback token    |
| Self-managed GitLab             | `GITLAB_TOKEN` → `CI_JOB_TOKEN` → stored fallback token                        |
| Custom publish registry         | Token stored for the selected registry URL                                     |

The `gh` and `glab` commands provide their tokens at request time. LPM CLI does not copy these tokens into its credential storage.

GitHub and GitLab login commands also recognize environment and stored credentials. They do not save an environment token unless you pass `--save-env-token`.

An install route from `.npmrc` uses the credential for the matching registry origin. This rule also applies to custom registries and package tarballs.

## Choose a credential method [#choose-a-credential-method]

| Method                   | Use it for                                                      | Persistence               |
| ------------------------ | --------------------------------------------------------------- | ------------------------- |
| Browser login            | Regular local work with the LPM.dev Registry                    | Stored session            |
| Environment variable     | CI, containers, or temporary access                             | Current process or CI job |
| `gh` or `glab` session   | Local GitHub Packages or GitLab Packages work                   | Managed by the host CLI   |
| Stored third-party token | Regular local work without a host CLI                           | Stored token              |
| `.npmrc` credential      | npm-compatible install routing and origin-scoped authentication | Project or home file      |

Use [`lpm login`](/docs/infra/login) to check an available source or create a stored session or token. You do not need authentication to install public packages.

## Sessions and tokens [#sessions-and-tokens]

A browser login to the LPM.dev Registry can store an access token and a refresh token. LPM CLI can refresh this session without another browser login.

`lpm setup ci npmrc`, `lpm swift-registry`, and automatic Swift setup use this refresh path before they copy a token to another client. A refresh failure stops the command.

Tokens from `--token`, `LPM_TOKEN`, CI, or older login flows do not refresh automatically. Third-party registry tokens also remain static until you replace them.

If the registry rejects `LPM_TOKEN`, replace it or unset it to use your saved login. LPM CLI preserves saved credentials and reports `env_token_rejected` in JSON output. See [environment-token recovery](/docs/infra/login#if-the-registry-rejects-lpm_token) for local and CI steps.

Use [`lpm token-rotate`](/docs/infra/token-rotate) to rotate a stored LPM.dev Registry session. This command does not rotate npm, GitHub, GitLab, or custom registry tokens.

## Project configuration and registry routing [#project-configuration-and-registry-routing]

Project configuration can select a registry, but it does not change the credential priority. Do not put registry tokens in `lpm.json`.

For example, this `lpm.json` selects a custom npm-compatible publish registry:

```json title="lpm.json"
{
  "publish": {
    "registries": ["npm"],
    "npm": {
      "registry": "https://npm.example.com"
    }
  }
}
```

Store a token for that registry, then publish:

```bash
lpm login --login-registry https://npm.example.com --token <TOKEN>
lpm publish
```

The registry URL selects the stored credential. A token for `registry.npmjs.org` does not become a token for `npm.example.com`.

Use `.npmrc` for install routes and credentials for npm-compatible registries:

```ini title=".npmrc"
@acme:registry=https://npm.example.com/
//npm.example.com/:_authToken=${NPM_TOKEN}
```

Set the variable for the install process:

```bash
NPM_TOKEN=<TOKEN> lpm install
```

Do not commit a literal token to `.npmrc`. Use an environment-variable reference or another protected credential source.

On Unix, each `.npmrc` layer can supply credentials only if no group or other permission bits are set.
Mode `0600` is recommended. Mode `0700` is also accepted because only the owner has access.

If a layer has group or other permissions, LPM CLI refuses `_authToken`, `_auth`, `_username`, and `_password` from that layer.
It still uses non-secret registry routes and TLS configuration from the same file.
LPM CLI prints a security warning in terminal and JSON modes.
A refused higher-priority credential does not replace a protected credential from a lower-priority layer.

Use this command to protect the file:

```bash
chmod 600 .npmrc
```

## Secure credential storage [#secure-credential-storage]

LPM CLI stores local credentials in the secure storage service for your operating system.

| Platform | Secure storage service            |
| -------- | --------------------------------- |
| macOS    | Keychain                          |
| Linux    | Secret Service-compatible keyring |
| Windows  | Credential Manager                |

The service name is `lpm-cli`. LPM CLI separates stored credentials by registry.

If secure storage is unavailable, LPM CLI uses an encrypted file at `~/.lpm/.credentials`. LPM CLI does not store tokens as plain text in this file.

The encrypted fallback is valid, but it has less protection than the operating-system service. [`lpm doctor`](/docs/infra/doctor) reports this fallback as degraded.

## macOS Keychain permission [#macos-keychain-permission]

LPM CLI first tries to read an authorized Keychain item without a prompt. If macOS requires permission, LPM CLI prints guidance and opens the permission sheet.

Select **Always Allow** to keep later authorized reads silent. If you deny access, LPM CLI cannot use the stored credential for that request.

## Common recipes [#common-recipes]

### Use the LPM.dev Registry on your computer [#use-the-lpmdev-registry-on-your-computer]

Sign in through the browser, then inspect the active account:

```bash
lpm login
lpm whoami
```

### Use npm in CI [#use-npm-in-ci]

Pass `NPM_TOKEN` to the package command. This method does not store the token:

```bash
NPM_TOKEN=<TOKEN> lpm install
NPM_TOKEN=<TOKEN> lpm publish --npm
```

### Use an existing GitHub CLI session [#use-an-existing-github-cli-session]

Authenticate `gh`, then publish to GitHub Packages:

```bash
gh auth login --hostname github.com
lpm publish --github
```

LPM CLI reads the `gh` token for the request. The token remains under the control of GitHub CLI.

### Use GitLab CI [#use-gitlab-ci]

GitLab CI provides `CI_JOB_TOKEN` to eligible jobs. LPM CLI selects it after `GITLAB_TOKEN`:

```bash
CI_JOB_TOKEN=<TOKEN> lpm publish --gitlab
```

The GitLab publish target also requires `publish.gitlab.projectId` in `lpm.json`. See the [`lpm publish` targets](/docs/packages/publish#targets).

### Use a custom publish registry [#use-a-custom-publish-registry]

Store the token for the exact publish registry:

```bash
lpm login --login-registry https://npm.example.com --token <TOKEN>
lpm publish --publish-registry https://npm.example.com
```

Interactive login can ask for the token with masked input. JSON mode and non-interactive shells require `--token`.

## Logout and revocation [#logout-and-revocation]

`lpm logout` clears only the stored LPM.dev Registry session:

```bash
lpm logout
```

Use `--all` to clear all credentials that LPM CLI stores:

```bash
lpm logout --all
```

These commands do not clear environment variables, `.npmrc` credentials, or `gh` and `glab` sessions. LPM CLI does not own those credential sources.

Add `--revoke` to revoke the LPM.dev Registry session and browser pairings. See the [`lpm logout` reference](/docs/infra/login#log-out) for valid flag combinations.

## Troubleshooting [#troubleshooting]

### You logged out but remain authenticated [#you-logged-out-but-remain-authenticated]

A higher-priority credential can remain active after local logout. Inspect `LPM_TOKEN`, the registry-specific environment variables, `.npmrc`, and host CLI sessions.

For example, clear `LPM_TOKEN` from the current shell:

```bash
unset LPM_TOKEN
lpm logout
```

### LPM CLI selects an unexpected token [#lpm-cli-selects-an-unexpected-token]

Read the credential-priority table and remove the earlier source. For example, `GITHUB_TOKEN` overrides both `gh` and the stored GitHub fallback token.

### LPM CLI selects `gh` or `glab` unexpectedly [#lpm-cli-selects-gh-or-glab-unexpectedly]

Disable host CLI authentication for one command:

```bash
LPM_DISABLE_HOST_CLI_AUTH=1 lpm publish --github
```

If a stored fallback token is available, LPM CLI uses it. An environment token still has higher priority.

### LPM CLI refuses an `.npmrc` credential on Unix [#lpm-cli-refuses-an-npmrc-credential-on-unix]

LPM CLI refuses `.npmrc` credential fields when group or other permission bits are set.
Registry routes and TLS configuration from the same file remain active.

Restrict the file permissions:

```bash
chmod 600 .npmrc
```

Mode `0700` is also accepted, but `0600` is the recommended mode for a credential file.

### The secure storage service is unavailable [#the-secure-storage-service-is-unavailable]

Run [`lpm doctor`](/docs/infra/doctor) to inspect the active storage service. If that service is unavailable, LPM CLI uses encrypted file storage.

## Inspect authentication [#inspect-authentication]

These commands report the active account, available credential sources, or storage health:

```bash
lpm whoami
lpm whoami --json
lpm doctor
lpm setup ci npmrc --json
```

`lpm whoami` reports available third-party credentials. It does not send a request to each third-party registry to test every token.

`lpm setup ci npmrc --json` reports a redacted file shape. The live bearer appears only in the protected `.npmrc` file on disk.

## See also [#see-also]

* [`lpm login`, `lpm logout`, and `lpm whoami`](/docs/infra/login) - manage stored registry credentials
* [`lpm setup`](/docs/infra/setup) - write `.npmrc` authentication for local use or CI
* [`lpm token-rotate`](/docs/infra/token-rotate) - rotate the LPM.dev Registry session token
* [Registries](/docs/registries) - route packages to npm-compatible registries
* [Environment variables](/docs/reference/env-vars) - use temporary credentials and registry overrides


# lpm cert (/docs/infra/cert)



`lpm cert` manages the local certificate authority (CA) and project certificates that LPM CLI uses for local HTTPS.

For daily development, enable HTTPS in `lpm.json` and run [`lpm dev`](/docs/dev/dev). Use `lpm cert` for manual setup and maintenance.

```bash
lpm cert <action> [flags]
```

## Quickstart with `lpm.json` [#quickstart-with-lpmjson]

Add `https: true` to the `lpm.json` file next to `package.json`:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "https": true
}
```

Start the project:

```bash
lpm dev
```

LPM CLI creates a project certificate and serves the local site through trusted HTTPS. Your application continues to use plain HTTP behind LPM CLI.

On first use, an interactive terminal asks before it installs the local CA. You approve this step only once on each machine.

Use these commands to override the project configuration for one run:

```bash
lpm dev --no-https        # disable HTTPS for this run
lpm dev --https           # enable HTTPS for this run
lpm dev --https --yes     # approve CA installation without a prompt
```

If the CA is not trusted in a non-interactive shell, pass `--yes`:

```bash
lpm dev --yes
```

You can also install the CA during machine setup. Then the development run does not require trust consent:

```bash
lpm cert trust
lpm dev
```

## Use a project hostname [#use-a-project-hostname]

Use `proxy.host` for one service with a friendly hostname:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "proxy": {
    "host": "app.localhost",
    "port": 9443,
    "httpRedirect": false
  }
}
```

```bash
lpm dev
```

LPM CLI prepares the certificate and registers the route. Open the URL that `lpm dev` prints.

If a project has multiple services, use `services.<name>.host`. Each service must have a different hostname.

The `localhost` name and names below `.localhost` do not need a hosts-file entry. Other local names can require a managed hosts-file entry.

See [`lpm proxy`](/docs/infra/proxy) for proxy ports, multiple services, and hosts-file behavior.

## Permit more DNS names [#permit-more-dns-names]

The `cert` block controls which DNS subtrees a project certificate can cover:

```json title="lpm.json"
{
  "https": true,
  "cert": {
    "extraPermittedDns": ["myapp.internal"],
    "allowPublicDns": false
  }
}
```

`extraPermittedDns` does not add a browser hostname or a proxy route. It only permits LPM CLI to issue project certificates below that DNS subtree.

Add the actual browser hostname with `proxy.host`, `services.<name>.host`, or `lpm cert generate --host`.

Each entry must be a bare, multi-label hostname. Do not include a scheme, port, path, wildcard, or leading dot.

By default, LPM CLI accepts these local suffixes:

* `.local`
* `.test`
* `.localhost`
* `.internal`
* `.home.arpa`

Public DNS names require an explicit opt-in:

```json title="lpm.json"
{
  "cert": {
    "extraPermittedDns": ["staging.example.com"],
    "allowPublicDns": true
  }
}
```

> CAUTION: Enable `allowPublicDns` only for DNS names that you control. This field increases the authority of your local CA.

## Examples [#examples]

```bash
lpm dev                                      # use https from lpm.json
lpm cert status                              # show CA and project status
lpm cert trust                               # install the local CA
lpm cert generate                            # create the project certificate
lpm cert generate --host api.app.local       # add a browser hostname
lpm cert rotate --keep-old-trusted 14        # rotate with a 14-day grace period
lpm cert rotate --project ../other-app       # include another project
lpm cert reconcile --dry-run                 # preview rotation cleanup
lpm cert uninstall                           # remove the CA from the trust store
```

## Manual certificate commands [#manual-certificate-commands]

| Goal                                                        | Command              |
| ----------------------------------------------------------- | -------------------- |
| Show the root CA and current project                        | `lpm cert status`    |
| Create and trust the root CA                                | `lpm cert trust`     |
| Remove the root CA from the trust store                     | `lpm cert uninstall` |
| Create or renew the current project certificate             | `lpm cert generate`  |
| Replace the root CA and reissue project certificates        | `lpm cert rotate`    |
| Finish interrupted rotation or remove expired grace entries | `lpm cert reconcile` |

### Show certificate status [#show-certificate-status]

Run the command from the project directory:

```bash
lpm cert status
```

The output shows the root CA status and the current project certificate. It includes trust, expiry, hostnames, renewal status, and unsafe file permissions.

When fewer than 30 days remain, project certificate renewal starts. When fewer than 60 days remain, root CA warnings start.

Use JSON output in scripts:

```bash
lpm cert status --json
```

The JSON output includes CA data, project data, renewal status, and permission problems.

### Repair unsafe permissions [#repair-unsafe-permissions]

On Unix systems, certificate directories must use mode `0700`. Private key files must use mode `0600`.

LPM CLI refuses to sign certificates with a group-readable or world-readable CA private key. Run `status` to get the exact repair command:

```bash
lpm cert status
```

Run the printed `chmod` command. Then repeat the command that failed.

### Trust the root CA [#trust-the-root-ca]

Install the local CA before a non-interactive run. You can also install it before an interactive `lpm dev` run:

```bash
lpm cert trust
```

If the root CA does not exist, the command creates it. It stores the CA files in `~/.lpm/certs/` and installs the certificate into the system trust store.

You usually run this command once on each development machine.

### Remove CA trust [#remove-ca-trust]

Remove the CA from the system trust store:

```bash
lpm cert uninstall
```

This command keeps the CA files in `~/.lpm/certs/`. You can trust the same CA again with `lpm cert trust`.

The on-disk CA certificate must exist during removal. LPM CLI uses its fingerprint to remove the correct trusted certificate.

### Generate a project certificate [#generate-a-project-certificate]

If another local tool needs the project certificate files, use `generate`:

```bash
lpm cert trust
lpm cert generate
```

Run `generate` from the project directory. It reads the project `lpm.json` and writes these files:

```text
<project>/.lpm/certs/cert.pem
<project>/.lpm/certs/key.pem
```

Do not commit these certificate and private key files.

The default certificate names are `localhost`, `127.0.0.1`, and `::1`.

Add more Subject Alternative Names (SANs) by repeating `--host`:

```bash
lpm cert generate --host app.local --host api.app.local
```

`generate` does not install an untrusted CA. If the CA is not trusted, run `lpm cert trust` first.

### Rotate the root CA [#rotate-the-root-ca]

If `status` reports that the root CA is near expiry, rotate it. If its private key is exposed, rotate the root CA immediately.

```bash
lpm cert rotate
```

Rotation changes the trust identity and reissues known project certificates. Active projects can fail certificate checks during the change.

Use a grace period to keep the old root trusted during a migration:

```bash
lpm cert rotate --keep-old-trusted 14
```

The grace period can be at most 90 days. After the period expires, run `lpm cert reconcile` to remove the old trust entry.

Add project directories that LPM CLI must reissue:

```bash
lpm cert rotate --project ../web --project ../api
```

By default, rotation skips project directories that no longer exist. If each project must be present, use `--fail-on-missing`:

```bash
lpm cert rotate --project ../web --fail-on-missing
```

### Repair interrupted rotation [#repair-interrupted-rotation]

Preview certificate cleanup before you change files or trust entries:

```bash
lpm cert reconcile --dry-run
```

Apply the cleanup:

```bash
lpm cert reconcile
```

Use `reconcile` after an interrupted rotation or after a trust grace period expires. It also removes stale files from unfinished rotation work.

## Platform trust stores [#platform-trust-stores]

| Platform      | Trust behavior                                                                               |
| ------------- | -------------------------------------------------------------------------------------------- |
| macOS         | Installs the CA in the user login Keychain. It does not use `sudo`.                          |
| Linux         | Copies the CA to `/usr/local/share/ca-certificates/` and runs `sudo update-ca-certificates`. |
| Windows       | Uses `certutil` to install the CA in the Root store. Windows can request elevation.          |
| Other systems | Requires manual CA installation.                                                             |

LPM CLI identifies trusted certificates by fingerprint. It does not rely only on the certificate name.

## Flags [#flags]

| Flag                        | Action      | Purpose                                                                                 |
| --------------------------- | ----------- | --------------------------------------------------------------------------------------- |
| `--host <NAME>`             | `generate`  | Add a SAN to the project certificate. Repeat the flag for more names.                   |
| `--project <DIR>`           | `rotate`    | Reissue a certificate for another project directory. Repeat the flag for more projects. |
| `--keep-old-trusted <DAYS>` | `rotate`    | Keep the old root trusted for up to 90 days.                                            |
| `--fail-on-missing`         | `rotate`    | Fail instead of skipping a missing project directory.                                   |
| `--dry-run`                 | `reconcile` | Report cleanup without changing files or trust entries.                                 |
| `--json`                    | All actions | Print machine-readable output. This is a [global flag](/docs/commands#global-flags).    |

## See also [#see-also]

* [`lpm dev`](/docs/dev/dev) — run the project with local HTTPS
* [`lpm proxy`](/docs/infra/proxy) — route friendly local hostnames
* [Local HTTPS](/docs/infra/local-https) — certificate model and file locations
* [`lpm.json`](/docs/reference/lpm-json#cert) — HTTPS and certificate configuration


# lpm config (/docs/infra/config)



Use `lpm config` to view or change user-level defaults for LPM CLI.

```bash
lpm config
lpm config get <key>
lpm config set <key> <value>
lpm config <delete|unset> <key>
lpm config <list|ls>
lpm config <setting> [--set <VALUE>]
```

The command stores its changes in [`~/.lpm/config.toml`](/docs/reference/config-toml).

## Quickstart [#quickstart]

Open the guided editor:

```bash
lpm config
```

The editor shows the current values for its security and install settings. It returns to the main menu after each change.

To view the effective configuration for the current project and process, run:

```bash
lpm config list
```

## Choose a command [#choose-a-command]

| Command                              | Purpose                                      | Use it when                                     |
| ------------------------------------ | -------------------------------------------- | ----------------------------------------------- |
| `lpm config`                         | Open the guided editor                       | You want to review settings interactively       |
| `lpm config get <key>`               | Print one saved top-level value              | You know the configuration key                  |
| `lpm config set <key> <value>`       | Save one top-level value                     | The key is a scalar or a supported nested alias |
| `lpm config delete <key>`            | Remove one top-level value                   | You want the built-in or project value again    |
| `lpm config unset <key>`             | Alias for `delete`                           | You prefer the `unset` name                     |
| `lpm config list`                    | Print all effective values and their sources | You want to inspect the active configuration    |
| `lpm config ls`                      | Alias for `list`                             | You want a shorter command                      |
| `lpm config <setting>`               | Open one focused editor                      | You want value selection and validation         |
| `lpm config <setting> --set <VALUE>` | Save a focused value without its menu        | You use a script or already know the value      |

`get` shows one value saved in `~/.lpm/config.toml`. It does not calculate defaults or overrides.

`list` resolves all known configuration keys. It includes built-in defaults, project configuration, user configuration, environment variables, and active security floors. It uses the current directory and process environment. It does not include flags from other commands.

Human output groups related keys and shows the effective value and source in separate columns. Named policy extensions add entries such as `policy.extensions.<name>.mode`. Unknown keys saved in `~/.lpm/config.toml` remain visible in the **Additional saved values** group.

## Focused settings [#focused-settings]

Omit `--set` to select a value in an interactive terminal. Use `--set` in scripts and non-interactive shells.

| Setting              | Accepted values                                   | Purpose                                                      |
| -------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
| `scripts`            | `deny`, `triage`, `allow`                         | Control dependency lifecycle scripts                         |
| `triage`             | `none`, `claude-cli`, `codex`, `ollama`           | Select an optional advisor for `triage` script policy        |
| `sandbox`            | `default`, `strict`, `none`                       | Control containment for lifecycle scripts                    |
| `sigstore`           | `deny`, `warn`, `off`                             | Control failures during provenance verification              |
| `sigstore`           | `scope=approved`, `scope=all`                     | Select packages for provenance verification                  |
| `sigstore`           | `availability=best-effort`, `availability=strict` | Control missing or unavailable attestations                  |
| `signatures`         | `true`, `false`                                   | Verify npm registry package signatures during install        |
| `trust-policy`       | `off`, `no-downgrade`                             | Block weaker npm publisher or provenance trust               |
| `typosquat`          | `default`, `on`, `off`                            | Control suspicious direct dependency name analysis           |
| `firewall`           | `off`, `monitor`, `enforce`                       | Control LPM Firewall verdicts for public npm packages        |
| `integrity`          | `source`, `tree`                                  | Control validation of reused package-store objects           |
| `release-age`        | `default`, `off`, `0`, or a duration              | Set the cooldown for new package releases                    |
| `release-age-policy` | `direct`, `strict`                                | Apply the cooldown to direct or all dependencies             |
| `source-analysis`    | `true`, `false`                                   | Control package source analysis during install               |
| `lpm-dev`            | Interactive editor only                           | Manage both LPM.dev Registry package preferences             |
| `lpm-skills`         | `true`, `false`                                   | Control automatic skills from `@lpm.dev/*` packages          |
| `lpm-insights`       | `true`, `false`                                   | Control install-summary enrichment for `@lpm.dev/*` packages |

The `lpm-dev` editor manages `lpm-skills` and `lpm-insights` together. It does not accept `--set` or `--json`.

The interactive `triage` editor lists the advisor providers that it detects. Cloud advisors receive lifecycle script text, while Ollama keeps the text local.

## Common generic keys [#common-generic-keys]

Use `lpm config set` for scalar user defaults and the supported nested aliases.

| Key                        | Accepted value               | Default                       | Purpose                                                                                                   |
| -------------------------- | ---------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------- |
| `save-prefix`              | `^`, `~`, or an empty string | `^`                           | Select the prefix for new dependency ranges                                                               |
| `save-exact`               | Boolean                      | `false`                       | Save exact dependency versions                                                                            |
| `linker`                   | `hoisted`, `isolated`        | `hoisted` for single packages | Select the user-level `node_modules` layout. Workspaces and default peer-conflict installs use `isolated` |
| `auto-install-peers`       | Boolean                      | `true`                        | Install missing, required peer dependencies                                                               |
| `strict-peer-dependencies` | Boolean                      | `false`                       | Fail on missing peers, version mismatches, and peer conflicts                                             |
| `engine-strict`            | Boolean                      | `true`                        | Fail when required LPM CLI or Node versions do not match                                                  |
| `workspace-concurrency`    | Positive integer             | Available parallelism         | Set the default number of concurrent workspace tasks                                                      |
| `audit-after-install`      | Boolean                      | `false`                       | Run an informational audit after each successful install                                                  |

See the [complete key reference](/docs/reference/config-toml#known-keys) for arrays, nested tables, policy extensions, and all precedence rules.

The generic `set` action stores most values as TOML strings. The readers for known Boolean and integer keys accept these strings.

The generic action also accepts these validated aliases for nested settings:

```bash
lpm config set sandbox strict
lpm config set sigstore scope=all
lpm config set firewall monitor
```

These aliases update one field in the related TOML table. They keep the other fields in that table. They use the same validation and security approval as the focused commands.

Use the focused `sandbox`, `sigstore`, and `firewall` commands in new scripts. The focused forms make the nested field clear.

The generic action rejects `policy`, `tunnel`, and dotted keys such as `sandbox.mode`. It does not change the file after this error. Edit `~/.lpm/config.toml` directly when you must change an unsupported nested value.

The generic action also rejects `minimum-release-age-exclude` because this key requires an array. Use `lpm config release-age-exclude add <selector>` instead.

If another key requires an array or nested table, edit the file directly. For example, `script-read-allow` requires a TOML array.

## Recipes [#recipes]

### Change saved dependency ranges [#change-saved-dependency-ranges]

Save new dependencies with a tilde range:

```bash
lpm config set save-prefix '~'
```

Save exact versions for all new dependencies:

```bash
lpm config set save-exact true
```

`save-exact` has priority over `save-prefix`. Project values in `lpm.toml` have priority over these user values.

### Select a `node_modules` layout [#select-a-node_modules-layout]

Use the isolated layout for installs by this user:

```bash
lpm config set linker isolated
```

This user value has priority over `LPM_LINKER` and `package.json > lpm > linker`. The `lpm install --linker` flag has higher priority.

To let each project select its layout, remove the user value:

```bash
lpm config delete linker
```

### Set workspace concurrency [#set-workspace-concurrency]

Run four workspace members at the same time:

```bash
lpm config set workspace-concurrency 4
```

This value applies to workspace mode for `lpm run`, `lpm test`, and `lpm bench`.

### Enable the audit summary after installs [#enable-the-audit-summary-after-installs]

Run a non-blocking audit after each successful install:

```bash
lpm config set audit-after-install true
lpm install
```

The audit prints one summary line. Its findings do not fail the install.

### Set a stricter security baseline [#set-a-stricter-security-baseline]

If you want stricter package and script checks on your computer, use this recipe:

```bash
lpm config scripts --set deny
lpm config sandbox --set strict
lpm config sigstore --set scope=all
lpm config sigstore --set availability=strict
lpm config signatures --set true
lpm config trust-policy --set no-downgrade
lpm config integrity --set tree
```

Each command changes one independent control. You can apply only the controls that you need.

### Typosquat guard [#typosquat-guard]

The guard is off by default. Enable suspicious-name checks for new direct dependencies:

```bash
lpm config typosquat --set on
```

`--set default` removes the explicit override and uses the disabled product default. `--set off` always disables the guard.

### Install-time source analysis [#install-time-source-analysis]

Source analysis is off by default. Enable scans as package bytes enter the local store:

```bash
lpm config source-analysis --set true
```

The next install fills missing analysis caches from stored package bytes without another tarball download. [`lpm audit`](/docs/packages/audit) can scan installed source independently.

Disabling an approved source-analysis setting or typosquat guard requires security approval. Managed policy can prevent either change.

### Increase the release cooldown [#increase-the-release-cooldown]

Require a three-day cooldown for direct and transitive dependencies:

```bash
lpm config release-age --set 3d
lpm config release-age-policy --set strict
```

Use `default` to remove the user cooldown override. The built-in cooldown is off. Resetting an approved cooldown to this default requires security approval.

```bash
lpm config release-age --set default
```

### Exclude selected packages from the release cooldown [#exclude-selected-packages-from-the-release-cooldown]

Add a user exclusion that applies to all projects on this computer:

```bash
lpm config release-age-exclude add react
lpm config release-age-exclude add '@company/*'
lpm config release-age-exclude add react@19.1.0
lpm config release-age-exclude list
```

Remove an exclusion with the same complete selector:

```bash
lpm config release-age-exclude remove react@19.1.0
```

The command validates each selector and removes duplicate values. It atomically writes a string array to `~/.lpm/config.toml`.

JSON output includes `changed`, `normalized`, `count`, and `exclusions`. Normalization can change the stored array without changing the requested selector.

It does not write to `package.json` or `lpm.lock`. Generic `lpm config set minimum-release-age-exclude` is rejected because it supplies a scalar value.

Use [`lpm trust release-age-exclude`](/docs/packages/trust#lpm-trust-release-age-exclude) for a shared project exclusion.

See the [`lpm trust` storage contract](/docs/packages/trust#storage-contract) for the merge order and workspace behavior.

### Monitor public npm packages with LPM Firewall [#monitor-public-npm-packages-with-lpm-firewall]

This recipe signs in, enables monitor mode, and runs an install:

```bash
lpm login
lpm config firewall --set monitor
lpm install
```

`monitor` prints verdicts and continues. `enforce` blocks a package when its effective policy action is `block`.

LPM Firewall is an LPM.dev Registry Pro or Org feature. See the [LPM Firewall guide](/docs/guides/firewall) for policy-group controls.

### Control LPM.dev Registry package features [#control-lpmdev-registry-package-features]

Disable automatic package skills and registry behavior enrichment:

```bash
lpm config lpm-skills --set false
lpm config lpm-insights --set false
```

Disabling package skills keeps existing `.lpm/skills/` files. Disabling insights does not disable local source analysis or explicit audits.

## Project configuration [#project-configuration]

`lpm config` does not write `lpm.json`. It changes user defaults in `~/.lpm/config.toml`.

Project configuration uses the file defined by each feature. For example, save policy uses `lpm.toml`, and linker configuration can use `package.json > lpm`.

Read the related feature page before you add a project override. Precedence is not the same for every key.

## Security-sensitive changes [#security-sensitive-changes]

CAUTION: Do not weaken a security control unless the weaker value is required. A user setting can affect every project on the computer.

LPM CLI guards changes that weaken an approved security setting. Guarded controls include:

* Lifecycle script policy
* The release-age duration and scope
* The lifecycle-script sandbox
* Sigstore verification mode
* Typosquat analysis
* LPM Firewall mode
* Install-time source analysis

In an interactive terminal, LPM CLI requests approval before it saves a weaker value.

In JSON mode, CI, or a non-interactive shell, the command fails with `security_approval_required`.

A managed policy can prohibit the change. In this case, the command fails with `security_floor`.

Use [`lpm security status`](/docs/infra/security#status) to inspect the active security floor and temporary approvals.

## JSON output [#json-output]

Use `--json` with explicit actions or focused setters:

```bash
lpm config list --json
lpm config get save-prefix --json
lpm config integrity --set tree --json
```

`list --json` returns one ordered entry for each effective key. The following shortened example shows the envelope and entry fields:

```json
{
  "success": true,
  "action": "list",
  "count": 2,
  "entries": [
    {
      "key": "save-prefix",
      "value": "^",
      "source": "built-in default",
      "group": "Dependency saving"
    },
    {
      "key": "sandbox.mode",
      "value": "strict",
      "source": "lpm.toml",
      "group": "Lifecycle scripts"
    }
  ]
}
```

The real `count` is the number of objects in `entries`. Named policy extensions and unknown saved keys can increase this number.

For a saved key, `get --json` returns a stable envelope:

```json
{
  "success": true,
  "action": "get",
  "key": "save-prefix",
  "value": "~",
  "found": true
}
```

If the key is not saved, the command returns the same envelope with a null value:

```json
{
  "success": true,
  "action": "get",
  "key": "save-prefix",
  "value": null,
  "found": false
}
```

The command exits successfully for both states. Use `found` to determine if the configuration file contains the key.

The bare guided editor and `lpm config lpm-dev` do not support JSON mode.

## Reset or recover the configuration [#reset-or-recover-the-configuration]

Remove a simple top-level user value to use the next source in its precedence chain:

```bash
lpm config delete save-prefix
```

For a focused setting, use its focused command to select the documented default:

```bash
lpm config release-age --set default
lpm config typosquat --set default
lpm config sandbox --set default
```

If the file contains invalid TOML, keep a backup and start with built-in defaults:

```bash
mv ~/.lpm/config.toml ~/.lpm/config.toml.backup
lpm config
```

After LPM CLI opens the editor successfully, copy required values from the backup.

## File location [#file-location]

The default path is `~/.lpm/config.toml`. On Windows, the path is `%USERPROFILE%\.lpm\config.toml`.

The [`LPM_HOME`](/docs/reference/env-vars#filesystem-layout) environment variable changes the root directory. LPM CLI creates the file when it saves the first value.

## Flags [#flags]

| Flag            | Purpose                                                          |
| --------------- | ---------------------------------------------------------------- |
| `--set <VALUE>` | Save a value through a focused setting without its menu          |
| `--json`        | Print structured output for an explicit action or focused setter |

Other [global flags](/docs/commands#global-flags) also apply.

## See also [#see-also]

* [`~/.lpm/config.toml`](/docs/reference/config-toml) - read all keys, types, defaults, and precedence rules
* [`lpm security`](/docs/infra/security) - inspect approvals and managed security floors
* [`lpm policy`](/docs/infra/policy) - inspect local install policy extensions
* [`lpm.toml`](/docs/reference/lpm-toml) - configure supported project-level behavior
* [`package.json > lpm`](/docs/reference/package-json-lpm) - configure supported project-level package behavior
* [Save policy](/docs/packages/save-policy) - select dependency range formats


# lpm doctor / lpm health (/docs/infra/doctor)



Use `lpm doctor` to find project and environment problems. Use `lpm health` to examine one registry.

```bash
lpm doctor [--all] [--fix] [-y|--yes] [--json]
lpm doctor list [--code <CODE>] [--category <SUBSTRING>] [--json]
lpm health [--registry <URL>] [--json]
```

## Quickstart [#quickstart]

Run the fast checks from the project directory:

```bash
lpm doctor
```

The command examines the current directory. It reports failures and warnings, then returns a summary.

## Choose a command or mode [#choose-a-command-or-mode]

| Command            | Purpose                                                   | Use it when                                                           |
| ------------------ | --------------------------------------------------------- | --------------------------------------------------------------------- |
| `lpm doctor`       | Run fast local checks for the current project             | An install, runtime, lockfile, or project state does not look correct |
| `lpm doctor --all` | Add network, tooling, global-install, and security checks | You need a complete environment report or a CI gate                   |
| `lpm doctor list`  | Show every check that doctor can report                   | You need a check code, description, or remediation                    |
| `lpm health`       | Send one health request to the selected registry          | You need to know whether the registry responds                        |

## Fast and full checks [#fast-and-full-checks]

`lpm doctor` runs the fast mode by default. This mode reads local project and LPM CLI state.

The fast mode examines:

* Local storage and install state
* `package.json`, `node_modules`, and lockfiles
* Dependency synchronization and local dependency paths
* `lpm.json` structure and runtime pins
* Node.js and Bun readiness
* Workspace cycles
* Policy extensions
* Local authentication and secret storage
* Sigstore settings

The fast mode does not contact a registry or tunnel service. It also does not run lint or format checks.

Use `--all` to add:

* Registry and authentication requests
* Tunnel ownership and reachability
* Lint and format checks
* TypeScript readiness and plugin updates
* Global-install health
* Sandbox and script-policy checks
* Manifest compatibility checks
* `.gitattributes` and store-orphan checks

```bash
lpm doctor --all
```

The full mode can take more time because it runs tools and sends network requests.

## Recipes [#recipes]

### Diagnose the current project [#diagnose-the-current-project]

Run doctor from the project root:

```bash
cd /path/to/project
lpm doctor
```

The fast output usually hides passing rows. It keeps the resolved linker mode and active policy extensions for context.

### Run a complete environment check [#run-a-complete-environment-check]

```bash
lpm doctor --all
```

Use this command after a machine change, a toolchain change, or a difficult install failure.

### Use doctor as a CI gate [#use-doctor-as-a-ci-gate]

```bash
lpm doctor --all --json > doctor-report.json
```

If a check fails, doctor exits with code `1`. Warnings keep exit code `0`.

The JSON report contains both the summary and every emitted check. Match automation on the stable `code` field.

### Apply fixes and run doctor again [#apply-fixes-and-run-doctor-again]

Save your project changes before you use `--fix`. Some fixes can change project files or installed dependencies.

```bash
lpm doctor --fix
lpm doctor
```

`--fix` applies supported fixes immediately. It does not ask for confirmation.

The first run reports the original check results. Run doctor again to make sure that the fixes resolved the problems.

Use `--all --fix` to include fixes from the full check set:

CAUTION: `--all --fix` can prune the shared store, update installed plugins, and claim a tunnel domain.

```bash
lpm doctor --all --fix
lpm doctor --all
```

The full fix mode can also update `.gitattributes` and project formatting.

### Find one check in the catalog [#find-one-check-in-the-catalog]

```bash
lpm doctor list --code node_modules_symlinked
```

The result explains when the check appears and how to resolve it. The `--code` value is an exact match.

### Find checks by category [#find-checks-by-category]

```bash
lpm doctor list --category runtime
lpm doctor list --category "project state" --json
```

The category filter is not case-sensitive. It matches any category that contains the supplied text.

### Examine a specific registry [#examine-a-specific-registry]

```bash
lpm health --registry https://registry.example.com
```

The command reports the registry URL, its health state, and one response-time measurement.

## `lpm.json` behavior [#lpmjson-behavior]

If `lpm.json` exists, doctor reads it automatically. You do not need a separate option.

For example:

```json
{
  "runtime": {
    "node": "22"
  },
  "tunnel": {
    "domain": "my-api.lpm.fyi"
  }
}
```

The fast mode checks the JSON structure and every top-level field in the canonical [`lpm.json` schema](/docs/reference/lpm-json). It reports unknown fields as warnings.

It also checks value types and the details of runtime, task, tool, and service fields.

The fast mode also checks the configured Node.js or Bun version.

The full mode checks the configured tunnel domain. If authentication is available, it also checks ownership and reachability.

Read [lpm tunnel](/docs/infra/tunnel) for the complete tunnel configuration.

## Automatic fixes [#automatic-fixes]

Doctor can apply fixes for selected results. The available fix depends on the emitted check code.

Supported fixes include:

* Install the pinned Node.js version
* Install the pinned Bun version
* Run [lpm install](/docs/packages/install) for missing or inconsistent project state
* Run `lpm fmt` for formatting failures
* Reconcile `lpm.lockb` with `lpm.lock`
* Add the binary lockfile rule to `.gitattributes`
* Replace a linked project `node_modules` path with a real directory
* Prune unreachable entries from the shared store during a full run
* Claim an available tunnel domain during a full run
* Update an installed plugin during a full run

`lpm doctor list --json` shows the supported action before a fix run. For each code, `auto_fix` identifies the action or is null.

The JSON field `fixes_applied` lists the fixes that doctor completed successfully.

`lpm doctor --fix` runs the selected checks before it changes the project. If automatic fixes exist, doctor shows the planned actions and asks once.

The default response is `No`. If you decline, doctor makes no changes.

`-y` and `--yes` imply `--fix` and skip the confirmation. Use either flag for CI, scripts, or JSON output:

```bash
lpm doctor --yes
lpm --json doctor --yes
```

Without `--yes`, `--fix` returns an error when standard input is not a terminal. It also returns an error with `--json`.

Doctor makes no changes in both cases. If no automatic fix exists, doctor returns the normal result without a prompt.

## Check catalog [#check-catalog]

This catalog explains every check that `lpm doctor` can report. The tables group related codes so that you can find a result quickly.

Match automation on the `code` value. Human-readable descriptions can change.

Run `lpm doctor list` for the same catalog in the terminal. Use `lpm doctor list --json` for the complete machine-readable metadata.

In the JSON catalog, `auto_fix` identifies the action that `--fix` supports for each code. A null value means that no automatic fix exists.

`pass` is a healthy or informational result. `warn` needs attention but keeps exit code `0`. `fail` causes exit code `1`.

### Infrastructure [#infrastructure]

| Code                        | Severity | Description                                                                                                                                                  |
| --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `registry_reachable`        | pass     | The configured registry responds to its health endpoint.                                                                                                     |
| `registry_unreachable`      | fail     | The configured registry did not respond. &#x2A;*Fix:** Examine the network, firewall rules, and registry status page.                                        |
| `global_store_accessible`   | pass     | The shared content-addressable store at `~/.lpm/store/` resolves.                                                                                            |
| `global_store_inaccessible` | fail     | LPM CLI cannot locate or read the shared package store. &#x2A;*Fix:** Make sure that `$HOME` is set and `~/.lpm/store/` is writable. Then run `lpm install`. |

### Auth [#auth]

| Code                        | Severity | Description                                                                                                                                                                                                             |
| --------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth_valid`                | pass     | A registry auth token is present and `whoami` succeeds.                                                                                                                                                                 |
| `auth_invalid`              | fail     | The registry rejected the selected credential. &#x2A;*Fix:** Replace or unset `LPM_TOKEN` if it supplies the token. For a rejected saved login, run [`lpm login`](/docs/infra/login#if-the-registry-rejects-lpm_token). |
| `auth_missing`              | fail     | No registry auth token is configured. &#x2A;*Fix:** Run `lpm login`.                                                                                                                                                    |
| `auth_storage_keychain`     | pass     | LPM CLI stores authentication material for this registry in the OS keychain.                                                                                                                                            |
| `auth_storage_fallback`     | warn     | LPM CLI stores authentication material in encrypted files instead of the OS keychain. &#x2A;*Fix:** Unlock or repair the keychain. Then run `lpm login` again.                                                          |
| `vault_storage_keychain`    | pass     | On macOS, vault secrets are unlocked through the OS Keychain.                                                                                                                                                           |
| `vault_storage_native`      | pass     | On Linux and Windows, vault blobs use an encrypted local key that the OS secure store protects.                                                                                                                         |
| `vault_storage_fallback`    | warn     | Vault secrets use the fallback key at `~/.lpm/.vault-fallback-key`. Processes from the same user can read this key. &#x2A;*Fix:** Unlock or repair the OS secure store.                                                 |
| `vault_storage_unavailable` | fail     | Encrypted vault files exist, but LPM CLI cannot access either local key. &#x2A;*Fix:** Unlock the secure store. If necessary, restore the fallback key from backup.                                                     |

### Policy extensions [#policy-extensions]

| Code                                   | Severity | Description                                                                                                                                          |
| -------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `policy_extensions_not_configured`     | pass     | No active install-time policy extensions are configured in `~/.lpm/config.toml`.                                                                     |
| `policy_extensions_configured`         | pass     | One or more active install-time policy extensions are configured. &#x2A;*Inspect:** Run `lpm policy status` or `lpm policy doctor`.                  |
| `policy_extension_report_mode`         | warn     | A configured policy extension is report-only. &#x2A;*Fix:** If the policy is ready to block installs, set `mode = "enforce"`.                        |
| `policy_extension_command_unavailable` | fail     | A configured policy extension command cannot be found or executed. &#x2A;*Fix:** Install the executable or fix `[policy.extensions.<name>].command`. |
| `policy_extension_config_invalid`      | fail     | Policy extension configuration is malformed. &#x2A;*Fix:** Repair the `[policy.extensions]` table, then rerun `lpm policy doctor`.                   |

### Project state [#project-state]

| Code                            | Severity  | Description                                                                                                                                                                         |
| ------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package_json_present`          | pass      | A readable `package.json` exists in the project directory.                                                                                                                          |
| `package_json_missing`          | fail      | No `package.json` exists in the project directory. &#x2A;*Fix:** Run `lpm init`, or `cd` into your project root before running doctor.                                              |
| `linker_mode_resolved`          | pass      | Shows the selected linker mode and its source: CLI, global configuration, environment, package, workspace, or default.                                                              |
| `node_modules_isolated_healthy` | pass      | `node_modules/` exists and is backed by an isolated `.lpm/wrappers/` store.                                                                                                         |
| `node_modules_hoisted_healthy`  | pass      | `node_modules/` exists and uses the hoisted layout.                                                                                                                                 |
| `node_modules_virtual_healthy`  | pass      | `node_modules/` symlinks point into the active global virtual store.                                                                                                                |
| `node_modules_symlinked`        | fail      | The project-level `node_modules` path is a symlink or directory junction. &#x2A;*Fix:** Run `lpm doctor --fix`, then run `lpm install`.                                             |
| `v2_store_orphans`              | pass/warn | The v2 or v3 store has no orphans, or reports entries that no project uses. &#x2A;*Fix:** Run `lpm cache prune`, then run `lpm cache prune --apply`.                                |
| `node_modules_mixed_layout`     | warn      | Both isolated and hoisted layout state are present in `node_modules/`. &#x2A;*Fix:** Re-run `lpm install` to converge on the configured linker layout.                              |
| `node_modules_no_store`         | warn      | `node_modules/` exists but no LPM CLI-owned store is present. &#x2A;*Fix:** Run `lpm install` to rebuild the layout under LPM CLI ownership.                                        |
| `node_modules_legacy_layout`    | warn      | An older LPM CLI layout is on disk and a one-time migration is pending. &#x2A;*Fix:** Run `lpm install` to migrate to the current layout.                                           |
| `node_modules_missing`          | fail      | `node_modules/` is missing — dependencies have not been installed. &#x2A;*Fix:** Run `lpm install`.                                                                                 |
| `lockfile_present`              | pass      | `lpm.lock` is present at the project root.                                                                                                                                          |
| `lockfile_missing`              | warn      | No `lpm.lock` was found at the project root. &#x2A;*Fix:** Run `lpm install` — it generates the lockfile alongside `node_modules/`.                                                 |
| `lockfile_binary_valid`         | pass      | `lpm.lockb` matches `lpm.lock` and parses cleanly.                                                                                                                                  |
| `lockfile_binary_missing`       | warn      | `lpm.lockb` is missing while `lpm.lock` is present and the graph fits the binary format. &#x2A;*Fix:** Run `lpm doctor --fix` to regenerate, or run `lpm install`.                  |
| `lockfile_binary_stale`         | warn      | `lpm.lockb` does not match the contents of `lpm.lock`, or stale binary bytes remain for a TOML-only graph. &#x2A;*Fix:** Run `lpm doctor --fix` to reconcile, or run `lpm install`. |
| `lockfile_binary_corrupt`       | warn      | `lpm.lockb` does not parse as a valid binary lockfile. &#x2A;*Fix:** Run `lpm doctor --fix` to reconcile from `lpm.lock`.                                                           |
| `gitattributes_lockb_marked`    | pass      | `.gitattributes` marks `lpm.lockb` as binary.                                                                                                                                       |
| `gitattributes_lockb_unmarked`  | warn      | `.gitattributes` does not mark `lpm.lockb` as binary. &#x2A;*Fix:** Run `lpm doctor --fix` or add `lpm.lockb binary`.                                                               |
| `gitattributes_missing`         | warn      | No `.gitattributes` file exists, so Git can treat `lpm.lockb` as text. &#x2A;*Fix:** Run `lpm doctor --fix` or add `lpm.lockb binary`.                                              |
| `deps_sync_clean`               | pass      | `lpm.lock` and `package.json` agree on the declared dependency set.                                                                                                                 |
| `deps_sync_drift`               | warn      | `lpm.lock` and `package.json` disagree — manifest changes have not been resolved. &#x2A;*Fix:** Run `lpm install` to reconcile.                                                     |
| `local_source_dir_ok`           | pass      | A `file:` / `link:` dependency points at a directory with a readable `package.json`.                                                                                                |
| `local_source_tarball_ok`       | pass      | A `file:` dependency points at a readable tarball.                                                                                                                                  |
| `local_source_dir_no_pkg`       | fail      | A `file:` / `link:` dependency points at a directory with no `package.json`. &#x2A;*Fix:** Add `package.json` to the local path or update the dep target.                           |
| `local_source_invalid_type`     | fail      | A `file:` / `link:` dependency points at an unexpected file type. &#x2A;*Fix:** Re-target the dependency at a directory or a tarball file.                                          |
| `local_source_link_to_file`     | fail      | A `link:` dependency points at a file instead of a directory. &#x2A;*Fix:** Use a project directory. Use `file:` for tarballs.                                                      |
| `local_source_unreadable`       | fail      | LPM CLI cannot read a `file:` or `link:` target. &#x2A;*Fix:** Restore the target or correct its permissions. Then run `lpm install`.                                               |

### lpm.json [#lpmjson]

| Code                       | Severity | Description                                                                                                                                           |
| -------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lpm_json_valid`           | pass     | Doctor accepts the `lpm.json` syntax and recognized fields.                                                                                           |
| `lpm_json_schema_warnings` | warn     | Doctor reports an unknown field or an incorrect value type. &#x2A;*Fix:** Review the detail and the [`lpm.json` reference](/docs/reference/lpm-json). |
| `lpm_json_invalid_syntax`  | fail     | `lpm.json` is not valid JSON. &#x2A;*Fix:** Fix the JSON syntax error reported in `detail`.                                                           |
| `lpm_json_not_object`      | fail     | `lpm.json`'s top-level value is not an object. &#x2A;*Fix:** Replace with a JSON object literal `{ ... }`.                                            |
| `lpm_json_unreadable`      | fail     | LPM CLI cannot read `lpm.json`. &#x2A;*Fix:** Correct the file permissions. Then run doctor again.                                                    |

### Runtime [#runtime]

| Code                     | Severity  | Description                                                                                                                                                                                                                                            |
| ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `node_engine_compatible` | pass      | The first Node on the constructed script `PATH` satisfies `package.json > engines.node`.                                                                                                                                                               |
| `node_engine_mismatch`   | fail/warn | The first Node on the script `PATH` violates `package.json > engines.node`. Strict mode fails, and non-strict mode warns. &#x2A;*Fix:** Select a compatible runtime, relax the constraint, or disable strictness in the project or user configuration. |
| `node_managed_match`     | pass      | A managed Node install matches the pinned spec.                                                                                                                                                                                                        |
| `node_pinned_unmet`      | warn      | A pinned Node version has no matching managed install. The first Node on script `PATH` can differ. &#x2A;*Fix:** Run `lpm use node@<version>`.                                                                                                         |
| `node_missing_pinned`    | fail      | Project pins a Node version and the constructed script `PATH` cannot resolve Node. &#x2A;*Fix:** Run `lpm use node@<version>` to install the pinned version.                                                                                           |
| `node_system_unpinned`   | pass      | No Node version is pinned, and the script `PATH` resolves Node. &#x2A;*Fix:** For reproducible runs, pin a version in `lpm.json > runtime.node`.                                                                                                       |
| `node_missing_unpinned`  | fail      | No Node version is pinned and the constructed script `PATH` cannot resolve Node. &#x2A;*Fix:** Install Node via `lpm use node@22` (or your preferred version).                                                                                         |
| `bun_managed_match`      | pass      | A managed Bun install matches `lpm.json > runtime.bun`.                                                                                                                                                                                                |
| `bun_pinned_unmet`       | warn      | Project pins a Bun version but no managed Bun install matches. &#x2A;*Fix:** Run `lpm use bun@<version>` to install and pin the managed version.                                                                                                       |
| `bun_missing_pinned`     | fail      | Project pins a Bun version and no Bun is reachable. &#x2A;*Fix:** Run `lpm use bun@<version>` to install the pinned version.                                                                                                                           |

### Tunnel [#tunnel]

| Code                           | Severity | Description                                                                                                                                            |
| ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tunnel_active`                | pass     | The configured domain is claimed by your account and responds to a request.                                                                            |
| `tunnel_idle`                  | pass     | The configured domain is claimed by your account, but no tunnel is active.                                                                             |
| `tunnel_unauthenticated`       | pass     | A tunnel domain is configured, but no authentication is available. &#x2A;*Fix:** Run `lpm login` to examine ownership.                                 |
| `tunnel_unverified`            | pass     | The registry request for domain ownership failed. &#x2A;*Fix:** Run `lpm health`. Then run doctor again.                                               |
| `tunnel_not_claimed`           | warn     | Your account does not own the configured tunnel domain. &#x2A;*Fix:** Run `lpm doctor --all --fix`, claim the domain, or change it.                    |
| `tunnel_owned_by_other`        | warn     | A different account owns the configured tunnel domain. &#x2A;*Fix:** Choose a different domain.                                                        |
| `tunnel_unreachable`           | warn     | Your account owns the domain, but the domain does not respond. &#x2A;*Fix:** Examine DNS and tunnel state.                                             |
| `tunnel_unknown_base`          | warn     | The LPM.dev Registry does not recognize the configured base domain. &#x2A;*Fix:** Use a supported base domain or contact the registry team.            |
| `tunnel_domain_no_dot`         | warn     | Configured tunnel domain has no dot separating subdomain from base. &#x2A;*Fix:** Set the full domain: `<subdomain>.lpm.fyi` or `<subdomain>.lpm.llc`. |
| `tunnel_domain_empty_label`    | warn     | Configured tunnel domain has an empty label, for example `..lpm.fyi`. &#x2A;*Fix:** Remove the empty label.                                            |
| `tunnel_domain_label_too_long` | warn     | A label in the configured tunnel domain exceeds 63 characters (DNS limit). &#x2A;*Fix:** Shorten the offending label.                                  |
| `tunnel_domain_too_long`       | warn     | Total tunnel domain length exceeds the DNS limit. &#x2A;*Fix:** Shorten the domain.                                                                    |
| `tunnel_subdomain_length`      | warn     | Tunnel subdomain length is outside the allowed range. &#x2A;*Fix:** Pick a subdomain between the documented bounds.                                    |
| `tunnel_subdomain_chars`       | warn     | Tunnel subdomain contains characters outside the allowed alphabet. &#x2A;*Fix:** Use only lowercase letters, digits, and hyphens.                      |
| `tunnel_subdomain_hyphen`      | warn     | Tunnel subdomain starts or ends with a hyphen. &#x2A;*Fix:** Subdomains must start and end with an alphanumeric character.                             |

### Code quality [#code-quality]

| Code               | Severity | Description                                                                                                     |
| ------------------ | -------- | --------------------------------------------------------------------------------------------------------------- |
| `lint_clean`       | pass     | Oxlint reports no issues for the project.                                                                       |
| `lint_warnings`    | warn     | Oxlint reported warnings for the project. &#x2A;*Fix:** Run `lpm lint` to inspect, then address.                |
| `lint_errors`      | fail     | Oxlint reported errors for the project. &#x2A;*Fix:** Run `lpm lint` and fix the reported errors.               |
| `lint_unparseable` | warn     | Doctor cannot parse the Oxlint output. &#x2A;*Fix:** Run `lpm lint` to see the raw output.                      |
| `fmt_clean`        | pass     | Biome reports the project formatting is clean.                                                                  |
| `fmt_unformatted`  | warn     | Biome found files that need reformatting. &#x2A;*Fix:** Run `lpm fmt` to apply formatting.                      |
| `fmt_other_issue`  | warn     | Biome reported a parse or configuration error. &#x2A;*Fix:** Run `lpm fmt --check` to see the raw Biome output. |

### TypeScript [#typescript]

| Code                              | Severity | Description                                                                                                                                                            |
| --------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `typescript_healthy`              | pass     | Project-local `tsc` resolves through the `node_modules/.bin` chain.                                                                                                    |
| `typescript_missing_for_tsconfig` | warn     | `tsc` is reachable only from the system `PATH`. The project has no local install. &#x2A;*Fix:** Run `lpm install -D typescript`.                                       |
| `typescript_unavailable`          | fail     | `tsc` is not reachable for a directory with `tsconfig.json`. &#x2A;*Fix:** If `typescript` is declared, run `lpm install`. Otherwise, run `lpm install -D typescript`. |

### Plugin [#plugin]

| Code                      | Severity | Description                                                                                                           |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `plugin_up_to_date`       | pass     | An installed plugin is at the latest known version.                                                                   |
| `plugin_update_available` | warn     | A newer version of an installed plugin is available upstream. &#x2A;*Fix:** Run `lpm plugin update <name>` to update. |

### Workspace [#workspace]

| Code                | Severity | Description                                                                                                                                |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `workspace_acyclic` | pass     | No dependency cycles among workspace members.                                                                                              |
| `workspace_cycle`   | fail     | A dependency cycle exists among workspace members. &#x2A;*Fix:** Break the cycle by removing or restructuring the offending workspace dep. |

### Global installs [#global-installs]

| Code                                   | Severity | Description                                                                                                                                                       |
| -------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `global_manifest_valid`                | pass     | `~/.lpm/global/manifest.toml` parses and is structurally valid.                                                                                                   |
| `global_manifest_absent`               | pass     | No global install manifest is present (no global installs yet).                                                                                                   |
| `global_manifest_corrupt`              | fail     | `~/.lpm/global/manifest.toml` is unreadable or malformed. &#x2A;*Fix:** Inspect and repair the file, or reinstall affected global packages.                       |
| `global_manifest_structurally_invalid` | fail     | The global manifest contains invalid package roots, aliases, or tombstones. &#x2A;*Fix:** Repair the affected rows or reinstall the affected global packages.     |
| `global_bin_on_path`                   | pass     | `~/.lpm/bin` is on the current `PATH`.                                                                                                                            |
| `global_bin_off_path`                  | warn     | `~/.lpm/bin` is not on the current `PATH`. &#x2A;*Fix:** Add `~/.lpm/bin` to the shell `PATH`.                                                                    |
| `global_shims_clean`                   | pass     | Every shim in `~/.lpm/bin` belongs to a recorded global install.                                                                                                  |
| `global_shims_no_dir`                  | pass     | Global bin directory does not yet exist.                                                                                                                          |
| `global_shims_orphans`                 | warn     | Files exist in `~/.lpm/bin` without a matching manifest entry. &#x2A;*Fix:** Remove the orphan files, or run `lpm install --global` to re-register.               |
| `global_shims_unreadable`              | warn     | LPM CLI cannot read the global binary directory. &#x2A;*Fix:** Correct its permissions. Then run doctor again.                                                    |
| `global_shim_targets_healthy`          | pass     | On macOS and Linux, each manifest-owned shim points to the expected global package binary.                                                                        |
| `global_shim_targets_stale`            | warn     | On macOS and Linux, a manifest-owned shim has a missing or incorrect target. &#x2A;*Fix:** Run `lpm install -g <pkg>` again.                                      |
| `global_install_roots_empty`           | pass     | No global installs are recorded.                                                                                                                                  |
| `global_install_roots_healthy`         | pass     | Every global install root exists and carries a ready marker.                                                                                                      |
| `global_install_roots_unhealthy`       | fail     | One or more global install roots are missing or incomplete. &#x2A;*Fix:** Reinstall the affected globals (`lpm install --global <pkg>`).                          |
| `global_trusted_deps_valid`            | pass     | `~/.lpm/global/trusted-dependencies.json` parses cleanly.                                                                                                         |
| `global_trusted_deps_absent`           | pass     | No host-global trusted-dependencies file is present yet.                                                                                                          |
| `global_trusted_deps_corrupt`          | fail     | `~/.lpm/global/trusted-dependencies.json` is unreadable, malformed, or uses a newer schema. &#x2A;*Fix:** Repair or delete it, then re-approve globals as needed. |

### Sandbox + scripts [#sandbox--scripts]

| Code                               | Severity | Description                                                                                                                                                                                                                                                              |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sandbox_available`                | pass     | The OS sandbox backend used by lifecycle scripts is available.                                                                                                                                                                                                           |
| `sandbox_helper_missing`           | warn     | On Windows, `lpm-sandbox-helper.exe` is not next to `lpm.exe`. Strict mode cannot deny network access. &#x2A;*Fix:** Reinstall `@lpm-registry/cli`. If the helper is elsewhere, set `LPM_SANDBOX_HELPER=<path>`.                                                         |
| `sandbox_degraded`                 | warn     | Strict mode is engaged but the host kernel forced the V1 fallback (filesystem containment only — no outbound network denial). &#x2A;*Fix:** Upgrade to kernel 6.7+ and unset `[sandbox] allow-degraded`, or drop back to default via `lpm config sandbox --set default`. |
| `sandbox_disabled_by_user`         | warn     | Sandbox is persistently disabled via `[sandbox] mode = "none"`. Lifecycle scripts run with no containment, including credential env vars. &#x2A;*Fix:** Restore via `lpm config sandbox --set default` (or `--set strict`).                                              |
| `sandbox_config_approval_required` | fail     | The requested sandbox configuration is weaker than the approved security floor. &#x2A;*Fix:** Approve the change or restore `default` or `strict` mode.                                                                                                                  |
| `sandbox_kernel_too_old`           | warn     | Linux kernel is too old to support Landlock at the required ABI. &#x2A;*Fix:** Upgrade the kernel, or accept that lifecycle scripts run unsandboxed.                                                                                                                     |
| `sandbox_unsupported_platform`     | warn     | No supported sandbox backend exists for this platform. &#x2A;*Fix:** Lifecycle scripts will not be sandboxed on this platform — review with `lpm approve-scripts`.                                                                                                       |
| `sandbox_probe_failed`             | fail     | The sandbox probe errored unexpectedly. &#x2A;*Fix:** File a bug with the `detail` text.                                                                                                                                                                                 |
| `policy_force_security_floor`      | warn     | An override lowers the default script-policy floor. &#x2A;*Fix:** Make sure that the override matches the project security requirements.                                                                                                                                 |

### Sigstore provenance [#sigstore-provenance]

| Code                        | Severity | Description                                                                                                                                                                |
| --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sigstore_verify_enforced`  | pass     | Sigstore provenance verification is fail-closed. Rejected attestations refuse the install or approval.                                                                     |
| `sigstore_verify_warn_mode` | warn     | Sigstore verification only warns and does not block installs. &#x2A;*Fix:** Unset `LPM_PROVENANCE_ENFORCE` (or set it to `deny`), or run `lpm config sigstore --set deny`. |
| `sigstore_verify_disabled`  | warn     | Sigstore verification is disabled entirely. &#x2A;*Fix:** Re-enable deny mode via `LPM_PROVENANCE_ENFORCE=deny` or `lpm config sigstore --set deny`.                       |

### Manifest compat [#manifest-compat]

| Code                          | Severity | Description                                                                                                                                                                                                                                                  |
| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `unsupported_override_values` | warn     | Top-level `overrides` or `resolutions` values are not strings, so LPM CLI ignores them. &#x2A;*Fix:** Use string targets and parent selectors such as `parent>dependency`.                                                                                   |
| `pnpm_overrides_drift`        | warn     | Entries in `pnpm.overrides` that LPM CLI is not honoring through `lpm.overrides`, top-level `overrides`, or `resolutions`. &#x2A;*Fix:** Run `lpm migrate` to translate, or mirror the entries verbatim in `lpm.overrides`.                                  |
| `pnpm_patches_drift`          | warn     | LPM CLI cannot bind some `pnpm.patchedDependencies` entries. &#x2A;*Fix:** Run `lpm patch`. Alternatively, mirror a patch only after it applies cleanly.                                                                                                     |
| `pnpm_peer_rules_drift`       | warn     | Sub-key entries (`ignoreMissing`, `allowedVersions`, `allowAny`) that LPM CLI is not honoring via `lpm.peerDependencyRules`. &#x2A;*Fix:** Run `lpm migrate` (the planner translates selector keys 1:1) or mirror the rules under `lpm.peerDependencyRules`. |
| `engines_npm_ignored`         | warn     | `engines.npm` is declared but LPM CLI is not the npm CLI and does not enforce its constraint. &#x2A;*Fix:** Remove the field, or accept that LPM CLI ignores it. `engines.node` and `engines.lpm` are enforced.                                              |
| `engines_pnpm_ignored`        | warn     | LPM CLI does not enforce the `engines.pnpm` version. &#x2A;*Fix:** Remove the field or accept that LPM CLI ignores it. `engines.node` and `engines.lpm` remain active.                                                                                       |
| `engines_yarn_ignored`        | warn     | LPM CLI does not enforce the `engines.yarn` version. &#x2A;*Fix:** Remove the field or accept that LPM CLI ignores it. `engines.node` and `engines.lpm` remain active.                                                                                       |
| `engines_bun_ignored`         | warn     | LPM CLI does not enforce the `engines.bun` version. &#x2A;*Fix:** Use `lpm.json > runtime.bun` for managed Bun. LPM CLI still enforces `engines.node` and `engines.lpm`.                                                                                     |

## JSON output and exit codes [#json-output-and-exit-codes]

Use `--json` for CI and scripts:

```bash
lpm doctor --all --json
```

```json
{
  "success": true,
  "mode": "fast",
  "no_failures": false,
  "clean": false,
  "has_warnings": false,
  "passed": 0,
  "failed": 1,
  "warnings": 0,
  "fixes_applied": [],
  "checks": [
    {
      "code": "node_modules_missing",
      "check": "node_modules",
      "passed": false,
      "severity": "fail",
      "detail": "not found — run: lpm install"
    }
  ]
}
```

In doctor JSON, `success: true` means that LPM CLI created the report. It does not mean that every check passed.

Use these fields for automation:

| Field                          | Meaning                                           |
| ------------------------------ | ------------------------------------------------- |
| `mode`                         | `fast` or `all`                                   |
| `no_failures`                  | `true` when no check failed                       |
| `clean`                        | `true` when no check failed or produced a warning |
| `has_warnings`                 | `true` when at least one warning exists           |
| `passed`, `failed`, `warnings` | Result counts                                     |
| `checks`                       | All emitted results                               |
| `fixes_applied`                | Fixes that doctor applied                         |

Match on `checks[].code`. The `check` and `detail` text can change.

Doctor uses these exit codes:

| Result                    | Exit code |
| ------------------------- | --------- |
| No failures               | `0`       |
| Warnings without failures | `0`       |
| One or more failures      | `1`       |

## Registry health [#registry-health]

`lpm health` sends one health request to the selected registry. It does not check the current project.

```bash
lpm health
lpm health --json
```

```json
{
  "success": true,
  "healthy": true,
  "registry_url": "https://lpm.dev",
  "response_time_ms": 87
}
```

The command does not collect repeated measurements. The `response_time_ms` value is the time for one request.

Human and JSON output have different behavior for an unhealthy response:

| Registry result            | Human output      | JSON output                                      |
| -------------------------- | ----------------- | ------------------------------------------------ |
| Healthy                    | Exit code `0`     | `healthy: true`, exit code `0`                   |
| Registry reports unhealthy | Nonzero exit code | `success: true`, `healthy: false`, exit code `0` |
| Request fails              | Nonzero exit code | Nonzero exit code                                |

For JSON automation, read `healthy`. Do not use `success` or the exit code as the only health signal.

## Recovery [#recovery]

### Repair project install state [#repair-project-install-state]

If doctor reports a missing install or dependency drift, run [lpm install](/docs/packages/install). Then run doctor again.

```bash
lpm install
lpm doctor
```

### Repair a linked project `node_modules` [#repair-a-linked-project-node_modules]

The `node_modules_symlinked` check reports a project-level symlink or directory junction. LPM CLI does not install through this path.

```bash
lpm doctor --fix
lpm install
lpm doctor
```

The fix removes only the link entry. It creates a real `node_modules` directory and does not change the linked target.

Do not run `lpm cache prune` or `lpm store clean` for this error. The global store is not the cause.

### Resolve registry or authentication failures [#resolve-registry-or-authentication-failures]

Run the registry check first:

```bash
lpm health
```

If the registry responds, view the current account:

```bash
lpm whoami
```

If the account is not correct, sign in again:

```bash
lpm login
```

Read [lpm login](/docs/infra/login) for authentication options.

### Read the remediation for a check [#read-the-remediation-for-a-check]

Use the code from doctor output:

```bash
lpm doctor list --code <CODE>
```

## Flags [#flags]

### `lpm doctor` [#lpm-doctor]

| Flag          | Effect                                                     |
| ------------- | ---------------------------------------------------------- |
| `--all`       | Run the fast and extended checks                           |
| `--fix`       | Show a plan, then ask once before applying supported fixes |
| `-y`, `--yes` | Imply `--fix` and skip the confirmation prompt             |
| `--json`      | Print structured output                                    |

If you use `lpm doctor list`, doctor ignores `--all`, `--fix`, and `--yes`.

### `lpm doctor list` [#lpm-doctor-list]

| Flag                     | Effect                                               |
| ------------------------ | ---------------------------------------------------- |
| `--code <CODE>`          | Select one exact check code                          |
| `--category <SUBSTRING>` | Select category names that contain the supplied text |
| `--json`                 | Print the catalog as structured output               |

### `lpm health` [#lpm-health]

| Flag               | Effect                          |
| ------------------ | ------------------------------- |
| `--registry <URL>` | Examine a specific registry URL |
| `--json`           | Print structured output         |

## See also [#see-also]

* [Install packages](/docs/packages/install)
* [Sign in to a registry](/docs/infra/login)
* [Configure tunnel domains](/docs/infra/tunnel)
* [Manage the package store](/docs/infra/store)
* [Manage policy extensions](/docs/infra/policy)
* [Review global flags](/docs/commands#global-flags)


# lpm hosts (/docs/infra/hosts)



LPM CLI manages system hosts-file entries for local hostnames that you define in `lpm.json`.

[`lpm dev`](/docs/dev/dev) adds and removes these entries during a development session. Use `lpm hosts clean` only to remove stale entries.

```bash
lpm hosts [action] [flags]
```

The only action is `clean`. A bare `lpm hosts` command equals `lpm hosts clean`.

## Quickstart with `lpm.json` [#quickstart-with-lpmjson]

Add a local hostname to the `proxy` block:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "proxy": {
    "host": "app.test",
    "port": 9443,
    "httpRedirect": false
  }
}
```

Start the project:

```bash
lpm dev
```

Before the hosts file changes, LPM CLI shows the file, backup, and hostname. Then it asks for your consent.

After approval and successful file access, LPM CLI adds a project block that maps `app.test` to `127.0.0.1`:

```text
# >>> lpm:project-... >>>
127.0.0.1 app.test
# <<< lpm:project-... <<<
```

At normal development-session exit, LPM CLI removes this block. It preserves entries that it does not manage.

For multiple services, add a `host` to each service:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "proxy": {
    "port": 9443,
    "httpRedirect": false
  },
  "services": {
    "web": {
      "command": "vite",
      "host": "web.test"
    },
    "api": {
      "command": "node api.js",
      "host": "api.test"
    }
  }
}
```

```bash
lpm dev
```

LPM CLI adds both hostnames to one block for the project.

See [`lpm proxy`](/docs/infra/proxy) for route and listener configuration.

## Avoid hosts-file changes [#avoid-hosts-file-changes]

If you do not want a system hosts-file entry, use `localhost` or a name below `.localhost`:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "proxy": {
    "host": "app.localhost",
    "port": 9443,
    "httpRedirect": false
  }
}
```

The operating system resolves `localhost` and `*.localhost` without a managed entry.

Other configured hostnames require an entry. Common local suffixes include `.test`, `.local`, `.internal`, and `.home.arpa`.

## Non-interactive development [#non-interactive-development]

Use `--yes` to approve the hosts-file change without a prompt:

```bash
lpm dev --yes
```

The `--yes` flag approves the change, but it does not grant file permission.

In a non-interactive shell, the process must already have permission to write the hosts file. Automatic elevation requires an interactive terminal.

If a CI environment cannot change the system hosts file, use a `.localhost` name.

## Examples [#examples]

```bash
lpm dev                          # manage entries from lpm.json
lpm dev --yes                    # approve changes without a prompt
lpm hosts clean                  # remove stale LPM CLI blocks
lpm hosts clean --yes            # clean without a prompt
lpm hosts clean --yes --json     # clean and write JSON output
```

## Remove stale entries [#remove-stale-entries]

A crash or forced process stop can leave a managed block in the hosts file. Remove all stale LPM CLI blocks with:

```bash
lpm hosts clean
```

The command shows the hosts-file path, backup path, and number of blocks. It asks for consent before it changes the file.

> CAUTION: Stop active `lpm dev` sessions before you run `lpm hosts clean`. The command removes LPM CLI blocks for all projects.

The command removes only complete blocks with LPM CLI markers. It preserves normal hosts-file entries and blocks from other tools.

If the hosts file has no LPM CLI blocks, the command exits successfully without a change.

### Clean without a prompt [#clean-without-a-prompt]

Use `--yes` in a script or another non-interactive environment:

```bash
lpm hosts clean --yes
```

The process must already have permission to change the hosts file in a non-interactive environment.

### Use JSON output [#use-json-output]

Combine `--json` with `--yes` for a non-interactive script:

```bash
lpm hosts clean --yes --json
```

The output contains these fields:

| Field           | Meaning                                                     |
| --------------- | ----------------------------------------------------------- |
| `success`       | The command completed without an error.                     |
| `cleaned`       | The command changed the hosts file.                         |
| `removedBlocks` | Number of complete LPM CLI blocks that the command removed. |
| `hostsFile`     | Path to the hosts file.                                     |
| `backupPath`    | Path to the saved backup.                                   |

The `--json` flag does not approve cleanup. If the command cannot show an interactive prompt, add `--yes`.

## Backup behavior [#backup-behavior]

Before the first hosts-file change, LPM CLI copies the file to:

```text
~/.lpm/hosts.bak
```

LPM CLI does not replace an existing backup. As a result, the backup can be older than later manual hosts-file changes.

> CAUTION: Do not automatically replace the current hosts file with this backup. The replacement can remove changes that you made later.

`lpm hosts clean` does not restore the backup. It removes only complete LPM CLI-managed blocks from the current hosts file.

## Permissions and elevation [#permissions-and-elevation]

| Platform        | Hosts file                              | Elevation behavior                                                             |
| --------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
| Linux and macOS | `/etc/hosts`                            | LPM CLI uses `sudo` after a direct-access failure in an interactive terminal.  |
| Windows         | `C:\Windows\System32\drivers\etc\hosts` | In an interactive terminal, LPM CLI requests Administrator access through UAC. |

Consent occurs before elevation. If you cancel elevation, LPM CLI leaves the hosts file unchanged.

Do not run the complete development command with elevated access only to avoid the consent prompt. Use `--yes` for consent.

## Repair an incomplete block [#repair-an-incomplete-block]

`lpm hosts clean` refuses to change a hosts file that contains an unterminated LPM CLI block. This behavior protects unrelated entries from accidental removal.

If this error occurs, open the system hosts file with elevated access. Find the opening marker that does not have a matching closing marker:

```text
# >>> lpm:project-... >>>
```

Review the lines below the marker. Then add the matching closing marker, or remove the incomplete block manually:

```text
# <<< lpm:project-... <<<
```

After you repair the markers, run cleanup again:

```bash
lpm hosts clean
```

## Flags [#flags]

| Flag          | Purpose                                                                              |
| ------------- | ------------------------------------------------------------------------------------ |
| `-y`, `--yes` | Approve removal without an interactive prompt.                                       |
| `--json`      | Write machine-readable output. This is a [global flag](/docs/commands#global-flags). |

## See also [#see-also]

* [`lpm dev`](/docs/dev/dev) — manage local hostnames during development
* [`lpm proxy`](/docs/infra/proxy) — route local hostnames to development services
* [`lpm cert`](/docs/infra/cert) — manage certificates for local HTTPS
* [`lpm.json`](/docs/reference/lpm-json#proxy) — configure proxy and service hostnames


# Infra (/docs/infra)





LPM CLI ships the account, settings, security, and development infrastructure most projects bolt together from separate tools as first-class commands. Everything in this section assumes [`lpm dev`](/docs/dev/dev) is your default; these commands are what `lpm dev` orchestrates under the hood and what you reach for when you need to inspect, configure, or fix something it touched.

## CLI [#cli]

<Cards>
  <Card title="lpm tunnel" href="/docs/infra/tunnel" description="Expose localhost to the internet. Claim a stable domain, capture and replay webhooks." />

  <Card title="lpm cert" href="/docs/infra/cert" description="Local HTTPS — generate certificates, install the LPM CLI CA into your trust store, inspect status." />

  <Card title="lpm proxy" href="/docs/infra/proxy" description="Run and inspect the local-domain proxy daemon and active route table." />

  <Card title="lpm hosts" href="/docs/infra/hosts" description="Remove LPM CLI-managed local-domain entries from the hosts file." />

  <Card title="lpm ports" href="/docs/infra/ports" description="List, kill, or reset dev-service port assignments." />
</Cards>

## Account + auth [#account--auth]

<Cards>
  <Card title="lpm login / logout / whoami" href="/docs/infra/login" description="Sign in, clear registry tokens, and inspect the active account identity." />

  <Card title="lpm setup" href="/docs/infra/setup" description="Generate scoped .npmrc authentication for npm-compatible CI clients and explain local Registry auth." />

  <Card title="lpm token-rotate" href="/docs/infra/token-rotate" description="Rotate the active LPM.dev Registry session token and store the replacement securely." />
</Cards>

## Settings + security [#settings--security]

<Cards>
  <Card title="lpm config" href="/docs/infra/config" description="Get, set, delete, or list user-level CLI configuration." />

  <Card title="lpm security" href="/docs/infra/security" description="Temporary approvals, effective machine floor, and managed security policy." />

  <Card title="lpm policy" href="/docs/infra/policy" description="Inspect and test local install-time policy extensions." />
</Cards>

## CLI maintenance [#cli-maintenance]

<Cards>
  <Card title="lpm doctor" href="/docs/infra/doctor" description="Project + environment health check. Auto-fix common issues." />

  <Card title="lpm store" href="/docs/infra/store" description="Verify, list, or garbage-collect the global content-addressable store." />

  <Card title="lpm self-update" href="/docs/infra/self-update" description="Update the LPM CLI using the right command for the current install method." />
</Cards>

## Features [#features]

<Cards>
  <Card title="Authentication" href="/docs/infra/authentication" description="Registry auth model and secure-storage backend behavior." />

  <Card title="Tunneling" href="/docs/infra/tunneling" description="Stable public domains, webhook capture, replay, and tunnel configuration." />

  <Card title="Local HTTPS" href="/docs/infra/local-https" description="Certificate authority setup and local HTTPS trust model." />

  <Card title="Secrets vault" href="/docs/infra/secrets-vault" description="Encrypted environment storage, cloud sync, pairing, sharing, and rotation." />

  <Card title="Port management" href="/docs/infra/port-management" description="How LPM CLI reserves, reuses, and resets service ports." />

  <Card title="Project health" href="/docs/infra/project-health" description="What project-level health checks inspect and how to fix common failures." />
</Cards>


# Local HTTPS (/docs/infra/local-https)



Local HTTPS gives your development site a browser-trusted HTTPS URL. LPM CLI handles TLS in front of your application.

Your application continues to use plain HTTP. Use local HTTPS for secure cookies, OAuth callbacks, service workers, secure WebSockets, and browser features that require a trusted origin.

The trust applies only to devices that trust your local certificate authority (CA).

## How local HTTPS works [#how-local-https-works]

LPM CLI accepts the HTTPS or secure WebSocket connection. It sends the request to your application over plain HTTP or WebSocket.

```text
Browser HTTPS/WSS → LPM CLI → Application HTTP/WS
```

LPM CLI creates one root CA for each machine. This CA signs the certificates for your local projects.

```text
Per-machine root CA → Project certificate → Browser trust
```

The root CA is not part of LPM CLI. Its private key stays on your machine.

If you run `lpm dev --https --port 4000`, the browser connects to port `4000`. LPM CLI gives the application a separate managed port.

Your application must use plain HTTP in this workflow. Disable framework HTTPS and let LPM CLI manage TLS.

## Choose a workflow [#choose-a-workflow]

| Goal                              | Configuration or command      |
| --------------------------------- | ----------------------------- |
| Use HTTPS for the project         | `"https": true` in `lpm.json` |
| Use HTTPS for one run             | `lpm dev --https`             |
| Disable project HTTPS for one run | `lpm dev --no-https`          |
| Prepare trust before development  | `lpm cert trust`              |

## Enable HTTPS in `lpm.json` [#enable-https-in-lpmjson]

Add `https: true` to the `lpm.json` file next to `package.json`:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "https": true
}
```

Start the project:

```bash
lpm dev
```

LPM CLI creates the project certificate and starts the HTTPS frontend. It renews the project certificate before it expires.

Use a command-line override for one run:

```bash
lpm dev --https       # enable HTTPS for this run
lpm dev --no-https    # disable HTTPS for this run
```

## Approve the local CA [#approve-the-local-ca]

The first HTTPS run can require approval to install the local CA. LPM CLI asks only when the trust-store installation is necessary.

Approve the installation in an interactive terminal. If you decline, LPM CLI stops the HTTPS development run.

For an approved non-interactive run, use `--yes`:

```bash
lpm dev --https --yes
```

If `lpm.json` enables HTTPS, use this command:

```bash
lpm dev --yes
```

You can also prepare the machine before a development run:

```bash
lpm cert trust
lpm dev
```

To remove the CA from the trust store, run:

```bash
lpm cert uninstall
```

This command keeps the CA files on disk. You can trust the same CA again with `lpm cert trust`.

## Use project hostnames [#use-project-hostnames]

If one local service needs a project hostname, use `proxy.host`:

```json title="lpm.json"
{
  "https": true,
  "proxy": {
    "host": "app.localhost",
    "port": 9443,
    "httpRedirect": false
  }
}
```

```bash
lpm dev
```

Open the HTTPS URL that `lpm dev` prints. For a project with multiple services, give each service a different `services.<name>.host` value.

Names below `.localhost` usually need no hosts-file entry. Names below these suffixes can require a managed hosts-file entry:

* `.local`
* `.test`
* `.internal`
* `.home.arpa`

See [`lpm proxy`](/docs/infra/proxy) and [`lpm hosts`](/docs/infra/hosts) for local routing and hosts-file management.

You can add a certificate hostname manually:

```bash
lpm cert generate --host api.app.local
```

This command adds a certificate name. It does not create a proxy route or a hosts-file entry.

## Permit more DNS names [#permit-more-dns-names]

The `cert` block controls the DNS subtrees that a project certificate can cover:

```json title="lpm.json"
{
  "https": true,
  "cert": {
    "extraPermittedDns": ["myapp.internal"],
    "allowPublicDns": false
  }
}
```

`extraPermittedDns` expands the permitted DNS subtrees. It does not add a certificate hostname, browser URL, proxy route, or hosts-file entry.

Add the actual browser hostname with one of these options:

* `proxy.host`
* `services.<name>.host`
* `lpm cert generate --host`

Public DNS names require an explicit opt-in:

```json title="lpm.json"
{
  "cert": {
    "extraPermittedDns": ["staging.example.com"],
    "allowPublicDns": true
  }
}
```

> CAUTION: Enable `allowPublicDns` only for DNS names that you control. This field increases the authority of your local CA.

## Protect the local CA [#protect-the-local-ca]

The root CA is unique to your machine. LPM CLI does not ship or download a shared root CA.

Anyone with the root CA private key can create certificates that your machine trusts. Do not copy or commit this key.

On Unix systems, certificate directories must use mode `0700`. Private keys must use mode `0600`.

LPM CLI refuses to sign with a group-readable or world-readable CA private key. Run `lpm cert status` to get the repair command.

Project certificates for custom hostnames use a constrained project chain. The requested DNS permissions limit that project chain.

## Platform and browser behavior [#platform-and-browser-behavior]

| Platform | Trust-store behavior                                                         |
| -------- | ---------------------------------------------------------------------------- |
| macOS    | LPM CLI uses the user login Keychain. The operation does not require `sudo`. |
| Linux    | LPM CLI uses the system CA store. The operation requires `sudo`.             |
| Windows  | LPM CLI uses the Root certificate store. Windows can request UAC approval.   |

Chrome, Edge, Safari, and many Linux browsers use the operating-system trust store.

Firefox can use a separate trust store. If Firefox shows a warning, use one of these options:

* Set `security.enterprise_roots.enabled` to `true` in `about:config`.
* Import `~/.lpm/certs/rootCA.pem` as a certificate authority in Firefox.

## Use HTTPS on another device [#use-https-on-another-device]

A phone or another computer must trust your local CA before it accepts the project certificate.

Start a network development server with CA bootstrap enabled:

```bash
lpm dev --https --network --allow-ca-bootstrap
```

LPM CLI prints a plain HTTP URL for CA installation. Open that URL on the other device, then install and trust the CA.

CA bootstrap is off by default. The operating system selects its LAN port.

> CAUTION: Anyone on the local network can download the CA certificate while the bootstrap server runs. The private key does not leave your machine.

Without CA bootstrap, copy `~/.lpm/certs/rootCA.pem` to the device through a trusted method. Then install and trust the certificate manually.

## Certificate files [#certificate-files]

| Scope   | Files                                                              |
| ------- | ------------------------------------------------------------------ |
| Machine | `~/.lpm/certs/rootCA.pem` and `~/.lpm/certs/rootCA-key.pem`        |
| Project | `<project>/.lpm/certs/cert.pem` and `<project>/.lpm/certs/key.pem` |

Do not commit the generated project files. If the project does not ignore `.lpm/`, add it to `.gitignore`.

The default project certificate contains these names:

* `localhost`
* `127.0.0.1`
* `::1`

## Renewal and rotation [#renewal-and-rotation]

LPM CLI creates a new project certificate in these cases:

* The certificate is missing.
* The certificate expires soon.
* The certificate does not contain a requested hostname.
* The certificate is not compatible with the active CA.

Project certificate renewal starts within 30 days of expiry. Root CA warnings start within 60 days and become urgent within 30 days.

LPM CLI never rotates the root CA automatically because rotation changes machine trust. When `lpm cert status` reports an expiry warning, rotate the CA:

```bash
lpm cert rotate
```

If rotation stops before it is complete, reconcile its state:

```bash
lpm cert reconcile --dry-run
lpm cert reconcile
```

The reconcile command also removes expired grace entries.

## Common problems [#common-problems]

### The browser shows a certificate warning [#the-browser-shows-a-certificate-warning]

Show the trust and certificate status:

```bash
lpm cert status
```

If the CA is not trusted, install it:

```bash
lpm cert trust
```

If only Firefox shows the warning, configure Firefox to use the operating-system trust store or import the CA.

### The browser reports a hostname mismatch [#the-browser-reports-a-hostname-mismatch]

Make sure that the URL hostname is in the project certificate. Add it with `proxy.host`, `services.<name>.host`, or `lpm cert generate --host`.

`extraPermittedDns` is not a certificate hostname. It only permits certificates below the configured DNS subtree.

### LPM CLI reports unsafe permissions [#lpm-cli-reports-unsafe-permissions]

Run `lpm cert status`. Then run the exact permission-repair command from its output.

### The root CA expires soon [#the-root-ca-expires-soon]

Run `lpm cert rotate`. LPM CLI replaces the CA and reissues the known project certificates.

### Root CA rotation stopped [#root-ca-rotation-stopped]

Preview the repair with `lpm cert reconcile --dry-run`. Then run `lpm cert reconcile`.

### Another device does not trust the site [#another-device-does-not-trust-the-site]

Install and trust `rootCA.pem` on that device. Use CA bootstrap or copy the certificate through a trusted method.

## Local HTTPS or a tunnel? [#local-https-or-a-tunnel]

If the site stays on your computer or local network, use local HTTPS. Each client device must trust your local CA.

If a remote person or service needs a public URL, use a [tunnel](/docs/infra/tunneling).

## See also [#see-also]

* [`lpm cert`](/docs/infra/cert) — manage the local CA and project certificates
* [`lpm dev`](/docs/dev/dev) — start the development server
* [`lpm proxy`](/docs/infra/proxy) — configure local hostnames and proxy ports
* [`lpm hosts`](/docs/infra/hosts) — manage local hosts-file entries
* [Tunneling](/docs/infra/tunneling) — expose a local service through a public URL
* [`lpm.json`](/docs/reference/lpm-json) — configure project HTTPS


# lpm login, logout, and whoami (/docs/infra/login)



Use these commands to manage registry credentials for LPM CLI.

```bash
lpm login [--npm | --github | --gitlab | --login-registry <URL>] [--token <TOKEN>]
lpm login (--github | --gitlab) --save-env-token
lpm whoami
lpm logout [--npm | --github | --gitlab | --logout-registry <URL> | --all] [--revoke]
```

For example, sign in to the LPM.dev Registry and confirm the active account:

```bash
lpm login
lpm whoami
```

You do not need to sign in to install public packages. Sign in for private packages, publishing, and other protected registry actions.

`lpm l` is an alias for `lpm login`. `lpm lo` is an alias for `lpm logout`.

## Choose a registry [#choose-a-registry]

| Registry                       | Command                            | Use it when                                                                                       |
| ------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------- |
| LPM.dev Registry               | `lpm login`                        | You use private packages, publishing, organizations, or other protected LPM.dev Registry features |
| npm                            | `lpm login --npm`                  | You need saved npm credentials for installs or publishing                                         |
| GitHub Packages                | `lpm login --github`               | You use an environment token, GitHub CLI authentication, or a saved fallback token                |
| GitLab Packages                | `lpm login --gitlab`               | You use an environment token, GitLab CLI authentication, or a saved fallback token                |
| Custom npm-compatible registry | `lpm login --login-registry <URL>` | You need a token for one exact registry URL                                                       |

Choose one registry target for each login command.

## Sign in to the LPM.dev Registry [#sign-in-to-the-lpmdev-registry]

Run `lpm login` when an LPM.dev Registry action requires an account:

```bash
lpm login
```

LPM CLI opens a browser for authentication. Complete the browser flow within two minutes.

If the browser does not open, copy the URL from the terminal into a browser. A valid existing session skips the browser flow.

Use the global `--registry` flag with another LPM-compatible registry:

```bash
lpm login --registry https://registry.example.com
lpm whoami --registry https://registry.example.com
```

This use of `--registry` starts the normal browser login. It is different from `--login-registry`, which stores an npm-compatible registry token.

## Sign in to npm [#sign-in-to-npm]

Start npm web login from an interactive terminal:

```bash
lpm login --npm
```

Complete the npm browser flow within five minutes. LPM CLI saves the token for `https://registry.npmjs.org`.

The command uses credentials in this order:

1. The `--token` value
2. The `NPM_TOKEN` environment variable
3. npm web login

Pass a token when a browser or interactive terminal is not available:

```bash
lpm login --npm --token <TOKEN>
NPM_TOKEN=<TOKEN> lpm login --npm
```

Both commands save the token. They do not configure a custom npm-compatible registry.

In CI, you can give the token directly to the package command. This method does not save the token:

```bash
NPM_TOKEN=<TOKEN> lpm install
NPM_TOKEN=<TOKEN> lpm publish --npm
```

Use npm Trusted Publishing when your CI provider supports it. See [`lpm publish`](/docs/packages/publish#npm-trusted-publishing) for the OIDC setup.

## Sign in to GitHub Packages or GitLab Packages [#sign-in-to-github-packages-or-gitlab-packages]

The login commands report whether an effective credential is available. They do not send the credential to GitHub or GitLab for validation.

LPM CLI uses GitHub credentials in this order:

1. The `--token` value
2. The `GITHUB_TOKEN` environment variable
3. The current `gh` session
4. A token in LPM secure storage

LPM CLI uses GitLab credentials in this order:

1. The `--token` value
2. The `GITLAB_TOKEN` environment variable
3. The `CI_JOB_TOKEN` environment variable
4. The current `glab` session
5. A token in LPM secure storage

Use an existing `gh` or `glab` session:

```bash
gh auth login --hostname github.com
lpm login --github

glab auth login
lpm login --gitlab
```

LPM CLI does not copy the host CLI token into its secure storage.

Environment tokens also satisfy the login commands:

```bash
GITHUB_TOKEN=<TOKEN> lpm login --github
GITLAB_TOKEN=<TOKEN> lpm login --gitlab
CI_JOB_TOKEN=<TOKEN> lpm login --gitlab
```

These commands use the environment token for the current process. They do not save it.

Pass a token to save it as a fallback credential:

```bash
lpm login --github --token <TOKEN>
lpm login --gitlab --token <TOKEN>
```

You can also import a long-lived environment token into secure storage:

```bash
GITHUB_TOKEN=<TOKEN> lpm login --github --save-env-token
GITLAB_TOKEN=<TOKEN> lpm login --gitlab --save-env-token
```

Use `--save-env-token` with one GitHub or GitLab target. Do not combine it with `--token`.

LPM CLI does not save `CI_JOB_TOKEN`. This token is short-lived and limited to one GitLab CI job.

Package commands can use the same environment tokens directly:

```bash
GITHUB_TOKEN=<TOKEN> lpm install
GITLAB_TOKEN=<TOKEN> lpm install
CI_JOB_TOKEN=<TOKEN> lpm install
```

In JSON mode, `source` identifies the selected credential source. Values include `explicit-token`, `env:GITHUB_TOKEN`, `env:GITLAB_TOKEN`, `env:CI_JOB_TOKEN`, `gh`, `glab`, and `stored`.

The `stored` field reports whether LPM owns the credential. Stored credentials also include `storage_backend` and `storage_degraded` values.

A missing import variable returns `credential_import_unavailable`. An attempt to save `CI_JOB_TOKEN` returns `credential_import_rejected`.

## Sign in to a custom registry [#sign-in-to-a-custom-registry]

Use `--login-registry` for a custom npm-compatible registry:

```bash
lpm login --login-registry https://npm.example.com --token <TOKEN>
```

LPM CLI scopes the saved token to the exact URL. Use the same URL that appears in your `.npmrc` file.

In an interactive terminal, you can omit `--token`. LPM CLI asks for the token with masked input and can save an expiry reminder.

```bash
lpm login --login-registry https://npm.example.com
```

In JSON mode or a non-interactive shell, you must pass `--token`.

## Check the active account [#check-the-active-account]

Use `lpm whoami` to check the selected LPM-compatible registry account:

```bash
lpm whoami
lpm whoami --json
```

The command shows:

* The username and a masked email address
* The plan, MFA status, and Pool access
* Storage and private-package usage
* Personal and organization scopes
* Available npm, GitHub, GitLab, and custom-registry credentials
* Credential expiry warnings and storage health

Human output masks the email address. JSON output includes the full email address and structured account data.

The registry list reports available credential sources. `lpm whoami` does not test each third-party credential with its registry.

Without local LPM.dev Registry authentication, human output shows login help and exits successfully. JSON mode returns an authentication error instead.

## Log out [#log-out]

Without flags, `lpm logout` clears only the saved LPM.dev Registry session:

```bash
lpm logout
```

Clear one third-party credential or all saved credentials:

```bash
lpm logout --npm
lpm logout --github
lpm logout --gitlab
lpm logout --logout-registry https://npm.example.com
lpm logout --all
```

GitHub and GitLab logout clears only fallback tokens saved by LPM CLI. It does not sign out of `gh` or `glab`.

## Revoke the LPM.dev Registry session [#revoke-the-lpmdev-registry-session]

Normal logout removes local credentials only. Add `--revoke` to also revoke the LPM.dev Registry session and browser pairings:

```bash
lpm logout --revoke
```

Revoke the LPM.dev Registry session and clear every saved registry credential:

```bash
lpm logout --all --revoke
```

Use `--revoke` with no target or with `--all`. Do not combine it with `--npm`, `--github`, `--gitlab`, or `--logout-registry`.

LPM CLI still tries to clear local credentials if remote revocation fails. The command exits with an error if it cannot complete a selected action.

## If the registry rejects `LPM_TOKEN` [#if-the-registry-rejects-lpm_token]

`LPM_TOKEN` overrides your saved login. If the registry rejects it, LPM CLI reports `LPM_TOKEN was rejected` and exits with an error. JSON output uses the error code `env_token_rejected` and includes recovery guidance in `next_steps`.

[`lpm doctor --all`](/docs/infra/doctor) reports this rejection as an `auth_invalid` check and shows the same recovery steps.

Replace the token, or remove the override to use your saved login:

```bash
unset LPM_TOKEN
lpm whoami
```

The rejected token does not change your saved credentials. If no valid saved session remains, run [`lpm login`](/docs/infra/login#sign-in-to-the-lpmdev-registry).

For CI, update `LPM_TOKEN` in the job or secret configuration. A new browser login does not replace the environment token.

## If `LPM_TOKEN` keeps you signed in [#if-lpm_token-keeps-you-signed-in]

`lpm logout` cannot remove an environment variable from its parent shell. Unset the variable, then run logout again:

```bash
unset LPM_TOKEN
lpm logout
```

For CI, remove `LPM_TOKEN` from the job or secret configuration.

## Login flags [#login-flags]

| Flag                     | Purpose                                                                  |
| ------------------------ | ------------------------------------------------------------------------ |
| `--npm`                  | Sign in to `registry.npmjs.org`                                          |
| `--github`               | Check GitHub CLI authentication or save a GitHub Packages fallback token |
| `--gitlab`               | Check GitLab CLI authentication or save a GitLab Packages fallback token |
| `--login-registry <URL>` | Save a token for one custom npm-compatible registry URL                  |
| `--token <TOKEN>`        | Provide an explicit token for the selected third-party registry          |

## Logout flags [#logout-flags]

| Flag                      | Purpose                                                                      |
| ------------------------- | ---------------------------------------------------------------------------- |
| `--npm`                   | Clear the saved npm token                                                    |
| `--github`                | Clear the saved GitHub Packages fallback token                               |
| `--gitlab`                | Clear the saved GitLab Packages fallback token                               |
| `--logout-registry <URL>` | Clear the token for one custom registry URL                                  |
| `--all`                   | Clear LPM.dev Registry credentials and all saved third-party credentials     |
| `--revoke`                | Revoke the LPM.dev Registry session and browser pairings before local logout |

The global `--registry` and `--json` flags also apply. See [global flags](/docs/commands#global-flags) for details.

## See also [#see-also]

* [Authentication](/docs/infra/authentication) - learn how LPM CLI selects and stores credentials
* [`lpm setup`](/docs/infra/setup) - configure project `.npmrc` authentication
* [`lpm token-rotate`](/docs/infra/token-rotate) - rotate an LPM.dev Registry token
* [`lpm publish`](/docs/packages/publish) - publish to a selected registry
* [Environment variables](/docs/reference/env-vars) - use registry credentials in CI


# lpm policy (/docs/infra/policy)



Use `lpm policy` to inspect and test local policy extensions for package installs.

```bash
lpm policy list [--json]
lpm policy status [--json]
lpm policy doctor [<EXTENSION>] [--json]
lpm policy test <EXTENSION> --package <NAME@VERSION> [--json]
```

A policy extension is a local program that reviews resolved package candidates. It returns an `allow`, `warn`, or `block` decision for each candidate.

For example, an organization can connect LPM CLI to a local deny list or an internal package-approval service. The extension runs before LPM CLI downloads registry tarballs or links packages.

## Quickstart [#quickstart]

Inspect the policy extensions that are active on this computer:

```bash
lpm policy status
```

The output shows the number of active extensions, their modes, and any command-availability errors.

## Choose a command [#choose-a-command]

| Command                                                | Purpose                                              | Use it when                                                                                 |
| ------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `lpm policy list`                                      | Show active extension configuration                  | You want names, commands, modes, error behavior, and timeouts                               |
| `lpm policy status`                                    | Show a policy overview and diagnostics               | You want a read-only summary that always exits successfully after valid configuration loads |
| `lpm policy doctor [EXTENSION]`                        | Diagnose all extensions or one named extension       | You need a CI gate or an exit code for failed diagnostics                                   |
| `lpm policy test <EXTENSION> --package <NAME@VERSION>` | Run one extension with a synthetic package candidate | You want to see its decision before an install                                              |

`list`, `status`, and `doctor` do not run extension programs. `test` runs the selected program.

## When to use a policy extension [#when-to-use-a-policy-extension]

Use a policy extension for a package rule that depends on your own system. Common uses include:

* A company package deny list
* An internal approval or compliance service
* A local license or package-source rule
* A custom warning feed during package evaluation

Policy extensions are separate from [LPM Firewall](/docs/guides/firewall) and the [`lpm security`](/docs/infra/security) floor.

The current policy-extension configuration is local user configuration. It is not part of the signed security floor or an OS-managed policy.

## Recipes [#recipes]

### Review all active extensions [#review-all-active-extensions]

List the resolved configuration first:

```bash
lpm policy list
```

Then review the policy posture and command availability:

```bash
lpm policy status
```

Disabled extensions do not appear in either command.

### Diagnose one extension [#diagnose-one-extension]

Run the diagnostic for one named extension:

```bash
lpm policy doctor local-feed
```

If the configured program is missing, is not a file, or is not executable, the command fails.

Use the unscoped command to diagnose every active extension:

```bash
lpm policy doctor
```

`doctor` exits with code `1` for a failed diagnostic. Report-only mode produces a warning and keeps exit code `0`.

### Test one package candidate [#test-one-package-candidate]

Send a synthetic package candidate to the extension:

```bash
lpm policy test local-feed --package react@19.0.0
```

For a scoped package, LPM CLI uses the final `@` as the version separator:

```bash
lpm policy test local-feed --package @scope/pkg@1.2.3
```

Use one npm-compatible concrete version. LPM converts accepted loose forms to a canonical version before it starts the extension. For example, `v19.0.0` and `19.0` become `19.0.0`.

Ranges such as `^19.0.0` and `~19.0.0` are invalid. Wildcards and dist-tags such as `latest` are also invalid.

The test does not contact a registry or download the package.

The synthetic candidate represents a direct public npm dependency. It has no integrity value or registry publication time.

The command reports `allow`, `warn`, and `block` decisions. A `block` result does not make this test command fail.

### Start in report mode [#start-in-report-mode]

Use report mode while you evaluate a new extension:

```toml title="~/.lpm/config.toml"
[policy.extensions.local-feed]
command = ["/usr/local/bin/lpm-policy-feed", "--deny-list", "/etc/lpm/deny.json"]
mode = "report"
on-error = "warn"
```

Run the diagnostic and a normal install:

```bash
lpm policy doctor local-feed
lpm install
```

Report mode prints warning and block decisions but does not stop the install.

After you review the results, change the extension to enforcement mode:

```toml title="~/.lpm/config.toml"
[policy.extensions.local-feed]
command = ["/usr/local/bin/lpm-policy-feed", "--deny-list", "/etc/lpm/deny.json"]
mode = "enforce"
on-error = "block"
```

Enforcement mode stops the install for a `block` decision.

## Configuration [#configuration]

Configure policy extensions in `~/.lpm/config.toml`:

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

| Key          | Accepted value                    | Default                                            | Purpose                                 |
| ------------ | --------------------------------- | -------------------------------------------------- | --------------------------------------- |
| `command`    | Non-empty string array            | Required for an active extension                   | Select the program and its arguments    |
| `enabled`    | Boolean                           | `true`                                             | Include or disable the extension        |
| `mode`       | `report` or `enforce`             | `report`                                           | Control the effect of package decisions |
| `on-error`   | `warn` or `block`                 | `warn` in report mode, `block` in enforcement mode | Control runner and protocol errors      |
| `timeout-ms` | Integer from `1` through `120000` | `5000`                                             | Limit the complete extension exchange   |
| `events`     | `["package.candidate"]`           | `["package.candidate"]`                            | Select the supported policy event       |

These are the only accepted keys. This rule also applies to disabled extensions.

### Command rules [#command-rules]

The first `command` entry must use one of these forms:

* An absolute executable path
* A program name found in an absolute `PATH` directory

LPM CLI rejects relative executable paths such as `./policy-extension`. It also ignores relative and empty `PATH` entries.

Later `command` entries are program arguments. LPM CLI starts the program directly and does not use a shell.

The program runs in the current project directory during install. `lpm policy test` uses the directory where you run the command.

LPM CLI provides a small process environment. It sends one JSON request on standard input and reads one JSON response from standard output.

Read the [policy-extension protocol](/docs/reference/config-toml#policy-extensions) before you write an extension.

## Project configuration [#project-configuration]

Policy extensions use only `~/.lpm/config.toml`.

You cannot configure them in `lpm.json`, `package.json`, or `lpm.toml`. A cloned repository cannot activate a local program.

This configuration is not part of the signed security floor yet. Protect write access to `~/.lpm/config.toml` with your operating-system permissions.

## How installs use extensions [#how-installs-use-extensions]

LPM CLI runs active extensions after dependency resolution and package filtering. It runs them before registry tarballs are fetched or packages are linked.

Each extension receives package metadata, including the name, version, source, integrity, direct status, and optional status. It does not receive package contents.

The extension can omit candidates that need no decision. Every returned decision must match a candidate by its exact name and version.

| Extension result | Report mode                     | Enforcement mode                |
| ---------------- | ------------------------------- | ------------------------------- |
| `allow`          | Continue                        | Continue                        |
| `warn`           | Print the decision and continue | Print the decision and continue |
| `block`          | Print the decision and continue | Stop the install                |

`on-error` controls program and protocol failures. It does not change package decisions.

| `on-error` value | Result                                   |
| ---------------- | ---------------------------------------- |
| `warn`           | Print the error and continue the install |
| `block`          | Stop the install                         |

The timeout includes the standard-input write, process exit, output reads, and response validation.

Warm lockfile and offline installs run the same policy extensions before linking. A warm install cannot bypass a local policy extension.

Active extensions delay registry tarball prefetch until their decisions pass. Direct remote tarball URL dependencies are not supported while an extension is active.

## Diagnostics [#diagnostics]

`lpm policy status` and `lpm policy doctor` report these diagnostic codes:

| Code                                   | Severity | Meaning                                             |
| -------------------------------------- | -------- | --------------------------------------------------- |
| `policy_extensions_not_configured`     | `pass`   | No active extension exists                          |
| `policy_extensions_configured`         | `pass`   | One or more active extensions exist                 |
| `policy_extension_report_mode`         | `warn`   | An active extension cannot stop an install          |
| `policy_extension_command_unavailable` | `fail`   | The configured program is missing or not executable |
| `policy_extension_config_invalid`      | `fail`   | LPM CLI cannot load the extension configuration     |

Only `doctor` converts a configuration-load error into `policy_extension_config_invalid`. `status` stops and returns the configuration error directly.

`status` reports failed diagnostics but returns exit code `0`. If automation must fail for these diagnostics, use `doctor`.

## Recovery [#recovery]

### The configuration does not load [#the-configuration-does-not-load]

Run the doctor command to get the configuration error:

```bash
lpm policy doctor
```

Remove unknown keys and correct invalid value types in `~/.lpm/config.toml`. Then run the command again.

### The command is unavailable [#the-command-is-unavailable]

Inspect the resolved command:

```bash
lpm policy list
```

Use an absolute executable path, or add the program to an absolute `PATH` directory. On Unix, make sure that the file is executable.

### The extension returns invalid JSON [#the-extension-returns-invalid-json]

Run the extension with one synthetic candidate:

```bash
lpm policy test local-feed --package react@19.0.0
```

Make sure that standard output contains only one valid response document. Send logs to standard error.

The response must use schema version `1` and contain only known fields. Read the [complete response schema](/docs/reference/config-toml#policy-extensions).

### A remote tarball dependency is rejected [#a-remote-tarball-dependency-is-rejected]

Replace the remote tarball URL with a supported package source. Active extensions cannot evaluate that URL before download.

## JSON output [#json-output]

Use JSON output for automation:

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

### List output [#list-output]

```json
{
  "success": true,
  "enabled_count": 1,
  "extensions": [
    {
      "name": "local-feed",
      "command": ["/usr/local/bin/lpm-policy-feed", "--deny-list", "/etc/lpm/deny.json"],
      "mode": "enforce",
      "on_error": "block",
      "timeout_ms": 5000,
      "events": ["package.candidate"]
    }
  ]
}
```

### Status and doctor output [#status-and-doctor-output]

`status --json` and `doctor --json` use the same response shape:

```json
{
  "success": true,
  "enabled": true,
  "enabled_count": 1,
  "enforce_count": 1,
  "report_count": 0,
  "no_failures": true,
  "has_warnings": false,
  "diagnostics": [
    {
      "code": "policy_extensions_configured",
      "severity": "pass",
      "detail": "1 enabled (1 enforce, 0 report)"
    }
  ]
}
```

For `doctor`, `success: true` means that the command produced a diagnostic response. Use `no_failures` and the process exit code for policy health.

### Test output [#test-output]

```json
{
  "success": true,
  "extension": "local-feed",
  "event": "package.candidate",
  "package": {
    "name": "react",
    "version": "19.0.0"
  },
  "duration_ms": 12,
  "allow_count": 0,
  "warn_count": 1,
  "block_count": 0,
  "decisions": [
    {
      "name": "react",
      "version": "19.0.0",
      "action": "warn",
      "code": "local-feed",
      "reason": "review required"
    }
  ]
}
```

## Install JSON [#install-json]

Successful `lpm install --json` output includes counters at `security.policy_extensions`.

Add `--timing` to include the same counters at `timing.policy_extensions`:

```bash
lpm install --json --timing
```

```json
{
  "enabled": true,
  "configured_count": 1,
  "ran_count": 1,
  "candidate_count": 42,
  "duration_ms": 24,
  "allow_count": 40,
  "warn_count": 2,
  "block_count": 0,
  "error_count": 0,
  "extensions": [
    {
      "name": "local-feed",
      "mode": "enforce",
      "on_error": "block",
      "duration_ms": 24,
      "allow_count": 40,
      "warn_count": 2,
      "block_count": 0
    }
  ]
}
```

## Flags [#flags]

| Command  | Argument or flag           | Purpose                                           |
| -------- | -------------------------- | ------------------------------------------------- |
| `doctor` | `[EXTENSION]`              | Diagnose only one named active extension          |
| `test`   | `<EXTENSION>`              | Select one named active extension                 |
| `test`   | `--package <NAME@VERSION>` | Send one exact package candidate to the extension |
| All      | `--json`                   | Print structured JSON                             |

`list` and `status` have no command-specific flags. See the other [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm install`](/docs/packages/install#policy-extensions) - learn where extensions run during install
* [`~/.lpm/config.toml`](/docs/reference/config-toml#policy-extensions) - read the configuration and protocol reference
* [`lpm doctor`](/docs/infra/doctor#policy-extensions) - include policy diagnostics in a broader environment check
* [`lpm security`](/docs/infra/security) - manage the separate signed security floor


# Port management (/docs/infra/port-management)



Port management keeps a multi-service development run usable during a preferred-port conflict.

LPM CLI assigns final service ports, saves local replacements, injects peer addresses, and selects the browser-facing endpoint.

Declare the project topology in `lpm.json`. Use [`lpm ports`](/docs/infra/ports) to inspect or reset machine-local port state.

## Understand the port types [#understand-the-port-types]

One development run can use four types of ports:

| Port type        | Purpose                                                      |
| ---------------- | ------------------------------------------------------------ |
| Preferred port   | The `services.<name>.port` value in `lpm.json`               |
| Assigned port    | The port that the child service uses after conflict handling |
| Public port      | The browser-facing HTTPS, proxy, or network listener         |
| Readiness target | The `readyPort` or `readyUrl` used during startup            |

These ports can have the same number in a simple HTTP project. HTTPS and proxy workflows often use separate public and child ports.

## Configure services in `lpm.json` [#configure-services-in-lpmjson]

Define each development service below `services`:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "services": {
    "db": {
      "command": "docker compose up postgres",
      "port": 5432,
      "readyTimeout": 60
    },
    "api": {
      "command": "node api.js",
      "port": 4000,
      "readyUrl": "http://localhost:4000/health",
      "dependsOn": ["db"]
    },
    "web": {
      "command": "vite",
      "port": 5173,
      "dependsOn": ["api"],
      "primary": true
    }
  }
}
```

Start the complete service graph:

```bash
lpm dev
```

The example starts `db`, then `api`, then `web`. LPM CLI starts independent services in the same dependency level together.

### Service fields related to ports [#service-fields-related-to-ports]

| Field          | Purpose                                                            |
| -------------- | ------------------------------------------------------------------ |
| `port`         | Preferred listener port for the service                            |
| `readyPort`    | Separate TCP readiness target                                      |
| `readyUrl`     | HTTP readiness target that must return a 2xx response              |
| `readyTimeout` | Maximum readiness wait, in seconds. The default is `30`.           |
| `dependsOn`    | Services that enter their ready state before this service starts   |
| `primary`      | Service that receives browser, HTTPS, network, and tunnel handling |
| `host`         | Friendly hostname that routes to the final assigned port           |
| `restart`      | Restart the service after a non-zero exit                          |
| `cwd`          | Working directory relative to the project root                     |

See the [`lpm.json` service reference](/docs/reference/lpm-json#servicesname) for every field.

## Assign service ports [#assign-service-ports]

LPM CLI assigns ports before it starts the service commands. A service gets a managed port in these cases:

* The service has a `port` value.
* The service is the primary service.
* The service has a `host` value.

If `port` is absent, the primary and host-routed services still receive a port. Automatic assignment starts at port `3000` and selects the next available port.

LPM CLI passes the final port through `PORT`. It also adds framework-specific port arguments where the framework requires them.

For example, Vite receives a strict assigned port. Next.js, Nuxt, SvelteKit, Remix, and Astro receive their supported port argument.

A service with only `readyPort` does not receive a managed service port. Its command must start the process that owns the readiness target.

## Replace a busy port [#replace-a-busy-port]

If a preferred port is busy, LPM CLI selects the next available port. It reports available process-owner information.

For example, a preferred port of `5173` can become `5174`:

```text
web → :5174 (port 5173 in use by another process)
```

LPM CLI does not edit `lpm.json` and does not stop the existing process. It saves the replacement for this project in:

```text
~/.lpm/ports.toml
```

The next `lpm dev` run tries the saved replacement before the preferred port. The service can continue to use `5174` after `5173` becomes free.

Concurrent `lpm dev` runs reserve assigned ports. This reservation prevents two LPM CLI processes from selecting the same replacement.

### Return to the preferred port [#return-to-the-preferred-port]

Inspect the preferred port before you stop its owner:

```bash
lpm ports inspect 5173
```

If the owner can stop safely, stop it and clear the saved replacement:

```bash
lpm ports kill 5173
lpm ports reset
lpm dev
```

`lpm ports reset` clears only the saved replacements for the current project. It does not stop processes or edit `lpm.json`.

If the preferred port remains busy, the next development run selects another replacement.

## Choose the primary service [#choose-the-primary-service]

The primary service provides the main development endpoint. It receives these features:

* Browser-open handling
* Local HTTPS
* Local network access
* Tunnel forwarding
* Top-level `proxy.host` routing

A project with one service has an implicit primary service.

If a multi-service project uses a main public endpoint, set `primary: true` on one service.

These options require one primary service in a multi-service project:

* `--port`
* `--https`
* `--network`
* `--tunnel`

More than one primary service causes an error.

## Separate public and child ports [#separate-public-and-child-ports]

Without HTTPS, `--port` requests the assigned port for the primary child service:

```bash
lpm dev --port 4000
```

With HTTPS, the same value belongs to the browser-facing TLS frontend:

```bash
lpm dev --https --port 4000
```

In the HTTPS example, the browser connects to port `4000`. The primary child receives a separate managed port and continues to use plain HTTP.

The local proxy has its own public listener. A tunnel has a public URL that forwards to the verified primary child endpoint.

See [Local HTTPS](/docs/infra/local-https), [`lpm proxy`](/docs/infra/proxy), and [Tunneling](/docs/infra/tunneling) for those public frontends.

## Connect services with environment variables [#connect-services-with-environment-variables]

LPM CLI gives each service the final ports of its peers:

```text
For service "web":
  API_PORT=4000
  API_URL=http://localhost:4000
  DB_PORT=5432
  DB_URL=http://localhost:5432

For service "api":
  WEB_PORT=5173
  WEB_URL=http://localhost:5173
  DB_PORT=5432
  DB_URL=http://localhost:5432
```

The variable pattern is `{SERVICE_NAME}_PORT` and `{SERVICE_NAME}_URL`. LPM CLI converts the service name to uppercase and changes hyphens to underscores.

For example, a service named `my-api` becomes `MY_API_PORT` and `MY_API_URL`.

A service does not receive variables for itself. It receives variables only for peers with an assigned service port.

The values use the final assigned ports. A replacement port appears in these variables automatically.

Peer URLs continue to use `http://localhost:<port>` during local HTTPS. LPM CLI terminates browser HTTPS in front of the plain HTTP services.

Service-specific `env` values are also available:

```json title="lpm.json"
{
  "services": {
    "api": {
      "command": "node api.js",
      "port": 4000,
      "env": {
        "LOG_LEVEL": "debug"
      }
    }
  }
}
```

## Wait for service readiness [#wait-for-service-readiness]

For a managed port, LPM CLI waits for a new listener that belongs to the launched service process.

This ownership rule prevents an unrelated process from satisfying the managed-port readiness step.

You can add an application readiness target:

| Configuration                       | Behavior                                                                        |
| ----------------------------------- | ------------------------------------------------------------------------------- |
| Managed `port` only                 | Wait for the assigned listener and make sure that the launched process owns it. |
| `readyPort`                         | Poll the separate TCP port after managed-listener ownership.                    |
| `readyUrl`                          | Poll the URL until it returns a 2xx response.                                   |
| No managed port or readiness target | Use a short launch grace period.                                                |

If both `readyUrl` and a distinct `readyPort` exist, `readyUrl` is the application readiness target.

LPM CLI also recognizes advertised loopback URLs in service output. It preserves the advertised scheme and base path after it verifies the listener.

The readiness wait uses `readyTimeout`. The default is 30 seconds.

If a service fails a readiness check, LPM CLI stops the initial startup.

LPM CLI terminates each service that it started and returns an error.

If a `dependsOn` service fails readiness, LPM CLI does not start the dependent service.

## Start services in dependency order [#start-services-in-dependency-order]

`dependsOn` defines the initial startup order:

```json title="lpm.json"
{
  "services": {
    "db": { "command": "docker compose up postgres", "port": 5432 },
    "api": { "command": "node api.js", "port": 4000, "dependsOn": ["db"] },
    "web": { "command": "vite", "port": 5173, "dependsOn": ["api"] }
  }
}
```

LPM CLI reports an error for a missing dependency or a dependency cycle.

Independent branches start together. A dependent service starts only after all its declared dependencies are ready.

During shutdown, LPM CLI stops dependents before their dependencies. This order gives dependent services time to close active connections.

## Restart crashed services [#restart-crashed-services]

Set `restart: true` for a service that must restart after a non-zero exit:

```json title="lpm.json"
{
  "services": {
    "worker": {
      "command": "node worker.js",
      "restart": true
    }
  }
}
```

LPM CLI uses increasing restart delays from 1 second to 30 seconds. It stops after 10 failed restart attempts.

If the service stays active for more than 60 seconds, the next crash starts a new retry sequence.

Before LPM CLI restarts a service, each direct `dependsOn` service must be ready.

If a dependency restarts, LPM CLI stops its active transitive dependents in reverse dependency order. These dependents wait for recovery.

After the dependency is ready, LPM CLI restarts the waiting dependents in dependency order. Each dependency level must become ready before the next level starts.

If a restarted service fails readiness, LPM CLI stops that process. Then it schedules another attempt with the existing restart delay.

Dashboard restarts use the same dependency rules. A service that you stop from the dashboard remains stopped during dependency recovery.

If a dependency exits or cannot restart, LPM CLI stops its transitive dependents. LPM CLI does not resume them automatically.

## Inspect port state [#inspect-port-state]

Use the complete command syntax:

```bash
lpm ports [action] [target]
```

Common read-only commands:

```bash
lpm ports                 # show ports for this project
lpm ports all             # show visible TCP listeners
lpm ports inspect 5173    # inspect one port
```

Reset machine-local replacements:

```bash
lpm ports reset
```

For an explicit service `port`, `lpm ports` reports the configured port. It does not replace that row with a saved conflict port.

Use the `lpm dev` startup output or `lpm ports all` to find the active replacement. A host-only service can appear with its saved assigned port.

See [`lpm ports`](/docs/infra/ports) for inspection output, process termination, JSON output, and platform differences.

## Common problems [#common-problems]

### `lpm dev` uses a different port on every restart [#lpm-dev-uses-a-different-port-on-every-restart]

Run `lpm ports all` to find listeners near the requested port. Concurrent development processes can make LPM CLI select another replacement.

### A service stays on its replacement after the preferred port is free [#a-service-stays-on-its-replacement-after-the-preferred-port-is-free]

Clear the project replacement, then restart the development run:

```bash
lpm ports reset
lpm dev
```

### `lpm ports` shows the preferred port, but `lpm dev` uses another port [#lpm-ports-shows-the-preferred-port-but-lpm-dev-uses-another-port]

This behavior applies to explicit service ports. The project table shows the configured value, while startup uses the saved replacement.

Use the startup output or `lpm ports all` to find the active listener.

### The browser port is not the child port [#the-browser-port-is-not-the-child-port]

HTTPS, proxy, and network frontends can use separate public ports. Read the URL that `lpm dev` prints.

### Startup stops after a readiness failure [#startup-stops-after-a-readiness-failure]

An ownership, `readyPort`, or `readyUrl` failure stops the initial startup.

Read the service output. Then correct the readiness target or increase `readyTimeout`.

### A peer variable is missing [#a-peer-variable-is-missing]

Make sure that the peer has an assigned service port. A `readyPort` without `port`, `primary`, or `host` does not create peer variables.

## See also [#see-also]

* [`lpm ports`](/docs/infra/ports) — inspect listeners and reset saved replacements
* [`lpm dev`](/docs/dev/dev) — run the development service graph
* [`lpm.json` services](/docs/reference/lpm-json#servicesname) — configure services and readiness
* [`lpm proxy`](/docs/infra/proxy) — route friendly hostnames to assigned ports
* [Local HTTPS](/docs/infra/local-https) — separate the browser TLS port from child ports
* [Tunneling](/docs/infra/tunneling) — forward a public URL to the primary service


# lpm ports (/docs/infra/ports)



`lpm ports` shows the service ports from the current project and reports whether each port is available.

It can also show visible TCP listeners, stop a process, or clear saved port overrides.

```bash
lpm ports [action] [target]
```

## Quickstart with `lpm.json` [#quickstart-with-lpmjson]

Define the development services and their preferred ports:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "services": {
    "web": {
      "command": "vite",
      "port": 5173
    },
    "api": {
      "command": "node api.js",
      "port": 4000
    }
  }
}
```

Show the declared ports and their current status:

```bash
lpm ports
```

The table shows each service, its declared port, and whether that port is ready or listening.

Start the project. Then run `lpm ports` from another terminal:

```bash
# Terminal 1
lpm dev

# Terminal 2
lpm ports
```

An explicit `port` remains the listed value. If `lpm dev` remaps the service, this table does not show the saved replacement.

Use the startup output or `lpm ports all` to find the active replacement port. Use `lpm ports reset` to clear saved replacements.

### Show the assigned port for a host-only service [#show-the-assigned-port-for-a-host-only-service]

If a service uses a managed hostname, you can omit its port:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "proxy": {
    "port": 9443,
    "httpRedirect": false
  },
  "services": {
    "web": {
      "command": "vite",
      "host": "web.localhost"
    }
  }
}
```

Start the project. Then show the saved assignment from another terminal:

```bash
# Terminal 1
lpm dev

# Terminal 2
lpm ports
```

The host-only service appears after `lpm dev` saves its assigned port.

A service without `port` or `host` does not appear. A service with only `readyPort` also does not appear.

## Examples [#examples]

```bash
lpm ports                   # show ports for the current project
lpm ports list              # same as lpm ports
lpm ports all               # show all visible TCP listeners
lpm ports --all             # same as lpm ports all
lpm ports 3000              # inspect port 3000
lpm ports inspect 3000      # same as lpm ports 3000
lpm ports kill 3000         # stop the process on port 3000
lpm ports kill 3000-3010    # stop processes in an inclusive range
lpm ports kill --pid 48213  # stop a known PID
lpm ports reset             # clear saved overrides for this project
```

## Choose an action [#choose-an-action]

| Goal                               | Command                      |
| ---------------------------------- | ---------------------------- |
| Show ports for the current project | `lpm ports`                  |
| Show all visible TCP listeners     | `lpm ports all`              |
| Inspect one port                   | `lpm ports inspect 3000`     |
| Stop the process on one port       | `lpm ports kill 3000`        |
| Stop processes in a port range     | `lpm ports kill 3000-3010`   |
| Stop a known PID                   | `lpm ports kill --pid 48213` |
| Clear saved port overrides         | `lpm ports reset`            |

`list`, `all`, and `inspect` are read-only. A bare number inspects a port, so `lpm ports 3000` equals `lpm ports inspect 3000`.

## Project scope [#project-scope]

Run `lpm ports` from the directory that contains `lpm.json`. The command reads `lpm.json` only from the current directory.

If `lpm.json` declares services, the command uses the service-port rules from the quickstart above.

Without declared services, LPM CLI shows visible listeners from processes whose current directory is inside the command directory.

Use `lpm ports all` to remove the project scope and show all visible TCP listeners.

## Recipes [#recipes]

### Restore a declared port after a conflict [#restore-a-declared-port-after-a-conflict]

Inspect the port before you stop its process. Make sure that the listener is the process that you want to stop.

```bash
lpm ports inspect 3000
lpm ports kill 3000
lpm ports reset
lpm dev
```

`lpm ports reset` clears only the saved overrides for the current project. It does not edit `lpm.json` or stop processes.

The next `lpm dev` tries the declared ports again. If a declared port remains unavailable, LPM CLI assigns another port.

### Find a development server from this project [#find-a-development-server-from-this-project]

Show the ports for the current project. Then inspect the port that you want to identify.

```bash
lpm ports
lpm ports inspect 5173
```

If the first command shows an unexpected port, inspect that port before you stop or reset anything.

For a remapped declared service, use `lpm ports all` and the `lpm dev` startup output.

### Clean a range of development ports [#clean-a-range-of-development-ports]

Review all visible listeners. Then stop the matching processes in the selected range.

```bash
lpm ports all
lpm ports kill 3000-3010
```

The range is inclusive. If the range contains matching listeners, LPM CLI groups them by PID and asks for confirmation.

For CI or another non-interactive environment, pass `--yes`:

```bash
lpm ports kill 3000-3010 --yes
```

> CAUTION: A range kill stops entire processes. A matched process can also own listeners outside the selected range.

### Stop a known PID [#stop-a-known-pid]

If you already know the PID, use `--pid`:

```bash
lpm ports kill --pid 48213
```

> CAUTION: `--pid` stops the specified process. The process does not have to own a TCP port.

## Output [#output]

### Project service output [#project-service-output]

For declared services, the terminal table shows the service, port, and status. A listening status also includes the available process owner.

Use JSON to get the service name, port, and `free` or `in_use` status:

```bash
lpm ports --json
```

### Listener output [#listener-output]

Listener commands show the port, process, PID, project, framework, and status:

```bash
lpm ports inspect 3000
lpm ports all
```

Use JSON for more available process details:

```bash
lpm ports inspect 3000 --json
lpm ports all --json
```

JSON can also include the address, command, current directory, project directory, and uptime. Available values depend on operating-system access.

## Kill behavior and safety [#kill-behavior-and-safety]

`lpm ports kill <port>` does not ask for confirmation. If you do not know the owner, inspect the port before you stop it.

Before a port kill, LPM CLI finds the PID. It then makes sure that the same PID still owns the target port.

If the owner changes, the command exits without stopping the new owner. The command also refuses to stop protected system PIDs or itself.

If a range matches processes, LPM CLI groups the ports by PID and asks for confirmation. Pass `--yes` to skip the prompt.

In JSON and other non-interactive runs, a matching range fails without `--yes`:

```bash
lpm ports kill 3000-3010 --yes --json
```

A bare numeric kill target is always a port. Use `--pid` to stop an explicit PID.

## Flags [#flags]

| Flag          | Applies to   | Effect                                                                                         |
| ------------- | ------------ | ---------------------------------------------------------------------------------------------- |
| `--all`       | `list`       | Show all visible TCP listeners. This flag equals `lpm ports all`.                              |
| `--yes`, `-y` | Range `kill` | Skip the interactive confirmation.                                                             |
| `--pid <PID>` | `kill`       | Stop the specified PID.                                                                        |
| `--json`      | All actions  | Write machine-readable output to stdout. This is a [global flag](/docs/commands#global-flags). |

## Platform support [#platform-support]

LPM CLI uses the listener table from each supported operating system. System permissions can limit the visible processes and available details.

| Platform      | Source                          | Available details                                                                          |
| ------------- | ------------------------------- | ------------------------------------------------------------------------------------------ |
| Linux         | System TCP tables               | Project data requires access to the current directory of the process.                      |
| macOS and BSD | System process tools            | Project data requires access to the current directory of the process.                      |
| Windows       | System network and process APIs | JSON can include the image path and uptime. Generic processes do not include project data. |

Windows does not expose the current directory of another process through a stable public API. Generic processes cannot include that project information.

## See also [#see-also]

* [`lpm dev`](/docs/dev/dev) — run multiple services from `lpm.json`
* [`lpm.json`](/docs/reference/lpm-json#services) — configure development services and ports
* [Port management](/docs/infra/port-management) — learn how port assignments and reservations work


# Project health (/docs/infra/project-health)



Project health combines the state of your manifest, installed packages, runtimes, security posture, tools, and connected services.

[`lpm doctor`](/docs/infra/doctor) turns these areas into named results. Use the report to find the cause of an install or environment problem.

This page explains the health model and common workflows. The command page contains the complete syntax, catalog, and flags.

## Understand the health layers [#understand-the-health-layers]

LPM CLI separates core project state from broader machine and service state.

| Health layer          | Default mode                                                                                                      | Full mode adds                                                                        |
| --------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Project and install   | Manifest, linker mode, `node_modules`, lockfiles, dependency synchronization, local sources, and workspace cycles | Store orphans, `.gitattributes`, and manifest compatibility                           |
| Runtime and security  | Node.js, Bun, policy extensions, auth storage, vault storage, and Sigstore posture                                | Sandbox and lifecycle-script policy                                                   |
| Project configuration | `lpm.json` structure, runtime pins, tasks, tools, and services                                                    | Tunnel ownership and reachability                                                     |
| Tools and machine     | None                                                                                                              | Lint, format, TypeScript readiness, plugin versions, global installs, PATH, and shims |
| Connected services    | None                                                                                                              | Registry health, registry authentication, and tunnel requests                         |

A healthy project does not require every optional tool or service. LPM CLI emits only the results that apply to the current project.

## Choose fast or full health [#choose-fast-or-full-health]

Run the default health report from the project root:

```bash
lpm doctor
```

The default mode reads local project and LPM CLI state. It does not contact a registry or tunnel service.

Runtime detection can run a bounded `node --version` process. The default mode does not run lint or format tools.

Run the full report for machine, tool, and service health:

```bash
lpm doctor --all
```

| Mode    | Use case                                                              | Cost                                                     |
| ------- | --------------------------------------------------------------------- | -------------------------------------------------------- |
| Default | An install, runtime, lockfile, or local project state looks incorrect | Local file access and runtime detection                  |
| `--all` | You need a complete environment report or CI evidence                 | Tool processes, machine inspection, and network requests |

The full mode includes every default result. Tool timeouts and network conditions can make it take longer.

## Use project configuration [#use-project-configuration]

LPM CLI reads `lpm.json` automatically. You do not need a separate doctor flag.

For example:

```json title="lpm.json"
{
  "runtime": {
    "node": "22"
  },
  "tunnel": {
    "domain": "my-api.lpm.fyi"
  }
}
```

The default mode examines the runtime pin and relevant project fields. The full mode also examines the configured tunnel domain.

Doctor reports invalid JSON and unrecognized top-level fields. It also reports runtime or workspace problems that the project configuration creates.

See the [`lpm.json` reference](/docs/reference/lpm-json) for the complete project configuration.

## Interpret health results [#interpret-health-results]

Each result has a stable code, a severity, a short name, and current details.

| Severity | Meaning                                                  | Exit code effect |
| -------- | -------------------------------------------------------- | ---------------- |
| `pass`   | The examined state is healthy or informational           | None             |
| `warn`   | The state needs attention but does not block the command | None             |
| `fail`   | The state is broken or required state is unavailable     | Exit code `1`    |

Warnings keep exit code `0`. A report with warnings is not clean, but it has no failures.

The default terminal output hides most passing results. It keeps failures, warnings, linker mode, and active policy extensions visible.

The full terminal output includes passing results. If you need every emitted result from either mode, use JSON.

## Find the meaning of a result [#find-the-meaning-of-a-result]

Use the stable code from terminal or JSON output:

```bash
lpm doctor list --code <CODE>
```

The catalog describes the result, its possible severities, and the recommended action.

List results by category:

```bash
lpm doctor list --category "project state"
lpm doctor list --category runtime --json
```

Match automation on `code`. Human-readable names and details can change.

## Apply fixes and examine the result again [#apply-fixes-and-examine-the-result-again]

CAUTION: Save your project changes before you use `--fix`. The command can change project files and installed dependencies.

Apply fixes from the default result set:

```bash
lpm doctor --fix
lpm doctor
```

LPM CLI applies supported fixes immediately. It does not ask for confirmation.

`-y` and `--yes` imply `--fix`. They currently have the same behavior as `--fix`.

The fix run reports the results that existed before the fixes. It lists completed actions in `fixes_applied`.

A successful fix does not change the first report or its exit code. Run doctor again to examine the new state.

Use the full fix mode for fixes from the extended result set:

CAUTION: `--all --fix` can prune the shared store, update installed plugins, and claim a tunnel domain.

```bash
lpm doctor --all --fix
lpm doctor --all
```

### Current automatic fixes [#current-automatic-fixes]

| Problem                                                   | Automatic action                            |
| --------------------------------------------------------- | ------------------------------------------- |
| Missing, mixed, legacy, or unmanaged `node_modules` state | Run [lpm install](/docs/packages/install)   |
| Missing lockfile or dependency drift                      | Run [lpm install](/docs/packages/install)   |
| Missing, stale, or corrupt `lpm.lockb`                    | Reconcile it with `lpm.lock`                |
| Missing managed Node.js version                           | Install the selected Node.js version        |
| Missing managed Bun version                               | Install the selected Bun version            |
| Project-level `node_modules` link                         | Replace only the link with a real directory |
| Format result from the full mode                          | Run [`lpm fmt`](/docs/dev/fmt)              |
| Missing `lpm.lockb` rule from the full mode               | Update `.gitattributes`                     |
| Store orphans from the full mode                          | Prune unreachable store entries             |
| Unclaimed configured tunnel from the full mode            | Claim the domain for the current account    |
| Plugin update from the full mode                          | Update the installed plugin                 |

Some failures require a user decision. For example, doctor does not add TypeScript or change a tunnel domain that another account owns.

In `lpm doctor list --json`, a non-null `auto_fix` value identifies the supported action for that code. A null value means no automatic fix exists.

Use `auto_fix` before a fix run. Use `fixes_applied` after the run to find the actions that completed.

## Use health results in CI [#use-health-results-in-ci]

Create a complete JSON report without changing the project:

```bash
lpm doctor --all --json > doctor-report.json
```

If the CI job must not change the workspace, do not add `--fix`.

Use these fields for automation:

| Field           | Meaning                                 |
| --------------- | --------------------------------------- |
| `success`       | LPM CLI created the diagnostic report   |
| `no_failures`   | No result has `fail` severity           |
| `clean`         | No result has `warn` or `fail` severity |
| `has_warnings`  | At least one result has `warn` severity |
| `checks[].code` | Stable identifier for one result        |
| `fixes_applied` | Actions that completed during this run  |

`success: true` does not mean that the project is healthy. Read `clean` for a warning-sensitive policy. Otherwise, read `no_failures`.

The process exits with code `1` for failures. Warnings alone keep exit code `0`.

If warnings must block CI, require `clean: true` in the JSON report.

## Compare project and registry health [#compare-project-and-registry-health]

Use `lpm doctor` for project and environment health. Use `lpm health` for one registry request.

```bash
lpm doctor
lpm health --registry https://registry.example.com
```

| Command            | Scope                                               | Project files | Network |
| ------------------ | --------------------------------------------------- | ------------- | ------- |
| `lpm doctor`       | Fast project and local environment health           | Yes           | No      |
| `lpm doctor --all` | Complete project, machine, tool, and service health | Yes           | Yes     |
| `lpm health`       | One selected registry                               | No            | Yes     |

For `lpm health --json`, read the `healthy` field. A reported unhealthy response can still produce `success: true` and exit code `0`.

## Know what doctor does not examine [#know-what-doctor-does-not-examine]

Project health is a diagnostic layer. It does not replace these focused commands:

| Need                                       | Command                                            |
| ------------------------------------------ | -------------------------------------------------- |
| Run project tests and type checks          | [`lpm check`](/docs/dev/check)                     |
| Examine local certificate trust and expiry | [`lpm cert status`](/docs/infra/cert)              |
| Examine package-store content integrity    | [`lpm store verify`](/docs/infra/store)            |
| Examine dependency vulnerabilities         | [`lpm audit`](/docs/packages/audit)                |
| Examine one registry only                  | [`lpm health`](/docs/infra/doctor#registry-health) |

Doctor examines TypeScript readiness in full mode. It does not run the complete project type check.

## Common workflows [#common-workflows]

### Diagnose an install problem [#diagnose-an-install-problem]

```bash
lpm doctor
lpm doctor list --code <CODE>
```

### Examine a machine after a toolchain change [#examine-a-machine-after-a-toolchain-change]

```bash
lpm doctor --all
```

### Repair supported problems [#repair-supported-problems]

```bash
lpm doctor --fix
lpm doctor
```

### Create a warning-sensitive CI gate [#create-a-warning-sensitive-ci-gate]

```bash
lpm doctor --all --json > doctor-report.json
```

Require `clean: true` in the next CI step.

## See also [#see-also]

* [`lpm doctor` and `lpm health`](/docs/infra/doctor) — use commands, flags, and the complete result catalog
* [Install packages](/docs/packages/install) — repair missing or inconsistent install state
* [Runtime management](/docs/dev/use) — install selected Node.js and Bun versions
* [Policy extensions](/docs/infra/policy) — inspect project policy results
* [Package store](/docs/infra/store) — examine and clean shared package content
* [Exit codes](/docs/reference/exit-codes) — use LPM CLI results in automation


# lpm proxy (/docs/infra/proxy)



`lpm proxy` manages the local daemon that routes friendly hostnames, such as `app.localhost`, to services that `lpm dev` starts.

For HTTPS routes, the proxy terminates TLS. The child dev server continues to use HTTP.

```bash
lpm proxy [action]
```

Use this command to inspect routes, run a temporary daemon, or install a persistent service for the current user.

## Examples [#examples]

```bash
lpm proxy status        # show the daemon, listeners, and active routes
lpm proxy list          # show only active routes
lpm proxy start         # run the daemon in the foreground
lpm proxy start --detach
lpm proxy start --http-port 8080
lpm proxy start --tls-port 9443
lpm proxy start --tls-port 9443 --http-redirect-port 9080
lpm proxy stop
lpm proxy install --tls-port 9443
lpm proxy install --privileged-ports
lpm proxy uninstall
lpm proxy uninstall --privileged-ports
lpm proxy status --json
```

## Choose an action [#choose-an-action]

| Goal                                          | Command                    |
| --------------------------------------------- | -------------------------- |
| Show the daemon, listeners, and active routes | `lpm proxy status`         |
| Show only active routes                       | `lpm proxy list`           |
| Start a foreground daemon                     | `lpm proxy start`          |
| Start a background daemon                     | `lpm proxy start --detach` |
| Stop the current daemon                       | `lpm proxy stop`           |
| Install a persistent user service             | `lpm proxy install`        |
| Remove the persistent user service            | `lpm proxy uninstall`      |

`status` is the default action. A bare `lpm proxy` command equals `lpm proxy status`.

## How proxy routing works [#how-proxy-routing-works]

The proxy separates the browser connection from the dev-server connection:

1. `lpm dev` starts the child service with a plain HTTP endpoint.
2. If no daemon runs, `lpm dev` starts the proxy daemon in the background.
3. `lpm dev` registers each configured hostname against the final service port.
4. The proxy selects a route from the request hostname and forwards the request to the service.
5. When the dev session exits, `lpm dev` releases its route lease.

HTTP forwarding streams response bodies and supports WebSocket and HMR upgrades.

`lpm dev` requires an HTTPS proxy listener for configured hostnames. A control-only daemon or a plain HTTP listener is not sufficient.

Do not enable framework HTTPS for a proxied child service. Let the proxy terminate TLS and forward to the plain HTTP service.

## Recipes [#recipes]

### Use `app.localhost` without privileged ports [#use-applocalhost-without-privileged-ports]

Add a high HTTPS port to `lpm.json`:

```json title="lpm.json"
{
  "proxy": {
    "host": "app.localhost",
    "port": 9443,
    "httpRedirect": false
  }
}
```

Then start the dev server:

```bash
lpm dev
```

If no daemon runs, `lpm dev` starts one in the background. It registers `app.localhost` for the lifetime of the dev session.

The startup output shows the exact URL with port `9443`. A `*.localhost` hostname does not require a hosts-file entry.

### Route multiple services [#route-multiple-services]

Give each service a different hostname:

```json title="lpm.json"
{
  "proxy": {
    "port": 9443,
    "httpRedirect": false
  },
  "services": {
    "web": {
      "command": "vite",
      "port": 5173,
      "host": "web.localhost"
    },
    "api": {
      "command": "node api.js",
      "port": 4000,
      "host": "api.localhost"
    }
  }
}
```

```bash
lpm dev
lpm proxy list
```

The route table shows each hostname, service, project directory, and final upstream port.

### Start the proxy manually [#start-the-proxy-manually]

Use a foreground daemon while you diagnose listener or certificate errors:

```bash
lpm proxy start --tls-port 9443 --http-redirect-port 9080
```

When another terminal does not need the proxy log, use a detached daemon:

```bash
lpm proxy start --detach --tls-port 9443
```

The detached command waits for the daemon to report that it is ready.

### Change the listeners [#change-the-listeners]

An existing daemon does not accept listener changes from another detached start. Stop the daemon before you select new listeners.

```bash
lpm proxy status
lpm proxy stop
lpm proxy start --detach --tls-port 9443
```

If an installed service owns the daemon, use `lpm proxy uninstall` to remove that persistent service.

## Listener selection [#listener-selection]

Listener flags are exact overrides. If one listener flag is present, LPM CLI does not read listener values from `lpm.json`.

| Command state                             | Listeners                                                                                                          |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| No flags and a configured local hostname  | HTTPS uses `proxy.port`, or `443` by default. HTTP redirect uses port `80` unless `proxy.httpRedirect` is `false`. |
| No flags and no configured local hostname | The daemon starts with only its control endpoint.                                                                  |
| One or more listener flags                | Only the specified listeners start. Unspecified listeners remain disabled.                                         |

When you want configuration-based listeners, run `start` or `install` from the directory that contains `lpm.json`.

The listener flags bind only to `127.0.0.1`. Port `0` lets the operating system select an available port.

`--http-redirect-port` requires `--tls-port` in the same command:

```bash
lpm proxy start --tls-port 9443 --http-redirect-port 9080
```

## Install a persistent service [#install-a-persistent-service]

Install a user-scoped service on a high port:

```bash
lpm proxy install --tls-port 9443
```

The installation also starts the service. The service uses the current directory as its project directory.

| Platform | Persistent service   |
| -------- | -------------------- |
| macOS    | LaunchAgent          |
| Linux    | systemd user service |
| Windows  | Logon Scheduled Task |

On Linux and macOS, a user service cannot bind ports from `1` through `1023`. Use high ports or install the privileged forwarder.

### Use ports 443 and 80 on Linux or macOS [#use-ports-443-and-80-on-linux-or-macos]

Run the command as the target user:

```bash
lpm proxy install --privileged-ports
```

Do not run the full command with `sudo`. LPM CLI uses `sudo` only for the root-owned forwarder files and service.

The command installs two services:

* A proxy daemon for the current user.
* A root-owned loopback forwarder for low ports.

Without explicit listener flags, the forwarder exposes HTTPS on port `443`. It also exposes the redirect on `80` unless `proxy.httpRedirect` is `false`.

The user daemon selects an available HTTPS backend port from `9443`. It selects a redirect backend port from `9080`.

The privileged forwarder supports HTTPS and HTTP redirects. It does not support plain `--http-port` forwarding.

> CAUTION: Do not use `--replace` unless you intend to replace the privileged forwarder of another user.

Remove only the user service:

```bash
lpm proxy uninstall
```

Remove the user service and the privileged forwarder:

```bash
lpm proxy uninstall --privileged-ports
```

The uninstall command refuses to remove a privileged forwarder that belongs to another user.

## Certificates and hosts files [#certificates-and-hosts-files]

An HTTPS listener selects the matching project certificate by SNI. Route registration prepares or refreshes the certificate chain for each hostname.

The proxy daemon does not install the root CA into the trust store. `lpm dev` uses the normal certificate consent flow.

`localhost` and `*.localhost` do not need hosts-file entries. Other local hostnames require a managed entry in the system hosts file.

`lpm dev` asks for consent before it changes the hosts file. When the dev session exits, it removes the project block.

If an interrupted session leaves an LPM CLI-managed block, remove it with:

```bash
lpm hosts clean
```

The proxy `start` and `stop` actions do not manage hosts-file entries. [`lpm hosts`](/docs/infra/hosts) owns that cleanup.

[`lpm dev --https`](/docs/dev/dev#https) uses a separate LPM CLI frontend. It does not configure the local-domain proxy listener.

## Status and JSON output [#status-and-json-output]

`lpm proxy status` shows the daemon status, PID, bound listeners, and active routes. `lpm proxy list` shows only the active routes.

Use JSON for scripts and diagnostics:

```bash
lpm proxy status --json
lpm proxy list --json
lpm proxy start --detach --json
```

The JSON object includes these fields:

* `running`
* `pid`
* `httpAddr`
* `httpRedirectAddr`
* `tlsAddr`
* `routes`
* `stale`
* `stateError`

Saved listener addresses can remain in the JSON object while `stale` is `true`. In that state, `running` is `false` and no routes are active.

Foreground `lpm proxy start --json` is not supported. Use `lpm proxy status --json` from another terminal.

## Flags [#flags]

| Flag                          | Applies to                                                         | Effect                                                                                                                    |
| ----------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `--detach`                    | `start`                                                            | Start the daemon in the background and wait for readiness.                                                                |
| `--http-port <PORT>`          | `start`, `install`                                                 | Bind a plain HTTP listener on `127.0.0.1:<PORT>`.                                                                         |
| `--tls-port <PORT>`           | `start`, `install`                                                 | Bind an HTTPS listener on `127.0.0.1:<PORT>`.                                                                             |
| `--http-redirect-port <PORT>` | `start`, `install`                                                 | Bind an HTTP redirect listener. This flag requires `--tls-port`.                                                          |
| `--privileged-ports`          | `install`, `uninstall`                                             | On Linux and macOS, install or remove the root-owned low-port forwarder.                                                  |
| `--replace`                   | `install --privileged-ports`                                       | Replace a privileged forwarder that belongs to another UID.                                                               |
| `--json`                      | `status`, `list`, `stop`, `install`, `uninstall`, detached `start` | Write machine-readable output. Foreground `start` is not supported. This is a [global flag](/docs/commands#global-flags). |

## Related configuration [#related-configuration]

| Field                  | Purpose                                                                                   |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| `proxy.host`           | Route the primary or single dev service through one hostname.                             |
| `proxy.port`           | Select the HTTPS listener for commands without listener flags. The default is `443`.      |
| `proxy.httpRedirect`   | Enable the port `80` redirect for commands without listener flags. The default is `true`. |
| `services.<name>.host` | Route one named service through its own hostname.                                         |

See the full [`lpm.json` reference](/docs/reference/lpm-json#proxy) for hostname validation and service fields.

## See also [#see-also]

* [`lpm dev`](/docs/dev/dev) — start services and register proxy routes
* [`lpm cert`](/docs/infra/cert) — manage the local CA and project certificates
* [`lpm hosts`](/docs/infra/hosts) — remove managed hosts-file blocks
* [`lpm ports`](/docs/infra/ports) — inspect local listeners


# Secrets vault (/docs/infra/secrets-vault)



Secrets vault keeps environment values outside your repository. Use [`lpm env`](/docs/dev/env) to store values locally or sync an encrypted copy.

You can also share values with an organization or send them to a deployment platform.

There is no `lpm vault` command. The vault is the protected storage model. `lpm env` is the supported command surface.

## How the secrets vault works [#how-the-secrets-vault-works]

The vault separates project configuration from secret values:

```text
lpm.json         → vault ID, environment names, and validation schema
Local vault      → secret values used by lpm dev and lpm run
LPM.dev Registry → encrypted cloud copy and access metadata
Platform         → values sent directly from LPM CLI
```

Commit `lpm.json` to the repository. Do not commit secret values, plaintext exports, or local vault files.

## Choose a workflow [#choose-a-workflow]

| Goal                                    | Plan or scope                    | Main commands                                     |
| --------------------------------------- | -------------------------------- | ------------------------------------------------- |
| Keep secrets on one machine             | All plans                        | `lpm env set`, `lpm env list`                     |
| Store an encrypted personal cloud copy  | Pro personal account             | `lpm env push`, `lpm env pull`                    |
| Share secrets with a team               | Active Organization plan         | `lpm env share --org <slug>`                      |
| Give an approved CI job one environment | Pro personal account             | `lpm env oidc allow`, `lpm env pull --oidc`       |
| Send values to a deployment platform    | Pro or active Organization scope | `lpm env connect`, `lpm env push --to <platform>` |

Local storage does not require a paid plan. Cloud features require the applicable personal or Organization plan.

## Project identity in `lpm.json` [#project-identity-in-lpmjson]

A command that creates a vault adds a vault ID to `lpm.json`. For example, `lpm env set` creates the ID on first use.

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "vault": "7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b"
}
```

The vault ID is an opaque project identifier. It is not a secret and does not grant access to stored values.

Commit this ID with the project. A renamed or moved project keeps the same vault because the ID does not depend on its path.

If you remove the `vault` field, the next vault-creation command makes a new ID. This action does not delete the previous local or cloud vault.

Cloud sync writes CLI-owned metadata below `vaultSync`. Its authority checkpoints bind each Registry and immutable user or organization identity to the highest accepted revision. Separate version and timestamp fields are non-authoritative summaries.

Do not edit or remove `vaultSync` manually. Removing its authority checkpoints resets local rollback and account-binding protection for this checkout.

Project configuration can also define environments and a validation schema:

```json title="lpm.json"
{
  "environments": {
    "staging": { "file": ".env.staging" },
    "production": { "file": ".env.production" }
  },
  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url", "secret": true }
    }
  }
}
```

The configuration describes environments and validation rules. It does not contain the secret values.

## Store secrets locally [#store-secrets-locally]

Store values in the default environment:

```bash
lpm env set DATABASE_URL=postgres://localhost/app
lpm env set API_KEY=...
lpm env list
```

Store values in a named environment:

```bash
lpm env set --env=staging API_URL=https://staging.example.com
lpm env set --env=production API_URL=https://api.example.com
lpm env list --env=staging
```

Without `--env`, local commands use the `default` environment.

Run a script with one environment:

```bash
lpm dev --env=staging
lpm run start --env=production
```

LPM CLI masks values in normal `list` and `get` output. Use `--reveal` only in a private terminal.

### Local storage by platform [#local-storage-by-platform]

| Platform | Storage                                                                                                         |
| -------- | --------------------------------------------------------------------------------------------------------------- |
| macOS    | The system Keychain stores each project vault.                                                                  |
| Linux    | An encrypted file stores the vault. The operating-system credential store protects its data key when available. |
| Windows  | An encrypted file stores the vault. Credential Manager protects its data key when available.                    |

Linux and Windows store encrypted vault files below `~/.lpm/vaults/`.

If the operating-system credential store is unavailable before first use, LPM CLI can use its protected file fallback.

After LPM CLI moves the key into the credential store, an unavailable store stops vault access. LPM CLI does not silently use weaker storage.

### Import and export values [#import-and-export-values]

Import an existing dotenv file:

```bash
lpm env import --env=production .env.production
```

Export an environment:

```bash
lpm env export --env=production .env.production.backup
```

> CAUTION: An exported dotenv file contains plaintext secret values. Store it securely and make sure that Git ignores it.

## Sync a personal vault [#sync-a-personal-vault]

A Pro personal account can store an encrypted cloud copy on the LPM.dev Registry.

```bash
lpm env diff
lpm env push
lpm env pull
lpm env log
```

`lpm env push` encrypts the complete local env project before upload. The LPM.dev Registry stores ciphertext and wrapped key data.

The LPM.dev Registry cannot decrypt values during normal cloud sync. The key that decrypts the payload stays with the local client.

`lpm env push` replaces the personal cloud vault with the local state. The interactive command asks for approval before upload.

`lpm env pull` replaces all local environments with the cloud state. The interactive command asks for approval before replacement.

Use `--yes` for an approved non-interactive operation:

```bash
lpm env push --yes
lpm env pull --yes
```

Each successful push advances the cloud version. If the known version is old, LPM CLI returns a conflict instead of silently overwriting newer data.

## Share secrets with an organization [#share-secrets-with-an-organization]

An active Organization plan can store one encrypted copy for the organization. Each approved member receives access through their sharing key.

Share the current env project:

```bash
lpm env share --org acme
```

Pull the organization copy:

```bash
lpm env pull --org acme
```

An organization pull merges remote keys into the matching local environments. A remote value replaces the local value for the same key.

Organization sharing has version-conflict protection. `lpm env share --force` does not overwrite an existing organization vault.

If a conflict occurs, pull the organization copy. Then reconcile the values and repeat `lpm env share --org <slug>`.

If an owner or administrator deleted the remote vault, a bound checkout can recreate it:

```bash
lpm env share --org <slug> --force
```

The checkout must have the same organization binding and a stored revision checkpoint. The command stops unless the exact remote vault is missing.

The recreated vault starts one revision after the stored checkpoint. Other organization and Registry checkpoints do not change.

Organization members receive access to the complete env project. The vault does not support permissions for individual keys.

Each member wrap includes the recipient key version and full SHA-256 fingerprint. It also includes the organization content-key version.

The server compares these bindings with the current database state before each pull. An absent or stale binding fails closed as **Needs share**.

### Rotate an organization content key [#rotate-an-organization-content-key]

After you remove a member, rotate each affected organization env project:

```bash
lpm env rotate-key --org acme
```

An organization owner or administrator must run this command from a machine with current access.

The CLI pulls the complete remote payload and creates a fresh content key. It re-encrypts all environments without changing their values.

The server locks the current member and public-key rows before the write. It rejects a stale or incomplete recipient set.

The server replaces the ciphertext and all member wraps in one transaction. A compare-and-swap conflict leaves the previous state unchanged.

This rotation prevents a removed key from decrypting future ciphertext. It cannot erase data or content keys that a former member copied.

### Rotate your sharing key [#rotate-your-sharing-key]

If the account sharing key is exposed, rotate it:

```bash
lpm env rotate-sharing-key
```

This command requires an interactive terminal. It asks you to type `ROTATE`, then requests a password or authenticator code.

The rotation invalidates your existing organization access entries. An owner or administrator must share each affected vault with you again.

If the command stops after the server accepts the new key, run it again. LPM CLI resumes the pending rotation.

## Pair the dashboard [#pair-the-dashboard]

The LPM.dev Registry dashboard cannot read normal cloud vaults until you pair the browser with an existing CLI machine.

1. Open the Secrets page in the dashboard.
2. Complete the passkey, password, or authenticator prompt.
3. Copy the six-character pairing code.
4. Run the command on a CLI machine that already holds the vault key:

```bash
lpm env pair <CODE>
```

The browser and CLI exchange temporary P-256 public keys. Both clients derive an eight-digit number from the shared ECDH secret.

The number has two groups of four digits. Make sure that the terminal number equals the dashboard number.

Type `y` only when the numbers are equal. The Registry relays the public keys, but it cannot calculate the shared secret.

The pairing code expires after five minutes. Open a personal project to pair for personal access. Open an organization project to pair for organization access.

Personal and organization pairing are separate. Organization pairing requires the CLI machine with your registered sharing key. Both clients must use a version that supports organization pairing.

The browser stores your organization sharing key as a non-extractable key, bound to your account. It decrypts only projects shared with your current account.

Owners, administrators, and maintainers can edit values in the browser. Other members can view values. The server checks membership and subscription access again before each save.

After a member removal, an owner or administrator must run `lpm env rotate --org <slug>`. Browser saves remain disabled until this rotation completes.

If your sharing key changes, pair the browser again. An owner or administrator must also share the current environment key with your account.

> CAUTION: Do not use `--yes` for a code from an untrusted source. This flag skips the browser-identity prompt.

Revoke all browser pairings:

```bash
lpm env unpair
```

Browser pairing sends an existing CLI key to the browser. It does not install the existing key on another CLI machine.

### Add a passkey for sensitive actions [#add-a-passkey-for-sensitive-actions]

Open **Dashboard > Settings > Security**. In **Passkeys**, enter a name and select **Add passkey**.

The dashboard first requests your current step-up credential. Your device then requires a screen lock, fingerprint, face, or security key.

LPM stores the credential public key and signature counter. LPM does not receive your biometric data or device PIN.

After registration, the dashboard requires a passkey for browser pairing and passkey changes. Add a second passkey or use a synced password manager.

You can remove a passkey from the same settings page. Removal requires a recent, user-verified passkey assertion.

## Use secrets in CI [#use-secrets-in-ci]

OIDC access supports personal Pro projects and organization projects with explicit CI decryption setup. It supports GitHub Actions and GitLab.com CI.

Self-managed GitLab issuers are not supported. Organization projects require an active Organization or Enterprise plan.

First, push the personal vault. Then create a policy for the permitted workflow and environment:

```bash
lpm env push
lpm env oidc allow \
  --provider=github \
  --repo=owner/repository \
  --workflow=.github/workflows/deploy.yml \
  --branch=main \
  --env=production
```

The command prints a server-issued policy ID after it enables CI escrow. Store this UUID as `LPM_OIDC_POLICY_ID` in trusted CI configuration.

The policy ID is not a bearer credential. A valid GitHub Actions or GitLab.com identity token is still required.

LPM CLI gets the immutable numeric repository ID from the GitHub API. For a private repository, set `GITHUB_TOKEN` or `GH_TOKEN`.

You can also pass the ID directly:

```bash
lpm env oidc allow \
  --provider=github \
  --repo=owner/repository \
  --repository-id=123456789 \
  --workflow=.github/workflows/deploy.yml \
  --branch=main \
  --env=production
```

The policy requires the repository name and numeric ID. A renamed or transferred repository must update its policy before CI access resumes.

If an existing policy has no repository ID, OIDC pulls fail closed. Run `lpm env oidc allow` again to update it.

The approved workflow can pull the selected environment:

```bash
lpm env pull --oidc --env=production --output=.env
```

The pull requires `LPM_OIDC_POLICY_ID`. You can use `--policy-id=<uuid>` to override the environment variable.

OIDC changes the normal trust boundary. For personal projects, LPM CLI sends the wrapping key to the Registry over TLS. For organization projects, it sends only the current project content key. The Registry encrypts the key before storage.

The LPM.dev Registry can then decrypt the selected environment for an authorized CI identity. It sends the values to the job through TLS.

OIDC access requires an opt-in for each project. For organizations, an owner or admin runs `lpm env oidc allow --org=<slug> --allow-server-decryption` with the complete provider policy. The command authenticates and decrypts the current shared revision before transferring its content key.

The LPM.dev Registry encrypts escrow material with a versioned server key. Personal escrow is bound to the server key version, project ID, and account ID. Organization escrow binds the server key version, project ID, organization ID, and content-key version.

Organization content-key rotation or membership key invalidation clears CI escrow and revokes issued tokens. An owner or admin must enable decryption again for the current key. `lpm env oidc disable --org=<slug>` disables access immediately, including during a billing lapse. Re-enabling access does not restore revoked tokens.

The LPM.dev Registry can rotate this server key. The env values and OIDC policy do not change. The rotation writes an env audit entry.

An `oidc allow` command replaces the complete policy instead of adding one value. Before you change a policy, inspect it:

```bash
lpm env oidc list
```

See the [`lpm env` OIDC guide](/docs/dev/env#oidc-policies-ci) for GitHub Actions and GitLab.com examples.

## Send values to a deployment platform [#send-values-to-a-deployment-platform]

LPM CLI can send local values to these platforms:

| Platform       | Value behavior                                                          |
| -------------- | ----------------------------------------------------------------------- |
| Vercel         | LPM CLI can compare, push, clean, and pull readable values.             |
| Coolify        | LPM CLI can compare, push, clean, and pull readable application values. |
| Fly.io         | Secrets are write-only. Pull reports names but cannot recover values.   |
| Railway        | LPM CLI can compare, push, clean, and pull service or shared values.    |
| GitHub Actions | Variables are readable. Secrets are write-only.                         |

Connect a platform, then send an environment:

```bash
lpm env connect vercel --project=<id> --linked-env=production
lpm env push --to vercel --env=production
lpm env status
```

Application values travel directly from LPM CLI to the platform. They do not pass through the LPM.dev Registry.

The LPM.dev Registry stores the encrypted platform credential and connection metadata. It handles the credential during connection and later retrieval.

Cloud storage and platform deployment are separate operations:

```bash
lpm env push                  # encrypted cloud copy
lpm env push --to vercel      # direct platform update
```

The two operations do not form one transaction. A successful operation remains successful after the other operation fails.

## Security boundaries [#security-boundaries]

| Boundary            | Behavior                                                                                                                              |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Repository          | `lpm.json` contains identifiers and rules, not secret values.                                                                         |
| Local vault         | The operating system or an encrypted local file protects values.                                                                      |
| Normal cloud sync   | The LPM.dev Registry stores ciphertext and cannot decrypt the values.                                                                 |
| Paired browser      | Personal pairing transfers your wrapping key. Organization pairing transfers your account sharing key. Values decrypt in the browser. |
| OIDC escrow         | The LPM.dev Registry can decrypt an approved environment for an authorized CI job.                                                    |
| Platform deployment | LPM CLI sends values directly to the selected platform.                                                                               |

Use `lpm env log` to read the latest audit entries for the current cloud vault:

```bash
lpm env log
```

The dashboard provides a longer audit view with filters.

## Common recovery [#common-recovery]

### A personal push reports a version conflict [#a-personal-push-reports-a-version-conflict]

Preserve each changed local environment before you pull. An export contains plaintext, so use a protected and ignored file:

```bash
lpm env export --env=staging .env.staging.local
lpm env pull
```

Reconcile the exported values with the pulled state. Then import the selected values and push again:

```bash
lpm env import --env=staging .env.staging.local --overwrite
lpm env push
```

Delete the plaintext recovery file after you make sure that the new cloud state is correct.

> CAUTION: `lpm env push --force` bypasses version protection. Use it only after you inspect the local and cloud state.

### A new CLI machine cannot decrypt a personal vault [#a-new-cli-machine-cannot-decrypt-a-personal-vault]

Dashboard pairing does not transfer a vault key to another CLI machine. Use the original CLI machine to export the required environments through a trusted method.

Personal cloud sync does not currently provide a CLI-to-CLI key-transfer command.

### A paired browser cannot decrypt the vault [#a-paired-browser-cannot-decrypt-the-vault]

Create a new dashboard pairing code. Run `lpm env pair <CODE>` on a CLI machine that holds the correct key.

### An OIDC pull reports a stale escrow key [#an-oidc-pull-reports-a-stale-escrow-key]

Run `lpm env oidc allow` again on the CLI machine that holds the current wrapping key.

### An OIDC pull reports an unavailable escrow key version [#an-oidc-pull-reports-an-unavailable-escrow-key-version]

The LPM.dev Registry configuration needs operator action. The pull fails and returns no secret values.

Do not replace the local wrapping key. Retry after the LPM.dev Registry restores the required server key.

### Organization access stops after sharing-key rotation [#organization-access-stops-after-sharing-key-rotation]

Ask an organization owner or administrator to run `lpm env share --org <slug>` for each affected env project.

### An organization member was removed [#an-organization-member-was-removed]

Run `lpm env rotate-key --org <slug>` for each affected env project. Use an owner or administrator machine with current access.

### A platform update has an uncertain result [#a-platform-update-has-an-uncertain-result]

Run `lpm env status`. Then compare the local environment with the authoritative platform state before another mutation.

## Current limits [#current-limits]

* Organization vaults do not support per-key permissions.
* Organization CI decryption requires explicit owner or admin approval for the current project content key.
* OIDC does not support self-managed GitLab.
* Personal cloud sync does not provide CLI-to-CLI key transfer.
* Cloud sync and platform deployment are not atomic.

## See also [#see-also]

* [`lpm env`](/docs/dev/env) — full command reference
* [Managing secrets](/docs/guides/managing-secrets) — task-oriented workflows
* [`lpm.json` environment schema](/docs/reference/lpm-json#envschema) — validation rules
* [`lpm.json` environments](/docs/reference/lpm-json#environments) — named environments and inheritance
* [Authentication](/docs/infra/authentication) — local credential storage and sign-in


# lpm security (/docs/infra/security)



Use `lpm security` to inspect or manage security approvals on your computer.

LPM CLI keeps the security floor that you approve. This floor is the minimum protection that LPM CLI accepts. A command, repository, or AI agent cannot lower it without your approval.

For example, an AI agent can try to bypass your 24-hour release-age rule with `--allow-new`. A repository can also request weaker release-age, script, or sandbox settings. LPM CLI blocks the weaker setting until you approve an exception. Approval uses the security method for your operating system, such as Touch ID, Windows Hello, or a system password.

```bash
lpm security status [--project <PATH> | --global]
lpm security unlock <SCOPE> [--project <PATH> | --global] [--ttl <DURATION>] [--package <PKG>...]
lpm security lock <SCOPE> [--project <PATH> | --global] [--package <PKG>...]
lpm security protect <status|enable|disable>
lpm security repair
```

The command does not run an install or change a package. It manages the security state that guarded commands use.

## Quickstart [#quickstart]

Inspect the security state for the current project:

```bash
lpm security status
```

The output shows the effective security floor, its sources, runtime overrides, and active project unlocks.

## Choose a command [#choose-a-command]

| Command                        | Purpose                                      | Use it when                                                   |
| ------------------------------ | -------------------------------------------- | ------------------------------------------------------------- |
| `lpm security status`          | Show the effective security state            | You want to inspect a project before changing anything        |
| `lpm security unlock <SCOPE>`  | Approve a temporary exception                | A command reports `security_approval_required`                |
| `lpm security lock <SCOPE>`    | Revoke a temporary exception                 | You no longer need an active unlock                           |
| `lpm security protect status`  | Inspect OS-managed local protection          | You want to see whether managed firewall protection is active |
| `lpm security protect enable`  | Enable OS-managed firewall protection        | You want protection that a different `LPM_HOME` cannot bypass |
| `lpm security protect disable` | Remove OS-managed firewall protection        | An administrator wants to remove that firewall entry          |
| `lpm security repair`          | Quarantine unverifiable local security state | Signed security files fail verification                       |

`unlock` and `lock` default to the global target. `status` defaults to the current project.

For clarity, use `--project .` or `--global` in scripts and saved instructions.

## Recipes [#recipes]

### Inspect a project [#inspect-a-project]

Run this command from the project root:

```bash
lpm security status
```

Use `--project` to inspect another project:

```bash
lpm security status --project /path/to/project
```

Use `--global` to inspect global unlocks:

```bash
lpm security status --global
```

### Approve one temporary project exception [#approve-one-temporary-project-exception]

Suppose an install reports that `sandbox-none` requires approval. Create a ten-minute project unlock:

```bash
lpm security unlock sandbox-none --project . --ttl 10m
lpm install --no-sandbox
```

The unlock command requests approval in an interactive terminal. It does not run the install.

If the original error includes `suggested_command`, copy that command. It contains the required scope and target.

### Limit an exception to one package [#limit-an-exception-to-one-package]

If the reported exception applies to a specific package, use `--package`:

```bash
lpm security unlock provenance-ignore-drift \
  --project . \
  --package esbuild \
  --ttl 10m
```

Repeat `--package` to include more packages. Package filters work only with one concrete scope.

### Revoke an exception early [#revoke-an-exception-early]

Use the same scope, target, and package filters that you used for the unlock:

```bash
lpm security lock provenance-ignore-drift \
  --project . \
  --package esbuild
```

The command removes matching temporary unlocks. It does not change project files or the approved machine floor.

### Protect npm installs on this computer [#protect-npm-installs-on-this-computer]

Enable managed LPM Firewall enforcement:

```bash
lpm security protect enable
lpm security protect status
```

Use monitor mode to report verdicts without blocking packages:

```bash
lpm security protect enable --firewall monitor
```

The `enable` and `disable` commands change an OS-managed policy file. Run each command as your normal user. On Unix, LPM CLI uses `sudo` for a restricted internal helper. The helper can change only the fixed managed-policy path and accepts only the supported protection actions.

### Repair invalid local security state [#repair-invalid-local-security-state]

If LPM CLI reports an invalid signature, use `repair`:

```bash
lpm security repair
lpm security status
```

The command moves invalid files to quarantine. It does not silently trust or delete them.

## Temporary unlocks [#temporary-unlocks]

```bash
lpm security unlock <SCOPE> [--project <PATH> | --global] [--ttl <DURATION>] [--package <PKG>...]
```

An unlock approves a guarded exception for a limited time. The default lifetime is `10m`, and the maximum is `365d`.

Durations can use seconds, minutes, hours, or days:

```bash
lpm security unlock cooldown-bypass --project . --ttl 600
lpm security unlock cooldown-bypass --project . --ttl 30m
lpm security unlock cooldown-bypass --project . --ttl 2h
lpm security unlock cooldown-bypass --project . --ttl 7d
```

The value must be at least one second. Supported suffixes are `m`, `h`, and `d`.

If you omit both target flags, the command creates a global unlock. A project unlock applies only to its project root.

### Scope selectors [#scope-selectors]

Use the concrete scope from the approval error. The available scopes are:

| Scope                     | What it temporarily permits                                                                        |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| `cooldown-bypass`         | Install releases below the approved minimum release age                                            |
| `cooldown-window`         | Use a narrower release-age policy                                                                  |
| `provenance-ignore-drift` | Ignore provenance drift for selected packages or all packages                                      |
| `provenance-unverified`   | Continue with weaker Sigstore provenance verification                                              |
| `scripts-triage`          | Use triage when it weakens the approved script policy                                              |
| `scripts-allow`           | Allow dependency scripts when the approved policy is stricter                                      |
| `trust-bulk-approve`      | Bulk-approve blocked dependency scripts or widen trusted dependencies                              |
| `trust-scope-widen`       | Add scope trust with [`lpm trust lifecycle-scope`](/docs/packages/trust#lpm-trust-lifecycle-scope) |
| `sandbox-default`         | Use default sandbox mode when the approved floor requires strict mode                              |
| `sandbox-none`            | Run lifecycle scripts without the sandbox                                                          |
| `sandbox-allow-degraded`  | Continue when the requested sandbox is unavailable                                                 |
| `capability-widen`        | Widen script access through `passEnv`, `readProject`, or `sandboxLimits`                           |
| `typosquat-disable`       | Disable suspicious package-name checks                                                             |
| `firewall-disable`        | Use a weaker LPM Firewall mode                                                                     |
| `source-analysis-disable` | Disable package source analysis during install                                                     |
| `floor-edit`              | Change the approved machine security floor                                                         |

You can also use a bundle selector:

* `default` includes the 12 common install and runtime scopes.
* `all` includes all 16 concrete scopes.

`default` excludes `trust-bulk-approve`, `trust-scope-widen`, `capability-widen`, and `floor-edit`.

Do not use `all` unless you need every exception. Neither bundle accepts `--package`.

## Revoke temporary unlocks [#revoke-temporary-unlocks]

```bash
lpm security lock <SCOPE> [--project <PATH> | --global] [--package <PKG>...]
```

`lock` is non-interactive. It revokes matching scopes from active unlocks.

If an unlock contains several scopes, LPM CLI removes only the selected scopes. The other scopes remain active.

Without `--package`, the command matches any package filter. With `--package`, it matches the exact normalized package set.

If no unlock matches, the command reports that it made no change.

## Interactive approval and automation [#interactive-approval-and-automation]

`lpm security unlock` requires an interactive terminal and device-owner approval.

It refuses to create an unlock in these cases:

* You use `--json`.
* Standard input or output is not a terminal.
* `CI` is set to `1`, `true`, or `yes`.

The error uses `security_approval_required` and includes a `suggested_command`.

Do not create permanent unlocks for CI. Keep the security floor enabled, or configure an approved managed policy for the machine.

Direct command-line exceptions can request approval during an interactive command. Examples include:

```bash
lpm install --no-sandbox
lpm install --allow-new
lpm install --yolo
LPM_PROVENANCE_ENFORCE=off lpm install
```

In CI, JSON mode, or a non-interactive shell, these commands fail instead of opening an approval prompt.

## Status output [#status-output]

```bash
lpm security status [--project <PATH> | --global]
```

Terminal output shows:

* The selected project or global target
* The script, release-age, sandbox, Sigstore, typosquat, LPM Firewall, and install-time source analysis floors
* The source of each displayed floor value
* The approved-posture path and active managed policy
* Active runtime overrides
* Active unlocks, package filters, and expiry times

Use JSON to get the same status in a structured object:

```bash
lpm security status --json
```

The command returns a stable top-level envelope:

```json
{
  "success": true,
  "status": {
    "target": "project",
    "project_root": "/path/to/project",
    "effective_floor": {
      "script_policy": "deny",
      "minimum_release_age_secs": 0,
      "release_age_policy": "direct",
      "sandbox_mode": "default",
      "sandbox_allow_degraded": false,
      "sigstore_verify": "deny",
      "typosquat_guard": "default",
      "firewall_mode": "off",
      "install_time_source_analysis": false
    },
    "active_runtime_overrides": [],
    "active_unlocks": []
  }
}
```

The full response also includes `floor_sources`, `approved_posture_path`, `approved_posture_source`, and an optional `managed_policy` object.

## Project and user configuration [#project-and-user-configuration]

`lpm security` does not read settings from `lpm.json`.

Security-sensitive settings can come from `package.json`, `lpm.toml`, environment variables, or `~/.lpm/config.toml`. These values are requests, not approval by themselves.

Examples of guarded project settings include:

* `package.json > lpm.scriptPolicy = "allow"`
* `package.json > lpm.minimumReleaseAge = 0`
* `package.json > lpm.minimumReleaseAgePolicy = "direct"`
* `lpm.toml > [sandbox] mode = "none"`
* Wider `trustedDependencies`, `trustedScopes`, `passEnv`, `readProject`, or `sandboxLimits` values

If a setting weakens the approved floor, LPM CLI rejects it until you approve the required scope.

Use [`lpm config`](/docs/infra/config#security-sensitive-changes) for persistent user settings. In an interactive terminal, it requests approval before it saves a weaker security value.

A managed policy has higher authority than user or project settings. You cannot unlock a change that the managed policy prohibits.

## Managed machine policy [#managed-machine-policy]

`lpm security protect` manages the LPM Firewall entry in the OS-managed policy file:

```text
Unix/macOS: /etc/lpm/security-policy.toml
Windows:    C:\ProgramData\lpm\security-policy.toml
```

`protect enable` uses `enforce` by default. It also accepts `--firewall monitor`. It does not accept `off`.

`protect disable` removes only the managed firewall entry. It preserves other controls in the same file.

Administrators can manage a complete policy directly:

```toml title="/etc/lpm/security-policy.toml"
script-policy = "deny"
minimum-release-age-secs = 86400
release-age-policy = "strict"
typosquat-guard = "on"
install-time-source-analysis = true

[policy]
name = "corp-default"
source = "mdm"

[sandbox]
mode = "strict"
allow-degraded = false

[sigstore]
verify = "deny"

[firewall]
mode = "enforce"
```

The policy path and its parent directories must be controlled by the operating-system administrator. LPM CLI rejects a policy that other users can replace or edit.

## Recovery [#recovery]

If a signed security file has an invalid signature, run `repair`. Also run it if the signing secret is missing:

```bash
lpm security repair
```

The command verifies the approved posture, approval state, unlocks, and the audit log. It renames each invalid file with an `.unverified-...` suffix.

If the operating-system credential store returns an unexpected error, `repair` stops. It does not quarantine files that it cannot verify.

After a repair, run:

```bash
lpm security status
```

LPM CLI uses its built-in security floor and any valid managed policy until you approve new local state.

Use JSON to record the repair result:

```bash
lpm security repair --json
```

```json
{
  "success": true,
  "repair": {
    "security_dir": "/Users/alice/.lpm/security",
    "quarantined": [
      {
        "original_path": "/Users/alice/.lpm/security/approved-posture.json",
        "quarantine_path": "/Users/alice/.lpm/security/approved-posture.json.unverified-20260531T121314.000Z",
        "reason": "signature verification failed"
      }
    ]
  }
}
```

## JSON output [#json-output]

Use `--json` with read-only commands and non-interactive changes:

```bash
lpm security status --json
lpm security lock sandbox-none --project . --json
lpm security protect status --json
lpm security repair --json
```

`unlock --json` always refuses because creating an unlock requires interactive approval.

Successful `lock`, `status`, `protect`, and `repair` responses use `success: true`. The response contains the affected scopes or result details.

## Flags [#flags]

| Command          | Argument or flag                | Purpose                                                           |
| ---------------- | ------------------------------- | ----------------------------------------------------------------- |
| `unlock`         | `<SCOPE>`                       | Select one concrete scope, `default`, or `all`                    |
| `unlock`         | `--project <PATH>`              | Create a project unlock                                           |
| `unlock`         | `--global`                      | Create a global unlock. This is the default target                |
| `unlock`         | `--ttl <DURATION>`              | Set the lifetime. The default is `10m`, and the maximum is `365d` |
| `unlock`         | `--package <PKG>`               | Limit one concrete scope to a package. This flag is repeatable    |
| `lock`           | `<SCOPE>`                       | Revoke one concrete scope, `default`, or `all`                    |
| `lock`           | `--project <PATH>`              | Revoke project unlocks                                            |
| `lock`           | `--global`                      | Revoke global unlocks. This is the default target                 |
| `lock`           | `--package <PKG>`               | Match an exact package filter set. This flag is repeatable        |
| `status`         | `--project <PATH>`              | Inspect another project. The default is the current directory     |
| `status`         | `--global`                      | Inspect global security state                                     |
| `protect enable` | `--firewall <monitor\|enforce>` | Select the managed firewall mode. The default is `enforce`        |
| All              | `--json`                        | Print structured JSON. `unlock --json` refuses approval           |

For each command, `--project` and `--global` are mutually exclusive. Bundle scopes do not accept `--package`.

See the other [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm install`](/docs/packages/install#guarded-weakeners-and-approvals) - run installs with guarded security options
* [`lpm config`](/docs/infra/config#security-sensitive-changes) - manage persistent user security settings
* [`~/.lpm/config.toml`](/docs/reference/config-toml#security-sensitive-keys-are-proposals) - review user configuration and precedence
* [`lpm.toml`](/docs/reference/lpm-toml#sandbox) - configure project sandbox behavior
* [Exit codes](/docs/reference/exit-codes) - handle approval and security-floor failures


# lpm self-update (/docs/infra/self-update)



Use `lpm self-update` to update LPM CLI or switch release channels. The command detects the installation method and uses its update process.

```bash
lpm self-update [--channel <stable|nightly>] [--refresh] [--json]
```

## Quickstart [#quickstart]

Update LPM CLI on its current release channel:

```bash
lpm self-update
```

## Choose a task [#choose-a-task]

| Task                                  | Command                             |
| ------------------------------------- | ----------------------------------- |
| Update on the installed channel       | `lpm self-update`                   |
| Switch to nightly                     | `lpm self-update --channel nightly` |
| Switch to stable                      | `lpm self-update --channel stable`  |
| Ignore the lookup cache and retry now | `lpm self-update --refresh`         |
| Get a structured result               | `lpm self-update --json`            |

This command manages the LPM CLI installation on your computer. `lpm.json` does not control or run CLI updates.

## Release channels [#release-channels]

Without `--channel`, LPM CLI uses the channel of the installed version. Stable versions continue on stable, and nightly versions continue on nightly.

A version such as `0.71.0-nightly.20260728.42.d82ceea` selects nightly. A version without the `nightly` prerelease identifier selects stable.

An explicit `--channel` value changes the channel. A switch from nightly to stable can install a lower semantic version.

| Installation method | Stable    | Nightly       |
| ------------------- | --------- | ------------- |
| npm                 | Supported | Supported     |
| Standalone          | Supported | Supported     |
| Homebrew            | Supported | Not supported |
| Cargo               | Supported | Not supported |

Homebrew and Cargo installations return an error for `--channel nightly`. Use npm or a standalone installation for nightly releases.

## How the update runs [#how-the-update-runs]

LPM CLI identifies the installation method from the current executable. Normal output and JSON output have different update behavior.

| Installation method | Normal output                                         | With `--json`                                              |
| ------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
| npm                 | Runs a version-pinned npm install                     | Returns the command and does not run it                    |
| Homebrew            | Runs `brew upgrade lpm`                               | Returns the command and does not run it                    |
| Cargo               | Verifies the release, then installs its source commit | Verifies the release and returns the commit-pinned command |
| Standalone          | Verifies and replaces the binary                      | Verifies and replaces the binary                           |

> **CAUTION:** `--json` is not a universal preview. It applies the update for a standalone installation.

If the installed version is current, the command makes no change. If the installed version is newer, the command also makes no change.

## Recipes [#recipes]

### Update on the installed channel [#update-on-the-installed-channel]

```bash
lpm self-update
```

Use this command for normal updates. Stable and nightly installations remain on their current channels.

### Switch to nightly [#switch-to-nightly]

```bash
lpm self-update --channel nightly
```

Use nightly for early access to unreleased changes. Nightly releases can change more frequently than stable releases.

### Return to stable [#return-to-stable]

```bash
lpm self-update --channel stable
```

This command can replace a newer nightly version with a lower stable version. The channel change is intentional.

### Retry a version lookup [#retry-a-version-lookup]

```bash
lpm self-update --refresh
```

Use `--refresh` after a lookup failure, or after a release appears during the 10-minute cache period.

The flag also bypasses the one-hour cooldown after a failed lookup. It does not change the verification or installation process.

### Get an external update plan [#get-an-external-update-plan]

> **CAUTION:** Use this recipe only for npm, Homebrew, or Cargo installations. A standalone installation applies the update.

```bash
lpm self-update --json
```

For an external installation method, read `install_method` and `update_command`. Then run the returned command in your normal update environment.

There is no universal dry-run flag for `lpm self-update`.

### Run the update command manually [#run-the-update-command-manually]

| Installation method | Manual command                                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Homebrew            | `brew upgrade lpm`                                                                                                   |
| npm                 | `npm install -g @lpm-registry/cli@<version>`                                                                         |
| Cargo               | `cargo install --git https://github.com/lpm-dev/rust-client --rev <verified-source-commit> lpm-cli --force --locked` |
| Standalone          | `lpm self-update`                                                                                                    |

For npm, you can also follow a channel tag:

```bash
npm install -g @lpm-registry/cli@latest   # newest stable
npm install -g @lpm-registry/cli@nightly  # newest nightly
```

## Cargo source verification [#cargo-source-verification]

For a Cargo installation, LPM CLI verifies the signed release manifest before it starts Cargo.

The signature identifies the exact release workflow, Git reference, and source commit. LPM CLI then uses that commit with `cargo install --rev`.

The `--locked` flag makes Cargo use the dependency versions in the release lockfile. A moved Git tag cannot change the selected source commit.

`lpm self-update --json` applies the same verification. It returns the verified `--rev` command without running Cargo.

## Update source trust [#update-source-trust]

LPM CLI uses a different trust model for each installation method.

| Installation method             | Trust source                                                                     |
| ------------------------------- | -------------------------------------------------------------------------------- |
| Standalone                      | The signed release manifest, release identity, source commit, and asset checksum |
| Cargo                           | The signed release source commit and the locked dependency versions              |
| npm, pnpm, Bun, Yarn, and Volta | The configured registry or mirror and package manager verification               |
| Homebrew                        | The configured Homebrew repository and formula                                   |

LPM CLI resolves each external manager from a filtered `PATH`. The manager must use an absolute, non-project path with trusted ownership.

LPM CLI verifies the manager file before it runs the update. It also verifies the installed LPM CLI version after the update.

These checks do not add independent artifact attestation to registry managers or Homebrew. Their configured repositories remain part of the trust boundary.

Cargo ownership detection reads the local Cargo install metadata. Stale metadata does not prove the build provenance of the current executable.

LPM CLI rejects intermediate manager and configuration symlinks. It accepts a leaf manager symlink only for a target that passes the same checks.

For manager ownership detection, `XDG_CONFIG_HOME` must resolve inside the account home. LPM CLI ignores an external configuration root.

## Version lookup and cache [#version-lookup-and-cache]

LPM CLI reads the npm `latest` or `nightly` tag first. If that lookup fails, it uses the corresponding GitHub Releases data.

Successful `lpm self-update` lookups remain cached for 10 minutes. Stable and nightly use separate cache state in `~/.lpm/update-check.json`.

A failed lookup starts a one-hour cooldown for that channel. During the cooldown, the command returns the previous error state without a network request.

Use `--refresh` to bypass the successful-result cache and the failure cooldown.

## Background update notices [#background-update-notices]

Normal LPM CLI commands can print a cached update notice after their command output. JSON commands do not print this notice.

LPM CLI refreshes the update information in a detached process. The command does not wait for this network request.

After a successful background lookup, LPM CLI waits 24 hours before another lookup. A failed lookup has a one-hour cooldown.

Disable background lookups and cached update notices for a command with this environment variable:

```bash
LPM_NO_UPDATE_CHECK=1 lpm install
```

`LPM_NO_UPDATE_CHECK` does not disable an explicit `lpm self-update`.

## Standalone update verification [#standalone-update-verification]

For a standalone installation, LPM CLI downloads the release files and completes these steps before it replaces the binary:

1. It verifies that the signed manifest comes from the expected LPM CLI release workflow.
2. It verifies that the signed Git reference and source commit match the requested version.
3. It verifies that the signature time matches the GitHub release time.
4. It verifies the checksum of the manifest.
5. It verifies the downloaded binary against the manifest.

If any verification step fails, LPM CLI refuses the update and retains the current binary.

The updater attempts to copy the current binary to `<binary>.previous`. This backup is best-effort and can be absent after a permission error.

### Supported standalone targets [#supported-standalone-targets]

| Operating system | Architecture  |
| ---------------- | ------------- |
| macOS            | ARM64 and x64 |
| Linux with glibc | ARM64 and x64 |
| Linux with musl  | x64           |
| Windows          | x64           |

There is no standalone Linux ARM64 musl asset. On that platform, [install from source](/docs/installation#from-source-cargo).

### Recover from a standalone update error [#recover-from-a-standalone-update-error]

If verification fails, do not replace the current binary manually with the rejected download. Retry later or install a trusted exact version.

If the new binary cannot start, look for `<binary>.previous` beside it. Restore that backup manually, or reinstall an exact release.

The standalone updater does not provide an automatic rollback command. See [Installation](/docs/installation) for exact-version and source installations.

Do not replace the binary with a raw `curl` or `Invoke-WebRequest` download. Those commands do not apply the verification steps on this page.

## Filesystem and process limits [#filesystem-and-process-limits]

The update lock prevents concurrent LPM CLI updates on local filesystems. Advisory lock behavior on NFS and SMB depends on the filesystem.

On Unix, the update lock is account-specific. The package manager must coordinate updates across accounts that share one installation.

> **CAUTION:** If the LPM CLI installation is on a shared filesystem, run updates from one host at a time.

A process with the same account permissions can replace parent directories, manager paths, or staged files after the final check.

The updater does not protect against a compromised user account or an administrator. Operating-system access controls provide this protection.

On Windows, trusted write access includes the current account, Administrators, SYSTEM, and TrustedInstaller. LPM CLI rejects reparse points in checked paths.

The Windows installation lock uses a global operating-system name. Another local account can block updates by reserving that name.

Homebrew can use user-owned, group-writable directories. LPM CLI accepts this layout only for directories owned by the current account.

LPM CLI terminates the manager process group after errors and timeouts. On Unix, a child that creates a new session can survive cleanup.

## JSON output [#json-output]

> **CAUTION:** On a standalone installation, `lpm self-update --json` verifies and applies the update.

### Already current [#already-current]

```json
{
  "success": true,
  "current": "0.71.0",
  "latest": "0.71.0",
  "up_to_date": true,
  "cache_hit": false,
  "channel": "stable",
  "target_channel": "stable",
  "channel_changed": false
}
```

### External update plan [#external-update-plan]

For npm, Homebrew, and Cargo, JSON output reports the update command but does not run it:

```json
{
  "success": true,
  "current": "0.71.0",
  "latest": "0.72.0",
  "up_to_date": false,
  "install_method": "npm",
  "update_command": "npm install -g @lpm-registry/cli@0.72.0",
  "cache_hit": false,
  "channel": "stable",
  "target_channel": "stable",
  "channel_changed": false
}
```

`success: true` means that version discovery and plan creation succeeded. It does not mean that the external update was applied.

### Applied standalone update [#applied-standalone-update]

For a standalone installation, JSON output applies the update and includes its verification record:

```json
{
  "success": true,
  "current": "0.71.0",
  "latest": "0.72.0",
  "up_to_date": false,
  "install_method": "standalone",
  "cache_hit": false,
  "channel": "stable",
  "target_channel": "stable",
  "channel_changed": false,
  "verified": true,
  "attestation": {
    "publisher": "<verified publisher>",
    "workflow_path": ".github/workflows/release.yml",
    "workflow_ref": "<verified workflow reference>",
    "integrated_time": "2026-08-05T12:00:00+00:00",
    "log_index": 123456789,
    "log_id": "<transparency log ID>",
    "leaf_cert_sha256": "<certificate SHA-256>",
    "manifest_sha256": "<manifest SHA-256>",
    "asset_sha256": "<binary SHA-256>",
    "asset_name": "lpm-darwin-arm64",
    "source_commit": "<verified source commit>"
  }
}
```

### Common fields [#common-fields]

| Field             | Type    | Meaning                                                                    |
| ----------------- | ------- | -------------------------------------------------------------------------- |
| `success`         | Boolean | `true` after version discovery and the action for this output mode succeed |
| `current`         | String  | Version of the running binary                                              |
| `latest`          | String  | Latest version found on the target channel                                 |
| `up_to_date`      | Boolean | `true` for a current or newer version without a channel change             |
| `cache_hit`       | Boolean | `true` after the command uses a recent successful lookup                   |
| `channel`         | String  | Channel of the current binary                                              |
| `target_channel`  | String  | Channel selected for this command                                          |
| `channel_changed` | Boolean | `true` for different `target_channel` and `channel` values                 |

### Outcome fields [#outcome-fields]

| Field            | Type    | Meaning                                                                 |
| ---------------- | ------- | ----------------------------------------------------------------------- |
| `install_method` | String  | Detected method for an available update                                 |
| `update_command` | String  | External update plan for npm, Homebrew, or Cargo                        |
| `verified`       | Boolean | `true` after an applied standalone update passes all verification steps |
| `attestation`    | Object  | Verification record for an applied standalone update                    |

### `attestation` fields [#attestation-fields]

| Field              | Type           | Meaning                                           |
| ------------------ | -------------- | ------------------------------------------------- |
| `publisher`        | String or null | Verified publisher from the release signature     |
| `workflow_path`    | String or null | Verified release-workflow path                    |
| `workflow_ref`     | String or null | Verified workflow reference                       |
| `integrated_time`  | String         | Signature time in RFC 3339 format                 |
| `log_index`        | Integer        | Entry index in the transparency log               |
| `log_id`           | String         | Identifier of the transparency log                |
| `leaf_cert_sha256` | String         | SHA-256 digest of the signing certificate         |
| `manifest_sha256`  | String         | SHA-256 digest of the signed manifest             |
| `asset_sha256`     | String         | SHA-256 digest of the installed binary            |
| `asset_name`       | String         | Release asset selected for the current platform   |
| `source_commit`    | String         | Source commit from the verified release statement |

## GitHub fallback limits [#github-fallback-limits]

GitHub permits 60 unauthenticated API requests each hour for one IP address. This limit matters only after the npm lookup fails.

Set `GITHUB_TOKEN` or `GH_TOKEN` to use the authenticated limit of 5,000 requests each hour. Public repository metadata access is sufficient.

If the fallback reaches its limit, LPM CLI reports the reset time. Wait for that time, or supply a token.

## CI [#ci]

Do not run `lpm self-update` during a CI job. An update changes the toolchain during the job and makes the build less reproducible.

Pin an exact npm package version or standalone release instead. Change that pin in a reviewed project update.

Use [`lpm health`](/docs/infra/doctor#lpm-health) for a registry connectivity test.

## Flags [#flags]

| Flag                          | Effect                                                                                       |
| ----------------------------- | -------------------------------------------------------------------------------------------- |
| `--channel <stable\|nightly>` | Select a release channel. Without this flag, LPM CLI follows the installed channel.          |
| `--refresh`                   | Bypass the 10-minute successful lookup cache and the one-hour failure cooldown.              |
| `--json`                      | Print structured output. External methods return a plan, but standalone updates still apply. |

See the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [Installation](/docs/installation) — install or reinstall LPM CLI
* [`lpm doctor`](/docs/infra/doctor) — inspect the project and local environment
* [`lpm health`](/docs/infra/doctor#lpm-health) — verify registry connectivity
* [LPM CLI releases](https://github.com/lpm-dev/rust-client/releases) — read release notes and download exact versions


# lpm setup (/docs/infra/setup)



Use `lpm setup` to configure registry access or create a starting workflow for CI secrets.

```bash
lpm setup ci npmrc [-r <URL>] [--oidc]
lpm setup ci github-actions [--env <NAME>]
lpm setup ci gitlab [--env <NAME>]
lpm setup local [-d <DAYS>]
```

Choose a command for the client or workflow that needs authentication:

```bash
lpm setup ci npmrc                     # create .npmrc for npm, pnpm, or Yarn in CI
lpm setup local                        # create a local read-only project token
lpm setup ci github-actions            # print a GitHub Actions OIDC workflow fragment
lpm setup ci gitlab                    # print a GitLab CI OIDC workflow fragment
```

## Choose a setup mode [#choose-a-setup-mode]

| Command                       | Result                                                        | Use it when                                                                  |
| ----------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `lpm setup ci npmrc`          | Writes a project `.npmrc` with a literal token                | npm, pnpm, Yarn, or another npm-compatible client runs in CI                 |
| `lpm setup local`             | Writes a project `.npmrc` with a new read-only token          | An npm-compatible client needs local access to private `@lpm.dev/*` packages |
| `lpm setup ci github-actions` | Prints an OIDC workflow fragment and an authorization command | GitHub Actions must pull project secrets with `lpm env`                      |
| `lpm setup ci gitlab`         | Prints an OIDC job fragment and an authorization command      | GitLab.com CI must pull project secrets with `lpm env`                       |

If CI uses only LPM CLI, set `LPM_TOKEN` on the package command. LPM CLI does not need a generated `.npmrc` file.

```yaml
- name: Install private packages
  run: lpm ci
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
```

## Project configuration in `lpm.json` [#project-configuration-in-lpmjson]

The GitHub Actions and GitLab targets prepare OIDC workflows for [`lpm env pull`](/docs/dev/env#oidc-policies-ci).

These workflows use the `vault` field in `lpm.json`. The generated deploy step also runs the `deploy` script from this file.

```json title="lpm.json"
{
  "vault": "7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b",
  "scripts": {
    "deploy": "./deploy.sh"
  }
}
```

Create the local secret project before you generate the workflow:

```bash
lpm env set API_URL=https://api.example.com
lpm env push
lpm setup ci github-actions --env production
```

The `lpm env` commands create the `vault` field. Commit `lpm.json` after this field exists.

The GitHub Actions target copies the vault ID into the printed `LPM_VAULT_ID` value. If the field is missing, it prints `<your-vault-id>`.

The GitLab job can read the vault ID from the checked-out `lpm.json` file. The setup command does not change `lpm.json`.

`lpm setup ci npmrc` and `lpm setup local` do not use `lpm.json`.

## Create `.npmrc` for CI [#create-npmrc-for-ci]

If an npm-compatible client needs LPM.dev Registry authentication, run this command:

```yaml
- name: Configure LPM.dev Registry access
  run: lpm setup ci npmrc
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
- run: npm ci
```

The command writes this scoped block to `.npmrc` in the current directory:

```ini title=".npmrc"
# LPM Registry (generated by lpm setup ci npmrc)
//lpm.dev/:_authToken=<resolved-token>
@lpm.dev:registry=https://lpm.dev/api/registry/
# End LPM Registry
```

Only `@lpm.dev/*` packages use this route. Existing default registries and unrelated `.npmrc` lines stay unchanged.

The command replaces its previous generated block. On Unix, LPM CLI sets the file permissions to `0600`.

CAUTION: Do not commit this `.npmrc` file. The file contains a literal bearer token.

This command does not add `.npmrc` to `.gitignore`. CI runners usually remove the workspace after the job.

### Choose the CI credential [#choose-the-ci-credential]

Without `--oidc`, the command uses one of these credentials:

1. The global `--token <TOKEN>` flag
2. `LPM_TOKEN`
3. A valid token from [`lpm login`](/docs/infra/login)

If neither credential is available, the command stops before it changes `.npmrc`.

LPM CLI refreshes an expired browser session before it writes `.npmrc`. If the refresh fails, the command stops and keeps `.npmrc` unchanged.

Use `--oidc` to require a CI identity exchange:

```bash
lpm setup ci npmrc --oidc
```

GitHub Actions needs `permissions: id-token: write`. GitLab.com CI needs `LPM_OIDC_TOKEN` with the `https://lpm.dev` audience.

An OIDC error stops the command. It does not use `LPM_TOKEN` or a saved login as a fallback.

Use `--registry` with another LPM-compatible registry:

```bash
lpm setup ci npmrc --registry https://registry.example.com
```

A stored session applies only to that exact registry URL. The command appends `/api/registry/` to the base URL in the generated scope route.

### JSON output [#json-output]

If automation needs the file path and credential source, use JSON output:

```bash
lpm setup ci npmrc --json
```

The command still writes `.npmrc`. The JSON response replaces the token with `<redacted>`.

## Create a local project token [#create-a-local-project-token]

If a local npm-compatible client needs a read-only project token, run these commands:

```bash
lpm login
lpm setup local
npm install
```

The LPM.dev Registry requires a valid browser-backed CLI session. The default token lifetime is 30 days.

Set a lifetime from 1 through 90 days:

```bash
lpm setup local --days 7
lpm setup local --days 90
```

The command changes two project files:

* `.npmrc` receives a scoped, read-only token for `@lpm.dev/*` packages.
* `.gitignore` receives one `.npmrc` entry.

Existing `.npmrc` lines stay unchanged. A custom default registry also stays unchanged.

CAUTION: The `.npmrc` file contains a token. Keep the file outside version control.

Run `lpm setup local` again before the token expires. The command replaces the previous project token instead of creating active duplicates.

JSON output reports the paths, scope, and expiry. It does not include the token.

```bash
lpm setup local --json
```

## Print a GitHub Actions workflow [#print-a-github-actions-workflow]

Print the workflow fragment for the `production` secret environment:

```bash
lpm setup ci github-actions
```

Select another environment with `--env`:

```bash
lpm setup ci github-actions --env staging
```

The command prints three items:

1. A complete GitHub Actions job fragment for `lpm env pull --oidc`
2. An `lpm env oidc allow` command for the repository
3. An instruction to configure the returned policy ID as `LPM_OIDC_POLICY_ID`

The command prints output only. It does not create or change a workflow file.

Run the printed authorization command from a logged-in development machine. Then add the workflow fragment to `.github/workflows/deploy.yml`.

The authorization command also uploads protected material for the personal env project. Continue only when LPM CLI reports that CI escrow is enabled.

If the escrow upload fails, correct the error and run the same authorization command again. The command updates the matching policy.

When you run the authorization command, LPM CLI gets the immutable repository ID from GitHub. Private repositories need `GITHUB_TOKEN`, `GH_TOKEN`, or `--repository-id`.

The job fragment uses `ubuntu-latest` and includes all required job keys.

If your deployment needs another runner, change `runs-on` after you add the fragment.

The job needs `id-token: write` permission. It uses the vault ID from `lpm.json` as `LPM_VAULT_ID`.

The fragment reads `LPM_OIDC_POLICY_ID` from the GitHub `vars` context. Create it as a repository variable before the first pull. To use an environment variable instead, add `environment: <name>` to the job.

The aliases `github` and `gha` select the same target.

## Print a GitLab CI workflow [#print-a-gitlab-ci-workflow]

Print the GitLab.com CI fragment:

```bash
lpm setup ci gitlab
lpm setup ci gitlab --env staging
```

The command prints three items:

1. A GitLab CI job fragment with an `LPM_OIDC_TOKEN` identity token
2. An `lpm env oidc allow` command for the numeric project ID
3. An instruction to configure the returned policy ID as `LPM_OIDC_POLICY_ID`

The identity token uses the `https://lpm.dev` audience. The checked-out `lpm.json` file supplies the vault ID.

The command prints output only. It does not create or change `.gitlab-ci.yml`.

Run the printed authorization command from a logged-in development machine. Then add the job fragment to `.gitlab-ci.yml`.

The authorization command also uploads protected material for the personal env project. Continue only when LPM CLI reports that CI escrow is enabled.

Create `LPM_OIDC_POLICY_ID` as a GitLab CI/CD variable before the first pull. Mark it protected only when every branch or tag allowed by the OIDC policy is protected.

The alias `gitlab-ci` selects the same target. Self-managed GitLab issuers are not supported for this workflow.

## Recover an interrupted setup [#recover-an-interrupted-setup]

If `lpm setup local` reports an interrupted token replacement, run the same command again. Use the same registry URL.

```bash
lpm setup local --registry https://registry.example.com
```

If you want to switch to CI setup, finish the pending local replacement first. Then run `lpm setup ci npmrc`.

If a generated local block belongs to another registry, use that registry URL for recovery. LPM CLI does not move the token between registries.

## Flags [#flags]

| Command                     | Flag                     | Purpose                                                                                    |
| --------------------------- | ------------------------ | ------------------------------------------------------------------------------------------ |
| `lpm setup ci npmrc`        | `-r`, `--registry <URL>` | Use another LPM-compatible registry base URL                                               |
| `lpm setup ci npmrc`        | `--oidc`                 | Require a CI OIDC exchange and disable credential fallback                                 |
| GitHub Actions or GitLab CI | `--env <NAME>`           | Set the secret environment in the workflow and policy command. The default is `production` |
| `lpm setup local`           | `-d`, `--days <DAYS>`    | Set the read-only token lifetime from 1 through 90 days. The default is 30                 |

Use only the flags listed for the selected target. The `npmrc` target rejects `--env`. The GitHub Actions and GitLab targets reject `--registry` and `--oidc`. The aliases use the same rules.

An invalid flag stops the command before it writes a file or prints a workflow fragment.

`--json` returns structured output for `npmrc` and `local`. The GitHub Actions and GitLab targets always print human-readable fragments.

## See also [#see-also]

* [Authentication](/docs/infra/authentication) - learn how LPM CLI selects credentials
* [`lpm login, logout, and whoami`](/docs/infra/login) - manage registry sessions
* [`lpm env`](/docs/dev/env) - manage project secrets and OIDC policies
* [CI/CD setup](/docs/guides/ci-cd-setup) - configure complete CI jobs
* [Environment variables](/docs/reference/env-vars) - use `LPM_TOKEN`, `LPM_OIDC_TOKEN`, and `LPM_VAULT_ID`


# lpm store (/docs/infra/store)



Use `lpm store` to find, verify, or remove the shared package store on your computer.

```bash
lpm store path [--json]
lpm store verify [--deep] [--fix] [--json]
lpm store clean [--json]
```

The `--deep` and `--fix` flags must follow `verify`. LPM CLI rejects `--deep` and `--fix` with `path` or `clean`.

## Quickstart [#quickstart]

Run a fast verification of every store version:

```bash
lpm store verify
```

## Choose an action [#choose-an-action]

| Action   | What it does                                      | Use case                                                         |
| -------- | ------------------------------------------------- | ---------------------------------------------------------------- |
| `path`   | Prints the root of the shared store               | You need the store path for inspection or a script               |
| `verify` | Verifies v1, v2, and v3 store data                | An install reports a store error, or you want an integrity check |
| `clean`  | Immediately removes all v1, v2, and v3 store data | You need a complete reset and accept new downloads and builds    |

## What the shared store does [#what-the-shared-store-does]

LPM CLI saves package data in `~/.lpm/store/`. Projects link to this shared data instead of keeping a separate package cache.

The store reduces repeat downloads and duplicate data across projects. It also contains package security caches and reusable native-build artifacts.

A store version identifies the layout of saved data. It does not identify a package version or the [`lpm.lock` format](/docs/reference/lockfile-format).

## Store versions [#store-versions]

| Version | Status       | Layout                                                                     | How to select it        | Use case                                             |
| ------- | ------------ | -------------------------------------------------------------------------- | ----------------------- | ---------------------------------------------------- |
| v1      | Legacy       | One complete directory for each package name and version                   | `LPM_STORE_VERSION=v1`  | Use it only for compatibility with the legacy layout |
| v2      | Default      | Shared package objects with dependency layouts for each project graph      | No setting is necessary | Use it for normal development and CI                 |
| v3      | Experimental | Reusable file blobs, package-tree records, and v2-style dependency layouts | `LPM_STORE_VERSION=v3`  | Use it to test the experimental file-level store     |

### v1 [#v1]

The v1 store keeps each extracted package under `~/.lpm/store/v1/<name>@<version>/`. It reuses matching package names and versions.

v1 is the legacy layout. Select it only for compatibility or downgrade testing.

### v2 [#v2]

The v2 store keeps package content in shared objects. It also keeps separate link entries for different dependency graphs.

Two projects can reuse the same package content. Each project can still have the dependency layout that its lockfile requires.

v2 is the current default. An unset or empty `LPM_STORE_VERSION` value selects v2.

### v3 [#v3]

The v3 store divides package trees into reusable file blobs. Tree and source records describe how LPM CLI reconstructs each package.

Identical files can share stored data across different packages. v3 retains the dependency-link model from v2.

v3 is experimental. LPM CLI never selects it automatically.

### Select a version for an install [#select-a-version-for-an-install]

Set `LPM_STORE_VERSION` for the install command:

```bash
LPM_STORE_VERSION=v1 lpm install  # use the legacy layout
LPM_STORE_VERSION=v3 lpm install  # use the experimental layout
```

The values `1`, `2`, and `3` are aliases for `v1`, `v2`, and `v3`. An unknown value produces a warning and selects v2.

`lpm.json` cannot select a store version. The `LPM_STORE_VERSION` environment variable is the only current selector.

The three versions can exist at the same time. `lpm store verify` and `lpm store clean` always act on all three versions.

## Recipes [#recipes]

### Print the store path [#print-the-store-path]

```bash
lpm store path
```

If a diagnostic tool or script needs the active store root, use this command.

### Run a fast verification [#run-a-fast-verification]

```bash
lpm store verify
```

The fast verification examines package directories, manifests, link metadata, object references, and v3 store metadata.

### Run a deep verification [#run-a-deep-verification]

Run this command from a project root:

```bash
lpm store verify --deep
```

Deep verification parses each `package.json`. It compares package names and versions with their store entries.

If the current directory contains `lpm.lock`, deep verification compares its integrity values with the saved values. It also rehashes v3 file blobs.

If `lpm.lock` is absent, LPM CLI skips the lockfile comparison. If the file is invalid or unreadable, verification fails.

### Refresh security caches [#refresh-security-caches]

```bash
lpm store verify --fix
```

`--fix` implies deep verification. It refreshes missing or stale `.lpm-security.json` files.

`--fix` does not restore missing packages or repair manifests and integrity errors. LPM CLI reports these errors for separate recovery.

### Use verification in CI [#use-verification-in-ci]

```bash
lpm store verify --deep --json > store-report.json
```

If LPM CLI finds corruption, it writes the JSON report and then exits with status `1`.

### Inspect v3 orphan counts [#inspect-v3-orphan-counts]

```bash
lpm store verify --json
```

Read the `cas.orphaned_*` fields for unreachable v3 data. Verification reports this data but does not remove it.

### Preview orphan removal [#preview-orphan-removal]

```bash
lpm cache prune          # preview removals
lpm cache prune --apply  # apply the removal plan
```

[`lpm cache prune`](/docs/packages/cache#prune) uses project references to retain required store entries. The preview does not remove data.

## Verification levels [#verification-levels]

| Command                   | Package and link structure | `package.json` contents | `lpm.lock` integrity               | v3 blob hashes  | Security cache                                |
| ------------------------- | -------------------------- | ----------------------- | ---------------------------------- | --------------- | --------------------------------------------- |
| `lpm store verify`        | Verifies                   | Does not parse          | Does not compare                   | Does not rehash | Does not analyze                              |
| `lpm store verify --deep` | Verifies                   | Parses and compares     | Compares with the current lockfile | Rehashes        | Reports missing or stale caches               |
| `lpm store verify --fix`  | Verifies                   | Parses and compares     | Compares with the current lockfile | Rehashes        | Reports and refreshes missing or stale caches |

## JSON output [#json-output]

Use the global `--json` flag with each action. Each action returns a different JSON object.

### `path` output [#path-output]

```bash
lpm store path --json
```

```json
{
  "success": true,
  "path": "/Users/you/.lpm/store"
}
```

| Field     | Type    | Meaning                                      |
| --------- | ------- | -------------------------------------------- |
| `success` | Boolean | `true` after LPM CLI resolves the store path |
| `path`    | String  | Absolute path to the store root              |

### `verify` output [#verify-output]

This example shows a successful deep verification:

```bash
lpm store verify --deep --json
```

```json
{
  "success": true,
  "check_kind": "lockfile_marker_consistency",
  "legacy_check_kind": "lockfile_marker_consistency",
  "bytes_integrity_recomputed": true,
  "entries_verified": 32,
  "verified": 32,
  "unique_coords": 30,
  "duplicated_entries": 2,
  "corrupted": 0,
  "issues": [],
  "securityMismatches": 1,
  "securityReanalyzed": 0,
  "cas": {
    "check_kind": "content_hash",
    "sources": 8,
    "trees": 8,
    "blobs": 240,
    "blobs_rehashed": 240,
    "materialized": 8,
    "orphaned_sources": 0,
    "orphaned_trees": 0,
    "orphaned_blobs": 0,
    "orphaned_materialized": 0,
    "blob_integrity_recomputed": true
  }
}
```

#### Verification fields [#verification-fields]

| Field                        | Type         | Meaning                                                                                 |
| ---------------------------- | ------------ | --------------------------------------------------------------------------------------- |
| `success`                    | Boolean      | If `issues` contains a corruption or write error, this value is `false`                 |
| `check_kind`                 | String       | Compatibility alias for `legacy_check_kind`                                             |
| `legacy_check_kind`          | String       | `presence` for fast verification or `lockfile_marker_consistency` for deep verification |
| `bytes_integrity_recomputed` | Boolean      | `true` after deep verification rehashes at least one v3 blob                            |
| `entries_verified`           | Integer      | Number of store entries that completed verification without corruption                  |
| `verified`                   | Integer      | Compatibility alias for `entries_verified`                                              |
| `unique_coords`              | Integer      | Number of unique package name and version pairs                                         |
| `duplicated_entries`         | Integer      | Entries repeated across store versions or dependency graphs                             |
| `corrupted`                  | Integer      | Number of corruption or write errors in `issues`                                        |
| `issues`                     | String array | Error messages for failed verification work                                             |
| `securityMismatches`         | Integer      | Missing or stale security caches found by deep verification                             |
| `securityReanalyzed`         | Integer      | Security caches that `--fix` refreshed successfully                                     |
| `cas`                        | Object       | v3 file-store verification results                                                      |

`securityMismatches` and `securityReanalyzed` appear only during deep verification. A security mismatch alone does not set `success` to `false`.

#### v3 `cas` fields [#v3-cas-fields]

| Field                       | Type    | Meaning                                                                  |
| --------------------------- | ------- | ------------------------------------------------------------------------ |
| `check_kind`                | String  | `metadata` for fast verification or `content_hash` for deep verification |
| `sources`                   | Integer | v3 source records examined                                               |
| `trees`                     | Integer | v3 package-tree records examined                                         |
| `blobs`                     | Integer | v3 file blobs examined                                                   |
| `blobs_rehashed`            | Integer | v3 file blobs rehashed during deep verification                          |
| `materialized`              | Integer | Reconstructed v3 package trees examined                                  |
| `orphaned_sources`          | Integer | Source records that no live store relationship reaches                   |
| `orphaned_trees`            | Integer | Package-tree records that no live source reaches                         |
| `orphaned_blobs`            | Integer | File blobs that no live package tree reaches                             |
| `orphaned_materialized`     | Integer | Reconstructed package trees that no live source reaches                  |
| `blob_integrity_recomputed` | Boolean | `true` after deep verification rehashes at least one v3 blob             |

### `clean` output [#clean-output]

> **CAUTION:** This command removes all store versions without a confirmation prompt.

```bash
lpm store clean --json
```

```json
{
  "success": true,
  "removed_bytes": 1048576,
  "removed": "1.0 MB",
  "path": "/Users/you/.lpm/store/v1",
  "v1_path": "/Users/you/.lpm/store/v1",
  "v2_path": "/Users/you/.lpm/store/v2",
  "v3_path": "/Users/you/.lpm/store/v3",
  "v1_removed_bytes": 0,
  "v2_removed_bytes": 1048576,
  "v3_removed_bytes": 0
}
```

| Field              | Type    | Meaning                                   |
| ------------------ | ------- | ----------------------------------------- |
| `success`          | Boolean | `true` after the clean action completes   |
| `removed_bytes`    | Integer | Total bytes removed from v1, v2, and v3   |
| `removed`          | String  | Formatted value of `removed_bytes`        |
| `path`             | String  | Compatibility alias for `v1_path`         |
| `v1_path`          | String  | Absolute path of the removed v1 directory |
| `v2_path`          | String  | Absolute path of the removed v2 directory |
| `v3_path`          | String  | Absolute path of the removed v3 directory |
| `v1_removed_bytes` | Integer | Bytes removed from v1                     |
| `v2_removed_bytes` | Integer | Bytes removed from v2                     |
| `v3_removed_bytes` | Integer | Bytes removed from v3                     |

## Reset the complete store [#reset-the-complete-store]

> **CAUTION:** If you need a complete reset, use `lpm store clean`. The command removes all store versions without a confirmation prompt.

```bash
lpm store clean
lpm install
```

The clean action removes package data, link entries, security caches, and native-build artifacts. It retains the outer `~/.lpm/store/` directory.

The next install downloads or rebuilds the required packages. For normal cleanup, use the reference-aware [`lpm cache prune`](/docs/packages/cache#prune) command.

If verification reports corruption, first run:

```bash
lpm cache prune --apply
lpm install
```

If the error remains, use `lpm store clean` for a complete reset.

## Concurrent operations [#concurrent-operations]

Verification can run with other read-only store operations. If another command changes or removes store data, verification waits.

The clean action waits for installs and other store readers to finish. New store readers wait while a clean action is queued.

After one second, a waiting command prints `Waiting for another lpm store operation to finish...`. LPM CLI releases its locks automatically.

## Flags [#flags]

| Flag     | Applies to  | Effect                                                               |
| -------- | ----------- | -------------------------------------------------------------------- |
| `--deep` | `verify`    | Parses manifests, compares lockfile integrity, and rehashes v3 blobs |
| `--fix`  | `verify`    | Refreshes missing or stale security caches and implies `--deep`      |
| `--json` | All actions | Prints the action result as JSON                                     |

See the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm cache prune`](/docs/packages/cache#prune) — preview or remove unreferenced store data
* [Content-addressable store](/docs/packages/content-addressable-store) — understand the package storage model
* [`lpm rebuild`](/docs/packages/rebuild#native-build-cache) — reuse or refresh native-build artifacts
* [`lpm doctor`](/docs/infra/doctor) — inspect store health with other project checks


# lpm token-rotate (/docs/infra/token-rotate)



Use `lpm token-rotate` after token exposure or during planned credential maintenance.

```bash
lpm token-rotate [--otp <CODE>] [--registry <URL>] [--json]
```

The command rotates the active token for the LPM.dev Registry or another LPM-compatible registry.

## Rotate a locally stored token [#rotate-a-locally-stored-token]

Sign in, rotate the token, and make sure that the new token works:

```bash
lpm login
lpm token-rotate
lpm whoami
```

Use this workflow when LPM CLI manages the registry credential on your computer.

## Complete an MFA-protected rotation [#complete-an-mfa-protected-rotation]

The registry requires MFA for some write-capable tokens.

For an interactive rotation, omit `--otp`:

```bash
lpm token-rotate
```

If the registry requires MFA, LPM CLI opens a masked prompt. Enter the six-digit authenticator code.

LPM CLI retries the rotation once. It does not print the code in terminal or JSON output.

If the command cannot use an interactive terminal, pass a fresh code with `--otp`:

```bash
lpm token-rotate --otp 123456 --json
```

LPM CLI rejects a code before the request unless the code contains exactly six ASCII digits.

Use the masked prompt for manual rotations. A command-line code can appear in shell history or process information.

If the registry rejects the code, the command stops after that retry. Run the command again with a fresh code.

## When to use it [#when-to-use-it]

| Situation                                        | Action                                                                  |
| ------------------------------------------------ | ----------------------------------------------------------------------- |
| You think a locally stored token was exposed     | Rotate it immediately                                                   |
| Your security policy requires regular rotation   | Rotate it during the planned maintenance window                         |
| You use another LPM-compatible registry          | Select that registry with `--registry`                                  |
| A CI secret contains `LPM_TOKEN`                 | Replace the token in the registry dashboard and update the secret store |
| You use npm, GitHub Packages, or GitLab Packages | Rotate the token with that provider                                     |

This command does not rotate npm, GitHub, or GitLab tokens.

## What the command changes [#what-the-command-changes]

The registry creates a new value for the current token record. The token keeps its owner, organization, access scope, and expiry.

After a successful rotation:

* The old token value becomes invalid.
* LPM CLI stores the replacement in local secure storage.
* The replacement token does not appear in terminal or JSON output.

The LPM.dev Registry can process five rotation requests per minute for one token record.

In rare cases, an edge cache can accept the old value for up to 60 seconds after rotation.

## Rotate a token for another registry [#rotate-a-token-for-another-registry]

Use the same registry URL for login, rotation, and verification:

```bash
lpm login --registry https://registry.example.com
lpm token-rotate --registry https://registry.example.com
lpm whoami --registry https://registry.example.com
```

The registry must support the LPM-compatible token rotation endpoint.

## External tokens are rejected [#external-tokens-are-rejected]

`lpm token-rotate` accepts only a token from local secure storage.

The command rejects `LPM_TOKEN`, `--token`, and CI-issued tokens before it sends a registry request. It does not change local credentials.

LPM CLI cannot update an environment variable or an external secret store. It also does not print the replacement token.

If `LPM_TOKEN` overrides a stored token, remove it before you run the command:

```bash
unset LPM_TOKEN
lpm token-rotate
```

For CI, rotate the token in the LPM.dev Registry dashboard. Then update `LPM_TOKEN` in the CI secret store.

## Rotate other registry credentials [#rotate-other-registry-credentials]

Use the token controls of each registry to revoke the old value and create a replacement.

| Credential                                   | Save the replacement                                                                      |
| -------------------------------------------- | ----------------------------------------------------------------------------------------- |
| npm token                                    | `lpm login --npm --token <TOKEN>` or update `NPM_TOKEN`                                   |
| GitHub Packages token                        | Update `gh` authentication, the saved fallback token, or `GITHUB_TOKEN`                   |
| GitLab Packages token                        | Update `glab` authentication, the saved fallback token, `GITLAB_TOKEN`, or `CI_JOB_TOKEN` |
| Project `.npmrc` token from LPM.dev Registry | Run [`lpm setup local`](/docs/infra/setup#create-a-local-project-token) again             |

## Recover from a failed rotation [#recover-from-a-failed-rotation]

First, make sure that the active credential still works:

```bash
lpm whoami
```

If authentication fails, sign in again and verify the new session:

```bash
lpm login
lpm whoami
```

## JSON output [#json-output]

Use JSON output in an automation that checks the result:

```bash
lpm token-rotate --json
```

```json
{
  "expires_at": null,
  "rotated": true,
  "storage_backend": "keychain",
  "storage_degraded": false,
  "success": true
}
```

| Field              | Meaning                                                                          |
| ------------------ | -------------------------------------------------------------------------------- |
| `success`          | The command completed successfully                                               |
| `rotated`          | The registry rotated the token                                                   |
| `expires_at`       | The expiry returned by the registry, or `null` if the registry returns no expiry |
| `storage_backend`  | `keychain` or `encrypted_file_fallback`                                          |
| `storage_degraded` | `true` when LPM CLI used the encrypted file fallback                             |

The LPM.dev Registry currently returns no expiry during rotation. It keeps the existing expiry, so `expires_at` is `null`.

If an external token is active, the command returns `unsupported_auth_source`:

```json
{
  "schema_version": 1,
  "success": false,
  "error_code": "unsupported_auth_source",
  "error": {
    "code": "UNSUPPORTED_AUTH_SOURCE",
    "message": "unsupported authentication source for `lpm token-rotate`: LPM_TOKEN",
    "command": "lpm token-rotate",
    "source": "LPM_TOKEN"
  }
}
```

If MFA is required without an interactive prompt, the command returns `otp_required`:

```json
{
  "schema_version": 1,
  "success": false,
  "error_code": "otp_required",
  "error": {
    "code": "OTP_REQUIRED",
    "message": "one-time password required for `lpm token-rotate`",
    "command": "lpm token-rotate"
  }
}
```

An invalid or expired code returns `otp_invalid`. Both errors leave the stored token unchanged.

## Flags [#flags]

The command supports one command-specific flag. The relevant [global flags](/docs/commands#global-flags) are also listed.

| Flag               | Purpose                                                                  |
| ------------------ | ------------------------------------------------------------------------ |
| `--otp <CODE>`     | Send a six-digit authenticator code for an MFA-protected rotation        |
| `--registry <URL>` | Select the LPM-compatible registry                                       |
| `--json`           | Print the result as JSON without printing the token                      |
| `--token <TOKEN>`  | Rejected. Remove this flag and rotate a token from local secure storage. |

## See also [#see-also]

* [`lpm login / logout / whoami`](/docs/infra/login) - manage and verify registry sessions
* [`lpm setup`](/docs/infra/setup) - create local project or CI authentication
* [Authentication](/docs/infra/authentication) - understand credential sources and storage backends
* [Environment variables](/docs/reference/env-vars) - use registry credentials in automation


# lpm tunnel (/docs/infra/tunnel)



`lpm tunnel` gives a public HTTPS URL to a local HTTP service.

Use it for webhook development, API callbacks, remote-device tests, and temporary previews.

For a repeatable project workflow, set a claimed domain in `lpm.json` and run `lpm dev`.

When a service already runs or you manage capture history, use standalone `lpm tunnel`.

```bash
lpm tunnel [<port>|<action>] [target] [flags]
```

## Quickstart with `lpm.json` [#quickstart-with-lpmjson]

When a project needs the same public URL across development sessions, use `lpm.json`.

First, claim the domain once:

```bash
lpm login
lpm tunnel claim acme-api.lpm.llc
```

Then add the domain to `lpm.json`, next to `package.json`:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "tunnel": {
    "domain": "acme-api.lpm.llc"
  }
}
```

Start the project:

```bash
lpm dev
```

The `domain` field enables the tunnel automatically. You do not need to add `--tunnel`.

`lpm dev` starts the project and waits for its final HTTP endpoint. Then it opens the tunnel to that endpoint.

If `services` is absent, `lpm dev` runs the `dev` script from `package.json`.

Use this setup for stable webhook URLs, callback URLs, and preview links.

The `tunnel` value is an object, not a boolean value. The object currently accepts only `domain`.

For an assigned domain instead of a saved domain, omit the `tunnel` object:

```bash
lpm dev --tunnel
```

Free users receive a random `lpm.fyi` domain. A claimed domain requires a Pro or Org plan.

### Override or disable the project tunnel [#override-or-disable-the-project-tunnel]

The `lpm dev` flags override `lpm.json` for one run:

```bash
lpm dev --domain preview-api.lpm.llc # use another claimed domain and enable the tunnel
lpm dev --no-tunnel                 # disable the configured tunnel for this run
```

The `--domain` flag belongs to `lpm dev`. Standalone `lpm tunnel` accepts the domain as a positional argument.

Tunnel access and inspector flags also work with `lpm dev`:

```bash
lpm dev --tunnel-auth               # protect the public URL with a session token
lpm dev --inspect-port 4500         # use a fixed browser-inspector port
lpm dev --no-inspect                # keep capture but skip the browser inspector
```

### Multi-service projects [#multi-service-projects]

If `lpm.json` contains one service, LPM CLI uses that service as the tunnel target.

If it contains multiple services, mark exactly one service as `primary`:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "tunnel": {
    "domain": "acme-api.lpm.llc"
  },
  "services": {
    "web": {
      "command": "vite",
      "port": 5173,
      "primary": true
    },
    "api": {
      "command": "node api.js",
      "port": 4000
    }
  }
}
```

```bash
lpm dev
```

The public URL routes to the final endpoint of `web`. The `api` service remains available to the project on its local endpoint.

See the [`lpm.json` tunnel reference](/docs/reference/lpm-json#tunnel) for the field definition.

## One-time tunnel [#one-time-tunnel]

Start your local HTTP service on port `3000`. Then open the tunnel:

```bash
lpm login
lpm tunnel 3000
```

The command prints a public URL and forwards requests to `http://127.0.0.1:3000/`.

While you use the URL, keep the command open. Press `q` or `Ctrl+C` to stop the tunnel.

## Examples [#examples]

```bash
lpm dev                                   # use tunnel.domain from lpm.json
lpm dev --tunnel                          # use an assigned domain for this run
lpm dev --no-tunnel                       # skip the lpm.json tunnel for this run
lpm tunnel                                # tunnel the only active lpm dev endpoint
lpm tunnel 4000                           # tunnel an explicit local port
lpm tunnel start acme-api.lpm.llc         # tunnel the active dev endpoint with a claimed domain
lpm tunnel 4000 acme-api.lpm.llc          # use a claimed domain
lpm tunnel claim acme-api.lpm.llc         # claim a personal domain
lpm tunnel claim staging.lpm.fyi --org acme # claim an organization domain
lpm tunnel unclaim acme-api.lpm.llc       # release a claimed domain
lpm tunnel list                           # list personal domain claims
lpm tunnel list --org acme                # list organization domain claims
lpm tunnel domains                        # list available base domains
lpm tunnel inspect --last 10              # show recent captured requests
lpm tunnel inspect --ui                   # open the browser inspector
lpm tunnel replay --last --port 4000      # replay the newest capture
lpm tunnel log --status 5xx               # show failed requests
```

## Choose a command [#choose-a-command]

| Goal                                             | Command                             |
| ------------------------------------------------ | ----------------------------------- |
| Start the tunnel from `lpm.json`                 | `lpm dev`                           |
| Start the project with an assigned domain        | `lpm dev --tunnel`                  |
| Disable the `lpm.json` tunnel for one run        | `lpm dev --no-tunnel`               |
| Tunnel the only active `lpm dev` endpoint        | `lpm tunnel`                        |
| Tunnel an explicit port                          | `lpm tunnel 3000`                   |
| Tunnel the active endpoint with a claimed domain | `lpm tunnel start acme-api.lpm.llc` |
| Tunnel with a claimed domain                     | `lpm tunnel 3000 acme-api.lpm.llc`  |
| Claim a stable domain                            | `lpm tunnel claim <domain>`         |
| Release a claimed domain                         | `lpm tunnel unclaim <domain>`       |
| List claimed domains                             | `lpm tunnel list`                   |
| List available base domains                      | `lpm tunnel domains`                |
| Show captured requests                           | `lpm tunnel inspect`                |
| Replay a captured request                        | `lpm tunnel replay <number>`        |
| Browse or clear capture history                  | `lpm tunnel log`                    |

The CLI also accepts `release`, `ls`, and `logs` as aliases.

## Authentication [#authentication]

The remote actions require a saved login session from [`lpm login`](/docs/infra/login).

This requirement applies to tunnel startup, domain claims, domain lists, and base-domain lists.

```bash
lpm login
lpm tunnel 3000
```

The remote actions do not accept `--token`, `LPM_TOKEN`, or CI-only tokens.

When you revoke a CLI session in the registry dashboard, its active tunnels close after the registry processes the revocation. Run `lpm login` again before restarting the tunnel.

LPM CLI renews the saved session credential before it expires. Renewal keeps the same public URL, active connections, and original Free-session deadline.
If renewal fails until the credential expires, the connection closes. A later reconnection can receive a new Free URL.
Older relay versions refresh credentials through reconnection, which can close active requests and WebSockets.

The `inspect`, `replay`, and `log` actions use local files. These actions do not require a login.

## Select the local service [#select-the-local-service]

### Use an explicit port [#use-an-explicit-port]

If you know the local listener, pass its port:

```bash
lpm tunnel 3000
```

An explicit port always targets `http://127.0.0.1:<port>/`. Valid ports are from `1` through `65535`.

The local service must use plain HTTP. The public tunnel URL still uses HTTPS.

If your framework uses HTTPS, disable framework HTTPS. Use [`lpm dev --https`](/docs/dev/dev#https) for trusted local browser TLS.

### Use an active `lpm dev` endpoint [#use-an-active-lpm-dev-endpoint]

Run these commands in different terminals:

```bash
# Terminal 1
lpm dev

# Terminal 2
lpm tunnel
```

Without a port, LPM CLI selects the only active `lpm dev` endpoint. It preserves the endpoint address, port, and base path.

If no active endpoint exists, pass a port. If multiple endpoints exist, pass the intended port.

Use [`lpm dev --tunnel`](/docs/dev/dev) to start the dev server and tunnel from one command.

The tunnel forwards HTTP requests and WebSocket connections. Empty WebSocket close frames remain empty, and normal close codes and reasons pass through.

## Recipes [#recipes]

### Receive webhooks on localhost [#receive-webhooks-on-localhost]

Run your webhook service and tunnel in different terminals:

```bash
# Terminal 1
lpm run dev

# Terminal 2
lpm tunnel 3000 --session stripe-test
```

Add the webhook path to the printed URL. For example, use `https://<domain>/webhooks/stripe` for a `/webhooks/stripe` route.

LPM CLI saves each request and local response in the current directory. The session name helps you find related requests later.

### Replay a failed webhook [#replay-a-failed-webhook]

List recent captures. Then inspect and replay the selected entry:

```bash
lpm tunnel inspect --last 10
lpm tunnel inspect --detail 3
lpm tunnel replay 3 --port 3000
```

Capture numbers use the newest-first list and start at `1`.

Replay sends the original method, path, headers, and body to the selected local service.

If one `lpm dev` endpoint is active, you can omit the replay port:

```bash
lpm tunnel replay --last
```

### Keep a webhook endpoint active [#keep-a-webhook-endpoint-active]

When the tunnel must accept requests without the local service, use `--auto-ack`:

```bash
lpm tunnel 3000 --auto-ack
```

When LPM CLI cannot reach the local service, it returns `200 OK`. It still saves the request for later inspection and replay.

> CAUTION: The provider records the request as successful, although your local service did not process it.

### Protect a preview URL [#protect-a-preview-url]

Pro and Org tunnels can require a per-session access token:

```bash
lpm tunnel 3000 acme-api.lpm.llc --tunnel-auth
```

The startup output prints an `X-Tunnel-Auth` header and a browser URL. Treat the token and browser URL as secrets.

### Use a fixed inspector port [#use-a-fixed-inspector-port]

The browser inspector starts with each tunnel and selects an available local port.

Use a fixed port for a stable local bookmark:

```bash
lpm tunnel 3000 --inspect-port 4500
```

If port `4500` is busy, the command fails. Without this flag, an inspector startup error does not stop the tunnel.

To run without the browser inspector, use:

```bash
lpm tunnel 3000 --no-inspect
```

When `--no-inspect` is active, capture continues.

## Stable domains [#stable-domains]

Free tunnels receive a random `lpm.fyi` domain for each session. Stable domain claims require a Pro or Org plan.

List the enabled base domains before you select a name:

```bash
lpm tunnel domains
```

Current base domains are:

* `lpm.fyi` for Free, Pro, and Org tunnels.
* `lpm.llc` for Pro and Org tunnels.

A Free tunnel can use `lpm.fyi` for its random domain. A stable claim on either base still requires Pro or Org.

Claim and use a personal domain:

```bash
lpm tunnel claim acme-api.lpm.llc
lpm tunnel list
lpm tunnel 3000 acme-api.lpm.llc
```

Organization owners and administrators can manage organization domains:

```bash
lpm tunnel claim staging.lpm.fyi --org acme
lpm tunnel list --org acme
lpm tunnel 3000 staging.lpm.fyi
```

The subdomain must contain from 3 through 32 lowercase letters, numbers, or hyphens. It must start and end with a letter or number.

When you no longer need the claim, release the domain:

```bash
lpm tunnel unclaim acme-api.lpm.llc
lpm tunnel unclaim staging.lpm.fyi --org acme
```

The release is immediate and does not stop for confirmation.

## Plans and limits [#plans-and-limits]

| Tunnel type   | Public domain               | Session limit  | Concurrent tunnels | Stable claims | Tunnel auth |
| ------------- | --------------------------- | -------------- | ------------------ | ------------- | ----------- |
| Free personal | Random `lpm.fyi`            | 1 hour         | 1                  | 0             | No          |
| Pro personal  | Account or claimed domain   | No fixed limit | 3                  | 3             | Yes         |
| Org           | Claimed organization domain | No fixed limit | 10                 | 10            | Yes         |

The startup output shows the current plan, expiry, active limits, and request usage.

Request allowances and rates apply across the account, not to each tunnel. See [Request usage and overage](/docs/infra/tunneling#request-usage-and-overage).

## Capture history [#capture-history]

LPM CLI stores captured requests, responses, and sessions in `.lpm/inspector.db` under the current directory.

Captured data can include request bodies, response bodies, cookies, authorization headers, and signature headers.

Add `.lpm/inspector.db*` to `.gitignore` so that Git does not track the database or its temporary files.

Run tunnel history commands from the same directory that started the tunnel. This keeps each project history separate.

### Inspect captures [#inspect-captures]

```bash
lpm tunnel inspect                       # show the newest 20 captures
lpm tunnel inspect --last 10
lpm tunnel inspect --detail 3            # show request and response details
lpm tunnel inspect --filter stripe       # filter by detected provider
lpm tunnel inspect --status 4xx          # filter by HTTP status class
lpm tunnel inspect --status 404          # filter by exact status
```

The status filter accepts `2xx`, `3xx`, `4xx`, `5xx`, `error`, `err`, or an HTTP status from `100` through `599`.

### Open the browser inspector [#open-the-browser-inspector]

```bash
lpm tunnel inspect --ui
lpm tunnel inspect --ui --inspect-port 4500
```

This action opens the history for the current directory. Press `Ctrl+C` to stop the inspector.

When a tunnel is active, press `o` in its terminal to open the same inspector.

### Browse or clear the log [#browse-or-clear-the-log]

```bash
lpm tunnel log                           # show the newest 50 captures
lpm tunnel log --last 10
lpm tunnel log --filter github
lpm tunnel log --status 5xx
lpm tunnel log --clear
```

> CAUTION: `lpm tunnel log --clear` deletes captured requests and completed sessions for the current directory.

An active session remains available for new captures after a clear operation.

## JSON and scripts [#json-and-scripts]

Use `--json` with local history actions or domain management:

```bash
lpm tunnel list --json
lpm tunnel inspect --last 10 --json
lpm tunnel replay --last --port 3000 --json
```

Local history actions work in non-interactive scripts without a login.

A remote tunnel still requires a saved interactive login session. The tunnel process also remains open until you stop it.

With `--json`, tunnel startup emits a JSON object after each successful connection. A temporary connection failure emits a retry object before the delay:

```json
{
  "schema_version": 1,
  "success": false,
  "event": "retry",
  "error_code": "tunnel_retry",
  "error": "disconnected, retrying in 2s... (relay unavailable)",
  "retrying": true
}
```

Read this output as a sequence of JSON objects. A retry event does not mean that the process exited.
After a permanent failure or exhausted retries, the command emits its final error and exits with a nonzero status.

## Flags [#flags]

| Flag                         | Applies to                 | Effect                                                                               |
| ---------------------------- | -------------------------- | ------------------------------------------------------------------------------------ |
| `--org <SLUG>`               | `claim`, `unclaim`, `list` | Manage domains for an organization.                                                  |
| `--tunnel-auth`              | Start                      | Require the generated access token. Pro and Org only.                                |
| `--auto-ack`                 | Start                      | Provide a `200 OK` fallback for an unavailable local service.                        |
| `--session <NAME>`           | Start                      | Name the capture session.                                                            |
| `--no-inspect`               | Start                      | Do not start the browser inspector. Capture continues.                               |
| `--inspect-port <PORT>`      | Start, `inspect --ui`      | Bind the inspector to an exact port.                                                 |
| `--ui`                       | `inspect`                  | Open the browser inspector.                                                          |
| `--last <N>`, `-n <N>`       | `inspect`, `log`           | Show the newest `N` captures.                                                        |
| `--last`, `-n`               | `replay`                   | Replay the newest capture.                                                           |
| `--detail <N>`, `-d <N>`     | `inspect`                  | Show one capture by its 1-based list number.                                         |
| `--filter <PROVIDER>`        | `inspect`, `log`           | Show captures from one detected provider.                                            |
| `--status <STATUS>`          | `inspect`, `log`           | Show captures with one status or status class.                                       |
| `--clear`                    | `log`                      | Delete capture history for the current directory.                                    |
| `--port <PORT>`, `-p <PORT>` | `replay`                   | Send the replay to this local port.                                                  |
| `--json`                     | All actions                | Write machine-readable output. This is a [global flag](/docs/commands#global-flags). |

## See also [#see-also]

* [`lpm dev --tunnel`](/docs/dev/dev) — start a tunnel as part of the dev pipeline
* [Tunneling](/docs/infra/tunneling) — learn about domains, plans, request usage, and tunnel behavior
* [`lpm cert`](/docs/infra/cert) — use trusted HTTPS for local development without a public tunnel


# Tunneling (/docs/infra/tunneling)



[`lpm tunnel`](/docs/infra/tunnel) connects a public HTTPS URL to one HTTP service on your computer. LPM CLI opens the connection from your computer.

Use a tunnel for webhooks, callbacks, remote-device tests, and temporary previews. A tunnel is a development tool, not permanent application hosting.

## How a tunnel works [#how-a-tunnel-works]

```text
public client
    │ HTTPS or WebSocket
    ▼
LPM.dev Registry tunnel service
    │ open tunnel connection
    ▼
LPM CLI
    │ local HTTP
    ▼
your local service
```

Public visitors use HTTPS. LPM CLI forwards each request to the local service and returns the local response.

Public WebSocket connections use the same route. You do not need to open an inbound port on your router or firewall.

The tunnel lasts while the LPM CLI process remains open. When the process stops, the public URL stops forwarding requests.

## Choose a workflow [#choose-a-workflow]

| Goal                                           | Workflow                                              |
| ---------------------------------------------- | ----------------------------------------------------- |
| Use the same domain for a project              | Add `tunnel.domain` to `lpm.json`, then run `lpm dev` |
| Get an assigned domain for one development run | Run `lpm dev --tunnel`                                |
| Expose an existing local port                  | Run `lpm tunnel <port>`                               |
| Expose the only active `lpm dev` endpoint      | Run `lpm tunnel`                                      |
| Use a claimed domain for one development run   | Run `lpm dev --domain <domain>`                       |
| Use a claimed domain with a standalone tunnel  | Run `lpm tunnel <port> <domain>`                      |

The [`lpm tunnel` command page](/docs/infra/tunnel) contains the complete syntax, recipes, actions, and flags.

## Project configuration [#project-configuration]

Use `lpm.json` for a stable project workflow:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "tunnel": {
    "domain": "acme-api.lpm.llc"
  }
}
```

Start the project:

```bash
lpm dev
```

The `domain` field enables the tunnel automatically. The `tunnel` object currently accepts only `domain`.

LPM CLI waits for the final local endpoint before it opens the tunnel. This behavior supports development servers that change their initial port.

Use a different claimed domain for one run:

```bash
lpm dev --domain preview-api.lpm.llc
```

Disable the project tunnel for one run:

```bash
lpm dev --no-tunnel
```

If `lpm.json` contains multiple services, mark exactly one service as `primary`. LPM CLI sends tunnel traffic to the final endpoint of that service.

## Domain types [#domain-types]

The LPM.dev Registry provides two tunnel base domains:

| Base domain | Availability       | Use                                   |
| ----------- | ------------------ | ------------------------------------- |
| `lpm.fyi`   | Free, Pro, and Org | Free random domains and stable claims |
| `lpm.llc`   | Pro and Org        | Stable claims                         |

Run `lpm tunnel domains` to list the base domains that are currently enabled.

### Free random domains [#free-random-domains]

A Free tunnel receives a random 10-character subdomain on `lpm.fyi`. The subdomain contains lowercase letters and numbers.

The domain lasts for one tunnel session. A new connection receives a new random domain.

### Pro personal domains [#pro-personal-domains]

A Pro personal tunnel without an explicit domain uses `<username>.lpm.fyi`. This domain remains the same while the username remains the same.

A Pro user can also claim up to three stable domains. Claims can use `lpm.fyi`, `lpm.llc`, or both.

### Organization domains [#organization-domains]

An Org account can claim up to ten stable domains. The claim belongs to the organization, not to the member who uses it.

Organization owners and administrators can manage organization claims. Any eligible member can connect with a claimed organization domain.

### Claim a stable domain [#claim-a-stable-domain]

Claim a personal domain:

```bash
lpm tunnel claim acme-api.lpm.llc
```

Claim an organization domain:

```bash
lpm tunnel claim staging.lpm.fyi --org acme
```

A claimed subdomain must contain from 3 through 32 lowercase letters, numbers, or hyphens. It must start and end with a letter or number.

Claims are separate for each base domain. For example, `acme.lpm.fyi` and `acme.lpm.llc` use two claim slots.

A reconnect to the same claimed domain replaces the old session slot. This replacement does not temporarily use an additional concurrent-tunnel slot.

See the [stable-domain commands](/docs/infra/tunnel#stable-domains) for listing, use, and release examples.

## Plans and limits [#plans-and-limits]

Tunnel limits apply to the selected personal or organization account.

| Limit                        | Free                 | Pro                        | Org                                        |
| ---------------------------- | -------------------- | -------------------------- | ------------------------------------------ |
| Concurrent tunnels           | 1                    | 3                          | 10                                         |
| Included requests            | 20,000 per UTC month | 100,000 per billing period | 100,000 per billed seat and billing period |
| Account request rate         | 4,000 per minute     | 20,000 per minute          | 20,000 per billed seat and minute          |
| Per visitor IP               | 600 per minute       | 600 per minute             | 600 per minute                             |
| Maximum request body         | 10 MB                | 100 MB                     | 100 MB                                     |
| Maximum session length       | 1 hour               | No fixed limit             | No fixed limit                             |
| Stable domain claims         | 0                    | 3                          | 10                                         |
| `--tunnel-auth`              | No                   | Yes                        | Yes                                        |
| After the included allowance | Hard stop            | Proportional overage       | Proportional overage                       |

The Org concurrent-tunnel and domain-claim limits do not increase with the seat count.

The startup output reports the active plan, session expiry, limits, request usage, and overage state.

### Request usage [#request-usage]

The included allowance belongs to the account, not to each tunnel. Multiple tunnels consume the same account allowance.

One accepted HTTP request counts once. If the local service returns an error or is unavailable, the request still counts.

A successful public WebSocket upgrade also counts once.

Messages on an open WebSocket do not consume more tunnel requests. Rejected authentication, interstitial pages, and rejected oversized requests do not count.

Free usage resets at the start of each UTC month. A Free tunnel stops accepting requests after the account uses its included allowance.

Pro and Org usage follows the billing period. Paid overage costs $1 for each 100,000 requests and uses proportional billing.

Complimentary Pro accounts without a subscription receive the Pro allowance each UTC calendar month. These accounts retain Pro domain and connection limits.
They stop accepting requests at the included allowance because paid overage requires a subscription.

Paid overage is enabled by default. You can disable it in the personal or organization tunnel dashboard.

If paid overage is disabled, the tunnel stops accepting requests at the included allowance. A change to overage affects only future requests.

## Authentication and access protection [#authentication-and-access-protection]

Tunnel startup and domain actions require a saved [`lpm login`](/docs/infra/login) session. `--token`, `LPM_TOKEN`, and CI-only tokens cannot replace this session.

The local `inspect`, `replay`, and `log` actions do not require a login. These actions use the capture database in the current project.

### Public access [#public-access]

A tunnel URL accepts public requests by default. Application authentication remains the responsibility of your local service.

Pro and Org tunnels can add tunnel-level access protection:

```bash
lpm tunnel 3000 acme-api.lpm.llc --tunnel-auth
```

LPM CLI creates a token for the session. The startup output shows an `X-Tunnel-Auth` header and a browser URL that contains the token.

Treat the header token and browser URL as secrets. The tunnel service rejects requests that do not contain the correct token.

### Free browser interstitial [#free-browser-interstitial]

The first browser visit to a Free random domain shows a **Visit Site** page. This page reduces phishing abuse on temporary domains.

The page appears only for requests that accept HTML. API clients and webhook providers reach the local service without this page.

After the visitor continues, the browser receives a host-only cookie for that domain and session. The cookie expires with the session.

The interstitial is not application authentication. It does not restrict API or webhook access.

## Local HTTP and public HTTPS [#local-http-and-public-https]

An explicit port always selects `http://127.0.0.1:<port>/`. The public tunnel URL still uses HTTPS.

Without a port, `lpm tunnel` selects the only active `lpm dev` endpoint. It preserves the endpoint address, port, and base path.

If no active endpoint exists, pass a port. If multiple endpoints exist, pass the intended port.

`lpm dev --https` remains compatible with tunnels. LPM CLI tunnels the underlying local HTTP endpoint while it provides trusted browser HTTPS locally.

Do not give a standalone tunnel a framework HTTPS port. Use a plain HTTP child service for tunnel forwarding.

## Server-sent events [#server-sent-events]

Responses with `Content-Type: text/event-stream` reach visitors as the local service sends data.
The tunnel waits up to 30 seconds for response headers. An open event stream can continue beyond 30 seconds.
Slow readers limit forwarding, and a visitor disconnect closes the local request.
Tunnel revocation and the Free-session deadline still close active streams.
This behavior requires a relay that supports streamed responses.

## Request capture and privacy [#request-capture-and-privacy]

LPM CLI captures each forwarded request and local response in the current project:

```text
<project>/.lpm/inspector.db
```

The database can contain full headers, request bodies, response bodies, and session details. The tunnel service does not store these request bodies.

Protect the database as sensitive development data. Do not commit it to source control.

Live capture memory has fixed limits. The request buffer retains up to 1,000 requests or 64 MiB.

The WebSocket buffer retains up to 5,000 events or 32 MiB.

Each captured WebSocket frame keeps a 64 KiB preview and records the original frame size. The inspector marks a partial preview as truncated.

For a completed event stream, the inspector retains up to 64 KiB of response data and marks a partial capture.
An interrupted stream may have no completed capture.

The database writer queues up to 256 captures and 64 MiB. If either limit is full, LPM CLI drops new captures.

Session start and end records use a separate reliable path.

Slow browser streams can miss live events. The browser then reloads persisted history through the inspector API.

The browser inspector starts with a tunnel and selects an available local port. `--no-inspect` disables the browser inspector, but capture continues.

Use the same capture history from the terminal or browser:

```bash
lpm tunnel inspect --last 10
lpm tunnel inspect --ui
lpm tunnel replay --last --port 3000
lpm tunnel log --status 5xx
```

Replay sends the saved method, path, headers, and body to the selected local service. This feature helps you repeat a failed webhook during development.

Session names group related captures:

```bash
lpm tunnel 3000 --session stripe-test
```

`lpm tunnel log --clear` clears current capture history and completed sessions for the project. It does not remove preserved legacy capture files.

### Auto-acknowledge webhooks [#auto-acknowledge-webhooks]

If a webhook endpoint must accept requests while the local service is unavailable, use `--auto-ack`:

```bash
lpm tunnel 3000 --auto-ack
```

LPM CLI returns `200 OK` and saves the request for later replay.

> CAUTION: The provider records the request as successful, but your local service did not process it.

## Reliability and recovery [#reliability-and-recovery]

A missed relay heartbeat triggers reconnection. LPM CLI reports the retry instead of exiting successfully.
Credential renewal keeps a healthy tunnel connected without resetting the Free one-hour limit.
At that limit, the tunnel stops and asks you to start a new session.

| Symptom                                     | Cause                                                     | Recovery                                                                     |
| ------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- |
| The public URL stops                        | The LPM CLI process stopped                               | Start the tunnel again                                                       |
| A Free URL stops after one hour             | The Free session expired                                  | Start a new session and use the new random URL                               |
| A request cannot reach the local service    | The service is stopped or uses another port               | Start the service or select the correct port                                 |
| Bare `lpm tunnel` cannot select an endpoint | No active endpoint exists, or multiple endpoints exist    | Pass the intended port                                                       |
| The inspector port is busy                  | An explicit `--inspect-port` is already in use            | Select another port or omit the flag                                         |
| Tunnel authentication fails                 | The saved login session is unavailable or expired         | Run `lpm login`, then start the tunnel again                                 |
| The relay certificate pin does not match    | The relay certificate changed or the connection is unsafe | Make sure that the certificate change is expected before you replace the pin |

### Relay certificate pins [#relay-certificate-pins]

LPM CLI stores one certificate pin for each relay host:

```text
~/.lpm/relay-pins/relay.lpm.fyi
~/.lpm/relay-pins/<other-host>
```

The first connection stores the pin. Later connections to the same host must present the same certificate identity.

> CAUTION: Do not remove a pin until you make sure that the relay certificate changed as expected. Removing it trusts the next certificate.

If the certificate change is expected, remove the exact pin that the error names. Then connect again:

```bash
rm ~/.lpm/relay-pins/relay.lpm.fyi
lpm tunnel 3000
```

## Advanced relay configuration [#advanced-relay-configuration]

The default relay URL is `wss://relay.lpm.fyi/connect`. Most users do not need to change it.

Use an environment variable for one process:

```bash
LPM_TUNNEL_RELAY=wss://relay-eu.lpm.fyi/connect lpm tunnel 3000
```

Use `~/.lpm/config.toml` for a persistent override:

```toml
[tunnel]
relay-url = "wss://relay-eu.lpm.fyi/connect"
```

The precedence is `LPM_TUNNEL_RELAY`, `~/.lpm/config.toml`, then the built-in default. An empty value moves to the next source.

Use only a trusted relay endpoint. A relay receives the public traffic that LPM CLI forwards to your local service.

Each relay host has a separate certificate pin. A relay override does not reuse the pin for another host.

## CI and long-running services [#ci-and-long-running-services]

A tunnel needs an open process to keep the connection active. This model does not fit a typical CI job or permanent service.

For automated webhook tests, use provider delivery logs or a deployed test endpoint. Use LPM CLI tunnels for interactive development.

## See also [#see-also]

* [`lpm tunnel`](/docs/infra/tunnel) - command syntax, recipes, domain actions, capture commands, and flags
* [`lpm dev`](/docs/dev/dev) - start a project and its tunnel together
* [`lpm.json` tunnel configuration](/docs/reference/lpm-json#tunnel) - configure a stable project domain
* [Authentication](/docs/infra/authentication) - understand the saved session requirement
* [`lpm cert`](/docs/infra/cert) - use trusted HTTPS without a public tunnel


# AI agent skills (/docs/reference/ai-agent-skills)



Agent skills are Markdown instructions that help an AI agent use a package, workflow, or project correctly. The LPM CLI supports two distinct systems under the [`lpm skills`](/docs/packages/skills) command family.

## Two systems, one command [#two-systems-one-command]

|                 | LPM.dev package skills                                  | Standalone skills                                             |
| --------------- | ------------------------------------------------------- | ------------------------------------------------------------- |
| Purpose         | Versioned guidance shipped with an `@lpm.dev/*` package | Reusable agent capability independent of a package install    |
| Source          | Publisher-authored `.lpm/skills/*.md` files             | Standard `SKILL.md` directories from GitHub or a local source |
| Delivery        | Validated and stored by the LPM.dev Registry            | Resolved and security-scanned by the LPM CLI                  |
| Project storage | `.lpm/skills/<owner.package>/`                          | `.lpm/managed-skills/` plus selected agent targets            |
| Lifecycle       | Reconciled with the package version                     | Added, updated, enabled, disabled, or removed explicitly      |
| Agent links     | None                                                    | Linked by default, or copied with `--copy`                    |

The two systems can coexist in one project. Their paths and ownership records keep publisher source, installed package guidance, and standalone managed content separate.

## Publish guidance with an LPM.dev package [#publish-guidance-with-an-lpmdev-package]

Publishers place each skill directly under `.lpm/skills/`:

```text
package-repository/
├── package.json
└── .lpm/
    └── skills/
        ├── usage.md
        └── migration.md
```

Each file contains YAML frontmatter followed by Markdown guidance:

```md
---
name: package-usage
description: Use the package through its supported public API.
version: "1.4.0"
globs:
  - "src/**/*.ts"
---

# Package usage

Import the public client, initialize it once, and reuse that instance. Follow the package's documented error handling and configuration conventions when changing an existing integration.
```

Nested publisher paths are not supported. `.lpm/skills/guides/usage.md` is rejected by both the LPM CLI preflight and the LPM.dev Registry instead of being silently ignored.

The publisher workflow is:

```bash
# Optional: install the standalone authoring guide
lpm skills add lpm-dev/lpm-guide

# Validate direct .lpm/skills/*.md files
lpm skills validate

# Publish the package and its skills
lpm publish
```

`validate` enforces the registry contract: required `name` and `description`, valid lowercase names, descriptions from 10 to 500 characters, at least 100 Markdown body characters, at most 15 KB per file, at most 10 skills and 100 KB total, unique names, and the package-skill security rules.

Optional `version` identifies the package release the guidance was authored for. It is stored per skill rather than inferred from the package release that carries the file, so older guidance remains identifiable after a later package publish. The LPM CLI preserves the field in installed package-skill frontmatter.

If `package.json > files` would exclude `.lpm/skills`, a real `lpm publish` adds the directory before creating the tarball; `--check` and `--dry-run` model that change in memory without touching the manifest. When `files` is omitted, the implicit publish walker traverses only `.lpm/skills`, includes only direct publisher-authored Markdown candidates, and excludes every other `.lpm` certificate, log, install hash, audit, and runtime-state file. LPM.dev Registry publishes validate those candidates first; other targets include them by path shape. Installed, manifest-owned dependency skill directories are ignored during publisher validation and excluded from both implicit and explicit tarballs. LPM CLI scans the actual final artifact for every target, so published skill content receives the same secret scan as the rest of the package.

## Use guidance from an LPM.dev package [#use-guidance-from-an-lpmdev-package]

Installing a direct `@lpm.dev/*` dependency fetches its published guidance by default:

```bash
lpm install @lpm.dev/owner.package
```

The LPM CLI materializes an exact, package-owned set:

```text
my-project/
└── .lpm/
    └── skills/
        └── owner.package/
            ├── .lpm-package-skills.json
            ├── usage.md
            └── gotchas.md
```

`.lpm-package-skills.json` records ownership and content digests. It lets the LPM CLI reconcile upgrades, verify integrity, and distinguish installed consumer content from publisher-authored files. Do not edit it.

Package skills remain in `.lpm/skills/<owner.package>/`. The LPM CLI does not create agent links or modify editor configuration for them. Point your agent at the relevant directory when you want it to use the package author's guidance:

```text
Read .lpm/skills/owner.package/ before changing code that uses @lpm.dev/owner.package.
```

### Automatic reconciliation [#automatic-reconciliation]

Package skill installation is enabled by default. Installing or upgrading a direct package reconciles its directory to the exact published set; removing the package removes its owned directory.

```bash
lpm config lpm-skills --set false
```

Use `--no-skills` with [`lpm install`](/docs/packages/install), [`lpm add`](/docs/packages/add), or `lpm ci` to opt out once. Use `--skills` to override a disabled preference once. Precedence is `--skills` / `--no-skills`, then `auto-install-lpm-skills`, then the legacy `noSkills` fallback, then the enabled default.

The preference controls future automatic fetching and reconciliation only. It does not delete existing content or disable explicit retrieval:

```bash
lpm skills add @lpm.dev/owner.package --dry-run
lpm skills add @lpm.dev/owner.package --yes
```

### Safe package cleanup [#safe-package-cleanup]

```bash
lpm skills clean --dry-run
lpm skills clean
```

`clean` previews the exact installed package sets, file count, and size. In a terminal it asks for confirmation with **No** selected by default; scripts must pass `--yes`.

Only package directories whose files and digests exactly match a valid LPM CLI ownership manifest are removed. Direct publisher files, untracked or modified content, invalid manifests, symlinks, standalone managed skills, and agent links are preserved. Re-run `lpm install` to restore removed package guidance.

## Manage standalone skills [#manage-standalone-skills]

Standalone skills come from a GitHub repository or tree URL, GitHub `owner/repository` shorthand, or a local directory:

```bash
lpm skills add vercel-labs/skills --skill find-skills
lpm skills add https://github.com/vercel-labs/agent-skills --skill web-design-guidelines
lpm skills add ./team-skills --skill release-notes --agent codex --project
```

Remote standalone inputs currently resolve through GitHub. A discovery catalog can point you to a skill, but the LPM CLI input is its underlying GitHub source. Local inputs are read directly from the selected directory.

In a terminal, `add` opens a wizard for source, skills, project or global scope, compatible agent targets, security review, and confirmation. Scripts provide those choices explicitly and pass `--yes` after reviewing `--dry-run`.

`lpm skills install` is a compatibility alias for `lpm skills add`; use `add` in new scripts.

### Storage and targets [#storage-and-targets]

Standalone content is stored once in an LPM-managed canonical location, then linked by default or copied with `--copy` to selected targets:

| Target          | Project path                |
| --------------- | --------------------------- |
| Codex           | `.agents/skills/<skill>/`   |
| Claude Code     | `.claude/skills/<skill>/`   |
| Cursor          | `.cursor/skills/<skill>/`   |
| Grok            | `.grok/skills/<skill>/`     |
| OpenCode        | `.opencode/skills/<skill>/` |
| Pi              | `.pi/skills/<skill>/`       |
| Kimi (Moonshot) | `.kimi/skills/<skill>/`     |

Project canonical content lives in `.lpm/managed-skills/`. Global canonical content lives under the LPM home directory. Project links are relative, so moving the project directory keeps healthy links intact.

Global targets use each agent's native home contract: `${CODEX_HOME:-~/.codex}/skills`, `~/.claude/skills`, `~/.cursor/skills`, `${GROK_HOME:-~/.grok}/skills`, `${XDG_CONFIG_HOME:-~/.config}/opencode/skills`, `${PI_CODING_AGENT_DIR:-~/.pi/agent}/skills`, and `~/.kimi/skills`.

### Source and security boundary [#source-and-security-boundary]

GitHub sources must use HTTPS. Branches, tags, and commit refs are resolved to an immutable commit before download. `GH_TOKEN` or `GITHUB_TOKEN` is used only for authenticated GitHub API requests and is not forwarded to the archive host.

The remote boundary limits downloads to 20 MB, expanded content to 64 MB, regular files to 1 MB, 500 archive entries, and 12 directory levels. Traversal, symlinks, hard links, special entries, and oversized selected files are rejected.

Before installation, the LPM CLI scans selected skills for prompt injection, secret access or exfiltration, shell-download execution, and destructive filesystem instructions:

| Severity  | Effect                                                                       |
| --------- | ---------------------------------------------------------------------------- |
| `warning` | Included in the plan; installation can continue after explicit confirmation. |
| `block`   | Stops installation before managed storage or agent targets are written.      |

`lpm skills update --dry-run` shows a bounded content diff and identifies new findings by rule, severity, file, and line. Binary auxiliary files are preserved and represented by SHA-256 digest rather than interpreted as text.

## Unified inventory and lifecycle [#unified-inventory-and-lifecycle]

```bash
lpm skills list
lpm skills view owner.package/usage
lpm skills view release-notes
lpm skills dashboard
```

Inventory uses three ownership categories:

* `package`: LPM.dev package guidance in `.lpm/skills/<owner.package>/`.
* `managed`: standalone content explicitly added through `lpm skills add`.
* `external`: agent-directory skills discovered but not managed by the LPM CLI.

The command boundary follows that ownership:

| Operation                               | Publisher files     | Installed package sets                 | Standalone managed skills           |
| --------------------------------------- | ------------------- | -------------------------------------- | ----------------------------------- |
| `validate`                              | Validates           | Ignores valid owned sets               | No effect                           |
| `clean`                                 | Preserves           | Removes after preview and confirmation | No effect                           |
| `update`, `enable`, `disable`, `remove` | No effect           | No effect                              | Manages selected skills and targets |
| `list`, `view`, `dashboard`             | Not inventory items | Inspects                               | Inspects and manages                |

`view` reports source, estimated context size, security findings, integrity, and target health. `doctor` diagnoses canonical content, targets, ownership, disabled targets, and broken external links. `prune --dry-run` previews exact stale target and state paths before cleanup.

The authenticated local dashboard always combines project and global discovery; its source and **needs attention** tabs filter that inventory in the browser. Selecting a skill loads its exact raw Markdown, rendered body, bounded file tree, and metadata by stable inventory ID. External aliases that resolve to the same canonical directory are one inventory item with multiple agent targets. Branded badges identify Codex, Claude Code, Cursor, Grok, OpenCode, Pi, and Kimi targets. A top-level agent alias is resolved for inspection, while symlinks inside the skill tree remain rejected. Broken aliases stay visible with `broken-link` integrity and explicit missing-target text.

Preview-and-apply actions are available only for managed standalone skills. Package and external skills are inspection-only. Agent labels mean configured targets, not observed use by a running agent; context numbers are estimates, not session or billing usage.

Global `--json` emits one structured result for every command, including no-op operations. Unified inventory includes total and per-category counts. Validation, cleanup, and prune can therefore report successful zero-count results without implying a mutation.

## See also [#see-also]

* [`lpm skills`](/docs/packages/skills) — command examples and flags for every workflow
* [`lpm install --no-skills`](/docs/packages/install) — opt out of package skill extraction
* [`lpm add --no-skills`](/docs/packages/add) — same for dependency installation
* [`lpm config lpm-skills`](/docs/infra/config#setup-wizards) — persist automatic package skill installation
* [MCP servers](/docs/reference/mcp-servers) — tools and agent skills are separate integrations


# ~/.lpm/config.toml (/docs/reference/config-toml)



`~/.lpm/config.toml` is the per-user CLI configuration file. It sets defaults that apply to every project on the machine, sitting **below** project-level config (`./lpm.toml`, `package.json > lpm`) and **above** built-in defaults in the precedence chain — except for `linker`, where the user-level value beats `package.json > lpm > linker`. For security-sensitive keys, this file is still a proposal layer: the effective machine floor can also be constrained by the signed approved-posture store and a managed policy. See [Precedence](#precedence) and [Security-sensitive keys are proposals](#security-sensitive-keys-are-proposals).

LPM CLI reads this file with a 16 MiB limit enforced before TOML parsing. A missing file keeps built-in defaults; an oversized file fails with its path and byte limit instead of being ignored or weakening the effective policy. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits) for the complete scope and exclusions.

You can edit the file directly, or use [`lpm config`](/docs/infra/config) to manage it from the CLI:

```bash
lpm config set save-prefix '~'
lpm config get save-prefix
lpm config list
lpm config delete save-prefix
```

## Known keys [#known-keys]

```toml title="~/.lpm/config.toml"
# Save policy
save-prefix = "^"
save-exact  = false

# Lifecycle scripts
script-policy  = "deny"
triage-advisor = "none"

# Recently-published-package cooldown
minimum-release-age-secs = 0
release-age-policy = "direct"
minimum-release-age-exclude = ["react", "@scope/pkg"]

# Workspace-root and dependency engine constraints
engine-strict = true

# Peer-dependency diagnostics
strict-peer-dependencies = false
auto-install-peers = true

# Package-published skills from @lpm.dev/* packages
auto-install-lpm-skills = true

# Analyze package source while extracted bytes enter the store
install-time-source-analysis = false

# Use registry behavior and lifecycle data for installed @lpm.dev/* packages
fetch-lpm-security-insights = true

# npm registry package signatures
signatures = false

# npm publisher/provenance trust downgrade policy
trust-policy = "off"

# Suspicious direct dependency name analysis
# Omit for the default enabled mode; set "on" to ignore the diagnostic env toggle.
typosquat-guard = "on"

# Reused v2 store object validation
integrity = "source"

# Workspace task fan-out for run/test/bench
workspace-concurrency = 4

# Add an audit summary after every successful install
audit-after-install = false

# node_modules layout (overrides package.json > lpm > linker)
linker = "isolated"

# Lifecycle-script secret-file exemptions (paths the sandbox normally
# blocks but you want readable across every project on this machine)
script-read-allow = [".env", ".npmrc"]

# Canonical roots allowed to cover absolute sandboxWriteDirs entries
max-sandbox-write-roots = ["/Users/alice/shared-build-output"]

# Lifecycle-script sandbox
[sandbox]
mode = "default"
allow-degraded = false

# firewall.lpm.dev verdict checks for npm packages
[firewall]
mode = "enforce"

[firewall.npm.policies]
trusted_public_malicious_advisories = "block"
lpm_ai_confirmed_malware = "block"
lpm_ai_agent_control_surface = "warn"
critical_vulnerability = "warn"
lpm_ai_suspicious = "warn"

# Local install policy extensions
[policy.extensions.local-feed]
enabled = true
command = ["/usr/local/bin/lpm-policy-feed", "--deny-list", "/etc/lpm/deny.json"]
mode = "enforce"
on-error = "block"
timeout-ms = 5000
events = ["package.candidate"]

# Provenance verification posture
[sigstore]
verify = "deny"
scope = "approved"
availability = "best-effort"

# Tunnel relay (override per-process with LPM_TUNNEL_RELAY)
[tunnel]
relay-url = "wss://relay.lpm.fyi/connect"
```

| Key                                                         | Type                                                  | Default                                                                      | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ----------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `save-prefix`                                               | `"^"` \| `"~"` \| `""`                                | `"^"`                                                                        | Prefix used when `lpm install <pkg>` writes to `package.json`. `*` is rejected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `save-exact`                                                | bool                                                  | `false`                                                                      | Force exact saves regardless of `save-prefix`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `script-policy`                                             | `"deny"` \| `"allow"` \| `"triage"`                   | `"deny"`                                                                     | Lifecycle-script gate for `lpm install`, `lpm install -g`, and `lpm rebuild`. Machine-wide policy: applies to project and global installs alike.                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `triage-advisor`                                            | `"none"` \| `"claude-cli"` \| `"codex"` \| `"ollama"` | `"none"`                                                                     | Optional LLM advisor for the [triage gate](/docs/packages/security-audit#optional-llm-advisor). Only active when `script-policy = "triage"`. Approvals are ephemeral — never written to disk.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `minimum-release-age-secs`                                  | u64                                                   | `0` (off)                                                                    | Cooldown in seconds before a freshly published version becomes installable. On project installs, applies to direct/root dependencies; on direct package-selection commands (`lpm install -g`, `lpm dlx`, `lpm upgrade`, `lpm outdated`), applies to selected or reported package versions.                                                                                                                                                                                                                                                                                                                       |
| `release-age-policy`                                        | `"direct"` \| `"strict"`                              | `"direct"`                                                                   | Cooldown scope. `"direct"` checks direct/root dependencies only; `"strict"` also checks transitive dependencies and lockfile replays using persisted publish timestamps. Set with [`lpm config release-age-policy`](/docs/infra/config#setup-wizards).                                                                                                                                                                                                                                                                                                                                                           |
| `minimum-release-age-exclude`                               | string array                                          | `[]`                                                                         | Package names, exact package versions, or `@scope/*` selectors exempted from the cooldown. Merged with CLI and package.json excludes. Protocol specifiers and version ranges are rejected. Manage with [`lpm config release-age-exclude`](/docs/infra/config#exclude-selected-packages-from-the-release-cooldown).                                                                                                                                                                                                                                                                                               |
| `sandbox.mode`                                              | `"default"` \| `"strict"` \| `"none"`                 | `"default"`                                                                  | Default lifecycle-script sandbox posture for this machine. Set with [`lpm config sandbox`](/docs/infra/config#setup-wizards).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `sandbox.allow-degraded`                                    | bool                                                  | `false`                                                                      | Allow degraded sandbox fallback for this machine when full containment is unavailable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `sigstore.verify`                                           | `"deny"` \| `"warn"` \| `"off"`                       | `"deny"`                                                                     | Provenance verification posture used by installs and related flows. Set with [`lpm config sigstore`](/docs/infra/config#setup-wizards).                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `sigstore.scope`                                            | `"approved"` \| `"all"`                               | `"approved"`                                                                 | Which resolved packages enter the provenance verifier. `"approved"` verifies packages with a previously approved provenance identity; `"all"` checks every resolved package and records verified evidence in the lockfile. Set with `lpm config sigstore --set scope=all`.                                                                                                                                                                                                                                                                                                                                       |
| `sigstore.availability`                                     | `"best-effort"` \| `"strict"`                         | `"best-effort"`                                                              | What happens when an attestation is absent or temporarily unavailable. `"best-effort"` preserves the absence/unknown signal without blocking; `"strict"` fails the install. A supplied but invalid bundle is still governed by `sigstore.verify`. Set with `lpm config sigstore --set availability=strict`.                                                                                                                                                                                                                                                                                                      |
| `signatures`                                                | bool                                                  | `false`                                                                      | Verify npm registry package signatures during install and fail closed when a registry package cannot be verified. Set with [`lpm config signatures`](/docs/infra/config#setup-wizards). Per-process override: `LPM_VERIFY_REGISTRY_SIGNATURES`.                                                                                                                                                                                                                                                                                                                                                                  |
| `trust-policy`                                              | `"off"` \| `"no-downgrade"`                           | `"off"`                                                                      | When set to `"no-downgrade"`, resolution refuses weaker npm trusted-publisher/staged-publish evidence than an earlier release, and an install that previously locked verified provenance for a package refuses a later version without verified provenance. The artifact-history floor takes precedence over `sigstore.verify = "warn"` / `"off"`, verification skip flags, and best-effort availability. Registry attestation pointers alone are not evidence. Set with [`lpm config trust-policy`](/docs/infra/config#setup-wizards).                                                                          |
| `typosquat-guard`                                           | `"default"` \| `"on"` \| `"off"`                      | unset / `"default"` (disabled)                                               | Suspicious direct dependency name analysis before a new name enters a project. `lpm config typosquat --set default` removes this key; `on` forces the guard on even when the diagnostic env toggle is set; `off` and `default` require approval when they weaken an explicitly approved or managed guard. Prefer project [`policy.typosquat.allow`](/docs/reference/lpm-toml#typosquat-policy) entries for intentional names instead of disabling the guard globally.                                                                                                                                            |
| `firewall.mode`                                             | `"off"` \| `"monitor"` \| `"enforce"`                 | `"off"`                                                                      | LPM Firewall verdict checks for selected public npm package versions. The hosted verdict service is an LPM.dev Registry Pro/Org feature, so active modes send LPM.dev Registry auth; use `lpm login` locally or `LPM_TOKEN` in CI. `monitor` warns and continues; `enforce` blocks packages whose effective firewall action is `block` or denied by entitlement. npm metadata and tarballs still use the direct npm route. `report` is accepted as a legacy alias for `monitor`. Set with [`lpm config firewall`](/docs/infra/config#setup-wizards). Per-process `LPM_NPM_FIREWALL` can only tighten this value. |
| `firewall.npm.policies.trusted_public_malicious_advisories` | `"allow"` \| `"warn"` \| `"block"`                    | `"block"`                                                                    | User action for public malicious-package advisories from OSV/OpenSSF/GHSA.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `firewall.npm.policies.lpm_ai_confirmed_malware`            | `"allow"` \| `"warn"` \| `"block"`                    | `"block"`                                                                    | User action for LPM Firewall AI-confirmed malware behavior such as exfiltration, remote payload execution, persistence/destructive behavior, dependency confusion, and classic malware.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `firewall.npm.policies.lpm_ai_agent_control_surface`        | `"allow"` \| `"warn"` \| `"block"`                    | `"warn"`                                                                     | User action for LPM Firewall AI-agent control-surface policy findings. This is a dangerous-capability policy, not necessarily a malware-intent finding.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `firewall.npm.policies.critical_vulnerability`              | `"allow"` \| `"warn"` \| `"block"`                    | `"warn"`                                                                     | User action for critical vulnerability advisories. The wizard offers block, warn, or allow for this warn-default group.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `firewall.npm.policies.lpm_ai_suspicious`                   | `"allow"` \| `"warn"` \| `"block"`                    | `"warn"`                                                                     | User action for suspicious signals that LPM Firewall AI does not classify as malware. Trusted public sources also do not identify these signals as malicious. The wizard offers warn or allow for this warn-default group. It preserves a hand-edited `"block"` value during profile review.                                                                                                                                                                                                                                                                                                                     |
| `policy.extensions.<name>.enabled`                          | bool                                                  | `true`                                                                       | Enable or disable this extension. A disabled extension does not require `command`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `policy.extensions.<name>.command`                          | string array                                          | required                                                                     | Direct-spawn command for a local install policy extension. The first entry must be an absolute path or a program name found on an absolute `PATH` directory; relative executable paths are rejected, and relative or empty `PATH` entries are ignored. Later entries are argv. LPM CLI never runs this through a shell.                                                                                                                                                                                                                                                                                          |
| `policy.extensions.<name>.mode`                             | `"report"` \| `"enforce"`                             | `"report"`                                                                   | `report` prints policy decisions and continues; `enforce` fails the install when the extension returns a `block` decision.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `policy.extensions.<name>.on-error`                         | `"warn"` \| `"block"`                                 | `warn` in report, `block` in enforce                                         | What to do when the extension cannot be spawned, times out, exits non-zero, emits oversized output, or returns invalid JSON.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `policy.extensions.<name>.timeout-ms`                       | positive integer                                      | `5000`                                                                       | Per-extension timeout for the full exchange: stdin write, process exit, stdout/stderr drain, and response validation. Maximum accepted value is `120000`.                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `policy.extensions.<name>.events`                           | string array                                          | `["package.candidate"]`                                                      | Events this extension handles. `package.candidate` is the only supported event today.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `integrity`                                                 | `"source"` \| `"tree"`                                | `"source"`                                                                   | Reused v2 store object validation. `"source"` verifies source identity without rehashing expanded files; `"tree"` rehashes expanded files before reuse to detect local store tampering/corruption. Set with [`lpm config integrity`](/docs/infra/config#setup-wizards). Per-process override: `LPM_V2_OBJECT_INTEGRITY`.                                                                                                                                                                                                                                                                                         |
| `engine-strict`                                             | bool                                                  | `true`                                                                       | Enforce workspace-root `engines.lpm` / `engines.node`, `lpm run` Node compatibility, and dependency `engines.node` constraints. Set to `false` for warning-only behavior; incompatible dependencies remain installed.                                                                                                                                                                                                                                                                                                                                                                                            |
| `strict-peer-dependencies`                                  | bool                                                  | `false`                                                                      | Turn missing required peers, peer version mismatches, and peer-conflict warnings into install failures. Per-invocation overrides: `--strict-peer-dependencies` / `--no-strict-peer-dependencies`. Project override: `package.json > lpm.strictPeerDependencies`.                                                                                                                                                                                                                                                                                                                                                 |
| `auto-install-peers`                                        | bool                                                  | `true`                                                                       | Auto-install missing non-optional `peerDependencies` at the project root after the main resolve pass. Project override: `package.json > lpm.autoInstallPeers`. Set to `false` for npm-classic / pnpm-classic peer-warning-only behavior.                                                                                                                                                                                                                                                                                                                                                                         |
| `auto-install-lpm-skills`                                   | bool                                                  | `true`                                                                       | Reconcile package-published skills from installed `@lpm.dev/*` packages. Set with [`lpm config lpm-skills`](/docs/infra/config#setup-wizards). Per-invocation overrides: `--skills` / `--no-skills` on `lpm install`, `lpm add`, and `lpm ci`. Disabling automatic installs does not delete existing `.lpm/skills/` content or affect explicit `lpm skills add`.                                                                                                                                                                                                                                                 |
| `install-time-source-analysis`                              | bool                                                  | `false`                                                                      | Analyze npm, custom-registry, and LPM.dev package source while extracted bytes enter the store. Disabling an explicitly approved or managed source-analysis setting requires approval. Re-enabling it backfills missing, malformed, or outdated store caches from existing package bytes on the next install. Set with [`lpm config source-analysis`](/docs/infra/config#install-time-source-analysis).                                                                                                                                                                                                          |
| `fetch-lpm-security-insights`                               | bool                                                  | `true`                                                                       | Use Registry behavioral tags and lifecycle-script data in the normal install summary for `@lpm.dev/*` packages. For JavaScript packages, `lpm audit` and audit-after-install add Registry security findings and vulnerabilities. For explicit Swift packages, audit-after-install adds them. This setting is independent of local source analysis. Set with `lpm config lpm-insights` or manage it alongside package skills with [`lpm config lpm-dev`](/docs/infra/config#lpmdev-settings).                                                                                                                     |
| `workspace-concurrency`                                     | positive integer                                      | available parallelism                                                        | User default for concurrent workspace members in `lpm run`, `lpm test`, and `lpm bench` workspace mode. Per-invocation override: `--workspace-concurrency <N>`. Project override: `lpm.toml > [workspace].concurrency`.                                                                                                                                                                                                                                                                                                                                                                                          |
| `audit-after-install`                                       | bool                                                  | `false`                                                                      | Add an audit summary after every successful `lpm install`. JavaScript installs use the [`lpm audit`](/docs/packages/audit) scan. Explicit Swift installs use fetched Registry metadata. The summary includes severity counts and each Critical finding. Findings never fail the install. Per-invocation overrides: `--audit-after-install` / `--no-audit-after-install`. Per-process environment variable: `LPM_AUDIT_AFTER_INSTALL`. See [Audit after install](/docs/packages/install#audit-after-install).                                                                                                     |
| `linker`                                                    | `"isolated"` \| `"hoisted"`                           | `"hoisted"` (workspaces and default peer-conflict installs use `"isolated"`) | `node_modules` layout for installs run by this user. &#x2A;*Overrides `package.json > lpm > linker`** so a user can pick their preferred layout regardless of what each project declares. Per-process override: `LPM_LINKER`. Per-invocation override: `lpm install --linker <isolated\|hoisted>`. Unknown values fail loudly at install time — there is no silent fallback.                                                                                                                                                                                                                                     |
| `script-read-allow`                                         | array of strings                                      | `[]`                                                                         | Explicitly authorize lifecycle-script reads of project-relative paths that match the [secret-file deny list](/docs/packages/rebuild#project-secret-files). Each entry is joined to the current `project_dir` at install time; traversal escapes (`..`) and absolute paths outside the project are rejected. Unioned with the per-project [`package.json > lpm > scripts > sandboxReadAllow`](/docs/reference/package-json-lpm#scripts) — duplicates are deduplicated. Allowlisted secrets remain readable; if every matching secret is allowlisted, Linux skips the namespace overlay.                           |
| `max-sandbox-write-roots`                                   | array of absolute paths                               | `[]`                                                                         | User-authorized roots that may cover absolute [`package.json > lpm > scripts > sandboxWriteDirs`](/docs/reference/package-json-lpm#the-lpmscripts-block) entries outside the project. LPM CLI resolves the root and candidate before granting access. A candidate cannot traverse a symlink, Windows junction, or reparse point below the authorized root, and dangerous system or credential roots remain denied even when listed here. Relative roots are ignored.                                                                                                                                             |
| `tunnel.relay-url`                                          | string                                                | `"wss://relay.lpm.fyi/connect"`                                              | WebSocket URL for the tunnel relay used by LPM CLI. Per-process override: `LPM_TUNNEL_RELAY`. LPM CLI stores one TOFU pin per host under `$LPM_HOME/relay-pins/<host>` (`~/.lpm/relay-pins/<host>` by default).                                                                                                                                                                                                                                                                                                                                                                                                  |

Unknown keys are accepted silently — forward-compatible by design.

## String coercion [#string-coercion]

`lpm config set` writes ordinary generic values as TOML strings. Known typed keys can preserve native TOML types; `lpm config set signatures true`, `lpm config set auto-install-lpm-skills false`, `lpm config set install-time-source-analysis true`, and `lpm config set fetch-lpm-security-insights false` write booleans. Readers accept both native types and the string form for typed keys:

```toml
minimum-release-age-secs = 86400      # native integer (hand-edit)
minimum-release-age-secs = "86400"    # string (lpm config set wrote this)
release-age-policy = "strict"
minimum-release-age-exclude = ["react", "@scope/pkg"]
workspace-concurrency = 4             # native integer (hand-edit)
workspace-concurrency = "4"           # string (lpm config set wrote this)
signatures = true                     # native bool
signatures = "true"                   # string form also accepted
auto-install-lpm-skills = false        # native bool
auto-install-lpm-skills = "false"      # string form also accepted
install-time-source-analysis = true    # native bool
install-time-source-analysis = "true"  # string form also accepted
fetch-lpm-security-insights = false    # native bool
fetch-lpm-security-insights = "false"  # string form also accepted
typosquat-guard = "on"                # canonical string
integrity = "tree"                    # canonical string

[firewall]
mode = "monitor"                      # canonical string
```

`minimum-release-age-exclude` is an array setting. Generic `lpm config set` rejects this key.

The typed `add` and `remove` commands validate selectors, remove duplicates, and atomically write the array.

Both forms parse to the same value. For `typosquat-guard`, the `default` value is accepted when hand-edited, but the CLI removes the key instead of persisting `typosquat-guard = "default"`. For `firewall.mode`, boolean-like aliases are accepted when hand-edited or provided through `LPM_NPM_FIREWALL`, and `report` is accepted as a legacy alias for `monitor`; `lpm config firewall` persists the canonical `off`, `monitor`, or `enforce` value. For `integrity`, config accepts only `source` or `tree`; the wizard persists those canonical strings. Invalid values (non-numeric strings, negative numbers, zero for positive-only keys, etc.) error at load time with the file path baked in.

## npm firewall policy profile [#npm-firewall-policy-profile]

The firewall policy table is only read when LPM Firewall is active. LPM CLI sends the profile to the LPM Firewall verdict API, which returns the user-effective action for each decision.

```toml
[firewall]
mode = "enforce"

[firewall.npm.policies]
trusted_public_malicious_advisories = "block"
lpm_ai_confirmed_malware = "block"
lpm_ai_agent_control_surface = "warn"
critical_vulnerability = "warn"
lpm_ai_suspicious = "warn"
```

Every policy group accepts `allow`, `warn`, or `block` in hand-edited config. If you choose `enforce`, the interactive wizard shows one profile review screen. `critical_vulnerability` offers block, warn, or allow. `lpm_ai_suspicious` normally offers warn or allow. It also preserves an existing hand-edited `block` value. If the canonical `lpm_ai_suspicious` key is absent, LPM CLI accepts the legacy `static_only_suspicious` key. `lpm config firewall` rewrites the value with the canonical key. See the [Firewall for npm guide](/docs/guides/firewall#policy-groups) for group meanings and examples.

## Policy extensions [#policy-extensions]

Policy extensions are local, verdict-only install checks. They run after dependency resolution and platform/dev filtering, before registry tarballs are fetched or packages are linked. Lockfile and offline replays run the same extension checks before linking, so a warm install does not bypass local policy. Direct remote tarball URL dependencies are rejected while policy extensions are active because V1 cannot identify the package candidate without downloading the tarball first.

Extensions are configured by hand in `~/.lpm/config.toml`:

```toml
[policy.extensions.local-feed]
enabled = true
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"]
```

LPM CLI spawns the command directly with the provided argv, sends one JSON document on stdin, and reads one JSON document from stdout. `command[0]` must be an absolute path or a program name found on an absolute `PATH` directory; relative executable paths such as `./policy-extension` are rejected, and relative or empty `PATH` entries are ignored. The child environment is cleared except for minimal process/runtime variables such as `PATH`; package names and versions are never interpolated into a shell command.

Only `command`, `enabled`, `events`, `mode`, `on-error`, and `timeout-ms` are accepted under each extension. `enabled` defaults to `true`. A table with `enabled = false` can omit `command`. Unknown fields fail config loading, so typos such as `mdoe = "enforce"` cannot fall back to report mode.

Use [`lpm policy`](/docs/infra/policy) to inspect and test active extensions:

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

Request:

```json
{
  "schema_version": 1,
  "event": "package.candidate",
  "packages": [
    {
      "name": "left-pad",
      "version": "1.3.0",
      "source": "registry+https://registry.npmjs.org",
      "source_kind": "registry",
      "integrity": "sha512-...",
      "registry_published_at": "2025-01-01T00:00:00.000Z",
      "is_direct": true,
      "is_lpm": false,
      "optional": false
    }
  ]
}
```

Response:

```json
{
  "schema_version": 1,
  "decisions": [
    {
      "name": "left-pad",
      "version": "1.3.0",
      "action": "block",
      "code": "local-deny-list",
      "reason": "matched local feed"
    }
  ]
}
```

The response envelope is strict: top-level fields must be `schema_version` and `decisions`, and each decision may contain only `name`, `version`, `action`, `code`, and `reason`. Missing, misspelled, or unknown fields are protocol errors. `action` must be `allow`, `warn`, or `block`. Every decision must reference a package candidate from the request by exact `name` and `version`; unknown or duplicate decisions are also protocol errors. `mode = "report"` warns and continues even for `block` decisions. `mode = "enforce"` blocks the install for `block` decisions. `on-error` controls runner and protocol failures, not package verdicts.

## Security-sensitive keys are proposals [#security-sensitive-keys-are-proposals]

For ordinary preferences, editing this file directly is fine.

For security-sensitive keys such as:

* `script-policy`
* `minimum-release-age-secs`
* `release-age-policy`
* `[sandbox] mode`
* `[sandbox] allow-degraded`
* `[sigstore] verify`
* `typosquat-guard`
* `[firewall] mode`
* `install-time-source-analysis`

the file is only one input to the decision. A weaker value written here does **not** automatically become authoritative.

LPM CLI only honors a weaker value when it is already covered by the current approved machine posture or by an active temporary unlock. Otherwise:

* `lpm config ...` asks for confirmation before persisting the weaker posture in an interactive TTY
* install / rebuild / approve-scripts and related mutating commands fail with `security_approval_required` if the raw file value is weaker than the approved floor
* a managed machine policy can fail the change with `security_floor` instead

For short-lived exceptions, use [`lpm security unlock`](/docs/infra/security#unlock). For persistent changes, use [`lpm config`](/docs/infra/config) rather than relying on a hand edit alone. Values that weaken the approved floor, such as `[sigstore].verify = "off"` or `install-time-source-analysis = false`, are reported by `lpm security status` as runtime overrides until they are removed, approved, or blocked by managed policy.

## Precedence [#precedence]

For most scalar settings, the precedence chain is (highest first):

1. **CLI flag** — e.g. `--exact`, `--policy=allow`, `--min-release-age=1h`, `--no-engine-strict`
2. **`package.json > lpm`** — for keys that exist there (`scriptPolicy`, `minimumReleaseAge`, `minimumReleaseAgePolicy`, `engineStrict`, `strictPeerDependencies`)
3. **`./lpm.toml`** — project-level (today: `save-prefix`, `save-exact`, `[workspace].concurrency`)
4. **`~/.lpm/config.toml`** — this file
5. **Built-in default**

List-valued cooldown excludes are merged instead of replaced: CLI `--min-release-age-exclude` entries first, then `package.json > lpm > minimumReleaseAgeExclude`, then this file's `minimum-release-age-exclude`; duplicates collapse.

Package-published LPM.dev skill installation has its own compact chain: `--skills` / `--no-skills` wins over `auto-install-lpm-skills`, which wins over the inverted legacy `noSkills` key, then the built-in default `true`. `lpm config lpm-skills` writes the canonical boolean and removes the legacy key.

Install-time source analysis defaults to disabled. A permitted `install-time-source-analysis = false` skips new scans and cache writes without deleting existing caches. Re-enabling it makes install validate the analysis-cache schema and backfill missing, malformed, or outdated caches directly from already-extracted store bytes. Disabling an explicitly approved source-analysis setting requires approval. A configured `force-security-floor` or managed policy can prohibit that change.

LPM.dev security enrichment also defaults to enabled. `fetch-lpm-security-insights = false` suppresses the install-time batch enrichment request without disabling local source analysis or cached local findings. Use `lpm config lpm-dev` to review this setting together with automatic LPM.dev package skills, or `lpm config lpm-insights` to change only enrichment.

On `lpm install -g` the `package.json > lpm` tier is N/A — global installs synthesize their own package.json that doesn't carry user-edited keys. The chain collapses to CLI flag > `~/.lpm/config.toml` > built-in default for `script-policy`, `triage-advisor`, `minimum-release-age-secs`, `release-age-policy`, `strict-peer-dependencies`, `trust-policy`, `integrity`, and the script-policy aliases (`--yolo` / `--triage`). `minimum-release-age-exclude` still merges with repeated CLI `--min-release-age-exclude` flags. Registry signature verification uses `LPM_VERIFY_REGISTRY_SIGNATURES` > `signatures` > default `false`. Sigstore verification posture uses `LPM_PROVENANCE_ENFORCE` > `[sigstore].verify` > built-in default, with weaker env/config values guarded by the security approval layer. `[sigstore].scope` and `[sigstore].availability` are independent config-only axes; their defaults remain `"approved"` and `"best-effort"`.

Typosquat analysis uses `typosquat-guard` first. If that key is absent, the diagnostic `LPM_TYPOSQUAT_GUARD=0|false|off|disabled` override can disable analysis for benchmarking/debugging unless `force-security-floor = true` is active or managed policy owns the typosquat floor. If the key is present, the config file wins over the env var. The built-in default is disabled, and `lpm config typosquat --set default` removes the explicit key.

LPM Firewall uses `[firewall].mode`, defaulting to `"off"`. `LPM_NPM_FIREWALL=monitor|enforce` can tighten the mode for one process, but it cannot weaken a stricter config value. `report` remains accepted as a legacy alias for `monitor`. Active modes send LPM.dev Registry auth because the hosted firewall.lpm.dev verdict service is an LPM.dev Registry Pro/Org feature; use `lpm login` locally or set `LPM_TOKEN` in CI. The legacy diagnostic `LPM_EXPERIMENT_NPM_FIREWALL` is accepted for local benchmark compatibility; prefer `lpm config firewall --set monitor|enforce` for persistent use.

Policy extensions use only `~/.lpm/config.toml` today. Active extensions disable install-time registry tarball prefetch until their `package.candidate` verdicts pass, then normal fetch/link work resumes. Direct remote tarball URL dependencies fail before download while policy extensions are active. Extensions are local machine policy, not part of the signed approved-posture or managed-policy floor yet.

Store object validation uses `integrity`, defaulting to `"source"`. `source` validates reused v2 store objects by source identity without rehashing every expanded file. `tree` rehashes expanded files before reuse to catch local tampering/corruption, at the cost of slower warm installs. `LPM_V2_OBJECT_INTEGRITY=source|tree` overrides the config value for one process; the legacy env aliases `sri` and `tarball` mean `source`. This setting is separate from `--strict-integrity`, which controls whether tarball URL dependencies must declare an inline SRI hash before first use.

### Exception — `linker` [#exception--linker]

The `linker` key inverts the user-vs-project order. Per-user preference (this file) wins over `package.json > lpm > linker`, because the layout choice is an environmental decision (which `node_modules` shape this developer's tools and editor expect) that shouldn't be overridden by every cloned repo:

1. `lpm install --linker <isolated|hoisted>` — per-invocation
2. `~/.lpm/config.toml > linker` — this file
3. `LPM_LINKER` — per-process env var
4. `package.json > lpm > linker` — project-declared default
5. Built-in default — `"hoisted"` for single packages, `"isolated"` when LPM CLI detects a workspace root (a `package.json > workspaces` glob or a `pnpm-workspace.yaml`)

At the built-in default tier only, a fresh resolve that finds incompatible peer requirements auto-switches the install to `"isolated"` and records `auto-isolated-peer-conflicts = true` in `lpm.lock`. Any explicit linker value in the four higher tiers opts out.

The resolved mode is folded into `.lpm/install-hash` so a post-install flip of any of these surfaces invalidates the "up to date" cache and triggers a re-link on the next `lpm install`.

## File location [#file-location]

| Platform      | Path                             |
| ------------- | -------------------------------- |
| macOS / Linux | `~/.lpm/config.toml`             |
| Windows       | `%USERPROFILE%\.lpm\config.toml` |

Override the entire `~/.lpm/` root with the [`LPM_HOME`](/docs/reference/env-vars#filesystem-layout) env var (also moves the store, cache, and global install root).

The file is created on demand the first time `lpm config set` writes to it.

## See also [#see-also]

* [`lpm config`](/docs/infra/config) — CLI surface for getting / setting these keys
* [`lpm policy`](/docs/infra/policy) — inspect and test local install policy extensions
* [`lpm security`](/docs/infra/security) — inspect the effective floor or create a temporary unlock
* [`lpm.toml`](/docs/reference/lpm-toml) — project-level overrides for save policy
* [`package.json` "lpm" key](/docs/reference/package-json-lpm) — project-shared LPM CLI config
* [Environment variables](/docs/reference/env-vars) — sits above this file in precedence for many settings


# Environment variables (/docs/reference/env-vars)



LPM CLI reads a number of environment variables to override behavior, route auth, or expose diagnostic knobs. Most users never need to touch these — they exist for CI, debugging, recovery, and uncommon workflows.

Variables are grouped by purpose. Anything marked **diagnostic** is a debug-only escape hatch — not a stable, documented user knob.

## Auth and routing [#auth-and-routing]

| Variable                    | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_TOKEN`                 | Use this token instead of the OS-keychain-stored token. Equivalent to the global `--token <T>` flag and also the highest-priority credential for the LPM.dev Registry MCP server. Useful in CI or editor environments without keychain access.                                                                                                                                                                                                                                                                            |
| `LPM_DISABLE_HOST_CLI_AUTH` | Set to `1` or `true` to disable credentials from `gh` and `glab`. Environment tokens and fallback tokens stored by LPM CLI remain available. Useful in automation that must not inherit a local host CLI session.                                                                                                                                                                                                                                                                                                         |
| `LPM_REGISTRY_URL`          | Override the registry URL. Equivalent to `--registry <URL>`. Use only trusted registry endpoints: this controls where package metadata requests and registry-scoped credentials are sent. When inherited by the editor, the MCP server uses an accepted HTTPS or HTTP-loopback URL for registry-scoped keychain lookup; it has no equivalent of the CLI's `--insecure` exception for remote HTTP. Direct non-loopback HTTP in the CLI requires `--insecure`, which does not allow an HTTPS redirect to downgrade to HTTP. |
| `LPM_OIDC_TOKEN`            | Canonical pre-supplied OIDC token for the `lpm publish` auto-exchange path and `lpm env pull --oidc`. The audience must be `https://lpm.dev`. When set, it takes precedence over the GitHub Actions runtime fetch, so self-hosted runners can opt out of that call. &#x2A;*Does not affect `lpm publish --provenance`** — Sigstore needs an audience-`sigstore` token instead.                                                                                                                                            |
| `LPM_OIDC_POLICY_ID`        | Required server-issued policy selector for `lpm env pull --oidc`. Get the UUID from `lpm env oidc allow` or `lpm env oidc list`. `--policy-id=<uuid>` takes precedence. The value is not a bearer credential, but CI configuration must protect its integrity. It does not affect package publishing.                                                                                                                                                                                                                     |
| `LPM_GITLAB_OIDC_TOKEN`     | Legacy GitLab alias. Accepted on the registry-exchange and provenance surfaces for back-compat. New setups should mint `LPM_OIDC_TOKEN` (registry exchange) or `SIGSTORE_ID_TOKEN` (provenance).                                                                                                                                                                                                                                                                                                                          |
| `SIGSTORE_ID_TOKEN`         | GitLab CI OIDC token with audience `sigstore`, minted via the `id_tokens` block. Drives `lpm publish --provenance` on GitLab. Not used by registry-exchange surfaces (wrong audience).                                                                                                                                                                                                                                                                                                                                    |
| `LPM_VAULT_ID`              | Vault ID for `lpm env pull --oidc` only. A non-empty, trimmed value takes precedence over `lpm.json > "vault"` and can bootstrap a checkout with no local vault field. An unset, empty, or whitespace-only value falls back to the local field; the pull fails if neither source is available. Other `lpm env` operations ignore it. See [Secrets vault — Per-project identity](/docs/infra/secrets-vault#per-project-identity).                                                                                          |

## npm publish and provenance [#npm-publish-and-provenance]

These npm-compatible variables are read by [`lpm publish`](/docs/packages/publish#provenance) and `lpm stage publish`. Provenance-file values attach only to npm-compatible targets; `lpm publish` rejects them when an lpm.dev target is active.

| Variable                                                    | Effect                                                                                                                                                                                                                                                      |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NPM_TOKEN`                                                 | Token auth for the default npm registry when npm Trusted Publishing is not available. For non-default npm-compatible registries, store an exact registry-scoped token with `lpm login --login-registry <URL> --token <T>`.                                  |
| `NPM_ID_TOKEN`                                              | Pre-supplied npm Trusted Publishing OIDC token. The audience must be `npm:registry.npmjs.org`. Used for `lpm publish --npm` and `lpm stage publish` against the default npm registry.                                                                       |
| `NPM_CONFIG_PROVENANCE` / `npm_config_provenance`           | Truthy values (`true`, `1`, `yes`, `on`) enable generated Sigstore provenance. Falsy values disable it unless a higher-precedence CLI flag or `package.json > publishConfig.provenance` overrides it. Mutually exclusive with `NPM_CONFIG_PROVENANCE_FILE`. |
| `NPM_CONFIG_PROVENANCE_FILE` / `npm_config_provenance_file` | Path to a pre-generated Sigstore bundle to attach to npm-compatible publish targets. Relative paths are resolved from the project directory. Mutually exclusive with `NPM_CONFIG_PROVENANCE`.                                                               |

## Filesystem layout [#filesystem-layout]

| Variable   | Effect                                                                                                                                                                                                                                                        |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_HOME` | Override the LPM CLI root (`~/.lpm` by default). This path contains the store, cache, global installs, user config, and tunnel pins. Use it for hermetic CI runs or local development. It does not move the OS-managed policy used by `lpm security protect`. |

## Install pipeline [#install-pipeline]

| Variable                         | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_LINKER`                     | `node_modules` layout for installs in this process. Accepts `isolated` (pnpm-style strict-deps) or `hoisted` (LPM CLI's v2 hoisted virtual-store layout). Useful for forcing a specific layout in CI without editing config or per-invocation flags; setting it also opts out of default peer-conflict auto-isolation. Equivalent to `--linker=<value>`. Sits below `--linker` and `~/.lpm/config.toml > linker` in the [precedence chain](/docs/reference/config-toml#exception-linker), and above `package.json > lpm > linker`. Unknown values fail loudly at install time — there is no silent fallback, even on the up-to-date cache fast path. Empty-string (`LPM_LINKER=""`) is treated the same as unset, so an `unset NAME \|\| NAME=""` automation pattern doesn't accidentally pin a layout. |
| `LPM_VERIFY_REGISTRY_SIGNATURES` | Enables install-time npm registry package signature verification for this process. Accepts truthy values such as `1`, `true`, `yes`, `on`, or `enabled`; otherwise falls back to `~/.lpm/config.toml > signatures`. Persistent equivalent: `lpm config signatures --set true`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `LPM_NPM_FIREWALL`               | Tighten LPM Firewall verdict checks for npm packages in this process. Accepts `off`, `monitor`, or `enforce`, but cannot weaken a stricter `~/.lpm/config.toml > [firewall].mode` value. `report` remains accepted as a legacy alias for `monitor`. Active modes send LPM.dev Registry auth because the hosted verdict service is an LPM.dev Registry Pro/Org feature; use `LPM_TOKEN` in CI. Persistent equivalent: `lpm config firewall --set monitor\|enforce`.                                                                                                                                                                                                                                                                                                                                      |
| `LPM_V2_OBJECT_INTEGRITY`        | Override reused v2 store object validation for this process. `source` is the default fast source-identity check; `tree` rehashes expanded files before reuse to detect local store tampering/corruption. Legacy aliases `sri` and `tarball` mean `source`. Persistent equivalent: `lpm config integrity --set source\|tree`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `LPM_AUDIT_AFTER_INSTALL`        | Toggle [audit after install](/docs/packages/install#audit-after-install) for this process. Accepts `1` / `true` / `yes` / `on` (enable) and `0` / `false` / `no` / `off` (disable). Anything else falls through to `~/.lpm/config.toml > audit-after-install`. Per-invocation overrides: `--audit-after-install` / `--no-audit-after-install`. Findings are informational — they never fail the install.                                                                                                                                                                                                                                                                                                                                                                                                |
| `LPM_CONCURRENT_DOWNLOADS`       | Cap the number of concurrent tarball downloads during install and `lpm fetch`. Integer in `1..=256`; default `24`. Out-of-range or unparseable values warn on stderr and fall back to the default — set this only when you have a measured reason.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `LPM_NO_AUTO_INSTALL`            | Set to `true` to disable automatic managed-runtime installation when a Node or Bun runtime selector is missing locally. It does not affect `package.json > engines.node`, which never installs a runtime.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `LPM_FORCE_TOOL_INSTALL`         | Set to `1` to force re-download a built-in tool even if the binary already exists in the cache. Useful when a downloaded binary has been corrupted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

## Standalone installer [#standalone-installer]

These variables are read by the shell installer, not by an installed `lpm` binary.

| Variable              | Effect                                                                                                                                  |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_INSTALL_CHANNEL` | Release channel to resolve when `LPM_INSTALL_VERSION` is unset. Accepts `stable` (default) or `nightly`.                                |
| `LPM_INSTALL_VERSION` | Exact GitHub release tag to install, including the leading `v`. Accepts stable and prerelease tags and skips the moving-channel lookup. |

## Security and recovery overrides [#security-and-recovery-overrides]

These are intentionally sharp controls. Use them only for recovery, local wrapper testing, or explicit security-policy exceptions.

| Variable                           | Scope                | Effect                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_PROVENANCE_ENFORCE`           | CLI runtime          | Sigstore provenance posture for this process: `deny` (default), `warn`, or `off`. `warn` and `off` weaken verification, are surfaced by [`lpm security status`](/docs/infra/security#status), and are guarded by the security-approval layer on install/trust mutation paths. Unknown values fail at startup.                                                                          |
| `LPM_ALLOW_UNVERIFIED_PLUGINS`     | Managed plugins      | Set to `1` to install a plugin without checksum verification when neither a bundled nor an upstream `<asset_url>.sha256` is available. The override is recorded on the plugin sidecar; reuse requires this variable on every subsequent invocation. See [Built-in tools — Checksum verification](/docs/dev/builtin-tools#checksum-verification).                                       |
| `LPM_BINARY_PATH`                  | npm wrapper          | For the `@lpm-registry/cli` JavaScript launcher only: execute this exact native binary instead of the platform package resolved from `optionalDependencies`. Useful for local wrapper testing or emergency recovery, but a polluted environment can replace the executable behind `lpm` / `lpx`.                                                                                       |
| `LPM_INSTALL_INSECURE`             | Standalone installer | Set to `1` for the `curl -fsSL https://cli.lpm.dev/install \| sh` installer to skip all download integrity checks. Emergency-recovery only; installed `lpm` binaries do not read this variable.                                                                                                                                                                                        |
| `LPM_INSTALL_MIN_VERSION_OVERRIDE` | Standalone installer | Set to `1` to bypass the installer's minimum-version floor when intentionally installing an older release. SHA-256 verification against the downloaded manifest still runs unless `LPM_INSTALL_INSECURE=1` is also set. When both `cosign` and the release bundle are available, the installer additionally authenticates the manifest through Sigstore and fails closed on rejection. |

## Remote task cache [#remote-task-cache]

| Variable                         | Effect                                                                                                                                                                                                                                                                                             |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_REMOTE_CACHE`               | `1` / `true` enables hosted task cache for cache-enabled tasks even when `lpm.json > remoteCache.enabled` is absent. `0` / `false` disables it for this process.                                                                                                                                   |
| `LPM_REMOTE_CACHE_TOKEN`         | Bearer token for hosted cache reads and writes. Falls back to `LPM_TOKEN` or the token from `lpm login` only when the cache endpoint shares the configured registry origin. Third-party cache hosts require this variable.                                                                         |
| `LPM_REMOTE_CACHE_URL`           | Override the hosted cache endpoint. Defaults to the configured registry plus `/v8`. Direct HTTP is accepted only for localhost, and an automatically followed HTTPS redirect cannot downgrade to HTTP. Hosts outside the configured registry origin also require `LPM_REMOTE_CACHE_SIGNATURE_KEY`. |
| `LPM_REMOTE_CACHE_TEAM`          | Organization slug/team namespace. Falls back to `lpm.json > remoteCache.team`; unset means the authenticated personal namespace.                                                                                                                                                                   |
| `LPM_REMOTE_CACHE_SIGNATURE_KEY` | HMAC key for signed remote artifacts. Required when `lpm.json > remoteCache.signature` is `true` or the cache endpoint is outside the configured registry origin.                                                                                                                                  |
| `LPM_REMOTE_CACHE_READ_ONLY`     | `1` / `true` reads hosted artifacts but skips uploads for this process.                                                                                                                                                                                                                            |

## Triage advisor cache [#triage-advisor-cache]

The Layer 5 [LLM advisor](/docs/packages/security-audit#verdict-cache) persists verdicts at `$LPM_HOME/cache/l4-verdicts.json` so repeat installs of the same `(name, version)` skip the advisor round-trip.

| Variable                | Effect                                                                                                                                                                        |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_L4_CACHE`          | Set to `0` to disable the verdict cache entirely (lookups miss, inserts are dropped). Default: enabled. Useful for measurement runs that need the cold-cache round-trip cost. |
| `LPM_L4_CACHE_PATH`     | Override the cache file location. Defaults to `$LPM_HOME/cache/l4-verdicts.json`.                                                                                             |
| `LPM_L4_CACHE_TTL_SECS` | Override the verdict TTL in seconds. Default: 30 days. Expired entries are re-classified on next encounter.                                                                   |

## Tunnel [#tunnel]

| Variable           | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_TUNNEL_RELAY` | Override the tunnel relay WebSocket URL (default: `wss://relay.lpm.fyi/connect`). Use only trusted relay endpoints. A different relay changes the broker for tunnel connections. Use this variable for a custom worker, staging, or future regional endpoints. Empty or whitespace-only values use the next precedence level. LPM CLI stores each TLS pin at `$LPM_HOME/relay-pins/<host>`. A new relay host gets a new TOFU pin. Persistent equivalent: `tunnel.relay-url` in `$LPM_HOME/config.toml`. |

## Resolver (advanced) [#resolver-advanced]

| Variable            | Effect                                                                                                                                                                                                                  |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_RESOLVER`      | `pubgrub` opts out of the default greedy-fusion resolver and uses the legacy PubGrub-with-split-retry resolver. Both speak npm-compatible semver.                                                                       |
| `LPM_GREEDY_FUSION` | `0` disables the fused dispatcher and falls back to the walker-arm orchestration for debugging.                                                                                                                         |
| `LPM_WALKER`        | Walker-dispatch picker. Diagnostic.                                                                                                                                                                                     |
| `LPM_NPM_FANOUT`    | Cap concurrent npm metadata fetches in the resolver.                                                                                                                                                                    |
| `LPM_NPM_ROUTE`     | `direct` or `proxy`. Debug-only override of the npm package routing decision (default routes `@lpm.dev/*` through the LPM.dev Registry worker, everything else direct to `registry.npmjs.org`). Not a stable user knob. |

## Diagnostic / experimental [#diagnostic--experimental]

These exist for benchmarking and debugging. Don't depend on them in production.

| Variable                                 | Effect                                                                                                                                                                                                                                                                                                                                    |    |        |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -- | ------ |
| `LPM_HTTP`                               | `h1-pool` forces an HTTP/1 connection pool instead of the default HTTP/2 path.                                                                                                                                                                                                                                                            |    |        |
| `LPM_STREAM_FETCH`                       | Streaming-fetch mode toggle for the install pipeline.                                                                                                                                                                                                                                                                                     |    |        |
| `LPM_STORE_VERSION`                      | Select the store layout for this process. Unset, empty, `v2`, or `2` uses the default v2 layout. `v3` or `3` explicitly selects the experimental file-level CAS; LPM CLI never selects it automatically from the filesystem or CI environment. `v1` or `1` is the legacy rollback path. Unknown values warn and fall back to v2.          |    |        |
| `LPM_TIMING_DETAIL`                      | Add env-gated install timing probes to `lpm install --json`. Set to any value for `timing.detail`; set to `trace` to also include slow package buckets and duplicate metadata rankings. Includes cache classification, reusable-object validation, v3 CAS validation, and virtual-store link-task timing. Diagnostic.                     |    |        |
| `LPM_SERIAL_LINK`                        | Disable parallel linking — useful for reproducing race conditions.                                                                                                                                                                                                                                                                        |    |        |
| `LPM_TYPOSQUAT_GUARD`                    | Diagnostic typosquat analysis toggle. `0`, `false`, `off`, or `disabled` skip suspicious-name analysis only when `~/.lpm/config.toml > typosquat-guard` is absent, `force-security-floor = true` is not active, and managed policy does not own the typosquat floor. Persistent user-facing control: \`lpm config typosquat --set default | on | off\`. |
| `LPM_EXPERIMENT_NPM_FIREWALL`            | Legacy benchmark alias for `LPM_NPM_FIREWALL`. Prefer the config wizard or `LPM_NPM_FIREWALL` for new runs.                                                                                                                                                                                                                               |    |        |
| `LPM_EXPERIMENT_NPM_FIREWALL_LOOKUP`     | Diagnostic firewall lookup mode. Defaults to package-only; `integrity`, `strict`, or `package-and-integrity` enables the stricter integrity sidecar lookup for data-contract testing.                                                                                                                                                     |    |        |
| `LPM_EXPERIMENT_NPM_FIREWALL_CHUNK_SIZE` | Diagnostic firewall verdict chunk size. Defaults to `64`; non-positive or invalid values fall back to the default.                                                                                                                                                                                                                        |    |        |
| `LPM_MAX_BLOCKING_THREADS`               | Cap the tokio blocking-pool size. A/B benching hook for the install hot path.                                                                                                                                                                                                                                                             |    |        |
| `LPM_DEEP_FOLLOWUP`                      | Resolver follow-up depth knob.                                                                                                                                                                                                                                                                                                            |    |        |

## Test-only / sandboxed environments [#test-only--sandboxed-environments]

| Variable               | Effect                                                                                                                                                                                                                                                                                                |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LPM_FORCE_FILE_AUTH`  | Disable the OS keychain for token storage and force the encrypted-file fallback. Used by the test suite; also useful in sandboxed environments where the keychain isn't reachable.                                                                                                                    |
| `LPM_FORCE_FILE_VAULT` | In debug/test builds, bypass native vault-key storage and force the encrypted-file fallback under `~/.lpm/vaults/` with `~/.lpm/.vault-fallback-key`. Release builds ignore this override and use the platform default. See [Secrets vault — Local storage](/docs/infra/secrets-vault#local-storage). |
| `LPM_TEST_*`           | Reserved for the test suite; not user-facing.                                                                                                                                                                                                                                                         |

## Precedence reminder [#precedence-reminder]

For settings that have both a CLI flag and an env var (`--token` / `LPM_TOKEN`, `--registry` / `LPM_REGISTRY_URL`, `--linker` / `LPM_LINKER`), the **CLI flag wins**.

## See also [#see-also]

* [Global flags](/docs/commands#global-flags) — CLI flags equivalent to several of these vars
* [`lpm config`](/docs/infra/config) — for persistent CLI defaults instead of env vars
* [`~/.lpm/config.toml`](/docs/reference/config-toml) — file-backed config alternative


# Exit codes (/docs/reference/exit-codes)



LPM CLI follows the conventional Unix exit-code convention: `0` for success, non-zero for failure. The CLI is intentionally narrow about which non-zero codes it produces — most failures exit `1`. A few specific scenarios warrant a different code.

| Code          | Meaning                                                                                                                                                                                                                                |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`           | Success. The command completed without error.                                                                                                                                                                                          |
| `1`           | General error. Network failure, registry error, parse failure, validation failure, missing dependency, audit found vulnerabilities, etc. The error message on stderr (or in `--json` output) describes the specific cause.             |
| `2`           | Usage error. `lpm` was invoked with no subcommand, or with arguments clap couldn't parse. The CLI's help text is printed before exit.                                                                                                  |
| *passthrough* | Forwarded subprocess exit code. Commands that wrap external processes (`lpm run`, `lpm <file>`, `lpm exec`, `lpm dlx` / `lpx`, `lpm test`, `lpm bench`, `lpm lint`, `lpm fmt`, `lpm check`) exit with the wrapped command's exit code. |

## Subprocess passthrough [#subprocess-passthrough]

When a wrapped tool exits non-zero — your test suite fails, your script crashes, oxlint reports an error — LPM CLI forwards the exit code so CI gates see the right signal:

```bash
lpm test
echo "tests exit code: $?"     # whatever vitest / jest exited with
```

This is implemented via `LpmError::ExitCode(code)` flowing up to the top of `main`, which calls `std::process::exit(code)`. No code mangling — `lpm test` exiting `1` from vitest looks identical to running vitest directly.

## In `--json` mode [#in---json-mode]

With `--json`, every error path also emits a structured JSON object on stdout before exiting:

```json
{
  "schema_version": 1,
  "success": false,
  "error": "registry: not found",
  "error_code": "not_found"
}
```

The `error_code` field is the canonical short name for the error variant. Use it for programmatic dispatch in CI / agents (the `error` message text is human-facing and subject to change; `error_code` is the stable contract).

`schema_version` identifies the JSON document shape. On top-level error envelopes, version `1` is the shared error-envelope schema. Command success envelopes may also expose `schema_version`; those versions are command-specific.

When LPM CLI can suggest a concrete remediation command, the envelope includes `next_steps`:

```json
{
  "schema_version": 1,
  "success": false,
  "error": "authentication required",
  "error_code": "auth_required",
  "next_steps": [
    {
      "description": "Authenticate with the LPM.dev Registry",
      "command": "lpm login"
    }
  ]
}
```

`next_steps` is optional and only appears when LPM CLI has a safe command to hand to a human, CI job, or agent.

Most errors put a human-facing string in `error`; rich diagnostics may use an object with a `message` field plus machine-readable context. Resolver failures use `error_code: "resolution_failed"` and include `package`, `requested`, `required_by`, `kind`, and candidate-summary fields; see [Resolver failures](/docs/packages/resolver#resolution-failures).

### Full `error_code` catalog [#full-error_code-catalog]

| `error_code`                 | Emitted when                                                                                                                                        |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth_required`              | Operation needs an authenticated session and none is present                                                                                        |
| `session_expired`            | A stored session token has expired and self-heal didn't recover                                                                                     |
| `forbidden`                  | The server accepted the token but rejected the operation (plan gate, ACL, etc.)                                                                     |
| `not_found`                  | Package, version, vault, or other addressable resource doesn't exist                                                                                |
| `rate_limited`               | Server (or a downstream like GitHub) returned a rate-limit response                                                                                 |
| `network`                    | Underlying network failure (DNS, TCP, TLS) before the server replied                                                                                |
| `http`                       | Server replied but the response was malformed or an unhandled HTTP status                                                                           |
| `package_install_denied`     | Current registry access or version state prevents installation, including from a warm cache                                                         |
| `registry`                   | Registry-side error not covered by a more specific code                                                                                             |
| `resolution_failed`          | Dependency resolution could not find a compatible package version, platform, policy-allowed candidate, or complete solution graph                   |
| `typosquat_suspected`        | A new direct package name looks like a popular package and is not allow-listed in `lpm.toml`                                                        |
| `invalid_package_name`       | Argument doesn't match the package-name grammar (e.g., bad scope, invalid chars)                                                                    |
| `invalid_integrity`          | An SRI string is structurally malformed                                                                                                             |
| `integrity_mismatch`         | A downloaded tarball's hash differs from the lockfile / manifest expectation                                                                        |
| `invalid_version`            | Argument isn't a valid semver                                                                                                                       |
| `invalid_version_range`      | Argument isn't a valid semver range                                                                                                                 |
| `engine_mismatch`            | A workspace-root `engines.lpm` / `engines.node` or required dependency `engines.node` constraint isn't satisfied                                    |
| `script`                     | A lifecycle script (or `lpm run` script body) failed (script field also covers `ScriptWithOutput`)                                                  |
| `task`                       | Task-runner failure (graph cycle, missing dep, cache corruption)                                                                                    |
| `workspace`                  | Workspace-config error (bad glob, member resolution, schema)                                                                                        |
| `env_validation`             | `envSchema` validation in `lpm.json` failed (missing required var, format mismatch)                                                                 |
| `plugin`                     | Managed plugin tool (Oxlint, Biome, Rolldown) failed to download, verify, or launch                                                                 |
| `cert`                       | mkcert / cert install error from `lpm cert`                                                                                                         |
| `tunnel`                     | Tunnel relay / domain claim / WebSocket failure                                                                                                     |
| `store`                      | Content-addressable store error (corruption, missing object, write failure)                                                                         |
| `io`                         | Filesystem I/O failed below a higher-level operation                                                                                                |
| `json`                       | JSON parse / serialize failure on a file or response body                                                                                           |
| `security_floor`             | A higher-authority floor or managed policy refused a weaker security posture                                                                        |
| `security_approval_store`    | Local signed security state could not be verified; run `lpm security repair` to quarantine stale state                                              |
| `security_approval_required` | A guarded weakening needs explicit approval before LPM CLI will proceed                                                                             |
| `sudo_not_supported`         | A user-facing command ran as root with a non-root `SUDO_USER`. Run the command without `sudo`.                                                      |
| `self_update_paused`         | Self-update was administratively paused for the resolved release channel                                                                            |
| `self_update_rate_limited`   | Self-update hit a per-IP probe rate limit                                                                                                           |
| `exit_code`                  | Subprocess passthrough — the exit code came from a wrapped tool (vitest, oxlint, etc.); see [Subprocess passthrough](#subprocess-passthrough) above |

For single-package subprocess passthrough (`lpm test`, `lpm bench`, single-package `lpm lint` / `fmt` / `check`), `--json` preserves the wrapped command's own stdout — LPM CLI does not wrap it. This keeps the "single JSON result" contract for tools that already emit JSON. `lpm run --json` is different: it emits an LPM CLI task metadata envelope even for one script.

Workspace and multi-task aggregation DOES emit an LPM CLI envelope on stdout, but the envelope shape varies by command:

* **`lpm lint` / `fmt` / `check` / `test` / `bench` workspace mode** (`--all` / `--filter` / `--affected`) capture each member's subprocess stdout/stderr and surface them inside the envelope only on failure (truncated at 10 MB). Spawn / config / plugin / detection failures appear as `exit_code: null` paired with an `error` string, distinguishing "ran and exited non-zero" from "couldn't even launch." For `test` / `bench`, a member with no installed runner (no vitest/jest/mocha for `test`, no vitest or `scripts.bench` for `bench`) shows up as a per-member detection failure rather than aborting the entire run.
* **`lpm run`** emits a metadata-only envelope for single-script, multi-script, and workspace runs — each task/member's status, exit code, duration, cache hit, skip reason — but never the captured subprocess stdout/stderr. If a workspace filter matches no packages and `--fail-if-no-match` is not set, JSON mode emits a successful zero-package envelope. To debug a failing member, re-run that member without `--json`.

## Common scenarios [#common-scenarios]

| Scenario                                                                        | Exit code                                                |
| ------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `lpm install` succeeds                                                          | `0`                                                      |
| `lpm install` finds an unresolvable dep                                         | `1`                                                      |
| `lpm install --json` finds an unresolvable dep                                  | `1` (and `error_code: "resolution_failed"`)              |
| `lpm install axois --json` hits the typosquat guard                             | `1` (and `error_code: "typosquat_suspected"`)            |
| `lpm audit` finds a vulnerability under the default `--fail-on=all`             | `1`                                                      |
| `lpm audit` finds nothing (or a finding excluded by `--fail-on`)                | `0`                                                      |
| `lpm audit` cannot complete its OSV or LPM.dev Registry vulnerability lookup    | `1` (never reported as a clean audit)                    |
| `lpm audit --level severe` uses an unknown severity                             | `2` (usage error)                                        |
| `lpm tidy` finds unused dependencies or phantom imports                         | `1`                                                      |
| `lpm tidy --fix` removes all fixable unused dependencies and no findings remain | `0`                                                      |
| `lpm doctor` finds an issue                                                     | `1`                                                      |
| `lpm doctor` finds nothing                                                      | `0`                                                      |
| `lpm test` runs vitest, vitest exits 0                                          | `0`                                                      |
| `lpm test` runs vitest, vitest exits 1                                          | `1` (forwarded)                                          |
| `lpm install --no-sandbox --json` without approval                              | `1` (and `error_code: "security_approval_required"`)     |
| A managed policy blocks `lpm config sandbox --set none`                         | `1` (and `error_code: "security_floor"`)                 |
| `sudo lpm install --json`                                                       | `1` (and `error_code: "sudo_not_supported"`)             |
| `lpm` (no args)                                                                 | `2`                                                      |
| `lpm install --frobnicate` (unknown flag)                                       | `2`                                                      |
| `lpm install` blocked by missing auth                                           | `1` (and `error_code: "auth_required"` in `--json` mode) |

## CI gating idioms [#ci-gating-idioms]

```yaml title=".github/workflows/ci.yml (excerpt)"
- run: lpm install --offline --strict-integrity
- run: lpm audit --fail-on vuln
- run: lpm test
- run: lpm lint
- run: lpm fmt --check
- run: lpm check
```

Each step passes / fails on its own exit code. No special handling required.

## See also [#see-also]

* [`lpm audit --fail-on`](/docs/packages/audit) — gate exit code by finding type
* [`lpm fmt --check`](/docs/dev/fmt) — exit non-zero on unformatted files
* [Global flags](/docs/commands#global-flags) — `--json` for structured failure output


# Glossary (/docs/reference/glossary)



A reference for terms that mean something specific in LPM CLI, the LPM.dev Registry, or LPM Firewall. Sorted alphabetically.

### Add (vs. install) [#add-vs-install]

**`lpm add`** is *source delivery* — copies a package's source files into your project, like `shadcn-ui&#x60;. &#x2A;*`lpm install`** is *runtime dependency* — resolves and links into `node_modules/`. Not aliases. See [`lpm add`](/docs/packages/add) and [`lpm install`](/docs/packages/install).

### Auto-build [#auto-build]

The signal that triggers the rebuild step for trusted packages immediately after install completes. Sources: `lpm install --auto-build` (works on both project and `-g` installs), `lpm install --yolo` / `--policy=allow` (auto-triggers under the allow policy), or — project only — `package.json > lpm > scripts.autoBuild = true`. If a trusted lifecycle script fails, install exits non-zero. Globals don't read per-project script knobs from `package.json`, so triage with mixed-trust trees on `-g` requires the explicit `--auto-build` flag.

### Behavioral tag [#behavioral-tag]

A static-analysis flag attached to an installed package by [`lpm audit`](/docs/packages/audit) and queryable via [`lpm query`](/docs/packages/query). Examples: `:eval`, `:network`, `:fs`, `:shell`, `:child-process`, `:obfuscated`. Used to make security gates more precise than blanket version pinning.

### Capability [#capability]

A widening request a package's lifecycle scripts can declare in `lpm.scripts.{passEnv, readProject, sandboxLimits}`. Beyond the user's defaults, it requires explicit approval through [`lpm approve-scripts`](/docs/packages/approve-scripts) — the system never silently grants elevated capabilities.

### Cold install [#cold-install]

An install where neither `node_modules` nor the global store has the dep tree yet — everything is downloaded from the registry. Reported in [benchmarks](https://github.com/lpm-dev/rust-client#benchmarks) as the slowest steady-state path.

### Config-aware package [#config-aware-package]

A package whose tarball ships an `lpm.config.json` at the root. [`lpm add`](/docs/packages/add#authoring-config-aware-packages-lpmconfigjson) treats it as a configurable installer (interactive prompts, conditional file copy, conditional dep injection). Without `lpm.config.json` it's a plain source copy.

### Distribution mode [#distribution-mode]

A per-package opt-in for how the package is sold or shared on lpm.dev: **private** (default), **pool**, or **marketplace**. Orthogonal to package *type* (normal / source / Swift). All lpm.dev packages start private.

### Greedy-fusion (resolver) [#greedy-fusion-resolver]

The default resolver. Streams metadata fetches into the dispatcher. Faster than the legacy PubGrub-with-split-retry resolver; opt out with `LPM_RESOLVER=pubgrub`.

### Hoisted layout [#hoisted-layout]

LPM CLI's v2 hoisted virtual-store layout. Root direct deps are surfaced at project `node_modules/<dep>`, while package-local dependency links live inside shared store link entries. &#x2A;*Starting default for single-package projects.** Workspaces auto-default to [isolated](#isolated-layout), and default installs with incompatible peer requirements auto-switch to isolated; override per-invocation with `lpm install --linker=hoisted`, per-user with `~/.lpm/config.toml > linker`, or per-project with `package.json > lpm > linker`.

### Integrity [#integrity]

The SRI hash (`sha512-…`) recorded in the lockfile against each resolved package. Verified on every install. Drift is a hard error.

### Isolated layout [#isolated-layout]

pnpm-style strict `node_modules` — each package gets exactly the deps it declared, no phantom-dependency access. Project `node_modules/<pkg>` is a symlink into the [global content-addressable store](/docs/packages/content-addressable-store). **Default for workspaces** (when LPM CLI detects a `package.json > workspaces` glob or a `pnpm-workspace.yaml`) and for default installs where resolution detects incompatible peer requirements; otherwise single-package projects start [hoisted](#hoisted-layout).

### Lockfile [#lockfile]

`lpm.lock` (TOML, git-diffable, authoritative) plus `lpm.lockb` (binary companion) when the graph fits the binary wire format. Commit `lpm.lock` always; commit `lpm.lockb` when LPM CLI writes it. See [Lockfile format](/docs/reference/lockfile-format) and [Lockb format](/docs/reference/lockb-format).

### lpm.dev [#lpmdev]

The LPM.dev Registry monetization layer. Hosts private, pool, and marketplace packages under the `@lpm.dev/` scope. **LPM CLI is registry-agnostic** — lpm.dev is one of several registries it can talk to. See [Registries](/docs/registries).

### Marketplace package [#marketplace-package]

A distribution mode where installs require a license purchase. Per-package opt-in.

### MCP [#mcp]

[Model Context Protocol](https://modelcontextprotocol.io). The LPM.dev Registry ships an MCP server that AI editors can wire up via [`lpm mcp setup`](/docs/reference/mcp-servers).

### Minimum release age [#minimum-release-age]

Cooldown before a new package version is installable. The cooldown is off by default. When enabled, it checks direct project dependencies unless you select strict scope.

Strict mode also checks transitive dependencies and lockfile replays. Use `--allow-new` to bypass the cooldown for one install.

Release-age exclusions accept a package name, an exact version, or `@scope/*`. Configure them for one run, one project, or one user.

Use `--min-release-age`, `minimumReleaseAge`, or `minimum-release-age-secs` to configure the cooldown duration.

Use `--min-release-age-exclude` for one run. Use `lpm trust release-age-exclude` for a project. Use `lpm config release-age-exclude` for a user.

### npm-alias dep [#npm-alias-dep]

A dep declared like `"my-react": "npm:react@^19.0.0"`. Local name (`my-react`) diverges from the canonical target (`react`). Tracked in lockfiles via `alias-dependencies` and `root-aliases`. Disables the binary lockfile (TOML-only).

### Plugin [#plugin]

A versioned, lazy-downloaded tool that backs one of LPM CLI's built-in commands — Oxlint powers [`lpm lint`](/docs/dev/lint), Biome powers [`lpm fmt`](/docs/dev/fmt), and Rolldown powers [`lpm bundle`](/docs/dev/bundle). First use downloads and verifies the tool into LPM CLI's global cache; the same machine reuses the verified copy across projects. Pin versions per-project in `lpm.json > tools`; manage installed plugins with [`lpm plugin`](/docs/dev/plugin) (`list`, `outdated`, `update`, `remove`). Tools that aren't plugin-backed, such as `tsc` via [`lpm check`](/docs/dev/check) or `tsdown` via [`lpm pack`](/docs/dev/pack), stay project-owned.

### Pool package [#pool-package]

A distribution mode where the package's metadata is public but installs are gated to pool subscribers. Publisher earns a share of the pool's revenue. Per-package opt-in.

### Provenance [#provenance]

Sigstore-signed attestation that proves which CI workflow built a tarball and which commit it came from. Generated at publish time with `lpm publish --provenance`. Verified at install / audit time.

### Provenance drift [#provenance-drift]

A change in a package's publisher identity between a prior approval and a candidate version. LPM CLI blocks installs on drift on both project and `-g` paths; opt out per-package via `lpm install --ignore-provenance-drift <pkg>` or fully via `--ignore-provenance-drift-all`. The reference snapshot is captured into the trust binding's `provenanceAtApproval` field at `lpm approve-scripts` time (or `--global` time for the global trust file).

### Quality score [#quality-score]

Per-package score (0–100) computed from readme / license / types / tests / maintenance signals. Run [`lpm quality`](/docs/packages/quality) to view; gate publishing with `lpm publish --min-score N`.

### Rebuild [#rebuild]

The dependency lifecycle-script step of the install pipeline. The executed dependency phases are `preinstall`, `install`, `postinstall` (in that order); dependency `prepare` and `prepublishOnly` are recognized for detection but never executed. Default-deny: dependency scripts don't run during `lpm install` (or `lpm install -g`) and require approval via [`lpm approve-scripts`](/docs/packages/approve-scripts) (project) / `lpm approve-scripts --global` (global) or a non-default [script policy](/docs/packages/install#lifecycle-scripts). Bare project installs have a separate root lifecycle runner for `pnpm:devPreinstall`, `preinstall`, `install`, `postinstall`, `preprepare`, `prepare`, and `postprepare`. The standalone `lpm rebuild` command targets the project; a `lpm rebuild --global` companion is a planned follow-up — until then, reinstalling the affected global is the post-approval rerun path.

### Sandbox [#sandbox]

The OS-level containment LPM CLI runs lifecycle scripts inside: Seatbelt on macOS, landlock on Linux, AppContainer on Windows. Three modes: **default** (filesystem containment + env scrubbing; outbound network allowed), **strict** (also denies outbound network), **none** (no containment; full host access including credential env). Set per-invocation with `lpm install --strict-sandbox` / `--paranoid` (alias) / `--no-sandbox`; persistently with `[sandbox] mode` in `~/.lpm/config.toml` (or `LPM_STRICT_SANDBOX=1` for strict). Setup wizard: [`lpm config sandbox`](/docs/infra/config#setup-wizards). Packages can request capability widening (extra write roots, env vars) via the [`lpm.scripts`](#capability) block in `package.json`, but widening requires explicit approval through [`lpm approve-scripts`](/docs/packages/approve-scripts).

### Save policy [#save-policy]

The rule that decides what range gets saved to `package.json` when `lpm install <pkg>` finishes. Defaults to `^resolvedVersion`. See [Save policy](/docs/packages/save-policy).

### Script policy [#script-policy]

`deny` (default) / `allow` / `triage`. Governs whether and how dependency lifecycle scripts run on both `lpm install` and `lpm install -g`; it does not gate the root project's bare-install lifecycle. Set per-invocation with `--policy` / `--yolo` / `--triage`, per-project with `package.json > lpm > scriptPolicy` (project installs only), per-user with `~/.lpm/config.toml > script-policy` (applies to both project and `-g`). On `-g` the project-config tier is N/A; the chain collapses to CLI flag > `~/.lpm/config.toml` > default. Setup wizard: `lpm config scripts`.

### Skill (agent skill) [#skill-agent-skill]

A Markdown file that teaches an AI agent how to use a package or workflow. Package authors publish LPM.dev package skills that extract into `.lpm/skills/<package>/` during [`lpm install`](/docs/packages/install) and [`lpm add`](/docs/packages/add) (skip with `--no-skills`). The LPM CLI also manages standalone standard `SKILL.md` sources from GitHub or local directories through [`lpm skills`](/docs/packages/skills). Package-published skills follow LPM.dev publishing validation; standalone sources are commit-pinned and security-scanned before installation. See [AI agent skills](/docs/reference/ai-agent-skills) for the full model.

### Triage advisor [#triage-advisor]

`none` (default) / `claude-cli` / `codex` / `ollama`. Optional LLM advisor for the [triage gate](/docs/packages/security-audit#optional-llm-advisor) — only active when script policy is `triage`. Set per-project with `package.json > lpm > triageAdvisor`, per-user with `~/.lpm/config.toml > triage-advisor`. Independent of script policy. Approvals are **ephemeral** — never written to disk — so a second install invokes the advisor again. Setup wizard: `lpm config triage`.

### Source package [#source-package]

A package whose tarball ships an `lpm.config.json` at the root — `lpm add` treats it as a configurable installer. Status is **content-based** (the file's presence), not naming or registry.

### Store [#store]

The global content-addressable package store at `~/.lpm/store/`. The default v2 layout keeps one canonical object per source hash and shares graph-keyed link entries across projects. Experimental v3, selected only with `LPM_STORE_VERSION=v3`, adds content-and-mode-addressed file blobs and tree manifests. Project `node_modules/<pkg>` is a **symlink** into the matching link entry; package files are materialized with independent writable inodes through copy-on-write clones/reflinks or independent copies, so writes cannot alter canonical object/blob bytes. Maintained by [`lpm store`](/docs/infra/store); see [Content-addressable store](/docs/packages/content-addressable-store) for the full mechanism.

### Trusted dependency [#trusted-dependency]

A package on a trust allowlist that's permitted to run lifecycle scripts under `scriptPolicy: "deny"`. Project trust lives in `package.json > lpm > trustedDependencies` (legacy array of names, or rich map bound to integrity + script hash). Global trust lives in `~/.lpm/global/trusted-dependencies.json` (rich map only, with an extra `provenanceAtApproval` snapshot used by the install-time drift gate). The two stores are independent — `lpm approve-scripts` writes the former, `lpm approve-scripts --global` writes the latter.

### Tunnel claim [#tunnel-claim]

Pro/Org-only — registering a stable tunnel domain so subsequent `lpm dev --tunnel` and `lpm tunnel` runs serve from the same URL. Free users get an ephemeral random domain on every run.

### Up-to-date install [#up-to-date-install]

An `lpm install` invocation that detects nothing has changed (mtime fast path) and exits in a few ms without doing any work. Reported in [benchmarks](https://github.com/lpm-dev/rust-client#benchmarks) as `~14 ms`.

### Warm install [#warm-install]

An install where the global store and lockfile are present but `node_modules` is missing. The install rebuilds `node_modules/` from already-extracted store entries — no downloads, no resolution. Reported in [benchmarks](https://github.com/lpm-dev/rust-client#benchmarks) as `~387 ms`.

### Workspace [#workspace]

A monorepo declared via `package.json > workspaces` (npm/yarn array form, or yarn-style object form). LPM CLI walks the globs, builds a topological graph, and exposes member-targeting via `--filter` and `-w`. See [Workspaces](/docs/packages/workspaces).


# Reference (/docs/reference)





Canonical, schema-style reference for everything LPM CLI exposes outside of the command surface itself, plus the related LPM.dev Registry formats.

## Configuration [#configuration]

<Cards>
  <Card title="package.json — &#x22;lpm&#x22; key" href="/docs/reference/package-json-lpm" description="The LPM CLI-specific block embedded in package.json." />

  <Card title="lpm.json" href="/docs/reference/lpm-json" description="Sibling file for runtime, tasks, services, tunnel, env, and publish config." />

  <Card title="lpm.config.json" href="/docs/reference/lpm-config-json" description="Tarball-root config that turns lpm add into a configurable installer." />

  <Card title="JSON Schemas" href="/docs/reference/schemas" description="Published schema URLs and the lpm schema command." />

  <Card title="lpm.toml" href="/docs/reference/lpm-toml" description="Project-level CLI defaults — save policy and friends." />

  <Card title="~/.lpm/config.toml" href="/docs/reference/config-toml" description="User-level CLI defaults — same keys as lpm.toml plus more." />

  <Card title="Environment variables" href="/docs/reference/env-vars" description="Every LPM_* variable LPM CLI reads, what it does, and when to use it." />
</Cards>

## Editor integrations [#editor-integrations]

<Cards>
  <Card title="MCP servers" href="/docs/reference/mcp-servers" description="Wire Claude Code, Cursor, and Windsurf to the LPM.dev Registry MCP server." />

  <Card title="AI agent skills" href="/docs/reference/ai-agent-skills" description="How package-published and standalone skills are installed and managed." />
</Cards>

## File formats [#file-formats]

<Cards>
  <Card title="lpm.lock format" href="/docs/reference/lockfile-format" description="The git-diffable TOML lockfile written next to package.json." />

  <Card title="lpm.lockb format" href="/docs/reference/lockb-format" description="The mmap-friendly binary lockfile companion." />
</Cards>

## Other [#other]

<Cards>
  <Card title="Terminal output security" href="/docs/reference/terminal-output" description="The trust boundary for human output, JSON, intercepted child output, and raw terminal ownership." />

  <Card title="Exit codes" href="/docs/reference/exit-codes" description="What each exit code means and how to handle them in CI." />

  <Card title="Glossary" href="/docs/reference/glossary" description="LPM CLI, LPM.dev Registry, and LPM Firewall terminology in one place." />
</Cards>


# lpm.lockb format (/docs/reference/lockb-format)



`lpm.lockb` is a legacy generated companion to [`lpm.lock`](/docs/reference/lockfile-format). It stores a subset of metadata from lockfile versions before 13.

`lpm.lock` is always the authoritative install input. When a lockfile carries TOML-only metadata that `lpm.lockb` cannot represent, LPM CLI skips the binary write and removes any stale `lpm.lockb` from a previous install.

Lockfile version 13 does not write `lpm.lockb`. LPM CLI removes a stale binary file when it writes the current lockfile.

For a repository that still has a legacy binary file, mark it as binary in `.gitattributes`:

```text title=".gitattributes"
lpm.lockb binary
```

## File location [#file-location]

`<project-root>/lpm.lockb`

## Layout (v3) [#layout-v3]

```text
[Header: 16 bytes]
  magic              [u8; 4]  = b"LPMB"
  version            u32 LE   = 3
  package_count      u32 LE
  string_table_off   u32 LE   — byte offset where the string table starts

[PackageEntry × N: 36 bytes each, sorted by name]
  name_off           u32 LE   — offset into the string table
  name_len           u16 LE
  version_off        u32 LE
  version_len        u16 LE
  source_off         u32 LE   — 0 = None
  source_len         u16 LE
  integrity_off      u32 LE   — 0 = None
  integrity_len      u16 LE
  deps_off           u32 LE   — offset into the deps table
  deps_count         u16 LE
  tarball_off        u32 LE   — 0 = None  (v2+)
  tarball_len        u16 LE                (v2+)

[DepsEntry × total_deps: 6 bytes each]
  str_off            u32 LE   — offset into the string table
  str_len            u16 LE

[ProvenanceEntry × M: 68 bytes each, sparse]
  package_index       u32 LE   — index into PackageEntry table
  publisher           (u32 off, u16 len)
  workflow_path       (u32 off, u16 len)
  workflow_ref        (u32 off, u16 len)
  cert_sha256         (u32 off, u16 len)
  subject_name        (u32 off, u16 len)
  subject_sha512      (u32 off, u16 len)
  log_id              (u32 off, u16 len)
  bundle_sha256       (u32 off, u16 len)
  integrated_time     u64 LE
  rekor_log_index     i64 LE

[ProvenanceFooter: 8 bytes]
  magic              [u8; 4]  = b"PRV3"
  evidence_count     u32 LE

[String table]
  packed UTF-8, no NUL terminators
```

All numeric fields are little-endian. Packages are sorted by their package identity so reads can binary-search the entry table. The provenance section is sparse: packages without verified evidence do not pay a 68-byte per-package cost.

## Wire-format version [#wire-format-version]

Current reader format: `v3` ([`BINARY_VERSION`](https://github.com/lpm-dev/rust-client/blob/main/crates/lpm-lockfile/src/binary.rs)). This binary version is independent from TOML lockfile version 13.

The binary reader rejects any file whose header `version` is not exactly `BINARY_VERSION` — strict by design. Layout differs across versions, so interpreting one version as another could produce garbage or drop security evidence. `lpm.lock` remains the authoritative input; the next compatible `write_all` rewrites `lpm.lockb` as the current version.

## Sentinel for optional fields [#sentinel-for-optional-fields]

Optional string fields (`source`, `integrity`, `tarball`, and the optional provenance identity fields) use `(off=0, len=0)` to mean `None`.

To keep this sentinel unambiguous, the writer **rejects empty strings at insert time**: an empty source URL, integrity hash, or tarball URL is nonsensical input regardless. Failing loud is correct.

## What's NOT stored in the binary lockfile [#whats-not-stored-in-the-binary-lockfile]

The binary format is intentionally smaller than the TOML format. These fields are not represented:

* **`alias-dependencies`** — npm-alias edges
* **`importers`** — manifest snapshots used by frozen installs
* **`workspace-packages`** — content-addressed workspace union rows and importer projections
* **`patches`** — patch paths and patch-file SHA-256 records
* **`root-aliases`** — root-level npm-alias map
* **`ambient-peer-installs`** — root-level auto-installed peer links
* **`peers`** — per-package resolved peer pinning
* **`catalogs`** — catalog protocol snapshots
* **`auto-isolated-peer-conflicts`** — linker orchestration state
* **`os` / `cpu` / `libc` / `optional` / `node-engine`** — platform, optional-reachability, and dependency engine state
* **`registry-signatures` / `registry-published-at`** — npm registry signature evidence
* **`instance-id`** — exact package-instance identity
* **`dependency-targets` / `peer-targets`** — exact edge targets
* **root selection `instance-id`** — exact root target

Lockfile version 13 always takes the TOML-only path. LPM CLI skips the binary write and removes any stale binary file.

Verified provenance itself is supported by v3. If the same project also uses frozen-install importer snapshots, npm aliases, peer pinning, dependency engine constraints, platform-specific optional packages, catalogs, or registry signatures, seeing only `lpm.lock` is still correct.

## Reading the file [#reading-the-file]

The reader [`BinaryLockfileReader`](https://github.com/lpm-dev/rust-client/blob/main/crates/lpm-lockfile/src/binary.rs) `mmap`s the file once, parses the header, validates the magic + version, and exposes by-name lookup via binary search over the entry table. No allocation per entry; string slices borrow directly from the mmap.

LPM CLI can read a valid legacy binary file. Current installs read the authoritative TOML lockfile for the exact graph.

## See also [#see-also]

* [`lpm.lock` format](/docs/reference/lockfile-format) — the canonical TOML lockfile
* [Lockfile concept](/docs/packages/lockfile) — design overview
* [`lpm install`](/docs/packages/install) — what reads each file


# lpm.lock format (/docs/reference/lockfile-format)



`lpm.lock` is the human-readable, git-diffable lockfile written next to `package.json` for a standalone project or at the root of a workspace. It pins every transitive dependency by exact version, source identity, SRI integrity hash, platform metadata, registry-signature evidence, and any cryptographically verified provenance evidence, so the next install can reproduce the same graph without trusting ambient registry state.

The current lockfile is TOML-only because the binary format cannot represent exact package instances. LPM CLI removes a stale [`lpm.lockb`](/docs/reference/lockb-format).

## File location [#file-location]

Standalone project: `<project-root>/lpm.lock`

Workspace: `<workspace-root>/lpm.lock`. Member commands read their importer projection from this file; successful recursive migration removes legacy member lockfiles.

## Top-level shape [#top-level-shape]

```toml title="lpm.lock"
[metadata]
lockfile-version = 13
resolved-with = "greedy-fusion"
# Only present when the default linker auto-switched to isolated
# after detecting incompatible peer requirements.
auto-isolated-peer-conflicts = true

[importers.".".dependencies]
react = "^19.0.0"

[importers.".".dev-dependencies]
vite = "^6.0.0"

[patches."left-pad@1.3.0"]
path = "patches/left-pad@1.3.0.patch"
sha256 = "sha256-..."
original-integrity = "sha512-..."

[provenance."react@19.0.0#npm-..."]
subject-name = "pkg:npm/react@19.0.0"
subject-sha512 = "..."
integrated-time-secs = 1740000000
log-id = "..."
log-index = 123456
bundle-sha256 = "sha256-..."

[provenance."react@19.0.0#npm-...".snapshot]
present = true
publisher = "github:facebook/react"
workflowPath = ".github/workflows/publish.yml"
workflowRef = "refs/tags/v19.0.0"
attestation_cert_sha256 = "sha256-..."

[[packages]]
instance-id = "d7d8fbc52d7d998935c243be78e563594fcb08459d8b71dfdf96c60ed307a4e8"
name = "react"
version = "19.0.0"
source = "registry+https://registry.npmjs.org"
integrity = "sha512-..."
dependencies = ["scheduler@0.25.0"]
tarball = "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
registry-published-at = "2025-01-01T00:00:00.000Z"
node-engine = ">=18"

[[packages.registry-signatures]]
keyid = "SHA256:..."
sig = "MEUCIQ..."

[packages.dependency-targets]
scheduler = "58b7a5e0c3f25598fdbd56da146a1841b4ce2b78f359414cd782817437512560"

[[packages]]
instance-id = "58b7a5e0c3f25598fdbd56da146a1841b4ce2b78f359414cd782817437512560"
name = "scheduler"
version = "0.25.0"
source = "registry+https://registry.npmjs.org"
integrity = "sha512-..."
os = ["darwin", "linux"]
cpu = ["arm64", "x64"]
libc = ["glibc"]
optional = true

[[packages]]
instance-id = "8d7fd69910cda17e7de89f87e3eed10a3f182ecf34ae46d2592046ae553f5296"
name = "wa-sqlite"
version = "1.0.0"
source = "git+https://github.com/rhashimoto/wa-sqlite.git#779219540f66cecaa159da32b3b8936697ba10a7"
integrity = "sha512-..."

# Only present when at least one root dep uses npm:<target>@<range>
[root-aliases]
my-react-alias = "react"

# Only present when the eager peer-drain pass synthesized installs to
# satisfy unmet peerDependencies (default-on under autoInstallPeers).
ambient-peer-installs = ["loose-envify"]
```

The example above shows the standalone package table. A workspace union replaces `[[packages]]` with content-addressed rows and gives every importer an explicit projection:

```toml title="workspace lpm.lock"
[metadata]
lockfile-version = 13
resolved-with = "greedy-fusion"

[importers."."]
locked-packages = ["sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]

[importers."packages/web"]
locked-packages = [
  "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
]

[importers."packages/web".dependencies]
react = "^19.0.0"

[workspace-packages."sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
instance-id = "d7d8fbc52d7d998935c243be78e563594fcb08459d8b71dfdf96c60ed307a4e8"
name = "react"
version = "19.0.0"
source = "registry+https://registry.npmjs.org"
integrity = "sha512-..."
```

## `[metadata]` [#metadata]

| Field                          | Type   | Notes                                                                                                                                                                                                                                                                                               |
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lockfile-version`             | u32    | Schema version. Current: `13`. Version `11` added local manifest fingerprints. Version `12` added structured peer edges. Version `13` added exact package-instance identities for rows, edges, and roots.                                                                                           |
| `resolved-with`                | string | Which resolver produced this file (e.g. `"greedy-fusion"`, `"pubgrub"`). Informational.                                                                                                                                                                                                             |
| `auto-isolated-peer-conflicts` | bool   | Present only when a default-hoisted install detected incompatible peer requirements and auto-switched the project to isolated layout. Warm installs read this before the resolver runs so the install hash and linker stay on the peer-preserving layout. Explicit linker config ignores this flag. |

## `[importers]` [#importers]

```toml
[importers.".".dependencies]
react = "^19.0.0"

[importers.".".dev-dependencies]
vite = "^6.0.0"

[importers.".".lpm-overrides]
"left-pad" = "1.3.0"

[importers.".".catalogs.default]
react = "^19.0.0"

[importers."."]
patches-fingerprint = "sha256-..."
peer-dependency-rules-fingerprint = "sha256-..."
auto-install-peers = true
```

Importer snapshots record the manifest inputs that must match before a frozen install can replay the lockfile. A standalone lockfile uses `"."`. A workspace union also uses member-relative keys such as `"packages/web"`; paths must remain safely below the lockfile root.

| Field                                       | Type         | Notes                                                                                                        |
| ------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------ |
| `dependencies`                              | table        | `package.json > dependencies` exactly as declared.                                                           |
| `dev-dependencies`                          | table        | `package.json > devDependencies` exactly as declared.                                                        |
| `optional-dependencies`                     | table        | `package.json > optionalDependencies` exactly as declared.                                                   |
| `peer-dependencies`                         | table        | `package.json > peerDependencies` exactly as declared.                                                       |
| `lpm-overrides`                             | table        | `package.json > lpm.overrides` after catalog references are resolved.                                        |
| `overrides`                                 | table        | npm-style `package.json > overrides` after catalog references are resolved.                                  |
| `resolutions`                               | table        | yarn-style `package.json > resolutions` after catalog references are resolved.                               |
| `catalogs`                                  | nested table | Catalog ranges visible to this importer.                                                                     |
| `patches-fingerprint`                       | string       | Hash of `lpm.patchedDependencies`; absent when no patches are declared.                                      |
| `peer-dependency-rules-fingerprint`         | string       | Hash of `lpm.peerDependencyRules`; absent when default/empty.                                                |
| `auto-install-peers`                        | bool         | Effective `autoInstallPeers` value captured for frozen replay.                                               |
| `workspace-root-peer-providers-fingerprint` | string       | Exact workspace-root provider graph used when satisfying this importer's peers.                              |
| `locked-packages`                           | string\[]    | Sorted SHA-256 IDs in `workspace-packages` that form this importer's package closure. Workspace unions only. |
| `root-aliases`                              | table        | Importer-local npm alias links.                                                                              |
| `root-resolutions`                          | table        | Importer-local exact root package selections.                                                                |
| `ambient-peer-installs`                     | string\[]    | Importer-local peers surfaced at its `node_modules` root.                                                    |
| `patches`                                   | table        | Importer-local patch evidence.                                                                               |
| `catalog_resolutions`                       | nested table | Importer-local resolved catalog snapshot.                                                                    |
| `provenance`                                | table        | Importer-local verified provenance evidence.                                                                 |
| `auto-isolated-peer-conflicts`              | bool         | Whether this importer automatically selected isolated linking for peer conflicts.                            |

## `[workspace-packages]` [#workspace-packages]

Workspace lockfiles store the union graph as tables keyed by `sha256:<hex>`. The digest includes each exact package instance and its exact edge targets.

Two rows can share one package name, version, and source. They remain separate when their dependency or peer contexts are different.

Every `importers.<path>.locked-packages` entry must reference an existing row. The reader rejects unsafe importer paths, duplicate IDs inside one importer, unreferenced union rows, content-address mismatches, and ambiguous package identities inside a projection. These checks run before commands consume the graph.

Projecting an importer rebuilds the standalone view expected by install and read-only commands: its package rows, root aliases and selections, ambient peers, patches, catalogs, provenance, and automatic linker state. Other importers' rows remain invisible.

## `[patches]` [#patches]

```toml
[patches."left-pad@1.3.0"]
path = "patches/left-pad@1.3.0.patch"
sha256 = "sha256-..."
original-integrity = "sha512-..."
```

Patch records bind `package.json > lpm.patchedDependencies` to the exact patch file bytes LPM CLI is allowed to replay. The table key is the exact patched package selector (`<name>@<version>`).

| Field                | Type   | Notes                                                                                                                             |
| -------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `path`               | string | Patch file path relative to the project root.                                                                                     |
| `sha256`             | string | SHA-256 digest of the patch file contents. Install hard-errors if the current file hash differs from this value.                  |
| `original-integrity` | string | SRI integrity of the pristine package bytes the patch was authored against. Install also verifies this before applying the patch. |

`[patches]` is TOML-only metadata. Projects with patch records skip `lpm.lockb` and remove any stale binary companion.

## `[[packages]]` and workspace package rows [#packages-and-workspace-package-rows]

Standalone lockfiles use one `[[packages]]` entry per resolved package. Workspace unions use the same fields below each `[workspace-packages."sha256:..."]` key. Standalone entries are sorted by package identity; workspace rows are sorted by content address for deterministic diffs and minimal merge conflicts.

GitHub packages use a commit-pinned `source` value and SHA-512 `integrity`. The user-written branch, tag, shorthand, or commit remains in the importer snapshot, while the package entry always records the resolved lowercase 40-character commit. This separation lets frozen and offline installs verify the manifest is unchanged while replaying immutable package bytes.

Root replay checks the manifest source kind and identity. A registry range cannot select a GitHub row with the same name and version.

Direct remote-tarball URLs must match exactly. Direct local paths must identify the same package or remain reachable through the declared local-source graph.

| Field                   | Type                 | Notes                                                                                                                                                                                                                                                                                                               |
| ----------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `instance-id`           | string               | Required 64-character lowercase hexadecimal ID for this exact graph instance.                                                                                                                                                                                                                                       |
| `name`                  | string               | Package name (e.g. `react`, `@lpm.dev/owner.pkg`)                                                                                                                                                                                                                                                                   |
| `version`               | string               | Exact resolved version                                                                                                                                                                                                                                                                                              |
| `source`                | string \| absent     | Typed source identity, such as `registry+https://registry.npmjs.org`, `tarball+https://…`, `directory+…`, `link+…`, or commit-pinned `git+https://github.com/…#<commit>`. Absent for packages without a known source.                                                                                               |
| `integrity`             | string \| absent     | SRI integrity hash (`sha512-…`). Populated from registry metadata or computed for supported remote sources. Required and verified when replaying registry, remote tarball, and GitHub packages.                                                                                                                     |
| `registry-signatures`   | table\[]             | npm-compatible `dist.signatures` entries persisted as nested `[[packages.registry-signatures]]` tables. Skipped when absent.                                                                                                                                                                                        |
| `registry-published-at` | string \| absent     | Publish timestamp used to verify npm registry signing-key expiry without rehydrating package metadata.                                                                                                                                                                                                              |
| `os`                    | string\[]            | `package.json > os` restrictions for this version. Skipped when empty.                                                                                                                                                                                                                                              |
| `cpu`                   | string\[]            | `package.json > cpu` restrictions for this version. Skipped when empty.                                                                                                                                                                                                                                             |
| `libc`                  | string\[]            | `package.json > libc` restrictions for this version. Skipped when empty.                                                                                                                                                                                                                                            |
| `node-engine`           | string \| absent     | The package's `package.json > engines.node` constraint. Warm and frozen installs revalidate it against the effective Node.js version and `engine-strict` setting.                                                                                                                                                   |
| `optional`              | bool                 | Present as `true` when the package is reachable only through optional dependency edges. Used with platform filtering so warm installs can skip incompatible optional packages and fail incompatible required packages.                                                                                              |
| `dependencies`          | string\[]            | Direct deps as `<local_name>@<version>` entries. Skipped from output when empty.                                                                                                                                                                                                                                    |
| `dependency-targets`    | table                | Maps each dependency-local name to one exact `instance-id`. The keys must match `dependencies`.                                                                                                                                                                                                                     |
| `alias-dependencies`    | `[string, string][]` | npm-alias edges as `[local_name, target_canonical_name]` pairs. Only present when the package uses `npm:<target>@<range>` aliases.                                                                                                                                                                                  |
| `peer-edges`            | table\[]             | Structured peer edges with local name, target name, version, and optional source wrapper ID.                                                                                                                                                                                                                        |
| `peer-targets`          | table                | Maps each peer-local name to one exact `instance-id`. The keys must match `peer-edges`.                                                                                                                                                                                                                             |
| `tarball`               | string \| absent     | Tarball URL hint cached from resolve time. Speeds up the warm-install fast path by letting it skip the per-package metadata round-trip. Only valid for `Source::Registry` packages — pairing this hint with a non-Registry source is rejected at parse time. Absent on lockfiles produced before this hint shipped. |

### `dependencies` format [#dependencies-format]

```toml
dependencies = ["scheduler@0.25.0", "loose-envify@1.4.0"]
```

`<local_name>@<version>` — the local-name is what the depending package writes in its own `dependencies` map. For non-aliased deps the local name equals the canonical registry name. For npm-alias deps (`npm:react@^19.0.0` masquerading as `react-canary`), the local name diverges from the target — see `alias-dependencies` below.

For a local, tarball, or Git target, the value after `@` can be its canonical source ID. The matching `dependency-targets` entry remains authoritative.

### Exact targets [#exact-targets]

```toml
[packages.dependency-targets]
scheduler = "58b7a5e0c3f25598fdbd56da146a1841b4ce2b78f359414cd782817437512560"

[[packages.peer-edges]]
local-name = "react"
target-name = "react"
target-version = "19.0.0"

[packages.peer-targets]
react = "d7d8fbc52d7d998935c243be78e563594fcb08459d8b71dfdf96c60ed307a4e8"
```

Each exact target must reference a package row in the same standalone graph or importer projection. LPM CLI rejects missing, ambiguous, or inconsistent targets.

Root selections also contain an `instance-id`. This field prevents a root link from selecting another row with the same package coordinates.

Lockfile versions 1–12 do not contain these exact targets. Upgrade a standalone project with `lpm install` before offline or frozen use.

For a workspace, run `lpm install --recursive` from the workspace root. This command creates exact projections for every retained importer.

### `alias-dependencies` format [#alias-dependencies-format]

```toml
alias-dependencies = [["my-react", "react"]]
```

Each `[local_name, target_canonical_name]` records that the package depends on `my-react@<version>` (in `dependencies`) but the actual target is `react`. Used to compute the right `.lpm/<target>@<version>/` store path on link.

### `tarball` field [#tarball-field]

```toml
tarball = "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
```

A dist-URL hint cache for `Source::Registry` packages. Lets the warm-install fast path skip the per-package metadata round-trip. For non-Registry sources (`Source::Tarball`, `Source::Git`), the URL is part of source identity (lives inside the source variant) — pairing them with this field is rejected at parse time.

### Platform fields [#platform-fields]

```toml
os = ["darwin", "linux"]
cpu = ["arm64", "x64"]
libc = ["glibc"]
optional = true
```

LPM CLI records `os`, `cpu`, and `libc` restrictions from the package version's manifest. Empty arrays are omitted. Warm installs replay the same host filtering as fresh installs: incompatible optional packages are skipped; incompatible required packages fail loudly.

The `optional = true` bit means this package is only reachable through optional dependency edges. It is what lets the lockfile fast path distinguish "safe to skip on this host" from "required package cannot run here."

### Registry signatures [#registry-signatures]

```toml
registry-published-at = "2025-01-01T00:00:00.000Z"

[[packages.registry-signatures]]
keyid = "SHA256:..."
sig = "MEUCIQ..."
```

When npm metadata includes `dist.signatures`, LPM CLI persists the signature payloads and the version publish timestamp. Install-time signature verification can then run from the lockfile fast path without fetching the full package metadata again. The verifier still checks the package integrity hash and registry signing keys; the lockfile stores evidence, not a cached pass/fail verdict.

## `[provenance]` [#provenance]

```toml
[provenance."axios@1.14.0#npm-036b43b16792c643"]
subject-name = "pkg:npm/axios@1.14.0"
subject-sha512 = "dd8f32ae..."
integrated-time-secs = 1774638099
log-id = "wNI9atQG..."
log-index = 1189268120
bundle-sha256 = "sha256-75564031..."

[provenance."axios@1.14.0#npm-036b43b16792c643".snapshot]
present = true
publisher = "github:axios/axios"
workflowPath = ".github/workflows/publish.yml"
workflowRef = "refs/tags/v1.14.0"
attestation_cert_sha256 = "sha256-9a9a6a..."
```

The table key is `<name>@<version>#<source-id>`, so evidence is attached to one exact package version from one exact source. Only successfully verified evidence is written.

| Field                              | Type             | Notes                                                                                                                        |
| ---------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `subject-name`                     | string           | Exact npm package URL signed by the attestation, `pkg:npm/<name>@<version>`.                                                 |
| `subject-sha512`                   | string           | Lowercase hexadecimal SHA-512 digest of the signed tarball subject. It must match the package entry's SHA-512 SRI integrity. |
| `integrated-time-secs`             | u64              | Rekor integrated timestamp from the verified transparency-log entry.                                                         |
| `log-id`                           | string           | Identity of the transparency log that recorded the attestation.                                                              |
| `log-index`                        | i64              | Verified transparency-log index.                                                                                             |
| `bundle-sha256`                    | string           | SHA-256 digest of the original Sigstore bundle bytes.                                                                        |
| `snapshot.present`                 | bool             | Must be `true`; absent or unverified provenance is never locked as evidence.                                                 |
| `snapshot.publisher`               | string \| absent | Publisher identity extracted from the verified certificate, such as `github:axios/axios`.                                    |
| `snapshot.workflowPath`            | string \| absent | Trusted publishing workflow path.                                                                                            |
| `snapshot.workflowRef`             | string \| absent | Git ref used for this publication.                                                                                           |
| `snapshot.attestation_cert_sha256` | string \| absent | SHA-256 digest of the verified leaf certificate.                                                                             |

The reader rejects orphaned evidence, empty required fields, malformed digests, non-npm subjects, subject name/version mismatches, source mismatches, and subject digests that do not match the locked package integrity. A frozen install can therefore replay internally consistent evidence without fetching the attestation again.

`lpm.lock` is trusted project input. It stores derived evidence, not the original Sigstore bundle, so frozen replay does not rerun the certificate-chain, transparency-log, or signed-statement verification and cannot independently detect a sophisticated edit that changes all related derived fields consistently. Protect and review lockfile changes like source code. Online attestation-cache hits do rerun verification because that cache stores the original bundle bytes.

## `[root-aliases]` [#root-aliases]

```toml
[root-aliases]
my-alias = "actual-package"
```

Maps root-level npm-alias edges so warm installs reproduce the original `node_modules/<local>/` layout without re-resolving. Empty for projects without root-level aliases — and omitted entirely from output in that case (backwards-compatible with older lockfiles that pre-date alias support).

## `ambient-peer-installs` [#ambient-peer-installs]

```toml
ambient-peer-installs = ["loose-envify", "scheduler"]
```

Canonical names the resolver auto-installed at root scope to satisfy unmet `peerDependencies` (the eager peer-drain pass, on by default via `autoInstallPeers = true`). These packages are already in `[[packages]]&#x60; (they were resolved and extracted like any other dep) — this list carries the orthogonal signal &#x2A;"surface them at `node_modules/<peer>/` even though they aren't in `pkg.dependencies`."* Symmetric with `[root-aliases]` — both are project-side install-orchestration metadata, not per-package state.

Empty and omitted from output on the common no-auto-install path (project's `dependencies` block already covers every declared peer). See [Resolver](/docs/packages/resolver#peerdependencies-and-optionaldependencies) for the auto-install contract + toggle.

## Determinism [#determinism]

The on-disk file is deterministic by construction:

* `[[packages]]` entries are sorted by name
* `[workspace-packages]` rows are sorted by content address
* importer package IDs are sorted and deduplicated
* `dependencies` arrays inside each package are sorted
* `[provenance]` entries are sorted by their full package/source key
* Optional fields are omitted when empty / `None` rather than written as null

This makes `git diff lpm.lock` actionable: a new dep adds entries, a version bump rewrites a single package's fields, and you can spot supply-chain surprises without parsing.

## Schema versioning [#schema-versioning]

`lockfile-version` is schema-versioned, not tool-versioned. A version changes only when the file structure changes. Older clients reject a newer schema.

## See also [#see-also]

* [`lpm.lockb` format](/docs/reference/lockb-format) — companion binary lockfile
* [Lockfile concept](/docs/packages/lockfile) — design overview, when each file is read
* [`lpm install --offline`](/docs/packages/install) — installs entirely from the lockfile
* [`lpm install --strict-integrity`](/docs/packages/install) — disallow trust-on-first-use for tarball-URL deps


# lpm.config.json (/docs/reference/lpm-config-json)



`lpm.config.json` is the **package author's** declaration of what `lpm add` should do when delivering this package's source into a consumer project. It lives at the tarball root and turns `lpm add` from a plain copy into a configurable installer — interactive prompts, conditional file copying, conditional dependency injection, import-path rewriting.

The file is **optional**. Without it, `lpm add` is a plain source copy of every file in the tarball. With it, `lpm add` becomes the installer the package author designed.

LPM CLI reads `lpm.config.json` with a 16 MiB limit enforced before JSON parsing. A missing file retains the plain-copy path; an oversized file fails instead of being treated as absent, so it cannot silently change file selection or dependency injection. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

When publishing to the LPM.dev Registry, the submitted canonical JSON representation has a separate 128 KiB ceiling. The smaller Registry limit protects the hosted publish and downstream metadata surfaces; the 16 MiB local ceiling protects local file reads. Other registries remain server-authoritative and may apply different limits.

This page is the schema-by-field reference. For the `lpm add` consumer flow, see [`lpm add`](/docs/packages/add).

## Editor autocomplete [#editor-autocomplete]

LPM CLI publishes a JSON Schema for `lpm.config.json` at [`https://cli.lpm.dev/schemas/lpm.config.json`](https://cli.lpm.dev/schemas/lpm.config.json). Add a `$schema` line to get inline validation and field completion in any editor that supports JSON Schema:

```json title="lpm.config.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.config.json",
  "ecosystem": "js"
}
```

## `type` [#type]

`type` is optional and controls LPM.dev Registry classification. When omitted, the Registry infers the classification from the published contents. The only supported public values are:

| Value       | Meaning                                                          |
| ----------- | ---------------------------------------------------------------- |
| `"package"` | Classify the package as a dependency package in LPM.dev Registry |
| `"source"`  | Classify the package as a source package in LPM.dev Registry     |

The CLI and Registry reject undocumented values—including `mcp-server`, `vscode-extension`, `cursor-rules`, `github-action`, and `xcframework`—as well as unknown top-level fields. New types must first become an explicit public contract; they are not accepted speculatively for forward compatibility.

Classification does not switch off this file's install behavior. The presence of a valid `lpm.config.json` always activates LPM CLI's configured `lpm add` path, including when `type` is `"package"`.

Or emit the schema yourself:

```bash
lpm schema lpm.config.json                       # to stdout
lpm schema lpm.config.json -o config.schema.json # to a file
```

The schema is hand-authored against the consumer code in the CLI (since the file's surface is partly dynamic — packages declare their own configurable fields). A corpus of real-world fixtures is validated against it on every CLI build, so the schema can't silently drift from the runtime.

The older `https://lpm.dev/schemas/lpm.config.json` URL remains a compatibility alias for existing packages.

When stdout is an interactive terminal, schema output may use syntax color. Redirected or piped output remains plain pretty-printed JSON, including when color is forced globally.

## Generate a config with an agent [#generate-a-config-with-an-agent]

Install the [`lpm-guide`](https://github.com/lpm-dev/lpm-guide) skill:

```bash
lpm skills add lpm-dev/lpm-guide
```

Then ask your coding agent to inspect the package rather than inventing a generic config:

```text
Use the lpm-guide skill's Source Config workflow to generate lpm.config.json for this package.

Inspect the source tree, package metadata, internal imports, and consumer-facing features. Identify the choices a consumer should make, then create configSchema, defaultConfig, files, conditional dependencies, and importAlias only where the source actually needs them.

Use required fields when including every option would create conflicts. Make lpm add --yes deterministic, include example lpm add commands with query parameters, and validate the result against https://cli.lpm.dev/schemas/lpm.config.json. Do not add fields that are absent from the published schema.
```

## Top-level shape [#top-level-shape]

```json title="lpm.config.json"
{
  "$schema":      "https://cli.lpm.dev/schemas/lpm.config.json",
  "type":           "source",
  "ecosystem":    "js",
  "importAlias":  "@/components",
  "configSchema": { "<field>": { /* ConfigField */ } },
  "defaultConfig": { "<field>": "<value>" },
  "files": [ { "src": "...", "dest": "...", "include": "always" } ],
  "dependencies": { "<configKey>": { "<value>": ["dep1", "dep2"] } }
}
```

Every top-level field is optional. The minimal valid file is `{}`.

## `ecosystem` [#ecosystem]

```json
{ "ecosystem": "js" }
```

| Value            | Meaning                                                                                                                                                     |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"js"` (default) | JavaScript / TypeScript source. Default install dir resolves via framework detection (Next.js → `components/`, Vite → `src/components/`, etc.).             |
| `"swift"`        | Swift source. Default install dir is `Sources/<target>/` for SPM projects, or `Packages/LPMComponents/Sources/<target>/` when an Xcode project is detected. |

Drives the default install-dir resolution. Override via `lpm add --path <dir>`.

## `importAlias` [#importalias]

```json
{ "importAlias": "@/components" }
```

The author's import alias prefix used in the source files. `lpm add` needs this field to recognize alias-qualified internal imports and rewrite them to the consumer project's detected `tsconfig.json > paths` alias or explicit `--alias` value.

Relative internal imports do not require `importAlias`. LPM CLI resolves them through the `files` source-to-destination map and can rewrite them when files move or the consumer uses an alias. Bare package imports remain unchanged.

## `configSchema` [#configschema]

```json
{
  "configSchema": {
    "component": {
      "type": "select",
      "label": "Which component?",
      "options": ["dialog", "popover", "menu"],
      "default": "dialog",
      "required": true
    },
    "withTests": {
      "type": "boolean",
      "label": "Include tests?",
      "default": true
    }
  }
}
```

Map of configurable field name → field specification. Each entry drives an interactive prompt during `lpm add` and feeds the inline-config map used by `files[].condition` and `dependencies` lookups.

| Field         | Type                                | Notes                                                                                                                                                                                             |
| ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`        | `"string" \| "boolean" \| "select"` | Default `"string"`. `boolean` produces a confirm prompt; `select` produces a single- or multi-choice list.                                                                                        |
| `label`       | string                              | Prompt label. Defaults to the field name.                                                                                                                                                         |
| `default`     | string \| boolean \| number         | Initial value. Native JSON types are accepted; legacy stringified values (`"true"`, `"false"`) are still honored for back-compat.                                                                 |
| `required`    | boolean                             | When `true`, `lpm add --yes` fills the field with its `default` instead of skipping it.                                                                                                           |
| `multiSelect` | boolean                             | Only meaningful when `type` is `select`. When `true`, the prompt accepts multiple values; the resolved inline-config value is comma-joined.                                                       |
| `options`     | array                               | Selectable options. Plain strings (`["dialog", "menu"]`) are shorthand for `{value, label}` pairs where both equal the string. Object form: `[{"value": "dialog", "label": "Dialog component"}]`. |

## `defaultConfig` [#defaultconfig]

```json
{ "defaultConfig": { "withTests": true, "component": "dialog" } }
```

Map of configurable field name → default value, overriding any `default` declared in the matching `configSchema` entry. Useful for sharing common defaults across packages without duplicating the field spec. Same value-type rules as `configSchema.<field>.default` (native booleans, numbers, and strings; legacy stringified values still accepted).

## `files` [#files]

```json
{
  "files": [
    { "src": "src/index.ts" },
    { "src": "src/styles.css", "dest": "styles/" },
    {
      "src": "src/test.ts",
      "include": "when",
      "condition": { "withTests": true }
    },
    { "src": "src/private/internal.ts", "include": "never" }
  ]
}
```

Ordered list of file copy rules. When `files` is omitted, `lpm add` copies every file in the tarball.

| Field       | Type                            | Notes                                                                                                                                                                                                                                                                               |
| ----------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src`       | string (required)               | Source path or glob (relative to the tarball root). Trailing `/**` and `/*` are supported.                                                                                                                                                                                          |
| `dest`      | string                          | Destination path (relative to the consumer project's resolved install dir). Trailing `/` treats the value as a directory; the source filename is preserved. Omit to keep the source-relative path.                                                                                  |
| `include`   | `"always" \| "when" \| "never"` | Default `"always"`. `"when"` requires every entry in `condition` to match. `"never"` skips.                                                                                                                                                                                         |
| `condition` | object                          | Required when `include` is `"when"`. Map of config field name → expected value. Native booleans and numbers are accepted alongside strings. Comma-separated multi-select values match if any element equals the expected value. Missing keys default to "include" (all-by-default). |

## `dependencies` [#dependencies]

```json
{
  "dependencies": {
    "icons": {
      "lucide": ["lucide-react"],
      "heroicons": ["@heroicons/react"]
    },
    "withMotion": {
      "true": ["framer-motion"]
    }
  }
}
```

Conditional dependency injection map. Outer keys match `configSchema` field names; inner keys match the user's selected value(s) for that field; values are arrays of **dependency entries** added to the consumer's `package.json`. Each entry is either a bare name or a `name@range` spec:

```json
{
  "dependencies": {
    "icons": {
      "lucide": [
        "lucide-react",                  // bare → ^resolvedLatest
        "lucide-react@^0.400.0",         // explicit range, preserved verbatim
        "lodash@4.17.21",                // explicit exact, preserved verbatim
        "react@latest",                  // dist-tag → resolved + saved per policy
        "@lpm.dev/owner.helpers@^1"      // any registry, range preserved
      ]
    }
  }
}
```

The save-spec policy mirrors `lpm install <pkg>`:

| Entry shape                            | What lands in consumer's `package.json`                                                              |              |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------ |
| Bare name (e.g., `"react"`)            | `^resolvedLatest` (e.g., `"^18.3.1"`), respecting the consumer's \`\~/.lpm/config.toml > save-prefix | save-exact\` |
| `name@^range` / `name@~range` etc.     | Preserved verbatim                                                                                   |              |
| `name@1.2.3` (exact)                   | Preserved verbatim                                                                                   |              |
| `name@latest` / `name@beta` (dist-tag) | Stable resolved → caret default; prerelease resolved → exact (no caret widening)                     |              |
| `name@*` (explicit wildcard)           | Preserved as `"*"` (the only path that lands `*` — never the default)                                |              |

### Registry agnostic [#registry-agnostic]

Names from any registry are accepted — npm-published, private-registry (declared in `.npmrc`), and `@lpm.dev/*` packages all flow through the same path. The package manager `lpm add` was invoked with (`--pm <lpm|npm|pnpm|yarn|bun|auto>`, where `auto` detects the project's existing package manager; default `lpm`) runs the install.

### Resolve-then-write, with rollback [#resolve-then-write-with-rollback]

`lpm add` resolves every bare/dist-tag entry against the registry **before** mutating `package.json`. Auth or access errors (a private package the user lacks a token for, an `@lpm.dev` marketplace package without a seat, a misspelled name) abort the whole `lpm add` cleanly — the manifest stays untouched. Without this fail-fast posture, an unresolvable entry would leave a stranded `*` range that the trailing install can't recover.

If resolution succeeds but a later step fails — file copy, manifest mutation, or trailing install (any `--pm`) — `lpm add` rolls back atomically and exits with the underlying error. The transaction snapshots:

| Path                                                                                                     | Rollback behavior                                                          |
| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Project `package.json`                                                                                   | Bytes restored to pre-`lpm add` state                                      |
| `lpm.lock`, `lpm.lockb`                                                                                  | Bytes restored, or removed if absent before                                |
| Selected PM's lockfile (`package-lock.json` / `pnpm-lock.yaml` / `yarn.lock` / `bun.lock` + `bun.lockb`) | Bytes restored, or removed if absent before                                |
| Every source file `lpm add` copied into the project                                                      | Pre-existing files restored to original bytes; newly-created files deleted |
| `.lpm/install-hash`                                                                                      | Always invalidated so the next `lpm install` re-derives clean state        |

What's outside the rollback surface, by design:

* **The install directory itself**, plus any parent directories materialized to host the copied source files. `lpm add` resolves `target_dir` and creates it (and any canonical parents on the resolution chain) before the transaction opens — that mkdir is what lets the transaction snapshot canonical-pinned dest paths in the first place. A rolled-back failure leaves these directories on disk; the files inside them were rolled back, so the directories may end up empty.
* **Recursive Swift `lpm add`** for SE-0292 source-package dependencies. Each recursive call owns its own transaction; the outer `lpm add` commits before triggering recursion so a Swift-dep failure doesn't roll back the root package's already-applied mutations.
* **Package-published agent skills** reconciled under `.lpm/skills/<package>/`. Fetch, validation, and write errors fail the install instead of leaving a partial or stale package-owned skill set.

If `lpm add` fails inside the rollback boundary, every file and lockfile under the transaction snaps back to its pre-`lpm add` state. The remaining residue is empty directories under the install target (cheap to clean up; `rm -rf` the directory if you don't want it). Re-run `lpm add` after fixing the underlying error to converge.

### Author opt-out of legacy fallback [#author-opt-out-of-legacy-fallback]

Declaring `dependencies` — even with conditional branches that don't match the consumer's config — opts out of the legacy fallback below. An empty match is the author's deliberate signal "no deps for this configuration," not a request to fall back.

### Legacy fallback [#legacy-fallback]

When `dependencies` is omitted entirely, `lpm add` falls back to reading `dependencies` + `peerDependencies` from the package's own `package.json` (legacy path). Each entry's declared range carries through and is preserved verbatim. The same registry-agnostic policy applies — every entry, including `@lpm.dev/*`, is installed.

## See also [#see-also]

* [`lpm add`](/docs/packages/add) — the consumer-side flow
* [`lpm.json`](./lpm-json) — runtime / task / publish config
* [JSON Schemas](/docs/reference/schemas) — published schema URLs and `lpm schema`


# lpm.json (/docs/reference/lpm-json)



`lpm.json` sits next to `package.json` and provides LPM CLI-specific configuration that doesn't fit npm's manifest shape — runtime pinning, task graph, dev services, local-domain proxy config, tunnel domain, env-file mapping, env-var schema, and multi-registry publish settings.

The file is **optional**. Every field has a default. When `lpm.json` is missing, LPM CLI falls back to `package.json` semantics (scripts, engines, etc.).

LPM CLI reads `lpm.json` with a 16 MiB limit enforced before JSON parsing. Missing retains the optional fallback above; an oversized file is a path-specific error and aborts before a configured script, service, or child process is spawned. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

This page is the schema-by-field reference. For a usage-oriented overview of what `lpm dev` reads from `lpm.json`, see [`lpm dev`](/docs/dev/dev#configuration-lpmjson).

## Editor autocomplete [#editor-autocomplete]

LPM CLI publishes a JSON Schema for `lpm.json` at [`https://cli.lpm.dev/schemas/lpm.json`](https://cli.lpm.dev/schemas/lpm.json) — auto-derived from the typed Rust struct that the CLI consumes. Add a `$schema` line to get inline validation and field completion in any editor that supports JSON Schema (VS Code, Cursor, JetBrains):

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json",
  "runtime": { "node": ">=22.0.0", "bun": "1.3.14" }
}
```

Or emit the schema yourself from the locally-installed CLI:

```bash
lpm schema lpm.json                       # to stdout
lpm schema lpm.json -o lpm.schema.json    # to a file
```

Both `lpm schema lpm.json` and the published copy are regenerated from the same Rust struct that parses your manifest at install time, so they describe the surface the CLI actually accepts. The published copy is updated alongside CLI releases — between releases, `lpm schema lpm.json` is the source of truth for whichever version is on your machine.

The older `https://lpm.dev/schemas/lpm.json` URL remains a compatibility alias for existing repositories.

When stdout is an interactive terminal, schema output may use syntax color. Redirected or piped output remains plain pretty-printed JSON, including when color is forced globally.

## Top-level shape [#top-level-shape]

```json title="lpm.json"
{
  "$schema":      "https://cli.lpm.dev/schemas/lpm.json",
  "vault":        "<uuid-v4>",
  "vaultSync":    { /* LPM-managed sync metadata */ },
  "runtime":      { "<runtime>": "<semver>" },
  "tools":        { "<tool>": "<version>" },
  "https":        true,
  "cert":         { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false },
  "tunnel":       { "domain": "<full-domain>" },
  "proxy":        { "host": "<local-hostname>", "port": 443, "httpRedirect": true },
  "env":          { "<script>": "<.env-file>" },
  "envSchema":    { "vars": { "<NAME>": { "required": true, "format": "url" } } },
  "environments": { "<name>": { "extends": "<base>", "file": "<.env-file>" } },
  "tasks":        { "<name>": { /* TaskConfig */ } },
  "services":     { "<name>": { /* ServiceConfig */ } },
  "publish":      { /* PublishConfig */ }
}
```

Every top-level field is optional and defaults to empty / `null`.

## `runtime` [#runtime]

```json
{ "runtime": { "node": ">=22.0.0", "bun": "1.3.14" } }
```

| Field  | Type   | Default | Notes                                                                                                                                                                   |
| ------ | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node` | string | —       | Managed Node version spec. Exact versions, prefixes, semver ranges, `latest`, and `lts` are accepted.                                                                   |
| `bun`  | string | —       | Managed Bun version spec. Exact versions, `vX.Y.Z`, `bun-vX.Y.Z`, `latest`, prefixes, and semver ranges are accepted. `lts` is rejected because Bun has no LTS channel. |

`runtime.node` takes precedence over `.nvmrc` and `.node-version`. `package.json > engines.node` validates the selected or `PATH` Node but never selects or installs one. `runtime.bun` is the only Bun runtime detection source; `package.json > engines.bun` is not enforced. Both runtime fields are auto-installed via [`lpm use`](/docs/dev/use).

When both runtimes are selected, script `PATH` is prepended as `node_modules/.bin`, managed Node `bin`, managed Bun `bin`, inherited `PATH`. `runtime.bun` exposes `bun` to scripts; it does not make `lpm run` invoke `bun run`.

Workspace members and dev services resolve selectors from their directories. A local selector replaces the same root runtime.

Each unselected runtime inherits from the root.

## `tools` [#tools]

```json
{ "tools": { "oxlint": "1.57.0", "biome": "2.4.8", "rolldown": "1.1.3" } }
```

| Field    | Type   | Notes                                                                                                                                                           |
| -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<tool>` | string | Pin a built-in managed plugin to a specific version. Honored by [`lpm lint`](/docs/dev/lint), [`lpm fmt`](/docs/dev/fmt), and [`lpm bundle`](/docs/dev/bundle). |

Only `oxlint`, `biome`, and `rolldown` are managed plugin keys today. Any other key (`tools.typescript`, `tools.tsgo`, `tools.eslint`, …) is ignored by the resolver — `lpm` warns once per command invocation that the pin is ignored. To pin the `lpm check` engines, declare `typescript` or `@typescript/native-preview` in `package.json > devDependencies` like any other project dep.

When an Oxlint or Biome pin isn't already in `~/.lpm/plugins/`, the next invocation downloads and verifies it against the upstream `<asset_url>.sha256` sidecar. A Rolldown pin must already be the bundled floor or an approved version from `lpm plugin update rolldown`; `lpm bundle` does not check upstream during normal execution. See [Built-in tools](/docs/dev/builtin-tools#checksum-verification) for the trust model.

## `https` [#https]

```json
{ "https": true }
```

| Type   | Default        | Notes                                                                                             |
| ------ | -------------- | ------------------------------------------------------------------------------------------------- |
| `bool` | `null` (unset) | When `true`, [`lpm dev`](/docs/dev/dev) serves over HTTPS by default. CLI `--no-https` overrides. |

## `cert` [#cert]

```json
{ "cert": { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false } }
```

| Field               | Type      | Default | Notes                                                                                                                                                      |
| ------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `extraPermittedDns` | string\[] | `[]`    | Extra validated DNS subtrees to permit on the constrained project intermediate. These are policy constraints, not extra browser SANs and not proxy routes. |
| `allowPublicDns`    | bool      | `false` | Allows public DNS names in `extraPermittedDns` and local-domain `host` fields. Leave off for purely local names.                                           |

`extraPermittedDns` entries must be bare multi-label names such as `myapp.local`; LPM CLI derives both the exact subtree and subdomain subtree for the project intermediate. Use `--host` or `proxy.host` / `services.<name>.host` for actual browser hostnames.

## `tunnel` [#tunnel]

```json
{ "tunnel": { "domain": "acme-api.lpm.llc" } }
```

| Field    | Type   | Notes                                                                                                                                                  |
| -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `domain` | string | Stable tunnel domain (must already be claimed via [`lpm tunnel claim`](/docs/infra/tunnel)). Pro/Org only — free users get an ephemeral random domain. |

CLI flags: `lpm dev --tunnel`, `lpm dev --domain <NAME>`, `lpm tunnel <port> <domain>`.

## `proxy` [#proxy]

```json
{ "proxy": { "host": "app.localhost", "port": 443, "httpRedirect": true } }
```

| Field          | Type   | Default | Notes                                                                                                                                                                                                                                       |
| -------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`         | string | —       | Friendly top-level local hostname registered by `lpm dev` when the proxy daemon has an HTTPS listener. `lpm dev` also prepares constrained project cert-chain coverage and updates the hosts file when this host is not under `.localhost`. |
| `port`         | number | `443`   | HTTPS proxy listen port used by `lpm proxy start` when no explicit listener flags are passed. `0` lets the OS pick a free port.                                                                                                             |
| `httpRedirect` | bool   | `true`  | Whether `lpm proxy start` also binds an HTTP-to-HTTPS redirect listener on port 80 when no explicit listener flags are passed.                                                                                                              |

`proxy.host` and `services.<name>.host` are normalized to lowercase, must use a local TLD (`.localhost`, `.test`, `.local`, `.internal`, `.home.arpa`) unless `cert.allowPublicDns` is true, and cannot duplicate another local-domain host in the same file.

When local-domain config is present and no daemon is running, `lpm dev` starts `lpm proxy start --detach` automatically. With no explicit listener flags, that start path uses `proxy.port` and `proxy.httpRedirect`. With a running HTTPS listener, `lpm dev` prepares the project certificate chain for configured hosts through the normal trust consent flow, writes managed hosts-file entries for hosts that are not `localhost` / `*.localhost`, registers routes against final assigned service ports, and releases both route and hosts-file entries when the dev session exits. On Unix, LPM CLI uses `sudo` for the system hosts file when the current process lacks permission; on Windows, it asks for Administrator elevation through UAC. Use [`lpm hosts clean`](/docs/infra/hosts) to remove orphaned managed blocks after interrupted sessions. The TLS daemon also prepares or refreshes project certificate chains during route registration, but it does not install the root CA into the trust store. The startup banner marks proxy endpoints as resolving until assignment; exact route lines print after registration, and dashboard service rows update to the final ports. LPM CLI does not inject certificate paths or framework HTTPS variables into the app; `https` / `--https` enables a separate LPM CLI-owned TLS frontend over the verified HTTP child endpoint.

## `vault` [#vault]

```json
{ "vault": "7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b" }
```

| Type   | Notes                                                                                                                                                                                                                                                                                             |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| string | This stable identifier links the project to a vault row on LPM.dev Registry. New values use UUID v4. Safe legacy slug IDs remain supported. The CLI creates the value during the first `lpm env` command if the field is missing. Commit this value to git. The value is a pointer, not a secret. |

The CLI rejects empty values, path separators, `..`, leading `~`, control characters, colons, and absolute drive prefixes. This validation prevents path traversal in the file fallback at `~/.lpm/vaults/<id>.enc`.

If you remove the field, the next `lpm env` command creates a new UUID. The previous server data remains available under the old identifier.

[`LPM_VAULT_ID`](/docs/reference/env-vars#auth-and-routing) is read only by `lpm env pull --oidc`. A non-empty, trimmed value takes precedence over this local `vault` field and can bootstrap a checkout without one. If the variable is unset, empty, or whitespace-only, LPM CLI falls back to this field; the pull fails if neither source exists. Other `lpm env` operations ignore it. See [Secrets vault — Per-project identity](/docs/infra/secrets-vault#per-project-identity).

## `vaultSync` [#vaultsync]

```json
{
  "vaultSync": {
    "personalVersion": 7,
    "personalSyncedAt": "2026-08-13T09:00:00Z",
    "orgVersions": { "acme": 12 },
    "orgSyncedAt": { "acme": "2026-08-13T09:01:00Z" },
    "personalPlatformBindings": {
      "https://registry.lpm.dev": {
        "registryUrl": "https://registry.lpm.dev",
        "principalId": "user_123"
      }
    },
    "authorityCheckpoints": {
      "personal": {
        "https://registry.lpm.dev": {
          "user_123": { "version": 7, "syncedAt": "2026-08-13T09:00:00Z" }
        }
      },
      "organizations": {
        "acme": {
          "https://registry.lpm.dev": {
            "org_123": { "version": 12, "syncedAt": "2026-08-13T09:01:00Z" }
          }
        }
      }
    }
  }
}
```

| Field                                                              | Type   | Notes                                                                                                                                           |
| ------------------------------------------------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `personalVersion`                                                  | int    | Non-authoritative summary of the last known personal cloud-vault version.                                                                       |
| `personalSyncedAt`                                                 | string | Non-authoritative summary of the last successful personal sync time in RFC 3339 format.                                                         |
| `orgVersions.<slug>`                                               | int    | Non-authoritative summary of the last known org-shared cloud-vault version per organization slug.                                               |
| `orgSyncedAt.<slug>`                                               | string | Non-authoritative summary of the last successful sync time per organization in RFC 3339 format.                                                 |
| `personalPlatformBindings.<registry>`                              | object | Pins platform credentials to the canonical Registry URL and authenticated user ID that created them.                                            |
| `authorityCheckpoints.personal.<registry>.<principal>`             | object | Authoritative personal-vault revision floor and optional last-sync time for one Registry and immutable user ID.                                 |
| `authorityCheckpoints.organizations.<slug>.<registry>.<principal>` | object | Authoritative organization-vault revision floor and optional last-sync time for one Registry, organization slug, and immutable organization ID. |

The CLI owns every `vaultSync` field. `authorityCheckpoints` provides the durable account binding and rollback floor used by personal and organization cloud operations. `personalPlatformBindings` prevents platform credentials created by one account from being reused by another account at the same Registry. The version and timestamp fields are non-authoritative summaries for status display and compatibility.

Do not edit or remove `vaultSync`. Removing its authority checkpoints resets local rollback and account-binding protection. A later operation can then establish a new principal and revision checkpoint for that checkout.

If a version does not match, the server returns `409 VaultConflictError`. Then pull the remote values, resolve the conflict, and push again.

Commands use a project-local lock when they update `lpm.json`. Each command preserves unrelated fields and atomically replaces the file.

The CLI rejects a symbolic-link or junction `lpm.json` during an update. This rule prevents a cloned project from redirecting the write.

The parser accepts unknown fields for forward compatibility. The `lpm doctor` command reports unknown nested fields with their complete paths.

For example, `lpm doctor` reports the ignored typo `proxy.httpRediect`. An older CLI can still open configuration from a newer project.

## `env` [#env]

```json
{
  "env": {
    "dev":     ".env.development",
    "staging": ".env.staging",
    "prod":    ".env.production"
  }
}
```

Map of script name to environment file. `lpm run dev` loads `.env.development`. `lpm run staging` loads `.env.staging`.

CLI `--env=<MODE>` overrides `tasks.<name>.env` and this mapping. The flag also applies to [`lpm dev`](/docs/dev/dev).

Existing environment files must resolve inside the project directory. LPM CLI rejects configured paths and symlinks that resolve outside the project.

## `envSchema` [#envschema]

```json
{
  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url" },
      "API_KEY":      { "required": true, "secret": true },
      "LOG_LEVEL":    { "default": "info", "pattern": "^(trace|debug|info|warn|error)$" }
    }
  }
}
```

Schema for env-var validation. Drives the env check that runs before `lpm run`, `lpm dev`, `lpm <file>`, and `lpm exec`. Per-var fields:

| Field         | Type      | Notes                                                                                       |
| ------------- | --------- | ------------------------------------------------------------------------------------------- |
| `required`    | bool      | Fail if not set                                                                             |
| `format`      | string    | Built-in validator. One of: `url`, `email`, `port`, `boolean`, `integer`, `hostname`, `ip`  |
| `pattern`     | string    | A Rust regex that the value must match. `^` and `$` require a complete-value match          |
| `enum`        | string\[] | The allowed values                                                                          |
| `default`     | string    | The default value for an unset variable. It must satisfy `format`, `pattern`, and `enum`    |
| `secret`      | bool      | Marks the value as sensitive. LPM CLI fully redacts the value in validation errors and logs |
| `client`      | bool      | Allows client-side exposure for integrations that support public variables                  |
| `description` | string    | Adds variable details to errors and generated `.env.example` files                          |

Variable names must contain 1 to 256 ASCII letters, numbers, or underscores. The first character must be a letter or underscore.

An invalid name, regex, or default stops the command before it starts a child process. An empty default cannot satisfy a required variable.
Validation errors escape terminal control characters from schema fields and non-secret values.

Skip the check per-invocation with `--no-env-check`.

## `environments` [#environments]

```json
{
  "environments": {
    "base":    { "file": ".env" },
    "staging": { "extends": "base", "file": ".env.staging" },
    "preview": { "extends": "staging", "file": ".env.preview" }
  }
}
```

Named environment definitions with inheritance. Useful when you have many `.env.*` variants and want to avoid duplicating shared keys. `extends` chains resolve transitively.

## `tasks.<name>` [#tasksname]

```json
{
  "tasks": {
    "build": {
      "command": "tsup",
      "dependsOn": ["^build"],
      "cache": true,
      "outputs": ["dist/**"],
      "inputs": ["src/**", "package.json"],
      "env": "production"
    }
  }
}
```

| Field       | Type      | Default                                       | Notes                                                                                                                             |
| ----------- | --------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `command`   | string    | falls back to `package.json > scripts.<name>` | Override the script body                                                                                                          |
| `dependsOn` | string\[] | `[]`                                          | Other tasks that must finish first. `"build"` = same package; `"^build"` = the same task in upstream workspace deps.              |
| `cache`     | bool      | `false`                                       | Enable task caching. Requires `outputs`.                                                                                          |
| `outputs`   | string\[] | `[]`                                          | Globs of files this task produces (e.g. `["dist/**"]`). Required when `cache: true`.                                              |
| `inputs`    | string\[] | sensible defaults\*                           | Globs that invalidate the cache.                                                                                                  |
| `env`       | string    | —                                             | Env mode for this task (`"production"` loads `.env.production`).                                                                  |
| `cacheEnv`  | string\[] | all inherited variables                       | Exact inherited variable names that affect cached output. An empty list selects none. Project-loaded values always remain inputs. |

Use [`cacheEnv`](/docs/dev/task-runner#cache-key) to share cache entries across machines or CI jobs with different incidental environment values.
Include every inherited variable that affects the task. This field does not change the child environment or permit secret-bearing remote uploads.

\*Default `inputs`: `src/**`, `lib/**`, `app/**`, `pages/**`, `components/**`,
`scripts/**`, `bin/**`, root `*.{js,cjs,mjs,ts,tsx,jsx}` files, `package.json`,
`lpm.json`, `tsconfig.json`, `tsconfig.*.json`, and `*.config.{js,ts,mjs}`.

A cached task also includes the cache identities of its transitive
same-project dependencies. LPM hashes direct dependency outputs as task
inputs. It validates each declared dependency output before a cache hit or
cache publication.

For workspace task runs, each task includes the identity of every reachable
`^task` dependency. An upstream input or execution-contract change invalidates
the cache of each reachable downstream task.

Each cache key for a workspace member also includes the root `package.json`,
`lpm.json`, and `pnpm-workspace.yaml` files. Root lockfiles use the same rules
as member lockfiles.

The cache key includes the complete canonical `package.json`, including helper
scripts and module settings. It also includes `lpm.lock` and supported npm,
Yarn, pnpm, Bun, and Deno lockfiles.

Each `inputs` and `outputs` item must be a valid, project-relative glob. LPM
rejects absolute paths, parent traversal, and invalid glob syntax before it
starts a task.

On a cache hit, LPM restores the exact set of declared output files. It removes
declared files that are absent from the cached result.

## `remoteCache` [#remotecache]

```json
{
  "remoteCache": {
    "enabled": true,
    "team": "acme",
    "url": "https://lpm.dev/v8",
    "signature": true,
    "readOnly": false,
    "env": {
      "include": ["CI", "NODE_ENV"],
      "exclude": ["*_TOKEN", "*_SECRET", "DATABASE_URL"]
    }
  }
}
```

Hosted cache settings for tasks that already opt in with `tasks.<name>.cache: true` and `outputs`. LPM CLI checks the local task cache first, then the hosted cache, then runs the task and uploads the result when the run succeeds.

| Field              | Type      | Default                     | Notes                                                                                                                                                     |
| ------------------ | --------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`          | bool      | `false`                     | Enable hosted cache reads and writes                                                                                                                      |
| `team`             | string    | personal namespace          | Organization slug/team namespace                                                                                                                          |
| `url`              | string    | configured registry + `/v8` | Remote cache endpoint. Hosts outside the configured registry origin require `LPM_REMOTE_CACHE_TOKEN` and `LPM_REMOTE_CACHE_SIGNATURE_KEY`.                |
| `signature`        | bool      | `false`                     | Require `LPM_REMOTE_CACHE_SIGNATURE_KEY`; downloads without a valid HMAC tag are treated as misses. Third-party cache hosts always require a signing key. |
| `readOnly`         | bool      | `false`                     | Read hosted artifacts but skip uploads                                                                                                                    |
| `env.include`      | string\[] | `[]`                        | Variable-name patterns explicitly allowed for remote uploads                                                                                              |
| `env.exclude`      | string\[] | `[]`                        | Variable-name patterns that block remote uploads                                                                                                          |
| `env.allowSecrets` | bool      | `false`                     | Permit secret-looking env var names after project opt-in                                                                                                  |

Secret-looking loaded or inherited env vars (`TOKEN`, `SECRET`, `PASSWORD`, `DATABASE_URL`, `*_KEY`, and similar names) block remote uploads by default. Local task caching still works.

## `services.<name>` [#servicesname]

```json
{
  "services": {
    "db":  { "command": "docker compose up postgres", "readyPort": 5432, "readyTimeout": 60 },
    "api": {
      "command": "node server.js",
      "port": 4000,
      "dependsOn": ["db"],
      "env": { "DATABASE_URL": "postgres://localhost:5432/myapp" }
    },
    "web": { "command": "next dev", "port": 3000, "primary": true, "host": "web.app.localhost" }
  }
}
```

| Field          | Type      | Default              | Notes                                                                                                                                                                                                                                                                                                                                                                                  |
| -------------- | --------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command`      | string    | **required**         | Shell command to run                                                                                                                                                                                                                                                                                                                                                                   |
| `port`         | number    | —                    | Port the service listens on. If omitted for a host-only service or the implicit or explicit primary service, `lpm dev` auto-assigns and persists one                                                                                                                                                                                                                                   |
| `dependsOn`    | string\[] | `[]`                 | Services that must be ready before this one starts                                                                                                                                                                                                                                                                                                                                     |
| `readyPort`    | number    | falls back to `port` | TCP port to poll for readiness                                                                                                                                                                                                                                                                                                                                                         |
| `readyUrl`     | string    | —                    | HTTP URL to poll for readiness — first 2xx wins                                                                                                                                                                                                                                                                                                                                        |
| `readyTimeout` | number    | `30`                 | Seconds before a readiness failure stops the initial startup                                                                                                                                                                                                                                                                                                                           |
| `env`          | object    | `{}`                 | Extra env vars injected into this service only                                                                                                                                                                                                                                                                                                                                         |
| `restart`      | bool      | `false`              | Auto-restart on crash with exponential backoff                                                                                                                                                                                                                                                                                                                                         |
| `primary`      | bool      | `false`              | Its verified endpoint receives HTTPS, tunnel, network, and browser-open handling                                                                                                                                                                                                                                                                                                       |
| `host`         | string    | —                    | Friendly local hostname registered by `lpm dev` against the final assigned service port when the proxy daemon has an HTTPS listener. `lpm dev` also prepares constrained project cert-chain coverage and updates the hosts file when the host is not under `.localhost`. With `port`, `localhost:<port>` keeps working; without `port`, `lpm dev` injects the assigned port as `PORT`. |
| `cwd`          | string    | project root         | Working directory, relative to project root                                                                                                                                                                                                                                                                                                                                            |

## `publish` [#publish]

```json
{
  "publish": {
    "registries": ["lpm", "npm"],
    "lpm":    { "name": "@lpm.dev/owner.pkg" },
    "npm":    { "name": "@scope/pkg", "access": "public", "tag": "latest", "otpRequired": true },
    "github": { "name": "@owner/pkg", "access": "public" },
    "gitlab": { "name": "pkg", "access": "public", "projectId": "12345", "registry": "https://gitlab.com" }
  }
}
```

### Top-level [#top-level]

| Field        | Type      | Default   | Notes                                                                                                                                                                                                                                                                                                                  |
| ------------ | --------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registries` | string\[] | `["lpm"]` | Targets a single `lpm publish` invocation hits. Values are `lpm`, `npm`, `github`, `gitlab`, or an `https://` custom registry URL. Custom URL entries require an exact token from `lpm login --login-registry <URL> --token <T>`. CLI flags (`--lpm`, `--npm`, `--github`, `--gitlab`, `--publish-registry`) override. |

### `publish.lpm` [#publishlpm]

| Field  | Type   | Notes                                                               |
| ------ | ------ | ------------------------------------------------------------------- |
| `name` | string | Override the lpm.dev package name. Must match `@lpm.dev/owner.pkg`. |

### `publish.npm` [#publishnpm]

| Field         | Type                         | Default                      | Notes                                                                                                                                                                                                                     |
| ------------- | ---------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | string                       | `package.json > name`        | Override the npm package name (e.g., `"@scope/pkg"`). Required if `package.json > name` starts with `@lpm.dev/`.                                                                                                          |
| `access`      | `"public"` \| `"restricted"` | `"public"`                   | Visibility. npm provenance requires effective access to be `"public"`.                                                                                                                                                    |
| `tag`         | string                       | `"latest"`                   | dist-tag for the published version                                                                                                                                                                                        |
| `registry`    | string                       | `https://registry.npmjs.org` | Custom npm-compatible registry URL. Non-default registries require an exact token from `lpm login --login-registry <URL> --token <T>`; `NPM_TOKEN` and `lpm login --npm` auth are only used for the default npm registry. |
| `otpRequired` | bool                         | `false`                      | Prompt for OTP before the first publish attempt (saves a round-trip)                                                                                                                                                      |

### `publish.github` [#publishgithub]

| Field    | Type                         | Notes                                                             |
| -------- | ---------------------------- | ----------------------------------------------------------------- |
| `name`   | string                       | Override the GitHub Packages name. Must be scoped (`@owner/pkg`). |
| `access` | `"public"` \| `"restricted"` | Visibility                                                        |

### `publish.gitlab` [#publishgitlab]

| Field       | Type                         | Default              | Notes                                                    |
| ----------- | ---------------------------- | -------------------- | -------------------------------------------------------- |
| `name`      | string                       | —                    | Override the GitLab Packages name                        |
| `access`    | `"public"` \| `"restricted"` | —                    | Visibility                                               |
| `projectId` | string                       | **required**         | GitLab project ID — needed to construct the registry URL |
| `registry`  | string                       | `https://gitlab.com` | GitLab instance URL                                      |

## Precedence with other config [#precedence-with-other-config]

For fields with multiple sources, the precedence chain is:

1. **CLI flag** (highest)
2. **`lpm.json`**
3. **`package.json`** (where applicable — `engines`, `scripts`)
4. **`~/.lpm/config.toml`** (user-level, where applicable)
5. **Built-in default** (lowest)

## See also [#see-also]

* [`lpm dev`](/docs/dev/dev) — the most common consumer, with usage-oriented examples
* [`lpm run`](/docs/dev/run) — reads `tasks` and `env`
* [`lpm publish`](/docs/packages/publish) — reads `publish`
* [`package.json` "lpm" key](/docs/reference/package-json-lpm) — sibling config block
* [JSON Schemas](/docs/reference/schemas) — published schema URLs and `lpm schema`
* [`lpm.toml`](/docs/reference/lpm-toml) — project-level CLI defaults
* [`~/.lpm/config.toml`](/docs/reference/config-toml) — user-level CLI defaults


# lpm.toml (/docs/reference/lpm-toml)



`lpm.toml` is an **optional** TOML file that sits next to `package.json`. It pins per-project defaults for behaviors that aren't part of publishable package metadata — most notably the [save policy](/docs/packages/save-policy) and the project sandbox posture.

LPM CLI reads this file with a 16 MiB limit enforced before TOML parsing. Missing remains equivalent to no project-level config; an oversized file fails with its path and byte limit instead of falling through to user defaults. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits) for the complete scope and exclusions.

The file is intentionally separate from `package.json > lpm` because these keys are tool behavior, not publishable metadata. Mixing them into the manifest creates avoidable diff churn whenever someone tweaks a personal preference. Keeping them in `lpm.toml` lets each team decide whether to commit the file or `.gitignore` it.

## Today's keys [#todays-keys]

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

[workspace]
concurrency = 4       # positive integer; caps workspace run/test/bench fan-out
changed-files-ignore-pattern = ["**/README.md", "docs/**"]
test-pattern = ["**/*.test.js", "**/*.spec.ts"]

[sandbox]
mode = "default"      # "default" | "strict" | "none"
allow-degraded = false

[tidy]
ignore-unused = ["eslint-config-next"]
ignore-phantom = ["virtual:*"]
ignore-paths = ["generated/**"]

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

| Key                                      | Type                                  | Default               | Notes                                                                                                                                                                                                                                |
| ---------------------------------------- | ------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `save-prefix`                            | `"^"` \| `"~"` \| `""`                | `"^"`                 | Prefix applied when `lpm install <pkg>` saves to `package.json`. `*` is **not** accepted.                                                                                                                                            |
| `save-exact`                             | bool                                  | `false`               | Force exact saves regardless of `save-prefix`.                                                                                                                                                                                       |
| `workspace.concurrency`                  | positive integer                      | available parallelism | Project default for concurrent workspace members in `lpm run`, `lpm test`, and `lpm bench` workspace mode.                                                                                                                           |
| `workspace.changed-files-ignore-pattern` | string or string array                | `[]`                  | Project default for git-diff paths to ignore before `[git-ref]` filters or `--affected` map changed files to members. CLI `--changed-files-ignore-pattern` entries append to this list.                                              |
| `workspace.test-pattern`                 | string or string array                | `[]`                  | Project default for changed files treated as test-only. Test-only packages stay selected, but they do not seed dependent fan-out for `[git-ref]` reverse closures or `--affected`. CLI `--test-pattern` entries append to this list. |
| `sandbox.mode`                           | `"default"` \| `"strict"` \| `"none"` | `"default"`           | Project-default lifecycle-script sandbox posture. CLI flags still win per invocation.                                                                                                                                                |
| `sandbox.allow-degraded`                 | bool                                  | `false`               | Allow degraded sandbox fallback for this repo on hosts that cannot provide the full posture.                                                                                                                                         |
| `tidy.ignore-unused`                     | string or string array                | `[]`                  | Suppress [`lpm tidy`](/docs/packages/tidy) unused-dependency findings by package-name glob.                                                                                                                                          |
| `tidy.ignore-phantom`                    | string or string array                | `[]`                  | Suppress `lpm tidy` phantom-import findings by package-name glob.                                                                                                                                                                    |
| `tidy.ignore-paths`                      | string or string array                | `[]`                  | Ignore source imports from matching project-relative path globs during `lpm tidy`.                                                                                                                                                   |
| `policy.typosquat.allow[]`               | table array                           | `[]`                  | Reviewable exceptions for intentional suspicious direct package names. Each entry needs `package`, `reason`, and optionally `similar-to`.                                                                                            |

Invalid values (`save-prefix = "*"`, `save-prefix = ">="`, etc.) are rejected at load time with a clear error pointing at the offending file. Unknown keys are accepted silently for forward compatibility.

## Precedence [#precedence]

### Save policy [#save-policy]

Save policy resolves in this order, highest first:

1. **CLI flag** — `--exact`, `--tilde`, or `--save-prefix '<p>'` on the install command
2. **`./lpm.toml`** — this file
3. **`~/.lpm/config.toml`** — user-level fallback
4. **Built-in default** — `save-prefix = "^"`, `save-exact = false`

See [Save policy](/docs/packages/save-policy) for the full table of how each combination flows into `package.json`.

### Workspace concurrency [#workspace-concurrency]

Workspace package fan-out for `lpm run`, `lpm test`, and `lpm bench` resolves in this order, highest first:

1. **CLI flag** — `--workspace-concurrency <N>`
2. **`./lpm.toml > [workspace].concurrency`** — this file
3. **`~/.lpm/config.toml > workspace-concurrency`** — user-level fallback
4. **Built-in default** — available host parallelism

### Git-diff workspace filters [#git-diff-workspace-filters]

`changed-files-ignore-pattern` and `test-pattern` under `[workspace]` are project defaults for workspace selections that read git changes: `[git-ref]` filter atoms and `--affected`. CLI flags append to the project lists for the current invocation.

### Sandbox [#sandbox]

Sandbox mode resolves in this order, highest first:

1. **CLI flag** — `--no-sandbox`, `--strict-sandbox`, or `--paranoid`
2. **`LPM_STRICT_SANDBOX`** — strict-mode env override
3. **`./lpm.toml > [sandbox]`** — this file
4. **`~/.lpm/config.toml > [sandbox]`** — user-level fallback
5. **Built-in default** — `mode = "default"`, `allow-degraded = false`

If this repo asks for a weaker sandbox than the machine currently allows, install or rebuild does **not** silently obey the file. It fails with `error_code: "security_approval_required"` and points you at [`lpm security unlock`](/docs/infra/security#unlock) instead.

### Tidy ignores [#tidy-ignores]

`[tidy]` keys are project-local only. They affect [`lpm tidy`](/docs/packages/tidy) report and fix mode; `ignore-unused` and `ignore-phantom` match package names, while `ignore-paths` matches project-relative source paths before imports are analyzed.

### Typosquat policy [#typosquat-policy]

`policy.typosquat.allow` is the committed exception list for suspicious direct package names. It is consulted by [`lpm install`](/docs/packages/install#typosquat-guard) and [`lpm add`](/docs/packages/add) before network access or manifest mutation.

```toml title="lpm.toml"
[[policy.typosquat.allow]]
package = "crossenv"
similar-to = "cross-env"
reason = "Internal migration package kept for compatibility"
```

`package` is the exact direct dependency name to allow. `similar-to` narrows the exception to the popular package LPM CLI detected; omit it only when the same local name may intentionally resemble more than one popular package. `reason` is required so the exception is reviewable in code review.

## See also [#see-also]

* [`~/.lpm/config.toml`](/docs/reference/config-toml) — user-level CLI defaults (broader surface)
* [`package.json` "lpm" key](/docs/reference/package-json-lpm) — publishable / project-shared LPM CLI config
* [`lpm security`](/docs/infra/security) — temporary approvals and floor status
* [`lpm.json`](/docs/reference/lpm-json) — dev-server, task-runner, and publish config
* [`lpm tidy`](/docs/packages/tidy) — dependency hygiene report and prune command
* [Save policy](/docs/packages/save-policy) — full rules and edge cases


# MCP servers (/docs/reference/mcp-servers)



```bash
lpm mcp <action> [name]
```

Manages [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server configuration for AI editors. `lpm mcp setup` writes the LPM.dev Registry MCP server entry into each supported editor that already has an MCP config file, and reports missing configs as skipped so the editor's agent can call LPM.dev Registry tools (search, install hint, audit, etc.) on your behalf.

## Examples [#examples]

```bash
lpm mcp setup                      # write the "lpm-registry" entry to existing editor configs
lpm mcp setup my-server            # use a custom server name instead of "lpm-registry"
lpm mcp status                     # which editors have any MCP server configured
lpm mcp remove lpm-registry        # remove the LPM.dev Registry entry from every editor that has it
```

The positional argument is the **server name** that gets written into each editor's `mcpServers` (or VS Code's `servers`) map — it is **not** an editor selector. `lpm mcp setup` always walks every editor in the supported list, writes the entry for editors with an existing config file, and prints a skipped row for missing configs; pass a name to override the default `lpm-registry`.

## Subcommands [#subcommands]

| Subcommand      | Effect                                                                                                                                                                                                   |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup [name]`  | Add an MCP entry under `name` (default `lpm-registry`) to every supported editor whose config file exists. Idempotent — re-running overwrites the entry in place. Missing configs are listed as skipped. |
| `serve`         | Run the verified stdio server configured by `setup`. Editors launch this automatically; direct terminal use and `--json` are rejected because stdout belongs to MCP JSON-RPC.                            |
| `remove <name>` | Remove the named entry from every editor whose config file exists. Editors without an existing config are skipped silently.                                                                              |
| `status`        | List every supported editor and which MCP servers (LPM.dev Registry or other) are configured in each.                                                                                                    |

## Supported editors [#supported-editors]

| Editor         | Platform | Config file                                                       | Key          |
| -------------- | -------- | ----------------------------------------------------------------- | ------------ |
| Claude Code    | all      | `~/.claude.json`                                                  | `mcpServers` |
| Cursor         | all      | `~/.cursor/mcp.json`                                              | `mcpServers` |
| Windsurf       | all      | `~/.codeium/windsurf/mcp_config.json`                             | `mcpServers` |
| VS Code        | macOS    | `~/Library/Application Support/Code/User/mcp.json`                | `servers`    |
| VS Code        | Linux    | `~/.config/Code/User/mcp.json`                                    | `servers`    |
| Claude Desktop | macOS    | `~/Library/Application Support/Claude/claude_desktop_config.json` | `mcpServers` |

`setup` does not create missing editor config files. When a supported editor's config is present, LPM CLI merges the server entry alongside any servers the user (or another tool) already wrote. Other keys in the config — themes, settings, unrelated MCP servers — are preserved. A malformed existing config is a hard error: the file is left untouched rather than overwritten.

Each editor configuration file has a 16 MiB limit enforced before JSON parsing. Missing files remain skipped; an oversized file is a hard, path-specific error and is left untouched. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

## What gets written [#what-gets-written]

```json
{
  "mcpServers": {
    "lpm-registry": {
      "command": "lpm",
      "args": ["mcp", "serve"]
    }
  }
}
```

VS Code uses the same shape under the `servers` key instead of `mcpServers`. The launcher resolves `@lpm-registry/mcp-server@latest` through the LPM CLI install pipeline and runs the newest release allowed by the active security posture.

The verified runtime lives at `~/.lpm/cache/mcp/runtime` and is revalidated after 24 hours. Refreshes install into a restricted staging directory and replace the active runtime only after the install and integrity checks complete. A registry transport outage, retryable HTTP failure, or rate limit can fall back to the last verified runtime; integrity, policy, firewall, provenance, authentication, and approval failures do not. Use `lpm cache path mcp` to inspect the cache or `lpm cache clean mcp` to remove it. Cleanup refuses while an MCP process is using the runtime.

LPM CLI writes editor configuration atomically and preserves the existing file's permissions, container key, unrelated servers, and other settings. Re-running `setup` replaces only the named entry with the same command and arguments.

## Authentication and custom registries [#authentication-and-custom-registries]

The launcher downloads the public MCP package anonymously, so editor startup does not inspect the LPM CLI credential store merely to refresh the runtime. The verified MCP server still receives `LPM_TOKEN`; when it is unset, the server reads the OS keychain credential created by `lpm login` for the effective registry URL. Keychain lookup is registry-scoped, so `https://lpm.dev`, local development registries, and custom registries do not accidentally share credentials.

The registry defaults to `https://lpm.dev`. To use another trusted registry, set `LPM_REGISTRY_URL` in the environment inherited by the editor or in that editor's MCP server environment configuration. The MCP server accepts HTTPS registries and HTTP loopback registries, including IPv4-mapped IPv6 loopback addresses. It has no equivalent of the CLI's `--insecure` exception for remote HTTP. `lpm mcp setup` does not copy the current shell's registry override into editor configuration.

After `setup`, restart the editor and ask the agent something like "find a JSON-schema validator on the LPM.dev Registry" — it'll route through the MCP tools to LPM.dev Registry search and quality endpoints rather than guessing from training data.

## Flags [#flags]

`lpm mcp` takes no specific flags besides the global flags. `--json` emits one structured document for `setup`, `remove`, and `status`; it is rejected for `serve` to keep stdout exclusively available to MCP JSON-RPC. Setup output includes `package`, `package_spec`, `version_policy`, `command`, and `args` alongside `success`, `server`, and `configured`; remove and status use `removed_from` and `editors`.

## See also [#see-also]

* [`lpm skills`](/docs/packages/skills) — separate from MCP; package-published and standalone agent skills ([concept page](/docs/reference/ai-agent-skills))
* [Model Context Protocol](https://modelcontextprotocol.io) — the upstream spec


# package.json — "lpm" key (/docs/reference/package-json-lpm)



LPM CLI reads its project-level configuration from a `"lpm"` block inside the project's `package.json`. Settings here are committed alongside dependencies, so the whole team picks them up automatically.

Every `package.json` read by LPM CLI has a 16 MiB limit enforced before JSON parsing. An oversized root or workspace-member manifest is a path-specific error, not a missing manifest or an empty configuration, and install fails before dependency resolution or registry access. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits) for the complete scope and exclusions.

**Project-only.** Every key in this block — `scriptPolicy`, `trustedDependencies`, `minimumReleaseAge`, `minimumReleaseAgePolicy`, `scripts.autoBuild`, `linker`, the rest — applies to `lpm install` (and other commands that run in the project context). `lpm install -g` does **not** read this file: globals synthesize their own package.json and use the equivalent CLI flags plus `~/.lpm/config.toml` for the user-config tier. Global trust lives in a separate file at `~/.lpm/global/trusted-dependencies.json`.

Security-sensitive project keys such as `scriptPolicy`, `minimumReleaseAge`, and `minimumReleaseAgePolicy` are treated as project **proposals**, not machine authority. If a repo asks for a weaker posture than this machine currently allows, install or rebuild fails and points you at [`lpm security unlock`](/docs/infra/security#unlock) or a persistent [`lpm config`](/docs/infra/config) change.

A few related fields live at the **top level** of `package.json` (not inside `"lpm"`) — `workspaces`, `engines`, `scripts`, `bin`, and `overrides` / `resolutions`. Those follow npm/pnpm/yarn conventions and are listed [at the bottom](#related-top-level-fields).

```json title="package.json (LPM CLI-relevant fields)"
{
  "name": "my-app",
  "version": "1.0.0",

  "engines": { "node": ">=22.0.0", "lpm": ">=0.32.0" },
  "workspaces": ["packages/*", "apps/*"],

  "lpm": {
    "linker": "isolated",
    "strictDeps": "warn",
    "strictPeerDependencies": false,
    "catalogMode": "manual",
    "cleanupUnusedCatalogs": false,

    "scriptPolicy": "deny",
    "triageAdvisor": "none",
    "trustedDependencies": ["esbuild", "sharp"],

    "minimumReleaseAge": 86400,
    "minimumReleaseAgePolicy": "direct",
    "minimumReleaseAgeExclude": ["react", "@scope/pkg"],
    "engineStrict": true,

    "overrides": {
      "lodash": "^4.17.21",
      "react@<18": "18.0.0"
    },

    "patchedDependencies": {
      "lodash@4.17.21": {
        "path": "patches/lodash@4.17.21.patch",
        "originalIntegrity": "sha512-..."
      }
    },

    "scripts": {
      "autoBuild": false,
      "denyAll": false,
      "trustedScopes": ["@myorg/*"],
      "passEnv": ["GITHUB_TOKEN"],
      "readProject": "narrow",
      "sandboxLimits": {
        "RLIMIT_AS": 4294967296,
        "RLIMIT_NPROC": 64,
        "RLIMIT_NOFILE": 1024,
        "RLIMIT_CPU": 60
      },
      "sandboxWriteDirs": ["build/", "dist/"]
    }
  }
}
```

## Top-level keys under `"lpm"` [#top-level-keys-under-lpm]

### `linker` [#linker]

```json
{ "lpm": { "linker": "isolated" } }
```

`node_modules` materialization style.

| Value        | Meaning                                                                                                                                                           |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"isolated"` | pnpm-style strict layout — each package sees only its declared deps                                                                                               |
| `"hoisted"`  | LPM CLI v2 hoisted virtual-store layout — root direct deps are surfaced at project `node_modules`, with package-local dependency links inside shared link entries |

**Default:** `"hoisted"` for single-package projects. LPM CLI auto-detects workspace roots (a `package.json > workspaces` glob or a `pnpm-workspace.yaml`) and defaults those projects to `"isolated"`. If a default-hoisted install detects incompatible peer requirements, LPM CLI switches that project to `"isolated"` for the install and records the decision in `lpm.lock` for warm installs. v2 hoisted mode does not intentionally flatten every transitive dependency to the project root.

CLI flag: `lpm install --linker <isolated\|hoisted>` overrides per-invocation. Precedence: `--linker` flag > `~/.lpm/config.toml > linker` > `LPM_LINKER` > this `package.json` value > built-in default (`"hoisted"`, or `"isolated"` if a workspace is detected). The peer-conflict auto-switch only applies at the built-in default tier, so any explicit value here opts out. Unknown values fail loudly at install time — there is no silent fallback.

### `strictDeps` [#strictdeps]

```json
{ "lpm": { "strictDeps": "warn" } }
```

How strictly LPM CLI enforces declared dependencies (no implicit access to phantom deps).

| Value      | Behavior                         |
| ---------- | -------------------------------- |
| `"strict"` | Hard error on undeclared imports |
| `"warn"`   | Print a warning, continue        |
| `"loose"`  | Permit silently                  |

### `strictPeerDependencies` [#strictpeerdependencies]

```json
{ "lpm": { "strictPeerDependencies": true } }
```

Whether peer-dependency diagnostics fail the install. Default `false`: missing required peers, peer version mismatches, and cross-consumer peer conflicts print warnings and the install continues. Set to `true` when those diagnostics should be hard failures, especially in CI. Optional peers that are absent still do not fail.

Precedence: `lpm install --strict-peer-dependencies` / `--no-strict-peer-dependencies` > `package.json > lpm.strictPeerDependencies` > `~/.lpm/config.toml > strict-peer-dependencies` > `false`.

### `catalogMode` [#catalogmode]

```json
{ "lpm": { "catalogMode": "prefer" } }
```

How `lpm install <pkg>` saves dependencies that match the root default catalog.

| Value                | Behavior                                                                                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"manual"` (default) | Keep normal raw save specs such as `"^4.3.6"` or the explicit range you typed.                                                                                            |
| `"prefer"`           | Save `"catalog:"` when the root default catalog entry satisfies the resolved version; warn and save the direct spec when it does not.                                     |
| `"strict"`           | Save `"catalog:"` when the root default catalog entry satisfies the resolved version; fail before committing `package.json` on mismatch or missing default-catalog entry. |

This affects `lpm install <pkg>` save policy only. Hand-written `"catalog:"` and `"catalog:<name>"` dependencies still resolve through the root catalog set, and `lpm install --catalog[=<name>] <pkg>` can force a catalog save for one invocation.

### `cleanupUnusedCatalogs` [#cleanupunusedcatalogs]

```json
{ "lpm": { "cleanupUnusedCatalogs": true } }
```

Prune root catalog entries that no root or workspace-member manifest references after a successful `lpm install`.

**Default:** `false`. When enabled, LPM CLI scans `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies` for `"catalog:"` and `"catalog:<name>"` references, then removes unreferenced entries from root `package.json > catalogs`. In pnpm-style workspaces, `pnpm-workspace.yaml > cleanupUnusedCatalogs: true` enables the same policy for `pnpm-workspace.yaml > catalog` / `catalogs`; an explicit `package.json > lpm.cleanupUnusedCatalogs` value wins when both are present.

### `scriptPolicy` [#scriptpolicy]

```json
{ "lpm": { "scriptPolicy": "deny" } }
```

Dependency lifecycle-script gate for `lpm install` and `lpm rebuild`. Bare root-project lifecycle scripts are separate; see [`lpm install`](/docs/packages/install#lifecycle-scripts) for the full semantics.

| Value              | Behavior                                                                                                                                                             |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"deny"` (default) | Dependency scripts blocked. `lpm approve-scripts` required per package.                                                                                              |
| `"allow"`          | Run every dependency lifecycle script during install.                                                                                                                |
| `"triage"`         | Tiered gate: greens auto-run in the sandbox; ambers and reds need manual review (or the [optional LLM advisor](/docs/packages/security-audit#optional-llm-advisor)). |

Precedence: CLI flag > `package.json > lpm > scriptPolicy` > `~/.lpm/config.toml > script-policy` > default (`deny`).

If this project value weakens the current approved machine floor, install or rebuild fails with `error_code: "security_approval_required"` instead of silently lowering the machine posture.

### `triageAdvisor` [#triageadvisor]

```json
{ "lpm": { "triageAdvisor": "claude-cli" } }
```

Optional LLM advisor for the [triage gate](/docs/packages/security-audit#optional-llm-advisor). Active only when `scriptPolicy: "triage"`.

| Value              | Provider                                                 |
| ------------------ | -------------------------------------------------------- |
| `"none"` (default) | No advisor — portable layers 1-4 only.                   |
| `"claude-cli"`     | [Claude CLI](https://github.com/anthropics/claude-code). |
| `"codex"`          | OpenAI [Codex CLI](https://github.com/openai/codex).     |
| `"ollama"`         | Ollama-served local model.                               |

Approvals are **ephemeral** — never written to `package.json` or any other file. A second `lpm install` invokes the advisor again. Use [`lpm approve-scripts`](/docs/packages/approve-scripts) (which writes to `trustedDependencies`) for durable trust.

Precedence: `package.json > lpm > triageAdvisor` > `~/.lpm/config.toml > triage-advisor` > default (`"none"`).

### `trustedDependencies` [#trusteddependencies]

```json
{ "lpm": { "trustedDependencies": ["esbuild", "sharp"] } }
```

Allowlist of dependency packages permitted to run lifecycle scripts under `scriptPolicy: "deny"`. Two accepted shapes:

**Legacy array form** — names only:

```json
{ "lpm": { "trustedDependencies": ["esbuild", "sharp"] } }
```

**Rich map form** — bound to integrity + script hash, so any change re-opens the package for review:

```json
{
  "lpm": {
    "trustedDependencies": {
      "esbuild@0.25.1": {
        "integrity": "sha512-...",
        "scriptHash": "sha256-..."
      }
    }
  }
}
```

Manage with [`lpm trust diff`](/docs/packages/trust) and [`lpm approve-scripts`](/docs/packages/approve-scripts).

> Only `package.json > lpm > trustedDependencies` is read. A top-level `"trustedDependencies"` field (outside the `"lpm"` block) is **not** consulted — convert legacy top-level entries by moving them under `"lpm"`.

### `minimumReleaseAge` [#minimumreleaseage]

```json
{ "lpm": { "minimumReleaseAge": 86400 } }
```

Cooldown in **seconds** before a new dependency version is installable. The default is `0`, which disables the cooldown.

By default, project installs apply this value to direct dependencies. Strict mode also checks transitive dependencies and lockfile replays.

For ranges, LPM CLI skips new candidates and can select an older candidate that satisfies the range. An exact pin fails during its cooldown.

The selected version does not exceed the current `dist-tags.latest` target in semantic-version order.

Use `lpm install --allow-new` to bypass the cooldown. Use [`minimumReleaseAgeExclude`](#minimumreleaseageexclude) to exempt selected packages.

Precedence: CLI > `package.json > lpm.minimumReleaseAge` > `~/.lpm/config.toml > minimum-release-age-secs` > disabled default (`0`).

If the repo lowers the cooldown below the current approved machine floor, install fails with `security_approval_required` and points you at a temporary [`lpm security unlock`](/docs/infra/security#unlock) or a persistent machine-level config change.

### `minimumReleaseAgePolicy` [#minimumreleaseagepolicy]

```json
{ "lpm": { "minimumReleaseAgePolicy": "strict" } }
```

Scope for the [`minimumReleaseAge`](#minimumreleaseage) cooldown.

| Value                | Behavior                                                                                                                                                                |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"direct"` (default) | Check direct/root dependencies only. This keeps normal installs fast and avoids downgrading a mature direct package because one of its children was published recently. |
| `"strict"`           | Check direct and transitive dependencies. Lockfile replays are revalidated from persisted `registry-published-at` timestamps when available.                            |

Precedence: `package.json > lpm.minimumReleaseAgePolicy` > `~/.lpm/config.toml > release-age-policy` > default (`"direct"`).

If the repo sets a looser policy than the current approved machine posture, install fails with `security_approval_required`. A machine with `force-security-floor = true` suppresses a project-level downgrade back to the stricter policy instead.

### `minimumReleaseAgeExclude` [#minimumreleaseageexclude]

```json
{
  "lpm": {
    "minimumReleaseAgeExclude": ["react", "@company/*", "react@19.1.0"]
  }
}
```

Package selectors exempted from the minimum-release-age resolver and install halt. Alias dependencies match by their canonical target name, not the local alias key.

The field accepts these selector types:

* A package name, such as `react`
* A scope wildcard, such as `@company/*`
* An exact package version, such as `react@19.1.0`

Version ranges and protocol specifiers are invalid. For example, `react@^19` and `npm:react` are invalid.

CLI, project, and user exclude lists merge in that order. LPM CLI removes duplicate selectors.

Use [`lpm trust release-age-exclude`](/docs/packages/trust#lpm-trust-release-age-exclude) to manage the project list.

### `engineStrict` [#enginestrict]

```json
{ "lpm": { "engineStrict": true } }
```

Whether workspace-root `engines.lpm` / `engines.node` and, during installs, selected dependencies' `engines.node` constraints are enforced. Default `true` — workspace-root mismatches abort install / rebuild / add, `lpm run` validates the effective Node, and required dependency mismatches abort installation. Packages reachable only through optional dependency edges are skipped when their Node constraint is incompatible.

Precedence: `lpm install --no-engine-strict` (CLI) > `package.json > lpm.engineStrict` > `~/.lpm/config.toml > engine-strict` > `true` default.

Set to `false` per-project to fall back to warning-only behavior. Incompatible dependencies remain installed and the unsatisfied constraints print warnings on stderr (suppressed under `--json`).

### `overrides` [#overrides]

```json
{
  "lpm": {
    "overrides": {
      "lodash": "^4.17.21",
      "react@<18": "18.0.0",
      "baz>foo@1": "2.0.0"
    }
  }
}
```

LPM CLI-native dependency overrides — same intent as pnpm's `pnpm.overrides` and npm's top-level `overrides`. Selectors:

| Selector                    | Matches                                             |
| --------------------------- | --------------------------------------------------- |
| `"foo"`                     | Every instance of `foo`                             |
| `"foo@<1.0.0"`              | Instances whose natural version satisfies the range |
| `"baz>foo"` / `"baz>foo@1"` | Instances reached **through** `baz`                 |

The target replaces the declared range of each matching consumer. An exact target pins one version, even when that version is outside the consumer range.

A range target selects the newest eligible published version in the target range. Security and platform policies still apply.

Override targets must be strings. LPM CLI does not apply npm's nested-object value form. Install and `lpm doctor --all` warn about each ignored top-level `overrides` or `resolutions` value. Use a parent selector such as `path-scurry>lru-cache` instead.

Workspace-root entries apply to the root and all members. Member entries extend the root map, and a member value wins for the same selector.

Field precedence is `lpm.overrides`, then top-level `overrides`, then `resolutions`. Multi-segment paths (`a>b>c`) are rejected at parse time.

Install summaries include only overrides that change a selected version. A matching override that selects the natural version does not appear as applied.

### `patchedDependencies` [#patcheddependencies]

```json
{
  "lpm": {
    "patchedDependencies": {
      "lodash@4.17.21": {
        "path": "patches/lodash@4.17.21.patch",
        "originalIntegrity": "sha512-..."
      }
    }
  }
}
```

Local-only patches applied after install, `patch-package`-style. Selector format is `"<name>@<exact-version>"`. Generated and registered automatically by [`lpm patch`](/docs/packages/patch) and [`lpm patch-commit`](/docs/packages/patch) — usually you don't edit this block by hand. The `path` field is relative to the directory of the `package.json` that declares it.

`lpm.lock > [patches]` records the patch file SHA-256 for each entry. On every install, LPM CLI verifies both the patch checksum and the store entry's `originalIntegrity`. Drift is a hard error.

### `peerDependencyRules` [#peerdependencyrules]

```json
{
  "lpm": {
    "peerDependencyRules": {
      "ignoreMissing": ["@types/react", "fsevents"],
      "allowedVersions": {
        "react": "16 || 17 || 18",
        "card>react": "17",
        "@scope/foo@^2>react": "17",
        "typescript": "5"
      },
      "allowAny": ["@babel/*"]
    }
  }
}
```

Tunes the post-resolution peer-dependency warning loop. Three independent sub-keys; each addresses a different complaint:

| Sub-key           | Effect                                                                                                                                                                                        | Key shape                                                 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `ignoreMissing`   | Suppress missing-peer warnings for the listed names. Use when a peer is intentionally optional (something else installs it; your code handles absence).                                       | Names + glob patterns (`*`, `@scope/*`, `*-suffix`, etc.) |
| `allowedVersions` | Widen the accepted range. When the resolved peer's version doesn't satisfy the package's declared range, this widened range is tested as a fallback. If either matches, no warning fires.     | Structured selectors (see below)                          |
| `allowAny`        | Accept any version when the peer is in the tree. Suppresses version-mismatch warnings for matched names. **Does not** suppress missing-peer warnings (combine with `ignoreMissing` for that). | Names + glob patterns                                     |

#### `allowedVersions` selector grammar [#allowedversions-selector-grammar]

The same grammar as [`lpm.overrides`](#overrides). The selector decides which `(consumer, peer)` pair the rule applies to:

| Selector                | Matches                                                      |
| ----------------------- | ------------------------------------------------------------ |
| `"react"`               | any peer named `react`, regardless of consumer               |
| `"@scope/foo"`          | scoped peer, any consumer                                    |
| `"foo>react"`           | `react` peer of `foo` (any version of foo)                   |
| `"foo@^2>react"`        | `react` peer of `foo` whose installed version satisfies `^2` |
| `"@scope/foo@^2>react"` | scoped parent + version range                                |

Multi-segment paths (`"a>b>c"`) and standalone version qualifiers on a bare peer name (`"foo@2"` without `>`) are rejected at install time. Glob patterns are **not** accepted in `allowedVersions` — use the structured selector grammar above.

Same shape as pnpm's `pnpm.peerDependencyRules` — `lpm migrate` translates the pnpm side verbatim, validating selector keys with the same parser the resolver uses at install time. Hand-authored typos in `lpm.peerDependencyRules.allowedVersions` (bad selector, unparseable widened range) abort the install with a named error — same fail-closed posture as `lpm.overrides`. Drift between the pnpm and LPM CLI sides post-migration is surfaced as the `pnpm_peer_rules_drift` code on `lpm doctor --json`.

## The `lpm.scripts` block [#the-lpmscripts-block]

Configuration for dependency lifecycle-script execution under `lpm rebuild` and the auto-build flow. Most fields control the sandbox and capability surface granted to scripts.

```json
{
  "lpm": {
    "scripts": {
      "autoBuild": false,
      "denyAll": false,
      "trustedScopes": ["@myorg/*"],
      "passEnv": ["GITHUB_TOKEN"],
      "readProject": "narrow",
      "sandboxLimits": { "RLIMIT_AS": 4294967296 },
      "sandboxWriteDirs": ["build/"],
      "sandboxReadAllow": [".env"]
    }
  }
}
```

| Field              | Type                   | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autoBuild`        | bool                   | If `true`, `lpm install` auto-runs `lpm rebuild` for trusted packages. Equivalent to `lpm install --auto-build`; if a trusted dependency lifecycle script fails, install exits non-zero.                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `denyAll`          | bool                   | Refuse to run any dependency lifecycle scripts, ever — even trusted ones. Hard kill switch.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `trustedScopes`    | string\[]              | Lowercase npm scope wildcards whose packages are trusted for script execution. Manage with [`lpm trust lifecycle-scope`](/docs/packages/trust#lpm-trust-lifecycle-scope).                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `passEnv`          | string\[]              | Extra env vars to forward into scripts. Each entry widens the capability surface — triggers user review.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `readProject`      | `"narrow"` \| `"full"` | Filesystem read scope for scripts. `"full"` widens the capability and triggers review.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `sandboxLimits`    | object                 | Per-resource limits. Keys: `RLIMIT_AS`, `RLIMIT_NPROC`, `RLIMIT_NOFILE`, `RLIMIT_CPU`. Values: non-negative integer. Any requested limit outside the approved baseline triggers review. Raw user-configured `[sandbox.limits]` ceilings are not authority on their own.                                                                                                                                                                                                                                                                                                                                                                     |
| `sandboxWriteDirs` | string\[]              | Extra directories the sandbox lets scripts write to. Relative entries stay inside the canonical project root. Absolute entries outside it require a covering [`max-sandbox-write-roots`](/docs/reference/config-toml) entry. Existing paths and the nearest existing ancestor of missing paths are resolved before use; symlink, junction, and reparse-point traversal below the authorized root is rejected. The dangerous-root denylist still has final veto.                                                                                                                                                                             |
| `sandboxReadAllow` | string\[]              | Project-relative paths explicitly authorized for lifecycle-script reads despite matching the [secret-file deny list](/docs/packages/rebuild#project-secret-files). Use when a build genuinely needs a normally blocked file (`.env` for `prisma generate`, a project-rooted CA bundle, etc.). Entries must canonicalize inside `project_dir`; traversal escapes (`..`) and absolute paths outside the project are rejected. Unioned with per-user [`~/.lpm/config.toml > script-read-allow`](/docs/reference/config-toml). Allowlisted secrets remain readable; if every matching secret is allowlisted, Linux skips the namespace overlay. |

The project root itself may be reached through a symlink; LPM CLI resolves it once and evaluates containment against its canonical target. Links or reparse points inside the project or an explicitly authorized write root are not accepted, including when the requested leaf does not exist yet.

Widening any of `passEnv`, `readProject`, or `sandboxLimits` beyond the approved baseline requires explicit approval through [`lpm approve-scripts`](/docs/packages/approve-scripts) — the system does not silently grant elevated capabilities from raw repo or user config edits.

Each `trustedScopes` entry applies to all package versions and script contents in that scope. Use strict [`trustedDependencies`](#trusteddependencies) bindings for individual packages.

An added scope requires a signed `trust-scope-widen` authorization. The authorization is local, but the scope list remains in the shared `package.json`.

LPM CLI removes the signed scope authorization when you run `lpm trust lifecycle-scope remove`.

## Related top-level fields [#related-top-level-fields]

These live at the top level of `package.json`, not inside `"lpm"`:

### `workspaces` [#workspaces]

```json
{ "workspaces": ["packages/*", "apps/*"] }
```

Or the object form:

```json
{ "workspaces": { "packages": ["packages/*"] } }
```

Both shapes accepted. See [Workspaces](/docs/packages/workspaces).

### `engines` [#engines]

```json
{ "engines": { "node": ">=22.0.0", "lpm": ">=0.32.0" } }
```

Runtime / CLI version constraints. LPM CLI enforces both `engines.node` (against the effective Node version) and `engines.lpm` (against the running CLI version) on the workspace root by default. It also enforces `engines.node` from selected dependency versions, persisting those ranges in `lpm.lock` for warm, frozen, and offline replay. Required mismatches abort; optional-only incompatible packages are skipped.

`engines.node` never selects or installs a runtime. LPM CLI applies `lpm.json > runtime.node`, `.nvmrc`, or `.node-version` first; without one, it preserves the Node already on `PATH`. Engine enforcement then validates that effective version. If no Node is available, the constraint fails with an explicit runtime-selection hint.

Other keys (`engines.npm`, `engines.pnpm`, `engines.yarn`, `engines.bun`) are recognized and surfaced as a one-line stderr 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`.

Opt out via `lpm install --no-engine-strict`, `package.json > lpm > engineStrict = false`, or `~/.lpm/config.toml > engine-strict = false`.

### `overrides` / `resolutions` [#overrides--resolutions]

npm-style `overrides` and yarn-style `resolutions` are read for compatibility. Workspace-root entries apply to every member.

A member value wins when the same selector exists in the same field. [`lpm.overrides`](#overrides) takes precedence across fields.

### `scripts`, `bin`, `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies` [#scripts-bin-dependencies-devdependencies-peerdependencies-optionaldependencies]

Standard npm fields — LPM CLI reads them as you'd expect.

### `catalogs` [#catalogs]

```json
{
  "catalogs": {
    "default": { "react": "^18.2.0", "react-dom": "^18.2.0" },
    "testing": { "jest": "^29.0.0", "vitest": "^1.0.0" }
  }
}
```

Centralized version catalogs for monorepos. Members reference catalog versions with `"catalog:"` or `"catalog:<name>"`.

LPM CLI reads catalogs from root `package.json > catalogs` and from pnpm-style `pnpm-workspace.yaml`:

```yaml title="pnpm-workspace.yaml"
packages:
  - "packages/*"
cleanupUnusedCatalogs: true
catalog:
  react: ^18.2.0
catalogs:
  testing:
    vitest: ^1.0.0
```

`pnpm-workspace.yaml > catalog` becomes LPM CLI's default catalog. `pnpm-workspace.yaml > catalogs` becomes the named catalog map. When both files define the same catalog package entry, `package.json > catalogs` wins and LPM CLI keeps the pnpm-workspace entry only for non-conflicting packages.

Catalog entry values must be concrete package ranges. A catalog entry cannot point at another catalog entry with `"catalog:"` or `"catalog:<name>"`; LPM CLI rejects recursive catalog definitions during install.

Set `package.json > lpm > catalogMode` to control whether `lpm install <pkg>` writes matching default-catalog entries back as `"catalog:"`. Set `cleanupUnusedCatalogs` to prune unused catalog entries after successful installs.

## See also [#see-also]

* [`lpm.json`](/docs/reference/lpm-json) — separate file for dev-server, task-runner, and publish config
* [`lpm.toml`](/docs/reference/lpm-toml) — project-level CLI defaults (save policy, etc.)
* [`~/.lpm/config.toml`](/docs/reference/config-toml) — user-level CLI defaults
* [Save policy](/docs/packages/save-policy) — how install ranges are written
* [Workspaces](/docs/packages/workspaces) — filter grammar and topology


# JSON Schemas (/docs/reference/schemas)



```bash
lpm schema lpm.json
lpm schema lpm.config.json
lpm schema lpm.json -o schema.json
```

LPM CLI ships JSON Schemas for the two JSON config files it owns: `lpm.json` and `lpm.config.json`. Use the published URLs for editor autocomplete, or use `lpm schema` to emit the schema from the exact CLI version installed on your machine.

## Published URLs [#published-urls]

| File              | Canonical schema URL                          | Compatibility alias                       |
| ----------------- | --------------------------------------------- | ----------------------------------------- |
| `lpm.json`        | `https://cli.lpm.dev/schemas/lpm.json`        | `https://lpm.dev/schemas/lpm.json`        |
| `lpm.config.json` | `https://cli.lpm.dev/schemas/lpm.config.json` | `https://lpm.dev/schemas/lpm.config.json` |

Use the canonical `cli.lpm.dev` URL in new files:

```json title="lpm.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.json"
}
```

```json title="lpm.config.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.config.json"
}
```

## Emit Locally [#emit-locally]

```bash
lpm schema lpm.json
lpm schema lpm.config.json
```

Without `-o`, LPM CLI pretty-prints the schema to stdout. With `-o` / `--out`, it writes the pretty-printed JSON to that path:

```bash
lpm schema lpm.json -o lpm.schema.json
lpm schema lpm.config.json --out lpm-config.schema.json
```

`lpm schema` accepts only these two kinds:

| Kind              | Source of truth                                                                                   |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| `lpm.json`        | Auto-derived from the typed Rust struct that parses `lpm.json`.                                   |
| `lpm.config.json` | Hand-authored alongside the `lpm add` consumer code, with drift tests against the published copy. |

Unknown kinds fail with a message listing the supported values.

## Flags [#flags]

| Flag                        | Effect                                        |
| --------------------------- | --------------------------------------------- |
| `-o <PATH>`, `--out <PATH>` | Write the schema to a file instead of stdout. |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm.json`](/docs/reference/lpm-json) - runtime, tasks, services, tunnel, env, and publish config
* [`lpm.config.json`](/docs/reference/lpm-config-json) - source-package installer config
* [`lpm add`](/docs/packages/add) - consumer flow for packages that ship `lpm.config.json`
* [`lpm dev`](/docs/dev/dev) - common `lpm.json` consumer


# Terminal output security (/docs/reference/terminal-output)



The LPM CLI treats text from registries, package metadata, project files, paths, URLs, task and service configuration, policy providers, and intercepted child processes as untrusted when it appears inside CLI-owned terminal output.

This boundary protects human-readable output without removing the CLI's own colors and formatting.

## Human-readable output [#human-readable-output]

The LPM CLI sanitizes externally controlled fields before adding its own colors, prefixes, status glyphs, or other terminal formatting.

Inline fields include package names and versions, dependency-graph labels and queries, CI setup values, process names, advisory summaries, registry and server messages, paths, URLs, git references, task and service names, workspace values, and similar data embedded in a CLI-owned row. Inline fields cannot add another terminal line or rewrite the current line: newline, carriage return, tab, backspace, delete, and other raw controls are rendered as `?`.

Terminal escape sequences are removed as complete units, including CSI, OSC, DCS, APC, PM, SOS, single-character ESC sequences, and their C1 forms. This prevents external text from changing colors, moving the cursor, setting the terminal title, creating an unsafe hyperlink, or writing to the clipboard. Printable Unicode is preserved.

For an intentionally multiline block, the CLI preserves legitimate line-feed and tab structure where that structure is part of the output contract. Carriage-return rewriting is not preserved; CRLF is normalized to LF and a lone carriage return is rendered as `?`.

## Intercepted child output [#intercepted-child-output]

Child output is inside the sanitization boundary whenever the LPM CLI intercepts it to add presentation or retain it for later display. This includes:

* live output with CLI-owned task or service prefixes
* buffered output printed after a task completes
* captured output replayed from the task cache
* lifecycle output intercepted by install or rebuild
* service logs stored and rendered by the dashboard

The CLI sanitizes each intercepted line before adding a prefix or color. Cached and structured data retains its original value; sanitization happens when the value crosses into human terminal output.

## JSON output [#json-output]

`--json` output keeps the original semantic values. The LPM CLI does not terminal-sanitize strings before placing them in a JSON envelope, so a registry message or captured output can be recovered exactly after parsing the JSON.

The JSON serializer structurally escapes characters as required to keep the byte stream valid JSON. Consumers should parse the JSON instead of rendering string values directly to a terminal. A consumer that later displays a parsed external string owns its own terminal-output boundary.

## Raw child-process output [#raw-child-process-output]

Some commands intentionally give a child process direct ownership of stdin, stdout, stderr, or the terminal. The LPM CLI does not see those bytes and cannot sanitize them. This compatibility boundary is required for interactive programs, terminal-aware tools, prompts, progress UIs, and normal shell/process semantics.

Directly inherited child output is therefore outside the CLI's sanitization guarantee. The boundary changes as soon as the CLI captures, prefixes, buffers, summarizes, or replays that output: those render paths are sanitized as described above.

This is an output-rendering protection, not a sandbox for child processes or a replacement for terminal-emulator security updates.

## Boundary summary [#boundary-summary]

| Output path                                            | Contract                                                                                        |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| CLI-owned human row                                    | External inline fields are sanitized before trusted CLI styling.                                |
| CLI-owned multiline block                              | Escape commands and rewrite controls are removed; documented LF/tab structure may be preserved. |
| Captured, prefixed, buffered, or replayed child output | Sanitized when rendered to a human terminal.                                                    |
| Dashboard service logs                                 | Sanitized before storage/rendering under the shared terminal policy.                            |
| Structured JSON                                        | Valid JSON with the original semantic string values.                                            |
| Directly inherited child terminal                      | Raw compatibility boundary; the child owns its terminal output.                                 |

## See also [#see-also]

* [`lpm run`](/docs/dev/run) — buffering, streaming prefixes, and task-cache replay
* [`lpm exec`](/docs/dev/exec) — direct source-file and project-binary execution
* [`lpm dev`](/docs/dev/dev) — prefixed service output and dashboard logs
* [`lpm rebuild`](/docs/packages/rebuild) — intercepted lifecycle output


# lpm add (/docs/packages/add)



```bash
lpm add <package>
```

`lpm add` extracts a package's source files into your project rather than installing it as a runtime dependency. Think `shadcn-ui` style: the code lands in your repo, you own it, you edit it.

`lpm install` is the runtime-dependency command. `lpm add` is the source-delivery command. The two are not aliases.

Works with **any registry** the resolver can reach — LPM.dev Registry, npmjs.org, or a `.npmrc`-declared private registry. Anything that ships a tarball is fair game.

Try the public npm example package:

```bash
lpm add lpm-source-package
```

[`lpm-source-package`](https://www.npmjs.com/package/lpm-source-package) is a minimal configurable source package published to npm, so it exercises the same registry-agnostic `lpm add` path without requiring an LPM.dev Registry package.

Like [`lpm install`](/docs/packages/install#typosquat-guard), `lpm add` rejects a likely typosquatted package name before any registry fetch. Intentional names use the committed [`policy.typosquat.allow`](/docs/reference/lpm-toml#typosquat-policy) list in `lpm.toml`.

When npm firewall mode is `monitor` or `enforce`, LPM CLI checks the selected source package with LPM Firewall at `firewall.lpm.dev` when it is a routed public npm package. LPM Firewall is an LPM.dev Registry Pro/Org feature, so active modes send LPM.dev Registry auth; run `lpm login` locally or set `LPM_TOKEN` in CI. `monitor` warns and continues when entitlement is denied, while `enforce` blocks packages whose effective firewall action is `block` or denied by entitlement before the source tarball is downloaded or copied. In human output, the source-package download line shows `🔥 LPM Firewall active` when that check is active. LPM.dev Registry packages and other `.npmrc`-declared private/custom registry packages are not sent to the npm firewall verdict API. Dependencies installed after a config-aware source copy use the normal install pipeline and its firewall checks. The legacy string `report` is still accepted as a `monitor` alias.

## Examples [#examples]

```bash
lpm add @lpm.dev/owner.ui-kit                      # add an LPM.dev Registry package
lpm add @lpm.dev/owner.ui-kit@1.2.0                # add a specific version
lpm add @lpm.dev/owner.ui-kit?component=dialog     # add only one component
lpm add lodash.merge                               # add an npm package's source
lpm add @my-co/internal --path ./src/vendor        # custom destination
lpm add my-pkg --dry-run                           # preview without writing
lpm add @lpm.dev/owner.ui-kit --no-skills          # skip package skills once
```

## How it works [#how-it-works]

1. Resolves the spec against the appropriate registry.
2. Downloads the tarball into a temporary file and extracts it to a tempdir. Unlike `lpm install`, the tarball is **not** persisted to `~/.lpm/store/` — `lpm add` is a copy-and-discard flow. Re-running `lpm add` re-downloads.
3. Picks where to put the files. With `--path`, honors it. Without `--path` in a TTY (and not `--yes` / `--json`), prompts for an install directory. Non-interactive (`--yes`, `--json`, or non-TTY) requires `--path` for simple-path packages (no `lpm.config.json`); config-aware packages auto-detect from their declared ecosystem.
4. Copies the source into the destination, prompting on conflicts.
5. If the package declares an `lpm.config.json`, runs the package's configured install steps (component selection, alias rewrites, dep installation). If not, it's a plain source copy and you handle dependencies yourself.

`lpm add` does **not** auto-install bare imports it discovers in the copied source. It will surface them at the end of the run so you can add them yourself.

With the default `--pm lpm`, declared dependencies honor the same [minimum release age](/docs/packages/install#recently-published-packages) policy as [`lpm install`](/docs/packages/install). A bare dependency or `name@latest` selects the newest version old enough to install, then saves the project range from that selected version. An author-written range such as `name@^1.11.5` remains unchanged: fallback can only select older versions that still satisfy that range. If no mature version satisfies it, `lpm add` fails and restores the copied files, `package.json`, and lockfiles to their pre-add state.

Human output is structured around the source-copy flow:

```text
› Downloading source package source-pkg@1.0.0
› Detecting project structure
    Framework:    Vite
    Install path: src/components
    Import alias: @/src/components/
✓ Files copied
+ Foo.tsx
› Installing declared dependencies
+ lucide-react@^0.400.0
✓ Done · added 1 file and 1 dependency in 486ms
```

File conflicts use a slim warning line (`! File exists: ...`) before the interactive choice.

## Config-aware vs simple copy [#config-aware-vs-simple-copy]

A package becomes "config-aware" by shipping an `lpm.config.json` at the tarball root. That file declares components, dependencies to auto-install, import aliases, and editor integration hints. Its optional `type` field controls LPM.dev Registry classification, not this command path: a valid config always activates the configured `lpm add` behavior, including with `"type": "package"`. Without a config, `lpm add` is a plain source copy — useful for any tarball, not just LPM.dev Registry packages.

For `@lpm.dev/*` source packages, `lpm add` also reconciles package-published agent guidance into `.lpm/skills/<package>/` by default. Use `--no-skills` to skip that step once, or `lpm config lpm-skills --set false` to persist the preference. `--skills` overrides the persistent opt-out for one run. The setting does not delete existing package skills or affect explicit `lpm skills add` commands; see [AI agent skills](/docs/reference/ai-agent-skills).

## Swift packages [#swift-packages]

For Swift packages, prefer &#x2A;*[`lpm install`](/docs/packages/install)** — it resolves SPM dependencies through the [SE-0292 Registry](/docs/packages/swift-package-registry) and edits `Package.swift`.

`lpm add` remains a legacy source-delivery path for Swift. On that path, `--target <NAME>` only appends a directory to the automatic destination: `Sources/<NAME>` for an SPM project or `Packages/LPMComponents/Sources/<NAME>` when an Xcode project or workspace is present. It does not edit `Package.swift` or wire an Xcode target.

## Authoring config-aware packages: `lpm.config.json` [#authoring-config-aware-packages-lpmconfigjson]

Ship `lpm.config.json` at the root of your tarball to control how `lpm add` extracts your package. With the file present, `lpm add` becomes a configurable installer — interactive prompts, conditional file copying, conditional dependency injection, import-path rewriting.

Install the [`lpm-guide`](https://github.com/lpm-dev/lpm-guide) skill if you want an agent to draft the config from your source tree:

```bash
lpm skills add lpm-dev/lpm-guide
```

Then paste a prompt like this into your agent:

```text
Use the lpm-guide skill's Source Config workflow. Inspect this package's source tree and generate an lpm.config.json for a configurable source package.

Identify the options consumers should choose, such as components, styling framework, variants, or optional examples. Use required fields when choices would conflict, add defaultConfig so lpm add --yes works, map files[] from the package root to the consumer install path, declare conditional dependencies, and include example lpm add commands with query params.
```

```json title="lpm.config.json (sketch)"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.config.json",
  "ecosystem": "js",
  "importAlias": "@/",
  "configSchema": {
    "component": {
      "type": "select",
      "label": "Which component?",
      "options": ["dialog", "popover"],
      "default": "dialog",
      "required": true
    },
    "withTests": { "type": "boolean", "label": "Include tests?", "default": false }
  },
  "files": [
    { "src": "src/dialog/**", "dest": "dialog", "include": "when",
      "condition": { "component": "dialog" } },
    { "src": "tests/**", "dest": "__tests__", "include": "when",
      "condition": { "withTests": true } }
  ],
  "dependencies": {
    "withTests": { "true": ["vitest"] }
  }
}
```

The full schema reference — every field, every value type, every default — lives at [`lpm.config.json`](/docs/reference/lpm-config-json). The published JSON Schema at [`https://cli.lpm.dev/schemas/lpm.config.json`](https://cli.lpm.dev/schemas/lpm.config.json) drives editor autocomplete (add `$schema` as shown above).

### Pre-answering prompts [#pre-answering-prompts]

Prompts can be pre-answered from the CLI by appending `?key=value&key=value` to the package spec:

```bash
lpm add @lpm.dev/owner.ui-kit?component=dialog&styling=panda
```

Inline values bypass the prompt entirely. With `--yes`, prompts are skipped and `defaultConfig` / `configSchema.<field>.default` fill in any `required` fields the user didn't supply.

### Behavior matrix [#behavior-matrix]

| State                                                          | What `lpm add` does                                                                                                                                                                                          |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No `lpm.config.json`                                           | Plain copy of the package's source. No prompts. No auto-install. Surfaces bare imports for you to install yourself.                                                                                          |
| `lpm.config.json` present, TTY                                 | Prompts for every `configSchema` key not already inlined; copies per `files[]` rules; installs `dependencies` matching the chosen values.                                                                    |
| `lpm.config.json` present, `--yes`                             | Skips prompts and fills missing `required` fields from `defaultConfig` or the field's `default`.                                                                                                             |
| `lpm.config.json` present, `--json` or non-TTY without `--yes` | Skips prompts but does not fill required defaults. Only query-string values are populated; missing file conditions use include-by-default behavior and unmatched conditional dependencies are not installed. |

### Prerequisites [#prerequisites]

If the source package declares dependencies (config-driven or via its own `package.json`), your project must already have a `package.json`. `lpm add` runs a preflight check before copying any files and exits early with a remediation hint if the manifest is missing — there's nowhere for the dep entries to land otherwise, and we'd rather fail loudly than copy source files you can't `import` from.

```
$ lpm add @author/ui-kit
error: this source package declares dependencies, but the project has no
       `package.json` to record them in.
       Run `lpm init` (or `npm init -y`) first to create a manifest, then
       re-run `lpm add`.
       To copy the source files without installing the declared dependencies,
       pass `--no-install-deps` and resolve the imports yourself.
```

If you only want the source files (and you'll handle the imports yourself), pass `--no-install-deps` to skip the preflight and let the copy proceed without touching `package.json`.

## Flags [#flags]

| Flag                 | Effect                                                                                                                                                                                                                                                                        |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--path <DIR>`       | Target directory (overrides interactive prompt and auto-detection)                                                                                                                                                                                                            |
| `-y`, `--yes`        | Skip interactive prompts, use defaults                                                                                                                                                                                                                                        |
| `--force`            | Overwrite existing files without prompting                                                                                                                                                                                                                                    |
| `--dry-run`          | Show what would be done without writing anything                                                                                                                                                                                                                              |
| `--no-install-deps`  | Skip dependency installation after copy (for config-aware packages)                                                                                                                                                                                                           |
| `--skills`           | Install package-published LPM.dev skills for this invocation, overriding user config                                                                                                                                                                                          |
| `--no-skills`        | Skip package-published LPM.dev skill auto-install for this invocation                                                                                                                                                                                                         |
| `--no-editor-setup`  | Compatibility flag. Package skills do not create editor integrations.                                                                                                                                                                                                         |
| `--pm <NAME>`        | Package manager for dep installation (`lpm`, `npm`, `pnpm`, `yarn`, `bun`, `auto`; default `lpm`)                                                                                                                                                                             |
| `--alias <ALIAS>`    | Import alias prefix (e.g., `@/components`) — overrides auto-detection                                                                                                                                                                                                         |
| `--target <NAME>`    | Swift source-copy destination suffix; does not perform SPM or Xcode target wiring                                                                                                                                                                                             |
| `--no-engine-strict` | Use warning-only root checks and, when LPM CLI installs dependencies, dependency checks (see [`lpm install`](/docs/packages/install#engines-enforcement)). Root preflight runs before any manifest mutation, so a constraint violation can't leave the project half-modified. |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm remove`](/docs/packages/remove) — undo an `lpm add`
* [`lpm install`](/docs/packages/install) — install runtime dependencies
* [Registries](/docs/registries) — how packages route
* [Swift Package Registry](/docs/packages/swift-package-registry) — SE-0292 design and the `lpm install`-preferred path for Swift


# lpm approve-scripts (/docs/packages/approve-scripts)



```bash
lpm approve-scripts [package]
```

Pairs with [`lpm install`](/docs/packages/install)'s default-deny dependency script policy. After install, dependency packages that declare any recognized lifecycle script — `preinstall`, `install`, `postinstall`, `prepare`, `prepublishOnly`, or the `*uninstall` family — and aren't already trusted are queued in a **blocked set**. `lpm approve-scripts` walks that set interactively, lists it, or bulk-approves.

The blocked set is broader than what actually runs. The detection layer queues every recognized dependency lifecycle name so you can review the surface area; the execution layer only runs dependency `preinstall`, `install`, and `postinstall`. A dependency package whose only declared script is `prepare` will appear in your review queue but won't execute anything when approved — see [What `lpm rebuild` runs](/docs/packages/rebuild#what-runs).

Approvals bind the package version, integrity, script hash, verified provenance, and requested capabilities. If a bound input changes, LPM requests approval again.

Use `lpm approve-scripts` for individual packages. `lpm trust lifecycle-scope` only manages scope-wide trust and does not create package bindings.

## Examples [#examples]

```bash
lpm approve-scripts                          # interactive walk through every blocked package
lpm approve-scripts esbuild                  # approve a specific package directly
lpm approve-scripts esbuild@0.25.1           # approve a specific version
lpm approve-scripts --list                   # read-only listing
lpm approve-scripts --yes                    # bulk approve, loud — emits a warning banner
lpm approve-scripts --dry-run                # preview decisions without writing
lpm approve-scripts --list --json            # structured listing for agents
lpm approve-scripts esbuild --json           # structured single-package approval

lpm approve-scripts --global                 # review globally-installed blocked set
lpm approve-scripts --global --group         # group by top-level globally-installed package
```

## What approval writes [#what-approval-writes]

The destination depends on scope: project approvals land in `package.json`, global approvals land in `~/.lpm/global/trusted-dependencies.json`. The binding shape and drift semantics are otherwise identical.

The [`lpm trust` storage contract](/docs/packages/trust#storage-contract) lists every policy destination. `lpm.lock` never stores lifecycle approvals.

### Project scope (default) [#project-scope-default]

Approvals land in `package.json > lpm > trustedDependencies` as the **rich-map form** (not the legacy array form):

```json title="package.json"
{
  "lpm": {
    "trustedDependencies": {
      "esbuild@0.25.1": {
        "integrity": "sha512-...",
        "scriptHash": "sha256-...",
        "provenanceAtApproval": {
          "present": true,
          "publisher": "github:evanw/esbuild",
          "workflowPath": ".github/workflows/release.yml",
          "workflowRef": "refs/tags/v0.25.1"
        },
        "capabilityHash": "sha256-..."
      }
    }
  }
}
```

`provenanceAtApproval` is present when verification supplies a provenance snapshot. `capabilityHash` is present when the project requests capabilities beyond the approved baseline.

### Global scope (`--global`) [#global-scope---global]

Approvals land in `~/.lpm/global/trusted-dependencies.json`, which is a separate per-machine trust file. Global approvals use the same integrity, script hash, and provenance fields:

```json title="~/.lpm/global/trusted-dependencies.json"
{
  "schema_version": 1,
  "trusted": {
    "esbuild@0.25.1": {
      "integrity": "sha512-...",
      "scriptHash": "sha256-...",
      "provenanceAtApproval": {
        "present": true,
        "publisher": "github:evanw/esbuild",
        "workflowPath": ".github/workflows/release.yml",
        "workflowRef": "refs/tags/v0.25.1"
      }
    }
  }
}
```

`approve-scripts --global` fetches the candidate's Sigstore attestation at approval time (best-effort — network failure or no-attestation degrades to `provenanceAtApproval = null`). On the next `lpm install -g`, the drift gate compares the freshly-fetched provenance against this snapshot. The project's `package.json` is not touched.

### Provenance posture during approval [#provenance-posture-during-approval]

Mutating approval flows use the same Sigstore posture chain as install: `LPM_PROVENANCE_ENFORCE` > `~/.lpm/config.toml > [sigstore].verify` > default `deny`. If that chain weakens verification to `warn` or `off`, `approve-scripts` requires a `provenance-unverified` unlock before it writes trust. Project approvals use a project unlock; `approve-scripts --global` uses a global unlock and suggests `lpm security unlock provenance-unverified --global --ttl 10m`.

`--list` and `--dry-run` are read-only and do not need that unlock. Under `--json`, CI, or non-TTY mode, LPM CLI refuses deterministically with `security_approval_required` instead of opening a native approval prompt.

### Drift semantics (both scopes) [#drift-semantics-both-scopes]

The script-hash binding is what makes drift detectable. If the next install resolves a different `integrity` or `scriptHash` for the same `name@version`, the approval doesn't apply and the package re-enters the blocked set. (See [`lpm trust diff`](/docs/packages/trust) to inspect drift events for project trust.)

## Modes [#modes]

| Mode                           | Behavior                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| (default — interactive at TTY) | Walk the blocked set one package at a time. Each card shows `name@version`, integrity, script hash, the lifecycle phases the package declares, the [static tier](/docs/packages/security-audit#layer-5-triage) (green / amber / red), and a binding-drift warning when this package was previously approved and the script content has changed since. Approve / skip / inspect per package. |
| `--list`                       | Read-only — print the blocked set, no prompts, no mutations. Mutually exclusive with `--yes`.                                                                                                                                                                                                                                                                                               |
| `--yes`                        | Bulk approve everything. Emits a warning banner. **Refuses to bulk-approve any package classified outside the green tier** — amber / amber-llm / red entries each require explicit per-package review via the interactive walk or `<pkg>` argument. Error message starts with `--yes refuses` so agents can branch on it. Mutually exclusive with `--list`.                                 |
| `<pkg>`                        | Approve a specific package directly. Skips the interactive walk and auto-confirms (no TTY prompt). Accepts `name` or `name@version`. Bare `name` must match exactly one blocked row; if multiple versions or bindings match, LPM CLI exits non-zero and lists the `name@version` candidates to disambiguate. This is the correct non-interactive path for single-package approvals.         |
| `--dry-run`                    | Run the review flow normally, but skip every write. JSON envelopes carry `"dry_run": true` so agents can detect the mode.                                                                                                                                                                                                                                                                   |

`--dry-run` composes with `--yes`, `<pkg>`, the interactive walk, and `--global` / `--json`. It's a no-op when combined with `--list` (already read-only).

`--json` is **mutually exclusive with the interactive walk**. Pair it with `--list`, `--yes`, or `<pkg>` for structured output — bare `lpm approve-scripts --json` errors out and names the valid pairings.

## Global vs project [#global-vs-project]

By default, `approve-scripts` operates on the current project's blocked set, and writes approvals into the project's `package.json`.

```bash
lpm approve-scripts --global
```

Operates on the **global** blocked set — the union of every `lpm install -g …` install root. Approvals write to `~/.lpm/global/trusted-dependencies.json` instead of any project's `package.json`.

```bash
lpm approve-scripts --global --group
```

When the global blocked set exceeds 10 entries (auto-enabled at that threshold), `--group` clusters list and review output by top-level globally-installed package. Persisted approvals still record per-dependency-binding rows under the hood.

If any global install root has a missing or unreadable `.lpm/build-state.json`, `--global --list` reports it in `unreadable_origins`. Mutating blanket approval (`--global --yes`) refuses while the aggregate is incomplete, because otherwise it could claim every global script was reviewed while some install roots were skipped. Reinstall the named globals to refresh their build-state, then approve again.

After a successful global approval, the banner enumerates the affected top-level globals and prints the matching `lpm uninstall -g <pkg> && lpm install -g <pkg>` command for each. The `--json` envelope carries the same information as `next_step.origins` and also includes `next_steps[]` command objects for agents. `--dry-run` omits both follow-up fields since no mutation occurred. Until `lpm rebuild --global` ships, reinstalling each affected origin is the only way to actually re-execute the approved scripts.

## JSON output [#json-output]

```bash
lpm approve-scripts --list --json            # structured listing
lpm approve-scripts --yes --json             # bulk approval envelope
lpm approve-scripts esbuild --json           # single-package envelope
lpm approve-scripts --list --json --dry-run  # preview plan, no writes
```

`--json` requires a companion mode (`--list`, `--yes`, or `<pkg>`) — it's not valid on its own because the interactive walk can't be expressed as JSON.

The envelope carries `schema_version`, `success`, `command`, `dry_run`, `blocked_count`, `approved_count`, `skipped_count`, and a `blocked[]` array of per-package rows: `name`, `version`, `integrity`, `script_hash`, `phases_present`, `static_tier`, `binding_drift`. Successful project approvals that actually write trust include `next_steps: [{description, command}]` with `command: "lpm rebuild"`. Under `--global`, each row also carries `origins` (the top-level globals that pulled it in), list/empty-set envelopes include `unreadable_origins`, and successful `--global --yes` runs add both `next_step: {kind: "reinstall_globals", origins: [...]}` and `next_steps[]` command objects telling the caller which `lpm uninstall -g … && lpm install -g …` pairs to run. `--dry-run` omits follow-up commands since no mutation happened.

## Flags [#flags]

| Flag        | Effect                                                                                                                             |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `<package>` | Approve a specific package directly (`name` or `name@version`). Auto-confirms in non-TTY / JSON mode.                              |
| `--yes`     | Bulk-approve every blocked package (loud). Refuses to bulk-approve amber / red tier packages.                                      |
| `--list`    | Read-only listing                                                                                                                  |
| `--dry-run` | Preview without writing                                                                                                            |
| `--global`  | Operate on the global blocked set                                                                                                  |
| `--group`   | (with `--global`) group rows by top-level globally-installed package. Auto-enabled when the global blocked set exceeds 10 entries. |

Plus the [global flags](/docs/commands#global-flags) — `--json` is especially useful here.

## See also [#see-also]

* [`lpm install`](/docs/packages/install#lifecycle-scripts) — what produces the blocked set
* [`lpm rebuild`](/docs/packages/rebuild) — runs scripts for already-trusted packages
* [`lpm trust`](/docs/packages/trust) — diff and prune the trust allowlist
* [`package.json` "lpm.trustedDependencies"](/docs/reference/package-json-lpm#trusteddependencies) — the underlying file


# lpm audit (/docs/packages/audit)



```bash
lpm audit
```

Walks every installed package and reports:

1. **Confirmed vulnerabilities** — npm packages are cross-referenced against [OSV.dev](https://osv.dev); `@lpm.dev/*` packages get vulnerability data from the registry's per-version metadata.
2. **Behavioral flags** — static analysis reports risky behaviors such as `eval`, child processes, network access, filesystem access, and native code. LPM.dev Registry metadata and local findings use the same severity policy. See the full tag list under [`lpm query`](/docs/packages/query).
3. **Quality score** — for `@lpm.dev/*` packages. Scores below 40 surface as moderate; below 20 surface as high.
4. **Dependency-confusion warnings** — when an installed `@lpm.dev/*` package's bare name collides with a popular npm package (`react`, `lodash`, etc.), the audit warns about the registry-confusion risk.
5. **Optional secret scan** — with `--secrets`, scans the installed source for hardcoded API keys, tokens, and private keys.

Works in any project, not just LPM.dev Registry-published ones. Discovery walks `lpm.lock`, `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, or `bun.lock` (text form). Without a supported lockfile, it recursively walks nested `node_modules/` package directories without following symlinks and reports that inventory as degraded.

The vulnerability scan fails closed. If OSV.dev is unavailable, returns an incomplete batch, or omits advisory details that cannot be hydrated, `lpm audit` exits non-zero and never reports a clean audit. The same applies when the LPM.dev Registry cannot return exact metadata for an installed `@lpm.dev/*` version. Under `--json`, an incomplete OSV scan returns the audit envelope with `success: false`, `osv_degraded: true`, and `osv_degraded_reason`; other registry failures use the standard error envelope.

The `signatures` subcommand is a focused check for npm registry package signatures. It verifies the installed package set against npm-compatible `dist.signatures` metadata and exits non-zero when any registry package cannot be verified.

## Foreign lockfiles [#foreign-lockfiles]

`lpm audit` reads one fixed snapshot of each selected foreign lockfile. It rejects malformed files and unsupported versions instead of using a degraded inventory.

The audit supports npm lockfile versions 1, 2, and 3. It supports pnpm lockfile major versions 5, 6, and 9.

Each foreign lockfile has these limits:

* The file size limit is 64 MiB.
* The package-entry limit is 200,000.

npm lockfiles have these additional limits:

* The package-path limit is 32 KiB.
* The package nesting limit is 256 levels.
* The dependency limit for one package is 10,000.
* The total dependency-edge limit is 1,000,000.

If a lockfile exceeds a limit, the audit exits non-zero. It does not use an ancestor lockfile or the `node_modules/` fallback.

For `bun.lockb`, LPM CLI copies the selected file before conversion. The Bun converter has these limits:

* The time limit is 30 seconds.
* The converted output limit is 64 MiB.
* The diagnostic-output limit is 64 KiB.

If Bun exceeds a limit, the audit stops the converter and exits non-zero.

## Examples [#examples]

```bash
lpm audit
lpm audit --level high              # only show high-severity findings
lpm audit --secrets                 # also scan for hardcoded credentials
lpm audit --fail-on vuln            # CI: exit non-zero only on confirmed vulns
lpm audit --fail-on behavior        # CI: exit non-zero on critical or high behaviors
lpm audit --secrets --fail-on secrets # CI: exit non-zero only on hardcoded secrets
lpm audit --fail-on all             # CI: strict union of all failure categories
lpm audit signatures                # verify npm registry package signatures
lpm audit signatures --json         # machine-readable signature report
lpm audit fix                       # bump vulnerable direct deps and reinstall
lpm audit --fix --dry-run           # plan the same fixes without writing files
lpm audit --json                    # structured output for tooling
```

## JSON package identity [#json-package-identity]

```bash
lpm audit --json
```

Each object in `packages` includes `name`, `version`, `instance_id`, and `path`.

For an LPM CLI-managed package, `instance_id` identifies the exact lockfile instance. It is `null` when the selected inventory does not contain an exact instance ID.

The `path` value identifies the directory that the audit analyzed. It points to the exact installed package materialization when that path is available.

For a `file:` or `link:` dependency, `path` identifies the original live source directory. The value is `null` when discovery cannot resolve an analyzed directory.

Two rows can have the same `name` and `version`. Their `instance_id`, `path`, and findings can differ.

Use `instance_id` as the row key when it is not `null`. Otherwise, use `path`. Do not use only `name` and `version` as a unique key.

## CI gating [#ci-gating]

`lpm audit` exits non-zero when its findings trip the active `--fail-on` policy. The implicit default is conservative — high-severity behavioral signals like `eval` don't fail CI unless you ask for them explicitly with `--fail-on all`.

An incomplete vulnerability scan always exits non-zero, regardless of `--fail-on`. A network or advisory-data failure is not evidence that the installed tree is clean.

| Policy                      | Exit code 1 when                                                                                                                                                   |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| (no flag, implicit default) | Vulnerabilities **or** critical behavioral flags (obfuscation and protestware). Excludes `high` behaviors so existing pipelines that tolerate `eval` do not break. |
| `vuln`                      | Any confirmed vulnerability — OSV (npm) or registry-side advisory (`@lpm.dev`)                                                                                     |
| `behavior`                  | Any critical **or** high behavioral flag                                                                                                                           |
| `secrets`                   | Any hardcoded-secret finding from `lpm audit --secrets`                                                                                                            |
| `all` (explicit)            | Vulnerabilities + critical behaviors + high behaviors + secrets — the strict union                                                                                 |

For finer control, use [`lpm query`](/docs/packages/query) — its CSS-like selectors target individual flags or combinations.

## Fixing vulnerabilities [#fixing-vulnerabilities]

```bash
lpm audit fix
lpm audit --fix
lpm audit fix --dry-run
```

`lpm audit fix` is the canonical form; `lpm audit --fix` is an alias for npm-compatible muscle memory. Both read the package exposed at each direct root `node_modules` path and match its exact name and version to the lockfile, so transitive copies cannot be selected merely because they have a higher matching version. If the lockfile contains multiple source identities for that same installed name and version, the dependency is reported as source-ambiguous and skipped rather than guessing or sending its name to a metadata endpoint. They support `dependencies`, `devDependencies`, `optionalDependencies`, npm aliases such as `npm:lodash@^4`, and registry advisories for `@lpm.dev/*` packages.

For npm packages, the fixer evaluates every relevant OSV affected range against available registry versions and chooses the lowest newer version proven unaffected. This preserves maintained release lines when a patch exists instead of jumping to the highest fixed version from another major line. On apply, the manifest entry is pinned to that exact planned version (npm aliases retain their `npm:` identity), and verification rejects any different installed version; dry-run and apply therefore report the same target. Complex or non-registry source protocols such as `catalog:`, `workspace:`, `file:`, git, and JSR are reported as skipped rather than rewritten into semver dependencies. Transitive-only findings are not changed.

The fix pass currently operates only in LPM CLI-managed projects. It does not rewrite npm, pnpm, Yarn, or Bun lockfiles.

Fix and dry-run planning hold the project's install transaction lock against other LPM CLI operations. Before the first write, a real fix compares the exact `package.json` and `lpm.lock` bytes used for planning.

If either file changed, the fix exits without an overwrite. It also exits if a different `lpm.lock` became active.

The fix then snapshots `package.json`, `lpm.lock`, and `lpm.lockb`. It invalidates the install cache, runs a silent install, and checks the exact installed version.

The version check uses OSV.dev or exact LPM.dev Registry metadata. Thus, `--json` emits one audit-fix document without nested install output.

A failed install or version check restores the snapshotted files. The transaction does not copy `node_modules/`.

If the installed tree differs after a failed fix, run `lpm install` to make it match the restored lockfile.

`--dry-run` reports the same plan without mutating `package.json`, `lpm.lock`, or `node_modules`.

## Registry signatures [#registry-signatures]

```bash
lpm audit signatures
```

The command verifies npm registry package signatures for the installed tree. LPM CLI reads stored signatures and publication times from `lpm.lock` when available.

If stored evidence is incomplete, LPM CLI gets exact metadata from the authorized registry. A signature with an expiring key requires a publication time.

For foreign lockfiles, the recorded tarball URL must match an authorized registry path. This path can match npm or LPM.dev Registry.

The path can also match a registry that is configured in `.npmrc`.

LPM CLI keeps the configured registry path and its authentication. It never trusts an unconfigured origin from a foreign lockfile.

Human output is intentionally compact:

```text
! Registry signatures · 1 verified · 1 not verified
  unsigned-pkg@1.0.0  missing dist.signatures
```

A clean tree exits zero:

```text
✓ Registry signatures verified · 247 verified
```

Under `--json`, the envelope includes aggregate counts and one row per package:

```json
{
  "success": false,
  "scanned": 2,
  "verified": 1,
  "not_verified": 1,
  "skipped": 0,
  "packages": [
    { "name": "signed-pkg", "version": "1.0.0", "status": "verified", "signatures_verified": 1 },
    {
      "name": "unsigned-pkg",
      "version": "1.0.0",
      "status": "not_verified",
      "reason": "missing_signatures",
      "reason_detail": "missing dist.signatures",
      "signatures_verified": 0
    }
  ]
}
```

Statuses are `verified`, `not_verified`, or `skipped`. Skipped rows cover packages outside this signature surface, such as non-registry sources and `@lpm.dev/*` packages. Any `not_verified` row makes the command exit with code `1`.

## Severity levels [#severity-levels]

```bash
lpm audit --level high
```

Suppresses anything below the threshold. Ladder from most-to-least severe: `critical` → `high` → `moderate` (alias `medium`) → `info` (alias `low`). Default is `info` (show everything). Any other value is rejected as a usage error before the audit starts.

Info results include behavioral metadata such as environment-variable access, URL literals, and high-entropy strings. Human output puts these results under `Behavioral metadata`. The final summary does not count them as suspicious packages or security issues. These results do not make the command fail by themselves.

Critical behavioral results mean high-confidence obfuscation or protestware. Scores above `0.3` through `0.7` are Info-level possible obfuscation. Scores above `0.7` are Critical.

The scanner detects capabilities, not data flow or command intent. Environment access plus network access does not prove exfiltration. A detected `rm -rf` shell command remains a High shell and child-process signal.

For `@lpm.dev/*` packages, the human report includes registry and local findings. Registry advisories and security findings keep their published severity, including Critical.

## Secret scanning [#secret-scanning]

```bash
lpm audit --secrets
```

Scans the installed source tree under `node_modules/` for hardcoded API keys, access tokens, private keys, and similar credentials — the same scanner that runs as part of `lpm publish`. Useful as a final check before shipping a Docker image or deploying.

Requires `node_modules/` to exist (errors with a `run \`lpm install\` first`hint otherwise).`--secrets`still runs as its own scan mode, but it participates in`--fail-on\`:

* `lpm audit --secrets` uses the command's default gate policy (`all`), so a detected secret exits non-zero.
* `lpm audit --secrets --fail-on secrets` gates on secret findings only.
* `lpm audit --secrets --fail-on vuln` or `--fail-on behavior` keeps the secret scan informational.
* `--level` is still ignored in secrets mode.

## Flags [#flags]

| Flag                                       | Effect                                                                                                          |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `--level <critical\|high\|moderate\|info>` | Minimum severity to report (default: `info`; aliases: `medium`, `low`)                                          |
| `--fail-on <vuln\|behavior\|secrets\|all>` | CI exit-code policy (`secrets` applies to `--secrets`; `all` includes secret findings too)                      |
| `--secrets`                                | Scan installed source for hardcoded secrets (separate scan mode; consults `--fail-on`, still ignores `--level`) |
| `--fix`                                    | Alias for `lpm audit fix`                                                                                       |
| `--dry-run`                                | With `--fix`, show the fix plan without changing files                                                          |

Plus the [global flags](/docs/commands#global-flags) — `--json` is especially useful for piping into CI gates.

## Subcommands [#subcommands]

| Subcommand   | Effect                                                                                        |
| ------------ | --------------------------------------------------------------------------------------------- |
| `fix`        | Update vulnerable direct dependency instances to verified unaffected versions, then reinstall |
| `signatures` | Verify npm registry package signatures for the installed tree                                 |

## See also [#see-also]

* [`lpm query`](/docs/packages/query) — inspect packages by behavioral tag
* [Security audit](/docs/packages/security-audit) — concept overview and tag reference
* [`lpm publish`](/docs/packages/publish) — runs the same secret scanner before upload


# lpm cache (/docs/packages/cache)



```bash
lpm cache <action>
```

`lpm cache` covers two adjacent concerns: ephemeral caches under `~/.lpm/cache/` (registry metadata, task results, dlx installs, and the verified MCP runtime) and the global package store at `~/.lpm/store/`.

## Examples [#examples]

```bash
lpm cache path                          # print ~/.lpm/cache/
lpm cache path metadata                 # print ~/.lpm/cache/metadata/
lpm cache path mcp                      # print ~/.lpm/cache/mcp/

lpm cache clean                         # wipe metadata + tasks + dlx + mcp
lpm cache clean metadata                # only wipe registry metadata
lpm cache clean tasks                   # only wipe task-runner cache
lpm cache clean dlx                     # only wipe dlx install cache
lpm cache clean mcp                     # only wipe the verified MCP runtime

lpm cache status                        # local task cache usage + remote status
lpm cache status --json                 # structured status for CI

lpm cache prune                         # preview orphan store entries (dry-run)
lpm cache prune --apply                 # actually remove orphans
lpm cache prune --max-age 30d --apply   # only entries last touched > 30d ago
lpm cache prune --project ./my-app      # manual repair: walk one project only
```

## Actions [#actions]

| Action   | Effect                                                                                                                    |
| -------- | ------------------------------------------------------------------------------------------------------------------------- |
| `path`   | Print the cache root (`~/.lpm/cache/`) or one of its subcategories                                                        |
| `clean`  | Wipe ephemeral caches (`~/.lpm/cache/`). `lpm cache clear` is an alias.                                                   |
| `status` | Report local task-cache usage and hosted remote-cache status for the current project                                      |
| `prune`  | Reference-aware orphan removal from the global store (`~/.lpm/store/`) plus a sweep of deferred global-install tombstones |

`clean` accepts an optional subcategory (`metadata`, `tasks`, `dlx`, `mcp`); without one, all four are wiped. `clean` never touches the global package store — for that, use `prune`.

The MCP runtime is locked while an editor is using it. A blanket `lpm cache clean` skips that one category and reports it as in use while still cleaning the others. `lpm cache clean mcp` fails with an actionable error instead of deleting files underneath a running server.

## Status [#status]

`lpm cache status` reports the local task-cache path and size. If the current project has `lpm.json > remoteCache.enabled`, it also calls the hosted cache status endpoint and reports the remote status, usage, and quota when the server returns them.

Remote status failures do not make `lpm cache status` fail; the JSON envelope includes `remote.error` so CI can decide whether to warn or ignore.

A paused or disabled remote cache does not stop local tasks. LPM CLI reports the cache state and continues locally. An authorization warning indicates that the token lacks the required permission.

Organization cache access continues during the billing grace period. After grace expires, remote reads and uploads stop, including uploads that are still in progress. The dashboard and `lpm cache status` report `paused`. Restore the organization subscription to resume cache access.

## Prune [#prune]

`lpm cache prune` walks both the default v2 store and the experimental v3 store, removes entries no project still references, then sweeps any deferred global-install tombstones left behind by `lpm uninstall -g`. Default is dry-run; pass `--apply` to mutate.
Human output ends with an elapsed summary; `--json` keeps the machine-readable report shape and includes `success`.

The tombstone sweep runs even when the orphan walk is skipped (registry missing or corrupt — see [Safety rails](#safety-rails)), so `lpm uninstall -g`'s deferred cleanup always has a way to reach disk.

If the project registry is corrupt, prune disables only the project-reachability
walk. It still plans or applies crash recovery and age-based eviction for
compatibility islands and native build artifacts, cleans their orphaned lock
files, and sweeps global-install tombstones. In JSON mode,
`registry_corrupt: true` and `registry_corrupt_reason` carry the registry
problem; tombstone inspection and sweep failures are reported separately.

### How it decides what to keep [#how-it-decides-what-to-keep]

LPM CLI tracks every project it has installed into in a registry at `~/.lpm/known-projects.json`. For each registered project, prune walks the project's `node_modules/` symlinks back into `~/.lpm/store/v2/links/<graph-key>/` or `~/.lpm/store/v3/links/<graph-key>/`, then BFS through each link entry's sidecar metadata to mark every reachable graph-key. Anything not marked is an orphan. Object directories under each layout's `objects/` directory are pruned by collecting every surviving link entry's object reference and removing the rest.

For v3, each surviving object identifies a live source record; source records identify tree manifests; manifests identify content-and-mode blobs. Prune retains that complete transitive closure and reports orphan source metadata, trees, blobs, and macOS materialized-tree caches separately. If a live source record or manifest is malformed, prune fails closed instead of guessing which CAS content is safe to delete.

On macOS, cached compatibility islands under each virtual-store layout's `compat/` directory are pruned by crash recovery and LRU age: incomplete islands are eligible immediately, and complete islands are eligible only when their last-used sentinel is older than `--max-age`.

Native lifecycle-build artifacts under `~/.lpm/store/v2/builds/` use the same
crash-recovery and LRU rule on macOS and Linux. An incomplete publication is
eligible immediately. A complete artifact is kept unless its last-used
sentinel is older than `--max-age`. This cleanup is independent of project
reachability, so it remains safe and active when the project registry is
missing or corrupt. `--apply` also removes advisory build-key lock files that
no longer have an artifact and graph-entry lock files whose link entry was
removed.

Stale registry entries (projects that have moved or been deleted) are dropped silently during the walk. The `--max-age` filter operates on each link entry's `last_referenced_at` timestamp (recorded in the entry's sidecar at first population, refreshed by `lpm install` cache hits) — not on the project registry's `last_seen`, which is set at install-time only.

### Manual repair: `--project <path>` [#manual-repair---project-path]

When the registry is missing, corrupted, or doesn't reflect actual machine state (after a restore, after copying `~/.lpm/` between machines, etc.), pass `--project <path>` to walk only that project's lockfile + `node_modules/` for roots. The registry is ignored.

This mode is for scripted cleanup of a single project's footprint. **Default mode is safer when you have multiple projects on the machine** — `--project` only sees the one you point it at, so any link entry referenced by another project would look like an orphan.

### Safety rails [#safety-rails]

* **Dry-run by default.** No deletions without `--apply`.
* **Reachability-degraded mode.** If the registry is missing, corrupt, or unreadable AND no `--project <path>` is supplied, prune cannot identify link/object or v3 CAS orphans safely (without trustworthy roots, every link entry would look unreachable). `--apply` skips those reachability-dependent removals. Compatibility-island and native-build-artifact cleanup, orphan lock cleanup, and the tombstone sweep still run. LPM CLI prints a warning naming the cause; for corrupt registries it includes the parser's reason ("not valid JSON" / "unknown schema version" / I/O details) so you can repair the file or delete it for a clean rebuild.
* **Malformed live CAS metadata fails closed.** Prune does not delete v3 trees or blobs when it cannot prove the complete live closure.
* **Canonicalized paths.** The registry stores fully resolved absolute paths, so symlink-cwd quirks don't accumulate aliased entries.
* **Atomic registry rewrites.** Updates go through `<path>.tmp.<pid>` → rename. No partial-write corruption.
* **In-flight installs are respected.** Prune coordinates with concurrent `lpm install`, `lpm audit`, and `lpm query` invocations through the store reader/writer lock — dry-run takes the shared half, `--apply` takes the exclusive half. See [`lpm store` — Locking model](/docs/infra/store#locking-model).

## Flags [#flags]

| Flag               | Applies to | Effect                                                                                                  |
| ------------------ | ---------- | ------------------------------------------------------------------------------------------------------- |
| `--apply`          | `prune`    | Actually remove orphans + sweep tombstones (default is dry-run)                                         |
| `--max-age <DUR>`  | `prune`    | Only prune entries last touched older than this. Accepts `<N>d` (days) or `<N>h` (hours) — nothing else |
| `--project <PATH>` | `prune`    | Manual repair: walk only this project's roots, ignore the registry                                      |

Plus the [global flags](/docs/commands#global-flags).

## JSON output [#json-output]

```bash
lpm cache status --json

lpm cache prune --json            # dry-run report
lpm cache prune --apply --json    # post-apply report (same shape, mutations recorded)
```

`status --json` emits:

| Field                                       | Meaning                                                      |
| ------------------------------------------- | ------------------------------------------------------------ |
| `local.path` / `local.bytes` / `local.size` | Local task-cache location and usage                          |
| `remote.enabled` / `remote.status`          | Hosted cache configuration and server status                 |
| `remote.usage_bytes` / `remote.limit_bytes` | Hosted usage and included quota when the server returns them |
| `remote.error`                              | Status lookup error, if any                                  |

`prune --json` reports both the orphan walk and the tombstone sweep:

| Field                                                                           | Meaning                                                                                                                                                                                                                    |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`                                                                       | `false` when prune could not complete every requested inspection or cleanup step                                                                                                                                           |
| `applied`                                                                       | `true` when `--apply` was passed                                                                                                                                                                                           |
| `projects_walked` / `registry_entries_dropped`                                  | Project-registry coverage stats                                                                                                                                                                                            |
| `link_entries_total` / `_reachable` / `_orphaned[]`                             | Link-entry walk results; `_orphaned[]` is a list of paths                                                                                                                                                                  |
| `object_entries_total` / `_reachable` / `_orphaned[]`                           | Object-entry walk results                                                                                                                                                                                                  |
| `cas_trees_total` / `cas_tree_files_orphaned[]`                                 | v3 tree-manifest inventory and orphan paths                                                                                                                                                                                |
| `cas_blobs_total` / `cas_blob_files_orphaned[]`                                 | v3 content-and-mode blob inventory and orphan paths                                                                                                                                                                        |
| `cas_source_record_files_orphaned[]` / `cas_source_validation_files_orphaned[]` | Unreferenced v3 source identity and validation metadata                                                                                                                                                                    |
| `cas_materialized_total` / `cas_materialized_entries_orphaned[]`                | v3 macOS materialized-tree cache inventory and orphan paths                                                                                                                                                                |
| `compat_islands_total` / `_orphaned[]`                                          | Cached compatibility-island results; `_orphaned[]` lists incomplete or stale LRU islands                                                                                                                                   |
| `build_artifacts_total` / `_orphaned[]`                                         | Native lifecycle-build artifact results; `_orphaned[]` lists incomplete or stale LRU artifacts                                                                                                                             |
| `bytes_freed_or_eligible` / `logical_bytes_freed_or_eligible`                   | Logical bytes the orphan walk would free (dry-run) or selected for removal (apply)                                                                                                                                         |
| `observed_physical_bytes_freed`                                                 | Filesystem usage actually released by an applied prune; may differ from logical bytes on copy-on-write or hardlinked store-internal data                                                                                   |
| `registry_missing` / `registry_corrupt` / `registry_corrupt_reason`             | Reachability-degraded flags — when either is true, link/object orphan arrays are empty because that walk was skipped; compatibility-island, native-build-artifact, lock, and tombstone cleanup still operate independently |
| `tombstones_pending` / `_swept` / `_retained[]` / `tombstone_bytes_freed`       | Tombstone sweep results — `_swept` and `_bytes_freed` are zero on dry-run                                                                                                                                                  |

`lpm cache clean` emits a parallel `{success, cleaned[], skipped[], total_bytes_freed, total_freed}` envelope per subcategory. A busy MCP runtime appears in `skipped` with `reason: "in use"`.

## See also [#see-also]

* [Content-addressable store](/docs/packages/content-addressable-store) — design overview, on-disk layout
* [`lpm store`](/docs/infra/store) — integrity checks, store path, blunt full wipe
* [`lpm doctor`](/docs/infra/doctor) — surfaces store-size hints alongside other checks


# lpm catalog (/docs/packages/catalog)



```bash
lpm catalog list
lpm catalog list --unused
lpm catalog show --resolved
```

`lpm catalog` inspects the version catalogs declared by the workspace root. Use it to find unused catalog pins before cleanup, or to verify which catalog references were resolved into `lpm.lock`.

Catalogs are declared in root `package.json > catalogs` or in pnpm-style `pnpm-workspace.yaml > catalog` / `catalogs`. Root and workspace-member manifests reference them with `"catalog:"` for the default catalog or `"catalog:<name>"` for a named catalog.

## Examples [#examples]

```bash
lpm catalog list                 # every declared catalog entry
lpm catalog list --unused        # only entries no manifest references
lpm catalog show --resolved      # resolved catalog snapshots from lpm.lock
lpm catalog show --resolved --json
```

## List Declared Entries [#list-declared-entries]

```bash
lpm catalog list
```

`list` reads the root catalog declarations and scans the root package plus every workspace member for `catalog:` references in dependency sections. Human output prints one row per entry:

```text
default react ^18.2.0 used
default react-dom ^18.2.0 used
testing vitest ^1.0.0 unused
```

Use `--unused` before enabling catalog cleanup:

```bash
lpm catalog list --unused
```

`--unused` filters out entries that are still referenced. If none remain, LPM CLI prints `No unused catalog entries.`

## Show Resolved Snapshots [#show-resolved-snapshots]

```bash
lpm catalog show --resolved
```

`show --resolved` reads catalog snapshots from `lpm.lock`. These snapshots record the catalog entry, the saved catalog reference, the resolved version, and the original catalog range:

```text
default react catalog: -> 18.2.0 (^18.2.0)
testing vitest catalog:testing -> 1.6.1 (^1.0.0)
```

`lpm catalog show` currently requires `--resolved`. If a manifest references a catalog entry that is missing from `lpm.lock`, LPM CLI exits non-zero and asks you to run `lpm install` so the lockfile snapshot cannot look falsely clean.

## JSON Output [#json-output]

Pass the global `--json` flag for automation:

```bash
lpm catalog list --unused --json
```

```json
{
  "success": true,
  "mode": "unused",
  "count": 1,
  "used_count": 2,
  "unused_count": 1,
  "entries": [
    {
      "catalog": "testing",
      "package": "vitest",
      "specifier": "^1.0.0",
      "used": false
    }
  ]
}
```

```bash
lpm catalog show --resolved --json
```

```json
{
  "success": true,
  "count": 1,
  "entries": [
    {
      "catalog": "default",
      "package": "react",
      "specifier": "^18.2.0",
      "version": "18.2.0",
      "reference": "catalog:"
    }
  ]
}
```

## Cleanup Policy [#cleanup-policy]

By default, LPM CLI preserves unused catalog entries exactly as written. Set `package.json > lpm.cleanupUnusedCatalogs = true` to prune unreferenced root `package.json > catalogs` entries after successful installs. In pnpm-style workspaces, `pnpm-workspace.yaml > cleanupUnusedCatalogs: true` does the same for `pnpm-workspace.yaml > catalog` / `catalogs`, unless `package.json > lpm.cleanupUnusedCatalogs` explicitly overrides it.

Run `lpm catalog list --unused` first when you want a dry inspection before the next install mutates catalog declarations.

## Flags [#flags]

| Command            | Flag         | Effect                                                       |
| ------------------ | ------------ | ------------------------------------------------------------ |
| `lpm catalog list` | `--unused`   | Show only catalog entries no root/member manifest references |
| `lpm catalog show` | `--resolved` | Read resolved catalog entries from `lpm.lock`                |

Plus the [global flags](/docs/commands#global-flags). `--json` emits structured envelopes for both subcommands.

## See also [#see-also]

* [Workspaces](/docs/packages/workspaces#catalogs) - catalog declarations and member references
* [Save policy](/docs/packages/save-policy#cli-flags) - saving dependencies as `catalog:` references
* [`lpm install --catalog`](/docs/packages/install) - force a catalog save for one invocation
* [`package.json > lpm`](/docs/reference/package-json-lpm#catalogs) - catalog fields and cleanup policy


# Content-addressable store (/docs/packages/content-addressable-store)



Every package LPM CLI downloads goes into a **single global store** at `~/.lpm/store/`. Canonical package content is deduplicated there, and projects reuse graph-keyed wrapper directories with their dependency symlinks already laid out. LPM CLI keeps the writable files exposed through project dependencies on independent inodes, so a write through `node_modules` cannot alter the canonical bytes used to materialize future entries.

The default layout is **v2**. An experimental **v3** layout adds file-level content addressing and can be selected explicitly with `LPM_STORE_VERSION=v3`; LPM CLI never selects it automatically based on the operating system, filesystem, or CI environment.

This page covers the design — what the store is, how packages get into it, how projects reach into it from `node_modules/`, and the maintenance surface.

## On-disk layout [#on-disk-layout]

```text
~/.lpm/store/
└── v2/
    ├── objects/                            ← extracted package bytes
    │   ├── sha512-aabb…/                   ← one entry per content hash
    │   │   ├── package.json
    │   │   ├── index.js
    │   │   └── …
    │   └── …
    ├── links/                              ← per-graph wrapper directories
    │   ├── react@19.2.4+a1b2c3d4/
    │   │   ├── node_modules/
    │   │   │   ├── react/                  ← link from objects/<sri>
    │   │   │   ├── scheduler/              ← symlink to a sibling link entry
    │   │   │   └── …
    │   │   └── .lpm-link-meta.json         ← sidecar (object SRI, dep targets)
    │   └── …
    └── compat/                             ← macOS cached compatibility islands
        ├── <island-key>/
        │   ├── <graph-key>/node_modules/<pkg>/
        │   └── .lpm-island-complete
        └── …
```

Three main arms under the default `v2/` layout:

* **`objects/<sri>/`** — content-addressable extracted bytes. One entry per unique tarball content; shared by every package version with that hash. The SRI is the same one verified against the lockfile.
* **`links/<graph-key>/`** — per-graph wrapper directories. The graph-key folds in the package's name, version, platform tuple, **linker mode** (hoisted vs isolated produces distinct keys), peer-context (empty in hoisted mode), dep edges, alias edges, root-link names, source disambiguator (Registry vs Tarball/Git), and **patch fingerprint** (a `lpm patch`'d install gets its own link directory and never shares bytes with the unpatched coords). Two installations of the same `(name, version)` are interchangeable iff every one of those inputs matches — so projects with compatible graphs share the same link entry, while a hoisted vs isolated install of the same dep set produce two link entries.
* **`compat/<island-key>/`** — macOS-only cached compatibility islands for framework/tooling bins that need a project-local dependency island. The island key includes each entry's graph key, source SRI, and current link-entry content digest, so stable-SRI local sources get a fresh island when their bytes change.

Each link entry's `node_modules/<pkg>/` is an independent copy-on-write clone or copy of the corresponding `objects/<sri>/` directory. Sibling entries (`scheduler/` next to `react/` above) are **symlinks** pointing at other graph-keys' link entries — not copies. The canonical object and the project-writable package tree never share a hardlink inode.

Experimental v3 retains `objects/`, `links/`, `compat/`, and build-cache directories, then adds a file-level CAS:

```text
~/.lpm/store/v3/
├── objects/<sri>/                     ← extracted object projections
├── links/<graph-key>/                 ← graph-keyed wrapper directories
├── blobs/blake3/<shard>/<digest>-<mode>
├── trees/<shard>/<tree-digest>.msgpack
├── sources/<shard>/<source-digest>.msgpack
├── metadata/source-validations/<shard>/…
└── materialized/<shard>/<tree-digest>/ ← macOS clone source cache
```

Blob identity includes the BLAKE3 content digest, normalized file mode, and size. Tree manifests describe directories, files, and permitted local-source symlinks. The `objects/` projection may share hardlink inodes with immutable blobs inside the store, but link entries still receive independent writable inodes before a project can reach them.

## How packages enter the store [#how-packages-enter-the-store]

With the default v2 layout, `lpm install` extracts a package into the store exactly once per content hash and materializes a link entry exactly once per graph-key.

1. **Existence check (object).** If `~/.lpm/store/v2/objects/<sri>/` exists, skip extraction entirely. Content-level dedup.
2. **Extract.** Gzip-decompress + tar-walk into a staging dir under `~/.lpm/store/v2/objects/`.
3. **Security scan.** Behavioral analysis runs against the extracted source as part of the same pass. Results persist into `.lpm-security.json` next to the package files so future installs and audits don't re-scan. See [Security audit](/docs/packages/security-audit).
4. **Finalize object.** Write `.integrity` (the verified SRI) plus `.lpm-object-integrity` (the reuse-validation sidecar), then atomic-rename the staging dir into its final `objects/<sri>/` location.
5. **Existence check (link entry).** If `~/.lpm/store/v2/links/<graph-key>/` exists, skip materialization.
6. **Materialize link entry.** Clone or copy the package bytes from `objects/<sri>/` into the link entry's `node_modules/<pkg>/`. Write sibling-dep symlinks for the package's declared deps. Write the `.lpm-link-meta.json` sidecar.
7. **Finalize link entry.** Atomic-rename the staging dir into its final `links/<graph-key>/` location.

The atomic renames are the visibility boundary. A concurrent install racing for the same content or graph-key either sees the entry or doesn't — never a partially-extracted state.

Experimental v3 follows the same object/link publication flow while ingesting each extracted regular file into `blobs/`, publishing a content-addressed tree manifest, and recording the source-to-tree mapping. Existing v2 content migrates lazily when an explicit v3 install needs it; switching back to v2 also rematerializes lazily and works offline when the required v3 content is present.

## Store integrity mode [#store-integrity-mode]

LPM CLI validates an existing `objects/<sri>/` directory before reusing it. The persistent mode lives in `~/.lpm/config.toml > integrity` and is managed by [`lpm config integrity`](/docs/infra/config#store-integrity):

```bash
lpm config integrity --set source   # default
lpm config integrity --set tree     # stricter, slower
```

`source` is the default. It checks the object's source identity sidecar against the expected source SRI without rehashing every expanded file, so warm installs stay fast.

`tree` rehashes the expanded object tree before reuse and records a `.lpm-tree-snapshot.json` metadata fast path for later checks. Use it when detecting local tampering or corruption inside `~/.lpm/store/v2/objects/` matters more than warm-install speed.

Experimental v3 validates source records and tree manifests, fingerprints every referenced blob's filesystem identity and change metadata, and rehashes blobs when that fingerprint changes. It does not weaken same-size tamper detection by trusting restored modification times.

This is separate from `lpm install --strict-integrity`: that flag controls whether tarball URL dependencies must declare an inline SRI before first use. `integrity = "tree"` controls how already-expanded v2 store objects are validated before reuse.

## How projects reach into the store [#how-projects-reach-into-the-store]

Project `node_modules/<pkg>/` is a **symlink** straight into the selected layout's global link entry:

```text
<project>/node_modules/react
  → ~/.lpm/store/v2/links/react@19.2.4+a1b2c3d4/node_modules/react

<project>/node_modules/.bin/
  └── (real dir; bin shims are project-local)
```

When Node resolves `require("scheduler")` from inside `react/`, it walks up to the link entry's `node_modules/` and finds `scheduler/` as a sibling symlink — pointing at *another* link entry that scheduler is materialized under. Same Node-resolver semantics as a flat or pnpm-isolated layout, just with the wrapper tree relocated to a global, shared location.

| Filesystem                                                                           | Object → link-entry mechanism                        |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| macOS on APFS                                                                        | Recursive `clonefile(2)` copy-on-write clone.        |
| Linux on Btrfs, reflink-enabled XFS, bcachefs, and other reflink-capable filesystems | Per-file copy-on-write reflink via `ioctl(FICLONE)`. |
| Linux on ext4 or another non-reflink filesystem                                      | Independent file copy.                               |
| Windows                                                                              | Independent file copy.                               |

There is no hardlink fallback from canonical object/blob bytes into project-writable package files. Copy-on-write filesystems provide physical sharing with independent inodes; other filesystems preserve the same mutation-isolation property by storing independent copies.

Project `node_modules/<pkg>` symlinks themselves are real symlinks on every platform (Windows uses directory symlinks / junctions).

## Cross-project sharing [#cross-project-sharing]

Because link entries are keyed by graph context — not by project — every project that resolves the same dependency graph reuses the same on-disk wrapper directory. Five projects using `react@19.2.4` with the same peer-context cost one materialization, not five. The first install pays the materialization cost; the rest are pure symlink creation.

This survives `rm -rf node_modules`. The canonical bytes and wrapper symlinks live in the selected store-version directory, not in the project. A re-install only needs to recreate the project-side `node_modules/<pkg>` symlinks — which is why warm install is fast.

## Isolated vs hoisted [#isolated-vs-hoisted]

Both linker modes use the same underlying objects + link entries — they differ only in how the project's `node_modules/` is shaped:

```text
# Hoisted (default — v2 virtual-store layout)
<project>/node_modules/
  react/      → ~/.lpm/store/<version>/links/react@…/node_modules/react
  …
  .bin/
    react → …
```

```text
# Isolated (--linker=isolated, auto-default for workspaces)
<project>/node_modules/
  react/  → ~/.lpm/store/<version>/links/react@…/node_modules/react
  .bin/
    react → …
```

Hoisted exposes root dependencies at the project root and lets each package see its package-local dependency links inside the shared link entry. Isolated only exposes direct root deps and keeps each consumer's dependency view strict. Both reach the same canonical bytes via the global store.

## Where the store lives [#where-the-store-lives]

| Path                      | Override with                                     |
| ------------------------- | ------------------------------------------------- |
| `~/.lpm/store/` (default) | `LPM_HOME=<path>` (moves the entire LPM CLI root) |

`LPM_HOME` is useful in CI for hermetic runs — point it at a workspace-local directory and the store survives only as long as the runner does.

If `LPM_HOME` ends up on a network filesystem, LPM CLI emits a one-time warning at startup pointing at performance issues — copy-on-write cloning, file copies, and metadata-heavy store operations can degrade badly over NFS.

## Maintenance [#maintenance]

The store grows monotonically as new graph contexts appear. Two surfaces clean it up:

* [`lpm cache prune`](/docs/packages/cache) — reference-aware orphan removal. Walks `~/.lpm/known-projects.json` (the registry of every project LPM CLI has installed into), traces each project's `node_modules/` symlinks back into the v2 and v3 stores, and removes link entries + objects no longer reachable from any registered project. For v3 it also removes unreferenced source metadata, trees, blobs, and materialized tree caches. Cached compatibility islands are removed by crash recovery and optional LRU age. Safe to run regularly. Default is dry-run; pass `--apply` to delete.
* [`lpm store`](/docs/infra/store) — verify integrity, print the store path, blunt full wipe.

```bash
lpm cache prune                         # preview orphans
lpm cache prune --apply                 # actually remove
lpm cache prune --max-age 30d --apply   # only prune entries last touched > 30d ago

lpm store path                          # print ~/.lpm/store/
lpm store verify                        # fast structural + CAS metadata check
lpm store verify --deep                 # also rehash v3 blobs and cross-check lpm.lock
```

If the project registry is missing, corrupt, or unreadable AND no explicit
`--project <path>` is supplied, `lpm cache prune` skips only the link/object
reachability walk (without trustworthy roots, every link entry would look
unreachable). `--apply` still performs reachability-independent crash recovery
and age eviction for compatibility islands and native build artifacts, removes
their orphaned locks, and runs the global-install tombstone sweep. The warning
names the cause; for corrupt registries it includes the parser's reason so you
can repair or delete the file. Run `lpm install` in a project to populate a
fresh registry, or pass `--project <path>` to walk a specific project.

## Concurrency [#concurrency]

The store is reads-mostly under normal operation. Three scenarios coordinate explicitly:

* **Concurrent extract for the same content.** The atomic rename + the existence check race-handle this: whichever install wins the rename publishes the entry; the other install sees the win on the next iteration and skips.
* **Concurrent install vs. `lpm cache prune`.** A writer-preference reader/writer protocol (data lock + writer-intent gate + writer-queue baton at `~/.lpm/store/.gc.lock*`) lets multiple installs run concurrently while serializing prune behind in-flight installs. See [`lpm store` — Locking model](/docs/infra/store#locking-model).
* **Concurrent prune invocations.** Serialize through the same protocol.

## Security caches [#security-caches]

After the security scan finishes, results persist alongside the package as `.lpm-security.json` inside the object directory. [`lpm audit`](/docs/packages/audit) reads from these caches; a second `lpm audit` run on an unchanged tree is essentially free. `lpm store verify --fix` refreshes stale security caches without re-extracting.

## When does this matter? [#when-does-this-matter]

* **Disk usage on a multi-project machine** — without the global link entries, every project would re-materialize its own wrapper tree. With them, the wrapper population is shared across every project with a matching graph context.
* **Warm install speed** — `rm -rf node_modules` survives the global store. Re-install is symlink creation, not extraction.
* **Cold install across projects** — the second project to need a given dep at a given graph context just makes a project-side symlink; no extraction, no clonefile.
* **Audit speed** — security analysis runs once per content hash, not per project.

## See also [#see-also]

* [`lpm cache prune`](/docs/packages/cache) — orphan removal for the global store
* [`lpm store`](/docs/infra/store) — integrity, path, blunt full wipe
* [Lockfile](/docs/packages/lockfile) — what records the integrity hash the store verifies against
* [`lpm install --linker`](/docs/packages/install) — picks isolated vs hoisted layout
* [Environment variables](/docs/reference/env-vars#filesystem-layout) — `LPM_HOME` to relocate the store
* [Security audit](/docs/packages/security-audit) — the analysis pipeline that writes `.lpm-security.json`


# Dependency graph (/docs/packages/dependency-graph)



The dependency graph shows the packages in `lpm.lock` and the relationships between them.

Use the graph to inspect resolved versions, trace transitive packages, find duplicates, or export dependency data.

[`lpm graph`](/docs/packages/graph) is the command interface. This page explains how to interpret its results.

## Understand the graph [#understand-the-graph]

The project and each resolved package are graph nodes. A connection from one node to another shows a dependency relationship.

| Node type          | Meaning                                                           |
| ------------------ | ----------------------------------------------------------------- |
| Project root       | The current project from `package.json`                           |
| Direct package     | A package in the selected `dependencies` or `devDependencies` set |
| Transitive package | A package required by another package                             |
| Duplicate package  | One package name that has more than one resolved version          |

A package name and version identify one node. For example, `react@18.3.1` and `react@19.0.0` are separate nodes.

LPM CLI marks both nodes as duplicates because the package name is the same. A repeated path to one version is not a duplicate.

## Know where the data comes from [#know-where-the-data-comes-from]

LPM CLI reads resolved packages and dependency relationships from `lpm.lock`. It does not run the resolver or contact a registry.

LPM CLI reads `package.json` for the project identity and direct-dependency classification. The command uses only local project files.

If `package.json` changed after the last install, the graph can differ from the current manifest. Run an install before you inspect the graph:

```bash
lpm install
lpm graph
```

The graph command requires a usable `lpm.lock`. The HTML format also writes `.lpm/graph.html` in the project.

## Inspect the complete tree [#inspect-the-complete-tree]

Show the complete dependency tree in the terminal:

```bash
lpm graph
```

The project is the first node. Direct packages follow the project, and transitive packages follow their parents.

Use `lpm ls` for the same command:

```bash
lpm ls
```

## Inspect one package subtree [#inspect-one-package-subtree]

Give the command a package name to show that package and its dependencies:

```bash
lpm graph react
```

If the graph contains multiple versions, a name-only query selects the version nearest to the project root.

Give the complete package identity to select an exact version:

```bash
lpm graph react@18.3.1
```

## Find why a package is installed [#find-why-a-package-is-installed]

Use [`lpm why`](/docs/packages/graph#lpm-why-package) to show each path from the project to a package:

```bash
lpm why lodash
```

You can use the equivalent graph form:

```bash
lpm graph --why lodash
```

Each path identifies the packages that require the selected package. The result also identifies multiple installed versions.

If an override or patch affected the package, the result includes the recorded change.

LPM CLI returns every path from the project root to each package version that matches the name.

Dense dependency graphs can produce large output because the number of paths can increase exponentially with graph depth.

## Find duplicate versions [#find-duplicate-versions]

Use the statistics format for a short dependency summary:

```bash
lpm graph --format stats
```

Example output:

```text
42 packages (3 LPM, 37 npm)
Max depth: 5
Duplicates: 1
  react@18.3.1, react@19.0.0
```

| Result       | Meaning                                                       |
| ------------ | ------------------------------------------------------------- |
| `packages`   | Resolved package nodes. This count excludes the project root. |
| `LPM`        | Packages from the LPM.dev Registry                            |
| `npm`        | Packages from `registry.npmjs.org`                            |
| `Max depth`  | Deepest graph level. The project root is level 1.             |
| `Duplicates` | Package names that have more than one resolved version        |

Custom-registry packages use `unknown` registry attribution. They count toward `packages`, but not toward the `LPM` or `npm` values.

The difference between the total and the registry values can therefore identify packages from other sources.

## Focus the graph [#focus-the-graph]

Use graph filters to reduce a large result:

```bash
lpm graph --depth 2
lpm graph --filter react
lpm graph --prod
lpm graph --dev
```

| Filter            | Result                                                                      |
| ----------------- | --------------------------------------------------------------------------- |
| `--depth <N>`     | Keep the first `N` levels. Level 1 is the project root.                     |
| `--filter <NAME>` | Keep matching package subtrees and the paths that connect them to the root. |
| `--prod`          | Keep production packages and their reachable transitive packages.           |
| `--dev`           | Keep development packages and their reachable transitive packages.          |

The name filter uses a substring match. For example, `--filter press` matches `express`.

All output formats use the selected subtree and filters.

## Choose an output format [#choose-an-output-format]

| Format    | Destination       | Use it for                            |
| --------- | ----------------- | ------------------------------------- |
| `tree`    | Standard output   | Terminal inspection                   |
| `dot`     | Standard output   | Graphviz diagrams                     |
| `mermaid` | Standard output   | Markdown documentation                |
| `json`    | Standard output   | Scripts, tools, and agents            |
| `stats`   | Standard output   | Package counts, depth, and duplicates |
| `html`    | `.lpm/graph.html` | Interactive browser inspection        |

Create a Graphviz image:

```bash
lpm graph --format dot | dot -Tpng > graph.png
```

Create an HTML graph without opening a browser:

```bash
lpm graph --format html --no-open
```

## Read JSON graph data [#read-json-graph-data]

Export the graph for a script or another tool:

```bash
lpm graph --format json
```

The top-level object contains these fields:

| Field          | Meaning                                              |
| -------------- | ---------------------------------------------------- |
| `success`      | Whether LPM CLI created the result                   |
| `root`         | Project name and version                             |
| `packages`     | Package count. This count excludes the project root. |
| `lpm_packages` | LPM.dev Registry package count                       |
| `npm_packages` | npm registry package count                           |
| `max_depth`    | Deepest graph level                                  |
| `duplicates`   | Package names with their resolved versions           |
| `nodes`        | Project and package nodes                            |
| `edges`        | Dependency relationships between nodes               |

Each item in `nodes` contains these fields:

| Field              | Meaning                                                  |
| ------------------ | -------------------------------------------------------- |
| `key`              | Package identity in `name@version` form                  |
| `name`             | Package name                                             |
| `version`          | Resolved package version                                 |
| `registry`         | `lpm`, `npm`, or `unknown`                               |
| `depth`            | Zero-based node depth in JSON                            |
| `is_direct`        | Whether the selected dependency set contains the package |
| `is_duplicate`     | Whether another resolved version has the same name       |
| `is_root`          | Whether the node represents the project                  |
| `dependency_count` | Number of dependency relationships from this node        |
| `deps`             | Package identities for direct child nodes                |

The `nodes` array includes the project root. The top-level `packages` count excludes that root.

## Fix common problems [#fix-common-problems]

### No usable lockfile [#no-usable-lockfile]

If LPM CLI cannot find a usable `lpm.lock`, create or update it:

```bash
lpm install
```

### The graph does not match `package.json` [#the-graph-does-not-match-packagejson]

If the manifest changed after the last install, update the lockfile:

```bash
lpm install
lpm graph
```

### The package is not in the graph [#the-package-is-not-in-the-graph]

Run the complete graph without filters:

```bash
lpm graph
```

Then use the exact `name@version` value for a subtree query.

### A browser cannot open the HTML graph [#a-browser-cannot-open-the-html-graph]

Create the file without the browser action:

```bash
lpm graph --format html --no-open
```

Then open `.lpm/graph.html` with another browser or transfer the file.

## See also [#see-also]

* [`lpm graph`](/docs/packages/graph) — use the complete command syntax and flags
* [`lpm query`](/docs/packages/query) — select installed packages by package properties
* [`lpm audit`](/docs/packages/audit) — inspect package security results
* [Lockfile](/docs/packages/lockfile) — understand the source of resolved dependency data
* [Overrides](/docs/packages/install#overrides) — replace dependency versions during installation
* [Patching dependencies](/docs/packages/patch) — apply local changes to installed packages


# lpm download (/docs/packages/download)



```bash
lpm download <package[@version]> [--version <V>] [--output <DIR>]
```

Fetches a package tarball and extracts it into a directory you choose. **No lockfile**, &#x2A;*no `node_modules`**, **no script execution**, **no `package.json` mutation**. Just the bytes.

Useful for inspecting a package's contents before you trust it, archiving a snapshot for offline reference, generating a fixture, or reading the source of a package you don't want to install.

Routing matches [`lpm install`](/docs/packages/install):

* `@lpm.dev/*` fetches from LPM.dev Registry.
* `@scope/pkg` follows the current project's `.npmrc` `@scope:registry=...` mapping when one exists.
* Unscoped names (`react`, `zod`, `lodash.merge`) use the current project's default `.npmrc` registry, or npmjs.org when no override is present.

If you specifically mean an LPM.dev Registry package, prefer the fully scoped `@lpm.dev/owner.pkg` form.

When npm firewall mode is `monitor` or `enforce`, LPM CLI checks routed public npm packages with LPM Firewall at `firewall.lpm.dev` after selecting the version and before downloading the tarball. LPM Firewall is an LPM.dev Registry Pro/Org feature, so active modes send LPM.dev Registry auth; run `lpm login` locally or set `LPM_TOKEN` in CI. `monitor` warns and continues when entitlement is denied, while `enforce` blocks packages whose effective firewall action is `block` or denied by entitlement before any package bytes are extracted. In human output, the download line shows `🔥 LPM Firewall active` when that check is active. LPM.dev Registry packages and other `.npmrc`-declared private/custom registry packages are not sent to the npm firewall verdict API. The legacy string `report` is still accepted as a `monitor` alias.

## Examples [#examples]

```bash
lpm download react                                   # npmjs.org (or project .npmrc default registry)
lpm download react@0.14.3                            # npm-style inline version
lpm download @my-co/internal                         # project .npmrc scoped registry
lpm download @lpm.dev/owner.pkg                      # force LPM.dev Registry
lpm download @lpm.dev/owner.pkg --version 2.1.0      # a specific version
lpm download react --version next                    # dist-tag or semver range
lpm download @lpm.dev/owner.pkg --json               # structured output (URL, integrity, paths)
```

## How it works [#how-it-works]

1. Fetches the package metadata from the routed registry to find the tarball URL and SRI integrity hash for the requested (or latest) version.
2. Downloads the tarball. Automatically followed redirects are checked before each hop; once the request chain has used HTTPS, a redirect to HTTP fails with `refused HTTPS-to-HTTP redirect`.
3. Verifies the SRI hash when the registry ships one. If the registry does not ship integrity, `lpm download` refuses to extract by default; re-run with `--allow-unverified` only when you intentionally accept an unverified tarball.
4. Extracts into the output directory.

The output directory defaults to the current working directory. The tarball's top-level `package/` is unwrapped during extraction (equivalent to `tar x --strip-components=1`), so you get the package contents directly.

Direct HTTP follows the same rules as [`lpm install`](/docs/packages/install): loopback is available for local development, and a deliberate registry `--insecure` override can allow a configured non-loopback HTTP endpoint. Neither case allows an HTTPS redirect to downgrade to HTTP. See [Redirect transport policy](/docs/registries#redirect-transport-policy).

## Archive safety limits [#archive-safety-limits]

LPM CLI validates the complete tar archive before it accepts the extraction:

* 100,000 archive entries.
* 100,000 GNU or PAX metadata entries.
* 256 path components, including the top-level `package/` component.
* 32 KiB for each encoded archive path.
* 1 MiB for each GNU or PAX metadata record.
* 500 MiB for each file.
* 5 GiB for all extracted file data.

If an archive exceeds a limit, LPM CLI stops the extraction and removes the files from that extraction.

These limits also apply when `lpm install` or `lpm add` extracts a registry package.

## What `lpm download` does NOT do [#what-lpm-download-does-not-do]

* Install dependencies — none of the package's `dependencies` are fetched
* Touch `package.json` or any lockfile
* Run lifecycle scripts (`preinstall`, `postinstall`, etc.)
* Place the result in `node_modules/`
* Update the global content-addressable store

For a real install, use [`lpm install`](/docs/packages/install). For source delivery into your project, use [`lpm add`](/docs/packages/add).

## Flags [#flags]

| Flag                   | Effect                                                                                                                                                                       |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--version <V>`        | Version spec to download: exact version, dist-tag (`latest`, `next`), or semver range. Defaults to `latest`. Alternative to inline `package@version`; do not pass both.      |
| `--output <DIR>`, `-o` | Output directory (default: current working directory). The `output_dir` field in `--json` mode is canonicalized to an absolute path regardless of how the flag was passed.   |
| `--allow-unverified`   | Waive the default integrity gate when the registry returns no SRI hash. Intended for legacy sources that genuinely do not ship integrity; you take on verification yourself. |

Plus the [global flags](/docs/commands#global-flags).

## `--json` envelope [#--json-envelope]

```json
{
  "success": true,
  "package": "@lpm.dev/owner.pkg",
  "version": "2.1.0",
  "tarball_url": "https://lpm.dev/api/registry/@lpm.dev/owner.pkg/-/owner.pkg-2.1.0.tgz",
  "integrity": "sha512-...",
  "integrity_verified": true,
  "size_bytes": 87654,
  "output_dir": "/abs/path/to/output",
  "files_extracted": 42,
  "elapsed_secs": 0.412
}
```

Fields:

| Field                | Meaning                                                                                                                                                                                                           |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tarball_url`        | Resolved tarball URL the bytes came from. Useful for mirroring or audit logs.                                                                                                                                     |
| `integrity`          | SRI string the registry advertised for this version (e.g. `sha512-…`), or `null` if the registry didn't ship one. A `null` value only reaches a success envelope when you explicitly passed `--allow-unverified`. |
| `integrity_verified` | `true` when the SRI was present and verified, `false` only on the explicit `--allow-unverified` path where extraction proceeds without a registry-provided hash. A verification failure still aborts the run.     |
| `output_dir`         | Absolute path where files were extracted (canonicalized).                                                                                                                                                         |
| `files_extracted`    | Count of files written from the tarball, after the `package/` prefix is stripped.                                                                                                                                 |

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — full install, with `node_modules` and lockfile
* [`lpm add`](/docs/packages/add) — source delivery into your project
* [`lpm info`](/docs/packages/info) — package metadata without downloading
* [`lpm resolve`](/docs/packages/resolve) — print the resolved dep tree without downloading


# lpm fetch (/docs/packages/fetch)



```bash
lpm fetch [--platform OS/ARCH[/LIBC]]
```

`lpm fetch` reads `lpm.lock` and downloads the remote package tarballs it names into the global LPM CLI store. It does not read `package.json`, does not resolve versions, does not create `node_modules`, and does not rewrite the lockfile.

Use it to build a reusable Docker or CI cache layer from the lockfile before the rest of the source tree is copied in.

## Docker layer cache [#docker-layer-cache]

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

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

The first layer depends only on `lpm.lock`, so ordinary source changes do not invalidate package downloads. The later install is offline and frozen: it links from the warmed store and fails if `package.json` does not match the importer snapshot in `lpm.lock`.

## What gets fetched [#what-gets-fetched]

`lpm fetch` considers every package entry in the lockfile, not only the current workspace member. That matches pnpm-style fetch behavior for monorepos: one lockfile can warm the store for the whole workspace graph.

Remote registry, tarball URL, and commit-pinned GitHub entries must have an SRI integrity string in the lockfile. Missing integrity is a hard error. Integrity mismatch during download is also a hard error, and the bytes are not stored.

GitHub entries are reconstructed from the commit-pinned `git+https://github.com/...#<commit>` source in `lpm.lock` and downloaded directly from GitHub's codeload origin. Redirects and credential-bearing sources are rejected. The fetched package is securely extracted and source-analyzed before it enters the global store, so a later `lpm install --offline --frozen-lockfile` can replay it without contacting GitHub.

When npm firewall mode is `monitor` or `enforce`, LPM CLI batches eligible public npm package-version rows from the lockfile with LPM Firewall at `firewall.lpm.dev` before starting tarball downloads. That includes current public npm registry rows, legacy source-less rows whose tarball URL is a canonical `registry.npmjs.org` package tarball for the same name/version, and canonical public npm `tarball+https:` rows for the same name/version. LPM Firewall is an LPM.dev Registry Pro/Org feature, so active modes send LPM.dev Registry auth; run `lpm login` locally or set `LPM_TOKEN` in CI. `monitor` warns and continues when entitlement is denied, while `enforce` blocks packages whose effective firewall action is `block` or denied by entitlement before the store is populated. In human output, the lockfile-fetch line shows `🔥 LPM Firewall active` when that batch check is active. Private/custom registry sources, arbitrary remote tarball URL sources, local sources, and LPM.dev Registry packages are not sent to the npm firewall verdict API. The legacy string `report` is still accepted as a `monitor` alias.

Local sources are skipped:

* `file:` directory packages
* `link:` packages
* local tarball paths

Those sources live in the project checkout, so they cannot be downloaded from a lockfile-only Docker layer. When the full project is copied in, `lpm install --offline --frozen-lockfile` reads them locally.

Other Git hosts and non-GitHub Git transports are not supported.

## Platform selection [#platform-selection]

By default, `lpm fetch` uses the current machine's platform. Pass `--platform` when warming a cache for a different target image:

```bash
lpm fetch --platform linux/x64/glibc
lpm fetch --platform linux/arm64/musl
lpm fetch --platform darwin/arm64
```

The filter applies to lockfile `os`, `cpu`, and `libc` metadata using npm semantics. Packages with no platform restriction are always fetched. Packages restricted to another platform are reported as skipped.

## JSON output [#json-output]

```bash
lpm fetch --platform linux/x64/glibc --json
```

```json
{
  "success": true,
  "lockfile": "lpm.lock",
  "platform": { "os": "linux", "cpu": "x64", "libc": "glibc" },
  "counts": { "total": 247, "fetched": 180, "cached": 65, "skipped": 2 },
  "packages": [
    { "name": "react", "version": "19.2.4", "source": "registry", "status": "cached" }
  ],
  "elapsed_ms": 412
}
```

`status` is one of `fetched`, `cached`, or `skipped`. Skipped entries include a `reason` such as `platform`, `local_source`, or `local_tarball`.

## Flags [#flags]

| Flag                        | Effect                                                                                                                                                                 |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--platform OS/ARCH[/LIBC]` | Target platform for `os`, `cpu`, and `libc` lockfile filters. Accepts npm-style values such as `linux/x64/glibc`, `linux/arm64/musl`, `darwin/arm64`, and `win32/x64`. |

Plus the [global flags](/docs/commands#global-flags). `--json` emits the envelope above. `--registry` is still accepted as a global flag for client configuration, but fetch uses the tarball URLs already pinned in `lpm.lock` instead of resolving metadata.

## See also [#see-also]

* [Docker deploys](/docs/guides/docker-deploys) — layer-cached installs and `lpm deploy`
* [`lpm install`](/docs/packages/install) — full install and frozen-lockfile behavior
* [Lockfile](/docs/packages/lockfile) — what `lpm.lock` pins
* [Content-addressable store](/docs/packages/content-addressable-store) — where fetched package bytes live


# lpm global (/docs/packages/global)



```bash
lpm global <subcommand>
```

CLI surface for the global package store at `~/.lpm/global/`. Globally installed packages — `lpm install -g <pkg>` — get their bins shimmed into `~/.lpm/bin/` and tracked in a manifest LPM CLI owns.

A package is committed as globally installed only after LPM CLI has created at least one safe, executable bin shim for it. Packages with no usable bins, unsafe bin names, or incomplete install roots fail closed and are rolled back before they reach the global manifest.

The same install / uninstall surface is reachable via the regular commands with `-g`:

| Equivalent                |                               |
| ------------------------- | ----------------------------- |
| `lpm install -g eslint`   | `lpm install --global eslint` |
| `lpm uninstall -g eslint` | `lpm global remove eslint`    |

This page documents the `lpm global` subcommands directly. For installing a global package, prefer [`lpm install -g`](/docs/packages/install).

## Examples [#examples]

```bash
lpm global list                          # what's installed globally
lpm global list --outdated               # which globals have updates available
lpm global list --verbose                # add install date, size, root path
lpm global bin                           # print ~/.lpm/bin/  (add this to PATH)
lpm global path eslint                   # print eslint's install root
lpm global link                          # expose the current package's bins globally
lpm global link ../my-cli                # expose a local checkout globally
lpm global unlink my-cli                 # remove a local global link
lpm global remove eslint                 # uninstall a global package
lpm global update                        # update every globally-installed package
lpm global update eslint                 # update one
lpm global update eslint@^9              # change the saved spec, then update
lpm global update --dry-run              # show the upgrade plan, change nothing
```

## Subcommands [#subcommands]

| Command             | Arguments          | Flags                     | Effect                                                                       |
| ------------------- | ------------------ | ------------------------- | ---------------------------------------------------------------------------- |
| `lpm global list`   | (none)             | `--outdated`, `--verbose` | List globally-installed packages and their exposed commands.                 |
| `lpm global bin`    | (none)             | (none)                    | Print the shim directory to add to `PATH`.                                   |
| `lpm global path`   | `<package>`        | (none)                    | Print one global package's install root.                                     |
| `lpm global link`   | `[path]`           | (none)                    | Link a local package's bins into the global PATH surface.                    |
| `lpm global unlink` | `<package>`        | (none)                    | Remove a local global link.                                                  |
| `lpm global remove` | `<package>`        | (none)                    | Remove a globally-installed package. Equivalent to `lpm uninstall -g <pkg>`. |
| `lpm global update` | `[package[@spec]]` | `--dry-run`               | Re-resolve one or all registry-backed globals against their saved specs.     |

### `list` [#list]

```bash
lpm global list [--outdated] [--verbose]
```

Lists every globally-installed package with its active version and exposed bin commands.

| Flag         | Effect                                                                                                                                                                                                                                        |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--outdated` | Resolve each install's saved spec against its registry and active cooldown policy. Flag packages only when the installable version is newer. `@lpm.dev/*` globals use LPM.dev Registry batch metadata. Public npm globals query npm directly. |
| `--verbose`  | Add install date, on-disk size, and the absolute install root for each row                                                                                                                                                                    |

With `--outdated`, the human table shows `Package`, `Current`, `Wanted`, `Latest`, and `Bins`. `Wanted` is the newest installable version that still satisfies the global install's saved spec; `Latest` is the registry's newest installable version regardless of the saved range, so mature major-version jumps are visible even when the saved range would not select them. Local links are skipped because their source of truth is your checkout, not the registry.

If registry metadata is missing or the saved spec no longer resolves, `lpm global list --outdated` exits non-zero instead of reporting a clean result. In `--json` mode, inspect `success`, `unresolved`, and `count_unresolved` to distinguish "everything checked" from "some globals could not be compared."

If the installed version is newer than the registry result, the command reports it as current. A registry rollback does not create an outdated row.

### `bin` [#bin]

```bash
lpm global bin
```

Prints `~/.lpm/bin/` — the directory where global-install shims live. Add this to your `PATH` so installed CLIs resolve.

```bash
export PATH="$(lpm global bin):$PATH"     # in your shell rc
```

### `path <package>` [#path-package]

```bash
lpm global path eslint
```

Prints the install root for one specific globally-installed package. Useful for tooling that needs to find the package's `node_modules/`, `package.json`, or files.

For local links, this prints the linked source checkout.

### `link [path]` [#link-path]

```bash
lpm global link
lpm global link ../my-cli
```

Links a local package into the global manifest and exposes its `bin` commands through `~/.lpm/bin/`. Omit `path` to link the current working directory.

The linked package must have `name`, `version`, and at least one valid `bin` entry in `package.json`; each bin target must exist inside the package directory. LPM CLI refuses command-name collisions with existing global packages so a local checkout cannot silently take over a command you already installed.

### `unlink <package>` [#unlink-package]

```bash
lpm global unlink my-cli
```

Removes a local global link, its shims, and its global manifest entry. Registry-backed global packages are removed with `lpm global remove` or `lpm uninstall -g`.

### `remove <package>` [#remove-package]

```bash
lpm global remove eslint
```

Uninstalls a globally-installed package. Drops the entry from the global manifest, removes the install root, and unshims any bins it owned. &#x2A;*Equivalent to `lpm uninstall -g eslint`** — both invocations route through the same uninstall pipeline.

### `update [package[@spec]] [--dry-run]` [#update-packagespec---dry-run]

```bash
lpm global update                        # update every global
lpm global update eslint                 # update one, preserving its saved spec
lpm global update eslint@^9              # rewrite the saved spec, then update
lpm global update --dry-run              # plan only, no work
```

Three behaviors based on the argument:

| Argument       | Behavior                                                                                                                                                                                        |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| (none)         | Iterate every registry-backed global package. Re-resolve each one against its persisted `saved_spec`. Upgrade any that have a newer matching version. Local links are skipped.                  |
| `<pkg>`        | Same flow scoped to one registry-backed package. Local global links are not updated by this command; edit the linked checkout directly, or unlink and link it again after its metadata changes. |
| `<pkg>@<spec>` | Rewrite the saved spec using the [save policy](/docs/packages/save-policy) decision logic, then upgrade. Same precedence as `lpm install <pkg>@<spec>` in a project context.                    |

| Flag        | Effect                                            |
| ----------- | ------------------------------------------------- |
| `--dry-run` | Print the upgrade plan without changing any state |

If registry metadata is missing, the selected version is no longer present, or the saved spec cannot resolve, `lpm global update` exits non-zero instead of reporting a clean skip. In `--json` mode, `success` is `false` and the affected row appears as `failed` in `results` or `plan_error` in `plans` for `--dry-run`. Bulk updates remain best-effort: successful package rows may still be applied before the final non-zero exit.

Global update revalidates registry metadata before it changes an installation. The install uses the same package metadata that produced the plan.

An update only selects a version that is newer than the installed version. If a registry rollback removes a newer release, the command keeps the installed version.

Global update uses the global minimum release age chain: `~/.lpm/config.toml`, then the disabled default (`0`). Candidate versions for the globally installed package are skipped during planning while they are still inside the cooldown window, including `--dry-run`, so the plan only shows versions the nested install would accept.

## Bin collisions [#bin-collisions]

When a global install would expose a bin name that another global package already owns, LPM CLI refuses to silently take it over. In an interactive terminal, LPM CLI prompts per colliding command: replace the existing shim, install the new command under an alias, or cancel. In JSON and non-TTY runs, it exits with a collision error and shows copy-pasteable `--replace-bin` / `--alias` examples.

Use `--replace-bin` when the new package should deliberately take ownership:

```bash
lpm install -g foo --replace-bin serve
```

The previous owner keeps its install but loses `serve` from PATH; the new shim points at `foo`'s `serve` binary.

Use `--alias <ORIG=ALIAS>` when both packages should stay reachable:

```bash
lpm install -g foo --alias serve=foo-serve
```

Only `foo-serve` is emitted as the new PATH command; the original `serve` shim stays with its current owner. See [`lpm install -g`](/docs/packages/install) for the full collision surface.

| Install flag           | Effect                                                                                                                                                                                   |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--replace-bin <CMD>`  | Transfer ownership of one colliding PATH command to the package being installed. Repeatable.                                                                                             |
| `--alias <ORIG=ALIAS>` | Install the declared bin `<ORIG>` under a different PATH command `<ALIAS>`. The original name is not emitted for the new package. Repeatable, and comma-separated mappings are accepted. |

## Security gates on `-g` [#security-gates-on--g]

`lpm install -g` runs through the same install pipeline as project installs, with the same install-time security gates applied to the synthesized global project. Machine-wide controls, including the npm firewall mode set by [`lpm config firewall`](/docs/infra/config#npm-firewall), apply to global installs too:

| Flag                                               | Behavior on `-g`                                                                                                                                                                                                                                                                               |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--allow-new`                                      | Bypasses the [minimum release age](/docs/packages/security-audit#layer-4-provenance--cooldown) cooldown                                                                                                                                                                                        |
| `--min-release-age=<DUR>`                          | Overrides the cooldown window (`<N>h`, `<N>d`, or seconds; `0` disables)                                                                                                                                                                                                                       |
| `--min-release-age-exclude <SELECTOR>`             | Exempts a package name, exact version, or `@scope/*` from the cooldown. Repeatable.                                                                                                                                                                                                            |
| `--ignore-provenance-drift <PKG>`                  | Waives the [drift check](/docs/packages/security-audit#layer-4-provenance--cooldown) for one package (repeatable)                                                                                                                                                                              |
| `--ignore-provenance-drift-all`                    | Waives the drift check for every resolved package                                                                                                                                                                                                                                              |
| `--policy=deny\|allow\|triage`                     | [Script-policy](/docs/packages/install#lifecycle-scripts) 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-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. Also useful under `deny` with an established global trust set. |
| `--strict-sandbox` / `--paranoid` / `--no-sandbox` | [Sandbox mode](/docs/packages/install#sandbox) for this invocation's dependency lifecycle scripts                                                                                                                                                                                              |

Two `-g`-specific notes:

* **No project-config tier.** Globals use a synthesized package.json that doesn't carry `lpm > scriptPolicy`, `lpm > minimumReleaseAge`, `lpm > minimumReleaseAgePolicy`, `lpm > minimumReleaseAgeExclude`, or `lpm > scripts.autoBuild`. The script-policy and cooldown precedence chains collapse to CLI flag > `~/.lpm/config.toml` > default. Triage on `-g` requires the explicit `--auto-build` flag for mixed-trust trees — there's no per-project knob.
* **Reinstall to rerun blocked scripts.** After `lpm approve-scripts --global`, the trust file is updated but the install root isn't re-executed. Run `lpm uninstall -g <pkg> && lpm install -g <pkg>` for each affected top-level global to actually run the approved scripts. `lpm rebuild --global` is a planned follow-up.

## Approving scripts for globals [#approving-scripts-for-globals]

Globally-installed packages have their own blocked-set for dependency lifecycle scripts. Manage it with:

```bash
lpm approve-scripts --global             # review the global blocked set
lpm approve-scripts --global --group     # group by top-level globally-installed package
```

Approvals write to `~/.lpm/global/trusted-dependencies.json` (separate from any project's `package.json`). The success banner enumerates the top-level globals affected so you can run the matching `lpm uninstall -g … && lpm install -g …` for each.

## See also [#see-also]

* [`lpm install -g`](/docs/packages/install) — install into the global root (with `--replace-bin` / `--alias`)
* [`lpm uninstall -g`](/docs/packages/uninstall) — equivalent to `lpm global remove`
* [`lpm approve-scripts --global`](/docs/packages/approve-scripts#global-vs-project) — script approval for globals
* [Save policy](/docs/packages/save-policy) — determines what `update <pkg>@<spec>` writes


# lpm graph (/docs/packages/graph)



```bash
lpm graph [package] [--format <FMT>] [filters...]
lpm ls [package] [--format <FMT>] [filters...]
lpm why <package>
```

Renders the project's dependency graph in one of six formats. With no `package` argument, shows the entire installed tree from the root; with a package name, shows just that subtree.

`lpm ls` is an alias for `lpm graph`. `lpm why <package>` is a top-level shortcut for `lpm graph --why <package>` with the same human and `--json` output.

## Examples [#examples]

```bash
lpm graph                              # full tree, terminal output
lpm graph react                        # subtree under react
lpm graph --format mermaid             # Mermaid for embedding in markdown
lpm graph --format dot > graph.dot     # GraphViz, pipe into `dot`
lpm graph --format html                # writes .lpm/graph.html, opens in browser
lpm graph --format html --no-open      # write the file but don't open it (CI-safe)
lpm graph --format json                # structured for tooling
lpm graph --format stats               # counts and aggregates only
lpm graph --why lodash                 # why is lodash in the tree?
lpm why lodash                         # same renderer, easier to remember
lpm ls --depth 2                       # alias for lpm graph --depth 2
lpm graph --depth 3                    # truncate tree at depth 3
lpm graph --filter react               # only subtrees whose package names contain 'react'
lpm graph --filter press               # substring match: keeps the express branch
lpm graph --prod                       # production deps only
lpm graph --dev                        # devDependencies only
```

## Output formats [#output-formats]

| Format           | Output destination                                                                                           | Best for                                            |
| ---------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
| `tree` (default) | stdout — indented terminal tree of resolved `name@version` nodes                                             | Quick eyeballing                                    |
| `dot`            | stdout — GraphViz DOT source                                                                                 | Pipe into `dot -Tpng > graph.png`                   |
| `mermaid`        | stdout — Mermaid graph block                                                                                 | Embedding in markdown / docs                        |
| `json`           | stdout — structured graph data                                                                               | Tooling, custom analyses                            |
| `stats`          | stdout — counts (total deps, max depth, duplicates)                                                          | High-level audits                                   |
| `html`           | **writes** `<project>/.lpm/graph.html` and auto-opens it in your default browser (suppress with `--no-open`) | Interactive exploration, sharing with non-CLI users |

The `html` format is the only one that writes to disk — every other format prints to stdout so you can pipe or redirect it. Pair `--format html` with `--no-open` in headless / CI environments where opening a browser would either fail or be unwanted.

## `lpm why <package>` [#lpm-why-package]

```bash
lpm why lodash
lpm graph --why lodash
```

Walks every path from the root to the named package and prints them. Useful for "why am I shipping this" investigations and for hunting down a transitive bump that pulled in something unexpected.

`lpm why <package>` and `lpm graph --why <package>` are the same renderer. With global `--json`, both emit the same structured object with `target`, `found`, `path_count`, `paths`, `applied_overrides`, and `applied_patches`.

Dense graphs can produce large output because the path count can increase exponentially.

## Filtering [#filtering]

`--depth`, `--filter`, `--prod`, and `--dev` are applied at the graph level — every output format (tree, dot, mermaid, json, stats, html) sees the same truncated set, including the stats summary embedded in the HTML header.

| Flag              | Effect                                                                                                                                                                                                |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--depth <N>`     | Truncate the graph at depth N (root counts as level 1, direct deps as level 2). `--depth 1` keeps just the root, `--depth 2` keeps root + direct deps, `--depth 3` keeps one transitive layer beyond. |
| `--filter <NAME>` | Only show subtrees that contain this substring in a package name. Diamond patterns (two parents to the same target) keep both branches.                                                               |
| `--prod`          | Production dependencies only (mutually exclusive with `--dev`)                                                                                                                                        |
| `--dev`           | devDependencies only                                                                                                                                                                                  |

## Flags [#flags]

| Flag              | Effect                                                                                                                                                                                                                                                      |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--format <FMT>`  | One of `tree`, `dot`, `mermaid`, `json`, `stats`, `html` (default: `tree`)                                                                                                                                                                                  |
| `--why <PKG>`     | Show every path from the root to `<PKG>`                                                                                                                                                                                                                    |
| `--depth <N>`     | Truncate the graph at depth N (applied to every format)                                                                                                                                                                                                     |
| `--filter <NAME>` | Only subtrees containing this substring in a package name                                                                                                                                                                                                   |
| `--prod`          | Production deps only                                                                                                                                                                                                                                        |
| `--dev`           | devDependencies only                                                                                                                                                                                                                                        |
| `--no-open`       | With `--format html`: write the file but skip the auto-open call. Useful in headless / CI environments. Used without `--format html`, prints a one-line warning that the flag has no effect in human mode; the warning is suppressed under global `--json`. |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm query`](/docs/packages/query) — selector-based queries against installed packages
* [`lpm audit`](/docs/packages/audit) — security and quality audit
* [Dependency graph](/docs/packages/dependency-graph) — understand package relationships and graph results


# Packages (/docs/packages)





LPM CLI's package-management surface. The CLI commands are listed first; the deeper-dive concept pages explaining how the resolver, lockfile, store, save policy, and security gates fit together come below.

For an end-to-end walkthrough, see [Publishing your first package](/docs/guides/publishing-a-package) or [Migrating from npm](/docs/guides/migrating-from-npm).

## Install + dependency management [#install--dependency-management]

<Cards>
  <Card title="lpm install" href="/docs/packages/install" description="Install dependencies from package.json, or add new ones. The foundational command." />

  <Card title="lpm uninstall" href="/docs/packages/uninstall" description="Remove from dependencies and node_modules." />

  <Card title="lpm add" href="/docs/packages/add" description="Source delivery — copy a package's source into your project (any registry)." />

  <Card title="lpm remove" href="/docs/packages/remove" description="Reverse of lpm add — remove source-delivered files." />

  <Card title="lpm global" href="/docs/packages/global" description="Manage globally-installed CLIs under ~/.lpm/global/." />

  <Card title="lpm cache" href="/docs/packages/cache" description="Ephemeral caches + reference-aware prune of the global store." />

  <Card title="lpm upgrade" href="/docs/packages/upgrade" description="Bump eligible LPM.dev Registry and npm deps to their latest matching range." />

  <Card title="lpm outdated" href="/docs/packages/outdated" description="Show dependencies with newer versions available." />

  <Card title="lpm patch" href="/docs/packages/patch" description="Patch dependencies locally with integrity binding." />
</Cards>

## Publish, search, discover [#publish-search-discover]

<Cards>
  <Card title="lpm init" href="/docs/packages/init" description="Create a new LPM.dev Registry or npm-compatible package manifest." />

  <Card title="lpm version" href="/docs/packages/version" description="Bump package.json version, with optional git commit/tag and JSON plans." />

  <Card title="lpm release" href="/docs/packages/release" description="Plan, apply, and publish workspace releases in dependency order." />

  <Card title="lpm publish" href="/docs/packages/publish" description="Publish to LPM.dev Registry (or --npm / --github / --gitlab). Provenance, quality gates, secret scan." />

  <Card title="lpm stage" href="/docs/packages/stage" description="Stage, inspect, approve, or reject npm staged publishes." />

  <Card title="lpm search" href="/docs/packages/search" description="Search LPM.dev Registry." />

  <Card title="lpm info" href="/docs/packages/info" description="Metadata for npm, custom-registry, or LPM.dev Registry packages." />
</Cards>

## Security + tools [#security--tools]

<Cards>
  <Card title="lpm audit" href="/docs/packages/audit" description="Vulnerability + behavioral audit. OSV + behavioral analysis tags." />

  <Card title="lpm tidy" href="/docs/packages/tidy" description="Find unused dependency declarations and phantom imports." />

  <Card title="lpm query" href="/docs/packages/query" description="CSS-like selector queries on installed packages — precision gates for CI." />

  <Card title="lpm graph" href="/docs/packages/graph" description="Render the dependency graph as tree, DOT, Mermaid, JSON, stats, or HTML." />

  <Card title="lpm sbom" href="/docs/packages/sbom" description="Export CycloneDX or SPDX SBOMs from lpm.lock." />

  <Card title="lpm licenses" href="/docs/packages/licenses" description="Inventory dependency licenses and fail CI on compliance policy." />

  <Card title="lpm trust" href="/docs/packages/trust" description="Manage project package trust and release-age exclusions." />

  <Card title="lpm rebuild" href="/docs/packages/rebuild" description="Run lifecycle scripts (the deferred step of install)." />

  <Card title="lpm approve-scripts" href="/docs/packages/approve-scripts" description="Approve packages blocked by the default-deny lifecycle script policy." />
</Cards>

## Migration + inspection [#migration--inspection]

<Cards>
  <Card title="lpm migrate" href="/docs/packages/migrate" description="Convert from npm / pnpm / yarn / bun. Backups + rollback." />

  <Card title="lpm catalog" href="/docs/packages/catalog" description="Inspect catalog usage and resolved lockfile snapshots." />

  <Card title="lpm fetch" href="/docs/packages/fetch" description="Populate the package store from lpm.lock without installing." />

  <Card title="lpm download" href="/docs/packages/download" description="Download and extract a tarball with no install side-effects." />

  <Card title="lpm resolve" href="/docs/packages/resolve" description="Print a resolved dependency tree without installing." />
</Cards>

## Specialized integrations [#specialized-integrations]

<Cards>
  <Card title="lpm pool" href="/docs/packages/pool" description="Pool revenue stats with per-package weighted downloads." />

  <Card title="lpm quality" href="/docs/packages/quality" description="Quality report for an LPM.dev Registry package." />

  <Card title="lpm swift-registry" href="/docs/packages/swift-registry" description="Configure SPM to install LPM.dev Registry-hosted Swift packages (SE-0292)." />

  <Card title="lpm skills" href="/docs/packages/skills" description="Manage package-published and standalone AI agent skills." />
</Cards>

## Features (concept pages) [#features-concept-pages]

<Cards>
  <Card title="Resolver" href="/docs/packages/resolver" description="Greedy-fusion by default, with PubGrub as the documented opt-out." />

  <Card title="Lockfile" href="/docs/packages/lockfile" description="Authoritative lpm.lock TOML plus lpm.lockb when the graph fits the binary format." />

  <Card title="Dependency graph" href="/docs/packages/dependency-graph" description="Resolved package relationships, duplicate versions, filters, statistics, and exports." />

  <Card title="Content-addressable store" href="/docs/packages/content-addressable-store" description="One on-disk copy per content hash; per-graph link entries; shared across projects." />

  <Card title="Save policy" href="/docs/packages/save-policy" description="How version ranges land in package.json — caret default, explicit preservation, prerelease safety." />

  <Card title="Security & audit" href="/docs/packages/security-audit" description="Defense in depth — script policy, behavioral analysis, OSV, provenance, typosquatting, triage gate." />

  <Card title="Workspaces" href="/docs/packages/workspaces" description="Monorepo support — declaration, filter grammar, workspace:* protocol, catalogs, deploy." />

  <Card title="npm compatibility" href="/docs/packages/npm-compatibility" description="What works the same as npm, what's different, and what LPM CLI adds." />

  <Card title="Swift Package Registry (SE-0292)" href="/docs/packages/swift-package-registry" description="The SE-0292 implementation — identity mapping, signing, trust model." />
</Cards>


# lpm info (/docs/packages/info)



```bash
lpm info <package[@version]> [--version <V>]
```

Fetches a package's metadata and prints it in a human-readable card: name, description, the chosen version's ecosystem, integrity hash, and full dependency list. With `--json`, it returns metadata from the selected registry, plus `success` and `_cache` diagnostics.

`lpm info` checks the registry for current metadata on every run. If the cached response has an ETag, the registry can return HTTP 304.

For HTTP 304, LPM CLI reuses the cached body. Install and resolution commands keep their five-minute metadata cache for speed.

Routing matches [`lpm install`](/docs/packages/install):

* `@lpm.dev/*` fetches from LPM.dev Registry.
* `@scope/pkg` follows the current project's `.npmrc` `@scope:registry=...` mapping when one exists.
* Unscoped names (`react`, `zod`, `lodash.merge`) use the current project's default `.npmrc` registry, or npmjs.org when no override is present.

If you specifically mean an LPM.dev Registry package, prefer the fully scoped `@lpm.dev/owner.pkg` form. That is the unambiguous spelling.

By default, `lpm info` shows the latest version. Use inline `package@version` or pass `--version <V>` to inspect a specific release.

## Examples [#examples]

```bash
lpm info react                                       # npmjs.org (or project .npmrc default registry)
lpm info react@0.14.3                                # npm-style inline version
lpm info @my-co/internal                             # project .npmrc scoped registry
lpm info @lpm.dev/owner.pkg                          # force LPM.dev Registry
lpm info @lpm.dev/owner.pkg --version 2.1.0          # specific version
lpm --verbose info @lpm.dev/owner.pkg                # show cache and revalidation details
lpm info @lpm.dev/owner.pkg --json | jq '.versions' # structured output for tooling
```

## What's shown [#whats-shown]

For the chosen version:

* `version` — the actual semver
* `ecosystem` — `js`, `swift`, etc.
* `integrity` — SRI hash (truncated for display, full in `--json`)
* `dependencies` — declared runtime deps with their ranges

JSON includes the version metadata that the selected registry returns. npm and custom registries can omit fields such as the README and maintainers.

JSON output also includes `_cache` diagnostics:

```json
{
  "_cache": {
    "status": "revalidated",
    "age_seconds": 143,
    "network_request": true,
    "not_modified": true
  }
}
```

Use the global `--verbose` flag to show the same cache and revalidation information in human output.

## Flags [#flags]

| Flag            | Effect                                                                                                |
| --------------- | ----------------------------------------------------------------------------------------------------- |
| `--version <V>` | Show a specific version instead of latest. Alternative to inline `package@version`; do not pass both. |

Plus the [global flags](/docs/commands#global-flags) — `--json` returns registry metadata plus LPM diagnostics.

## See also [#see-also]

* [`lpm search`](/docs/packages/search) — search npm, custom registries, or LPM.dev Registry depending on the query
* [`lpm quality`](/docs/packages/quality) — quality report
* [`lpm download`](/docs/packages/download) — fetch the tarball without installing


# lpm init (/docs/packages/init)



```bash
lpm init [--lpm | --npm] [-y] [--name <name>] [--owner <owner>] [--no-agents]
```

Generates a minimal `package.json` for a new package, plus repository hints that make the project clearly managed by LPM CLI.

In an interactive terminal, prompts for the package target first: **LPM.dev Registry package** or **npm-compatible package**. With `-y`, skips every prompt and keeps the backward-compatible default: an LPM.dev Registry package named `@lpm.dev/<owner>.package`. Refuses to overwrite an existing `package.json`.

For LPM.dev Registry packages, the interactive **owner** prompt is pre-filled from your `lpm whoami` profile username (with a 3-second timeout to keep `lpm init` from blocking on the network). If you're offline or not logged in, the placeholder is the literal string `username` — change it before publishing.

Successful human output is a short status transcript:

```text
✓ Wrote package.json
✓ Wrote AGENTS.md
✓ Added lpm.lockb binary to .gitattributes
✓ Done · initialized @lpm.dev/<owner>.<name>
```

## Examples [#examples]

```bash
mkdir my-pkg && cd my-pkg
lpm init                         # interactive walk
lpm init -y                      # default LPM.dev Registry package
lpm init --lpm -y --owner acme --name design-system
lpm init --npm -y --name @acme/widget
lpm init --npm -y --name widget --no-agents
```

## What it writes [#what-it-writes]

For LPM.dev Registry packages:

```json title="package.json"
{
  "name": "@lpm.dev/<owner>.<name>",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "module",
  "license": "MIT",
  "files": ["dist"],
  "packageManager": "lpm@<version>"
}
```

For npm-compatible packages:

```json title="package.json"
{
  "name": "@scope/name",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "module",
  "license": "MIT",
  "files": ["dist"],
  "packageManager": "lpm@<version>"
}
```

```json title="lpm.json"
{
  "publish": {
    "registries": ["npm"]
  }
}
```

All targets also write an agent/tooling hint by default:

```md title="AGENTS.md"
## Package Manager

This project uses lpm.

- Install dependencies with `lpm install`.
- Add source packages with `lpm add <package>`.
- Run scripts with `lpm run <script>`.
- Use `--json` when you need machine-readable output from lpm commands.
- CLI docs: https://cli.lpm.dev/
```

`lpm init` writes `AGENTS.md` only; it does not create provider-specific files such as `CLAUDE.md`.

And an entry in `.gitattributes`:

```text title=".gitattributes"
lpm.lockb binary
```

## Package name format [#package-name-format]

LPM CLI publishes under `@lpm.dev/<owner>.<name>` through LPM.dev Registry. `init` constructs the full name from your `owner` and `name` answers. You can publish under any owner you've claimed on LPM.dev Registry — see [`lpm publish`](/docs/packages/publish) for the upload flow.

Npm-compatible packages use the normal npm name contract, such as `widget` or `@acme/widget`. `lpm init --npm` writes `publish.registries = ["npm"]` in `lpm.json` so a later plain [`lpm publish`](/docs/packages/publish) targets npm instead of LPM.dev Registry.

`packageManager` is written as an LPM CLI environment signal for agents and tooling. Corepack does not currently install LPM CLI from that field.

## Defaults under `-y` [#defaults-under--y]

| Field         | Value                                                                                      |
| ------------- | ------------------------------------------------------------------------------------------ |
| `target`      | `lpm`                                                                                      |
| `owner`       | Your `lpm whoami` profile username, or the literal `"username"` if not logged in / offline |
| `name`        | `"package"` (literal — not the directory name)                                             |
| `version`     | `1.0.0`                                                                                    |
| `description` | (empty)                                                                                    |

Pass `--npm`, `--owner`, or `--name` when those placeholder defaults aren't what you want.

## Flags [#flags]

| Flag              | Effect                                                                                                                                           |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `-y`, `--yes`     | Skip prompts, use defaults                                                                                                                       |
| `--lpm`           | Create an LPM.dev Registry package name                                                                                                          |
| `--npm`           | Create an npm-compatible package name and write npm publish config                                                                               |
| `--name <name>`   | Package name to write. For `--lpm`, this can be the package half (`widget`), short form (`owner.widget`), or full form (`@lpm.dev/owner.widget`) |
| `--owner <owner>` | LPM.dev Registry owner or org slug. Only valid with LPM.dev Registry packages                                                                    |
| `--no-agents`     | Do not create or update `AGENTS.md`                                                                                                              |

Plus the [global flags](/docs/commands#global-flags). `--json` emits a brief envelope confirming what was created, including `{success, target, name, version, path, package_manager, agents_status, lpm_json_status}`.

## See also [#see-also]

* [`lpm publish`](/docs/packages/publish) — push to LPM.dev Registry or another registry
* [`lpm.json`](/docs/reference/lpm-json) — publish target configuration
* [`package.json` "lpm" key](/docs/reference/package-json-lpm) — what to add for advanced config
* [Project setup](/docs/project-setup) — broader walkthrough


# lpm install (/docs/packages/install)



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

With no arguments, installs every dependency declared in `package.json` against the lockfile. At a workspace root, the same no-argument command installs every member and then the root. With one or more package names, resolves and adds them to `package.json` and installs them.

The typosquat guard is off by default. Enable it with `lpm config typosquat --set on` to check new direct dependency names before resolution. See [Typosquat guard](#typosquat-guard) for interactive choices and project exceptions.

## Project discovery [#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.

In a workspace, bare `lpm install` from the workspace root recurses by default. Running it from a member remains local to that member. Use `lpm install --recursive` from a member when you want to widen the operation to its owning workspace.

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 [#examples]

```bash
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 --recursive       # install the owning workspace explicitly
lpm install --no-recursive    # install only the root project
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
```

### Publication review [#publication-review]

If an `@lpm.dev` package has published versions but no available `latest` version, the install stops before it changes project files.
LPM CLI reports pending review, manual review, or processing when the registry provides that status.
For pending review or processing, wait and retry the install.
For manual review, open the package page to see its publication status.
If the registry does not provide a status, LPM CLI reports that no latest version is available without guessing the cause.

With `--json`, this failure returns `success: false` and `error_code: "publication_unavailable"`.
See [lpm publish](/docs/packages/publish) for publication status and `--wait`.

### Cached package access [#cached-package-access]

During an online install, LPM CLI checks current access to every resolved `@lpm.dev` dependency, including transitive dependencies.
This check also applies to cached packages and projects that are already up to date.

If a version is quarantined, unpublished, or unavailable, the install fails before it links packages or commits project changes.
A revoked license or organization membership also prevents a cached install.
With `--json`, a denied version returns `error_code: "package_install_denied"` and identifies the package and reason.

If access is denied, check the package page, license, and organization membership before you retry.
If the registry cannot complete the check, the online install fails.
`--offline` cannot detect registry changes and uses the existing offline behavior.
Existing files remain on disk; access changes do not erase downloaded copies.

For an allowed deprecated version, LPM CLI shows the registry's deprecation message.
With `--json`, the message appears in `warnings`, including when the project is already up to date.
Lockfiles continue to pin the original package content.

### `--verbose` footer [#--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.

```text
✓ 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 [#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:

```bash
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. `commit_wait_ms` reports time a recursively installed workspace target spent waiting for its deterministic importer-commit turn. `post_resolve_work_ms` reports the remaining work between resolution and fetch; `pre_fetch_ms` remains as a compatibility alias for that value.

Timing objects declare their measurement scope. A standalone or per-workspace-target object uses `scope: "target"`, `phase_aggregation: "target_wall_clock"`, and `work_is_cumulative: false`. The recursive command root uses `scope: "recursive_command"` and reports summed target phase work under `timing.work`, serialized-importer waiting under `timing.wait`, and process-wide registry and resolver-policy metrics once under `timing.process`.

Install JSON also includes an explicit `counts` object that distinguishes resolved package rows, authoritative fetch candidates, store-reuse observations, newly created and reused linker entries, project-root symlinks, and bin links. Recursive roots mark these as `aggregation: "sum_of_target_observations"` because the same package can be observed by more than one workspace target.

`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.

Resolver concurrency appears under `timing.resolve.metadata_dispatcher` and, in detail mode, `timing.detail.resolve.scheduler.metadata_dispatcher`; `dispatcher` remains as a compatibility alias. These objects describe metadata scheduling only and set `tarball_downloads_included: false`. `configured_fanout` is the metadata-fetch permit limit, `active_fetch_high_water` is the peak number of direct jobs holding permits, and `pending_high_water` is the peak number of canonical requests pending in the resolver dispatcher, including direct jobs waiting for permits and Worker root or tail batch candidates. `semaphore_wait_count` and `semaphore_wait_ms` show direct-route permit contention. The compatibility field `inflight_high_water` has the same value as `active_fetch_high_water`. Tarball dispatch and overlap counters are reported separately by the fetch timing objects.

`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.

## Swift packages [#swift-packages]

Use `lpm install` for Swift dependencies hosted by LPM.dev Registry:

```bash
lpm install @lpm.dev/owner.swift-pkg
lpm install -y @lpm.dev/owner.swift-pkg
```

LPM CLI performs the required [Swift Registry setup](/docs/packages/swift-registry), updates `Package.swift`, and runs `swift package resolve`. If the package has one eligible non-test target, LPM CLI selects it automatically. With multiple eligible targets, a normal install opens the target selector; `-y` / `--yes` skips it and selects the first eligible target, matching the selector's default.

Swift manifest changes are transactional. If resolution fails—or a later JavaScript package in the same mixed install fails—LPM CLI restores the selected package's original `Package.swift` and `Package.resolved`; a `Package.resolved` created during the failed run is removed.

For a fresh Swift dependency, `--json` includes the automatic setup disposition:

```json
{
  "registry_setup": {
    "scope": "repaired",
    "signing_certificate": "retained",
    "signing_trust": "retained"
  }
}
```

Each value is `"repaired"` when LPM CLI changed that part of the setup or `"retained"` when the existing state already matched. `registry_setup` is omitted when the dependency already existed and no automatic setup ran.

Use [`lpm swift-registry --force`](/docs/packages/swift-registry#--force) only to repair stale or corrupt Registry configuration or refresh the signing certificate. It is not a prerequisite for the normal install flow. `lpm add` remains the legacy Swift source-copy path.

## How it works [#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](/docs/registries) for routing).
2. **Download.** Tarballs go into the global content-addressable store at `~/.lpm/store/`. Anything already present is reused. Package extraction uses the [archive safety limits](/docs/packages/download#archive-safety-limits). Canonical store bytes and project-writable package files use independent inodes: APFS and reflink-capable Linux filesystems preserve physical sharing through copy-on-write, while ext4 and other non-reflink filesystems use independent copies. Automatically followed redirects are checked at every hop and cannot return to HTTP after the chain has used HTTPS; see [Redirect transport policy](/docs/registries#redirect-transport-policy).
3. **Link.** `node_modules/<pkg>` is created as a symlink into the selected virtual store's `links/<graph-key>/` directory. The default is v2; experimental v3 is used only when explicitly selected with `LPM_STORE_VERSION=v3`. Single packages start **hoisted**: root direct dependencies are surfaced at the project root, while package-local dependency links live inside shared link entries. Workspaces auto-default to **isolated** (strict dependency visibility). 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](#lifecycle-scripts) below.
5. **Summarize cached security results.** LPM CLI reads the local behavioral-analysis cache for every installed package and reports actionable findings. LPM.dev Registry packages can also receive registry-side enrichment.

Source identity is preserved during linking. A registry package, tarball, Git package, `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.

## Project `node_modules` path [#project-node_modules-path]

The project-level `node_modules` path must be a real directory. Do not replace this path with a symlink or directory junction.

LPM CLI checks the project root before it accesses the registry. A recursive install checks the root and each selected workspace member before it starts install work.

Give each worktree its own `node_modules` directory. LPM CLI still shares canonical package data and graph link entries through `~/.lpm/store/`.

If LPM CLI reports `Project layout error`, run:

```bash
lpm doctor --fix
lpm install
```

The doctor command removes only the project link entry. It does not remove the target or its contents.

`lpm cache prune` and `lpm store clean` do not repair this project layout.

## GitHub dependencies [#github-dependencies]

`package.json` can declare public GitHub repositories with GitHub shorthand or a public `git+https` URL:

```json title="package.json"
{
  "dependencies": {
    "wa-sqlite": "github:rhashimoto/wa-sqlite#779219540f66cecaa159da32b3b8936697ba10a7",
    "ci-info": "git+https://github.com/watson/ci-info.git#main"
  }
}
```

An exact 40-character commit is used directly. A branch, tag, or omitted ref is resolved through GitHub's public API, then `lpm.lock` records the exact commit. Package bytes are downloaded directly from GitHub's codeload origin, assigned SHA-512 integrity, securely extracted, and passed through the same install-time source analysis as registry packages. Public GitHub traffic does not pass through LPM.dev Registry services.

GitHub URLs must use public `git+https://github.com/<owner>/<repository>.git` form. Credentials, custom ports, query parameters, redirects, non-GitHub hosts, and non-HTTPS Git transports are rejected. Private/authenticated Git repositories are not currently supported.

Frozen and offline installs replay the commit and integrity from `lpm.lock` without contacting GitHub when the package is already in the global store. [`lpm fetch`](/docs/packages/fetch) can populate that store from the lockfile. A frozen offline install fails if the required Git object is missing instead of resolving or downloading a replacement.

Direct GitHub dependencies work at a project root and in recursive workspace members. Registry dependencies declared by the Git package remain part of its locked graph; nested non-registry dependencies inside that package are rejected.

## Post-install security summary [#post-install-security-summary]

When install-time source analysis is enabled, LPM CLI analyzes package source locally as extracted bytes enter the store. The scanner records behavioral, supply-chain, and manifest signals in `.lpm-security.json` next to the stored package bytes. This applies to npm, custom-registry, and `@lpm.dev/*` packages; it does not depend on an LPM.dev Registry package being present.

Install-time source analysis is disabled by default. Manage it with [`lpm config source-analysis`](/docs/infra/config#install-time-source-analysis):

```bash
lpm config source-analysis --set true   # enable analysis
lpm config source-analysis --set false  # default; approval required after an approved opt-in
```

The disabled setting skips new install-time scans and cache creation without deleting existing caches. [`lpm audit`](/docs/packages/audit) still scans installed package source when no usable cache exists and stores its fallback result in the project audit cache. Re-enabling source analysis makes the next install backfill any missing, malformed, or outdated store cache from the already-extracted package bytes; it does not need to download the tarball again.

At the end of an install, LPM CLI reads those cached results for every installed package. It does not upload package source during this summary step. For installs larger than 50 packages, progress is reported as `Checking cached security results for N packages` / `Checked cached security results for N packages`, reflecting that this phase reads analysis already produced during extraction rather than rescanning source.

A normal install keeps `Security summary` compact. It shows the total number of Critical, High, and Medium findings, then shows package details only for Critical findings. Run `lpm audit` for the complete report, or use `lpm --verbose install` to include High and Medium details plus exact query selectors.

Info results describe common package capabilities or artifact traits. Examples include environment-variable access, URL literals, cryptography, and minified source. LPM CLI still detects and caches these signals, but a normal install does not show them as security findings.

Use one of these commands to inspect Info signals:

```bash
lpm --verbose install  # show all finding details and Behavioral metadata
lpm audit              # include Info signals in the complete audit
lpm query :info        # select packages with any Info tag
```

Verbose install output uses the heading `Behavioral metadata`. Its query hint contains the tags that were found. For example, environment access plus URL literals produces `lpm query ":env,:url-strings"`, not a generic `:critical` hint.

If the same `name@version` has findings from multiple sources, the summary distinguishes registry packages by credential-free origin and uses an opaque source ID for tarball, directory, link, and git packages. Registry credentials, paths, queries, and fragments are never included in these labels.

For `@lpm.dev/*` packages only, LPM CLI makes one batch metadata request to the configured LPM.dev Registry. The normal summary uses registry behavioral tags and lifecycle-script metadata.

The normal summary does not use registry vulnerabilities or AI security findings. For JavaScript packages, [`lpm audit`](/docs/packages/audit) and audit-after-install add those findings.

For a Swift package named in `lpm install`, audit-after-install adds findings from the Registry metadata that LPM CLI already fetched. `lpm audit` does not currently discover Swift dependencies from `Package.resolved`.

The request contains package names. It does not contain source bytes, local findings, versions, or filesystem paths.

This best-effort enrichment is enabled by default and is independent of the local scanner:

```bash
lpm config lpm-insights --set false  # keep local findings; skip enrichment
lpm config lpm-insights --set true
```

The combined `lpm config lpm-dev` editor manages this setting alongside automatic LPM.dev package skills. npm-only installs still receive local analysis and actionable summaries. They do not trigger this LPM.dev Registry enrichment request. The separately configured [LPM Firewall](/docs/guides/firewall) can make its own verdict requests for npm packages.

`--no-security-summary` skips the post-install cache aggregation and human report. It does not change the install-time source-analysis setting or remove existing `.lpm-security.json` cache files.

With `--json --timing`, source analysis from the authoritative fetch path appears under `timing.fetch_breakdown.source_scan` as `sum_ns` and `max_ns`. Fused overlap-prefetch tasks report the same counters under `timing.detail.fetch.overlap.breakdown.source_scan`, so use detail mode when measuring the complete cold-install scan attribution. Both paths scan while extraction is running: the work is already included in cold install wall time and `extract_ms`, and the counters must not be added to the total again.

## Package-published LPM.dev skills [#package-published-lpmdev-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.

```bash
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`](/docs/packages/skills) to remove them. Explicit `lpm skills add @lpm.dev/owner.package` commands remain available regardless of the setting. See [AI agent skills](/docs/reference/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 [#jsr-dependencies]

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

```json title="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:

```ini title=".npmrc"
@jsr:registry=https://npm-jsr-mirror.example.com
```

See [Registries](/docs/registries#jsr-packages) for the routing details.

## Peer dependency strict mode [#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:

```bash
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`.

```json
{
  "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 [#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 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):

```toml title="~/.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.

## Interrupted installs [#interrupted-installs]

If a JavaScript package install fails or receives Ctrl+C, LPM CLI restores its staged manifest changes and invalidates the install cache.

After a forced stop, the next install restores staged `package.json` changes before it proceeds. This also covers selected workspace members. An intentional `"*"` range remains unchanged.

Repeat the original command to complete the interrupted operation:

```bash
lpm install zod
```

A bare `lpm install` instead installs the dependencies from the restored manifest. The retry also repairs incomplete `node_modules` state.

If you edit a staged manifest after interruption, automatic recovery stops and preserves your edits. The error identifies the backup directory under `.lpm/install-recovery`.

Save your current edits separately. Restore the matching backup record's `original` text to its `package.json` path. Repeat the install, then reapply your edits. Keep the backup until recovery succeeds.

## Typosquat guard [#typosquat-guard]

The guard is off by default. Enable it for this machine:

```bash
lpm config typosquat --set on
```

When enabled, `lpm install <pkg>` and bare `lpm install` guard **new direct dependency names** before resolution. The detector covers low-noise cases such as adjacent transpositions (`axois` → `axios`), delimiter variants (`crossenv` → `cross-env`). Flag-shaped package names after `--` (`--legacy-peer-deps`) remain invalid even when the guard is off. 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`:

```toml title="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 (disabled); `on` keeps the guard enabled even if the diagnostic env toggle is set; `off` always disables the guard. Switching an approved guard from `on` to `off` or `default` requires security approval. 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 [#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 set `LPM_TOKEN` 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:

```bash
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`](/docs/infra/security). See the [Firewall for npm guide](/docs/guides/firewall) for custom policy groups.

## Policy extensions [#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.

```toml title="~/.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`](/docs/infra/policy) to inspect and test configured extensions:

```bash
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](/docs/reference/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`:

| 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](/docs/packages/save-policy).

## Production and omitted deps [#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.

```bash
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 [#lifecycle-scripts]

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

| Timing                              | Root project scripts                                                           |
| ----------------------------------- | ------------------------------------------------------------------------------ |
| Before dependency install           | `pnpm:devPreinstall`                                                           |
| After successful dependency install | `preinstall`, `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:

| Policy                | Behavior                                                                                                        |
| --------------------- | --------------------------------------------------------------------------------------------------------------- |
| `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:

```bash
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`:

```json
{ "lpm": { "scriptPolicy": "allow" } }
```

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

```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`) [#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](/docs/packages/security-audit#optional-llm-advisor) for the full contract.

```bash
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-in** — `triage-advisor: "none"` is the default, and `script-policy: "triage"` alone gives you the portable layers 1-4.

Override per-invocation with `--advisor`:

```bash
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`](/docs/packages/rebuild) and [`lpm approve-scripts`](/docs/packages/approve-scripts) for the manual-approval flow.

### Sandbox [#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](/docs/packages/rebuild#project-secret-files) for the exact policy and remaining limits.

```bash
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](/docs/reference/package-json-lpm#the-lpmscripts-block) for declaring extra write directories per-package.

## Guarded weakeners and approvals [#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:

```bash
lpm install --no-sandbox
```

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

Automation behavior:

```bash
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:

```bash
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`](/docs/infra/security) to create the unlock explicitly or to inspect the current floor with `lpm security status`.

## Dependency overrides [#dependency-overrides]

Use `package.json > lpm.overrides`, top-level `overrides`, or top-level `resolutions` to replace direct or transitive dependency versions.

```json title="package.json"
{
  "lpm": {
    "overrides": {
      "ms": "2.1.3"
    }
  }
}
```

An override target replaces the declared range of each matching consumer. An exact target pins that version, even when the consumer declares another range.

This behavior changed from older LPM CLI releases. Older releases intersected the override target with each consumer range. Current releases replace the consumer range, so an existing lockfile can select different versions after an upgrade.

A range target selects the newest eligible published version in the override range. Security and platform policies still apply to the selected version.

Workspace-root overrides apply to the root and all members. A member can add overrides or replace a root selector in the same field.

Field precedence is `lpm.overrides`, then `overrides`, then `resolutions`. LPM CLI lists an override as applied only when it changes the selected version.

If an override target is unavailable or blocked by policy, LPM CLI warns and keeps the natural version. See [`package.json > lpm.overrides`](/docs/reference/package-json-lpm#overrides) for selector forms.

## Workspaces [#workspaces]

At a workspace root, no-argument install is recursive by default:

```bash
lpm install                   # every member in dependency order, root last
lpm install --no-recursive    # root project only
```

LPM CLI shares workspace discovery across member installs and refreshes the root configuration once before installing the root, so lifecycle edits and merged `pnpm-workspace.yaml` settings stay current. On a cold install, eligible importers share one union resolution, with shared expansion passes as needed, and receive isolated projections of the resulting graph; an importer that cannot be projected safely falls back independently. Each selected project still runs through the normal materialization, engine, lifecycle, and security pipeline. Member project lifecycles run in dependency order; the root lifecycle runs last. A failure stops later targets. Cyclic graphs fall back to stable path order.

Recursive installs write one schema-v10 `lpm.lock` at the workspace root. The file stores distinct package rows once under `workspace-packages` and records each member's package closure and root state under `importers`. It is committed only after every selected target succeeds. Member-local commands read and update their own projection in that root file; workspace unions are TOML-only and do not write `lpm.lockb`.

When a successful recursive install finds legacy member lockfiles, it absorbs them into the root union and then removes the obsolete member `lpm.lock` / `lpm.lockb` files. A failed recursive install does not commit the new root union or remove the legacy lockfiles. Targets that already changed `node_modules` remain provisional; their install hashes force a later install to converge them with the authoritative lockfile.

`lpm install --recursive` and `lpm -r install` request the same behavior explicitly. From inside a member, the explicit form widens to the owning workspace. `--recursive` is a manifest refresh operation and cannot be combined with package specs.

Filter a recursive refresh when only part of the workspace needs work:

```bash
lpm install --filter web              # web plus all workspace dependencies
lpm install --filter-prod web         # production dependency closure only
lpm install --filter web --fail-if-no-match
```

A filtered refresh omits unrelated members and the workspace root. Required workspace dependencies are included automatically, even when the filter only names the consuming member.

`workspace:` references resolve by declared package name against both discovered members and a named workspace root. A selected project's own `devDependencies` are installed normally unless production filtering omits them. When another workspace package is consumed as a dependency, its `dependencies`, `peerDependencies`, and `optionalDependencies` can extend the local dependency closure; its `devDependencies` do not become transitives.

When adding a package, target a specific member or the workspace root:

```bash
lpm install react --filter web    # add react to packages/web/
lpm install -w typescript -D      # add to the root package.json
lpm install react --filter './apps/*' # any glob the workspace filter accepts
lpm install react --filter-prod ...web # production 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.

With `--json`, a recursive install emits one aggregate envelope with `recursive: true`, `workspace_root`, a `targets` array, and success counts under `summary`. Per-target install envelopes are suppressed, so stdout remains one JSON document.

Workspace selection is ecosystem-aware. An `@lpm.dev/*` version whose Registry metadata says `ecosystem: "swift"` is routed through the SE-0292 installer for each selected member that owns a direct `Package.swift`; it is never staged into `package.json`. Root (`-w`), member-cwd, and filtered installs mutate and resolve from that same selected Swift package directory. Mixed requests keep JavaScript packages on the existing `package.json` path. One eligible Swift target is automatic; multiple targets prompt unless `-y` selects the first, and zero eligible targets fail without mutating an unrelated manifest.

See [Workspaces](/docs/packages/workspaces) for filter grammar.

## Frozen Lockfile And CI [#frozen-lockfile-and-ci]

```bash
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`](/docs/infra/config#store-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.

Artifact availability does not weaken that contract. If a registry no longer serves an artifact pinned by an existing lockfile, mutable `lpm install`, `lpm install --frozen-lockfile`, and `lpm ci` all fail without deleting, truncating, rewriting, or replacing `lpm.lock` or `lpm.lockb`. A mutable install does not silently select a newer version or a different source.

When the unavailable package is a direct dependency, the error reports an upgrade command using its `package.json` key:

```bash
lpm upgrade <manifest-key>
```

For an npm alias such as `"local": "npm:canonical@1.0.0"`, that command is `lpm upgrade local`, not `lpm upgrade canonical`. When the unavailable package is transitive, LPM CLI does not emit an unusable command action. Restore the artifact, or update the owning direct dependency or an override in a mutable development environment.

Review the result and commit the updated lockfiles before retrying a frozen or CI install.

Artifact-unavailable errors identify the pinned package and version but show only a sanitized registry/source identity. Registry URL credentials, paths, query strings, and fragments are not printed. Authentication, rate-limit, timeout, transport, and registry 5xx failures keep their own error classifications instead of being reported as unavailable pinned artifacts.

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

```dockerfile
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](/docs/guides/docker-deploys).

## Recently published packages [#recently-published-packages]

The minimum release age is **off by default** (`0` seconds). Enable a one-day cooldown for this machine:

```bash
lpm config release-age --set 1d
```

When enabled, the default scope 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:

```json title="package.json"
{ "lpm": { "minimumReleaseAge": 86400, "minimumReleaseAgePolicy": "strict" } }
```

```toml title="~/.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.

```bash
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 in `package.json` or `~/.lpm/config.toml`. Exclusions accept package names, exact versions, and `@scope/*` selectors.

For `"local": "npm:real-pkg@1.0.0"`, use the canonical target name `real-pkg`. Do not use the alias name `local`.

Manage project exclusions with [`lpm trust release-age-exclude`](/docs/packages/trust#lpm-trust-release-age-exclude). Manage user exclusions with [`lpm config release-age-exclude`](/docs/infra/config#exclude-selected-packages-from-the-release-cooldown).

CLI, project, and user exclusion lists merge in that order. LPM CLI removes duplicate selectors.

See the [`lpm trust` storage contract](/docs/packages/trust#storage-contract) for persistent locations and workspace-member behavior.

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](/docs/packages/security-audit#layer-4-provenance--cooldown) check still applies unless you also pass `--ignore-provenance-drift <pkg>` or `--ignore-provenance-drift-all`. Under `--policy=triage`, the [identity-match widening](/docs/packages/security-audit#layer-5-triage) 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-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:

```bash
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:

```bash
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:

```bash
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:

```bash
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](/docs/packages/security-audit#layer-4-provenance--cooldown) for artifact binding, cache, and frozen-replay details.

## Audit after install [#audit-after-install]

After a successful install, LPM CLI can add a compact audit summary. This feature is optional.

```text
✓ Done · installed 247 packages in 1.31s
! Audited 247 packages, 2 vulnerabilities, 67 suspicious, 3 critical in 412ms — run `lpm audit`
  Critical
    @lpm.dev/acme.tool@2.0.0 LPM-ADV-101 — remote code execution [registry/vulnerability]
```

The audit is **informational only**. Its findings never fail the install. Run [`lpm audit --fail-on=all`](/docs/packages/audit) for a gating audit. The feature is **disabled by default**.

Without this option, a normal install does not query OSV. It also does not report LPM.dev Registry advisories or AI security findings.

For JavaScript packages, audit-after-install uses the same discovery, scan, and severity policy as `lpm audit`. It counts each advisory separately and preserves Critical Registry security findings.

For a Swift package named in `lpm install`, audit-after-install uses the same Registry issue collection and severity policy. It uses the metadata fetched for that package. It does not query OSV, scan Swift source, or discover transitive Swift dependencies.

The summary prints every Critical finding below the first line. For JavaScript packages, findings with lower severity remain available through `lpm audit`. `lpm audit` does not currently discover Swift dependencies.

For JavaScript packages, the vulnerability count includes OSV.dev findings and exact-version LPM.dev Registry advisories. If either lookup cannot complete, LPM CLI suppresses the `! Audited` line and the JSON `audit_summary`. The install still succeeds, and logs contain the audit failure.

For an explicit Swift package, the vulnerability count includes its exact-version LPM.dev Registry advisories. The Swift summary does not include OSV findings.

Enable per-invocation:

```bash
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`:

```json
{
  "success": true,
  "audit_summary": {
    "packages_audited": 247,
    "vulnerabilities": 2,
    "suspicious": 67,
    "severity_counts": {
      "critical": 3,
      "high": 4,
      "moderate": 12,
      "low": 0,
      "info": 51
    },
    "critical_findings": [
      {
        "package": "@lpm.dev/acme.tool",
        "version": "2.0.0",
        "message": "LPM-ADV-101 — remote code execution",
        "category": "vulnerability",
        "source": "registry"
      }
    ],
    "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 [#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                                                                                                                                                                                                                                                                                      |
| `--min-release-age-exclude <SELECTOR>`                              | Exempts a package name, exact version, or `@scope/*` from the cooldown. Repeatable.                                                                                                                                                                                                                                |
| `--ignore-provenance-drift <PKG>` / `--ignore-provenance-drift-all` | Waives the drift check                                                                                                                                                                                                                                                                                             |
| `--policy=<deny\|allow\|triage>`, `--yolo`, `--triage`              | Sets the dependency script-policy                                                                                                                                                                                                                                                                                  |
| `--strict-peer-dependencies` / `--no-strict-peer-dependencies`      | Overrides the peer-dependency strictness setting for the synthesized install                                                                                                                                                                                                                                       |
| `--no-engine-strict`                                                | Uses warning-only dependency engine checks 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 -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`](/docs/packages/approve-scripts#global-vs-project) 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 [#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:

| Key            | Compared against                                                                                                                                                                            |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `engines.lpm`  | The running CLI version (`env!("CARGO_PKG_VERSION")`)                                                                                                                                       |
| `engines.node` | The first Node on the constructed script `PATH` — project/workspace `node_modules/.bin`, then a managed runtime selected by `lpm.json`, `.nvmrc`, or `.node-version`, then inherited `PATH` |

```text
$ 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 CLI 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, a declared Node constraint fails and suggests selecting one explicitly with `lpm use node@<version>`; LPM CLI never installs a runtime from the engine range.

```bash
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`](/docs/packages/rebuild) and [`lpm add`](/docs/packages/add). Add runs it before manifest mutation, then applies dependency checks during its install phase.

## Flags [#flags]

| Flag                                          | Effect                                                                                                                                   |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `-r`, `--recursive`                           | Install every package in the owning workspace; dependencies first, root last                                                             |
| `--no-recursive`                              | Install only the current project, even at a workspace root                                                                               |
| `-D`, `--save-dev`                            | Save under `devDependencies`                                                                                                             |
| `-g`, `--global`                              | Install 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`, `--production`                      | Production install; equivalent to omitting dev dependencies                                                                              |
| `--offline`                                   | Never touch the network                                                                                                                  |
| `--frozen-lockfile`                           | Refuse to update lockfiles; fail if `package.json` and `lpm.lock` disagree                                                               |
| `--no-frozen-lockfile`                        | Disable the CI auto-frozen default for this invocation                                                                                   |
| `--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)                                                                         |
| `--min-release-age-exclude <SELECTOR>`        | Exempt a package name, exact version, or `@scope/*` from the cooldown. Repeatable.                                                       |
| `--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` in the v2 virtual-store layout; 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 dependency lifecycle scripts (filesystem containment + env scrubbing + outbound network denial) |
| `--paranoid`                                  | Alias for `--strict-sandbox`                                                                                                             |
| `--no-sandbox`                                | Drop all sandbox containment for this install's dependency 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 interactive install prompts: confirm multi-member workspace mutations and select the first eligible Swift target                    |
| `--skills`                                    | Install package-published LPM.dev skills for this invocation, overriding user config                                                     |
| `--no-skills`                                 | Skip package-published LPM.dev skill auto-install for this invocation                                                                    |
| `--no-editor-setup`                           | Accepted with no effect. Package skills do not create editor integrations.                                                               |
| `--no-security-summary`                       | Skip post-install security-cache aggregation and the human report; extraction-time local analysis still runs                             |
| `--timing`                                    | Include install timing diagnostics in `--json` output                                                                                    |
| `--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`                          | Install with warnings instead of enforcing workspace-root and dependency engine mismatches                                               |
| `--audit-after-install`                       | Run [audit after install](#audit-after-install) for this run (informational only — never fails the install)                              |
| `--no-audit-after-install`                    | Skip [audit after install](#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](/docs/commands#global-flags): `--token`, `--registry`, `--json`, `--verbose`, `--insecure`.

## Pool install reports [#pool-install-reports]

After a successful install, LPM CLI reports the resolved graph for `@lpm.dev/*` packages to the LPM.dev Registry. The report follows actual dependency and peer targets, including overrides and npm aliases. Downloads from a failed install do not create Pool credit.

Older LPM CLI versions that send only package roots remain compatible. Their reports credit eligible roots, but can omit credit for transitive packages. Access checks still apply.

If the registry cannot confirm the report, the command returns `pool_attribution_unconfirmed`. Retry the same command to reuse cached packages and resend the report.

See [Pool weighting and limits](/docs/packages/pool#how-weighting-works).

## See also [#see-also]

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


# lpm licenses (/docs/packages/licenses)



```bash
lpm licenses
lpm licenses --json
lpm licenses --fail-on copyleft
lpm licenses --deny GPL-3.0 --deny AGPL-3.0
```

`lpm licenses` lists the licenses declared by installed packages in the current `lpm.lock` graph. It reads the same installed package manifest metadata that [`lpm sbom`](/docs/packages/sbom) uses: `node_modules/` first, then the LPM CLI store when present.

Use [`lpm query :copyleft`](/docs/packages/query) when you want selector-style security queries. Use `lpm licenses` when you need the exact license inventory and a CI compliance gate.

## Examples [#examples]

```bash
lpm licenses                                      # table output
lpm licenses --json                               # structured inventory
lpm licenses --fail-on copyleft                   # fail if any GPL-family license is present
lpm licenses --fail-on copyleft,missing           # fail on copyleft or absent license metadata
lpm licenses --deny GPL-3.0 --deny AGPL-3.0       # fail on exact denied expressions
```

## Inventory source [#inventory-source]

`lpm licenses` is local-first and does not fetch registry metadata. Run [`lpm install`](/docs/packages/install) first so `lpm.lock`, `node_modules/`, and the store have the package manifests to inspect.

The JSON output includes:

* `root` — the current project's own license metadata
* `packages` — installed dependency packages with `name`, `version`, `scope`, `licenses`, `license_expression`, `missing`, `copyleft`, and denied-license fields
* `summary` — counts for `copyleft`, `missing`, and `denied`
* `policy` — the active `--fail-on` / `--deny` gate and whether it failed

`scope` is derived from root reachability in `lpm.lock`, including `root-aliases` for npm alias declarations. Required paths win over optional paths, and optional paths win over excluded/dev-only paths, so a dev-only transitive package is marked `excluded` unless a production or optional root also reaches it.

Policy applies to dependency packages. The root project license is included for context.

## Policy gates [#policy-gates]

```bash
lpm licenses --fail-on copyleft
```

`--fail-on copyleft` uses the same copyleft classifier as [`lpm audit`](/docs/packages/audit) and [`lpm query :copyleft`](/docs/packages/query). It catches GPL-family and other known reciprocal license identifiers inside SPDX-style expressions.

```bash
lpm licenses --fail-on missing
```

`--fail-on missing` fails when a package has no license declaration or only no-license markers such as `UNLICENSED`, `NONE`, `PROPRIETARY`, `NOASSERTION`, or `SEE LICENSE IN ...`.

```bash
lpm licenses --deny GPL-3.0
```

`--deny <LICENSE>` fails on exact declared license-expression matches, case-insensitive. Repeat it or pass comma-separated values:

```bash
lpm licenses --deny GPL-3.0,AGPL-3.0
lpm licenses --deny GPL-3.0 --deny AGPL-3.0
```

When a policy fails, the command exits `1`. Under `--json`, stdout is still a single JSON object with `"success": false`, summary counts, and per-package denial details.

## Flags [#flags]

| Flag                            | Effect                                                                        |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `--fail-on <copyleft\|missing>` | Fail when matching packages are present. Repeatable and comma-separated.      |
| `--deny <LICENSE>`              | Fail on an exact declared license expression. Repeatable and comma-separated. |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm sbom`](/docs/packages/sbom) — CycloneDX/SPDX export with license metadata
* [`lpm query`](/docs/packages/query) — selector queries such as `:copyleft` and `:no-license`
* [`lpm audit`](/docs/packages/audit) — vulnerability and behavioral audit
* [Security & audit](/docs/packages/security-audit) — how the audit/query/license surfaces fit together


# Lockfile (/docs/packages/lockfile)



LPM CLI's authoritative lockfile is &#x2A;*`lpm.lock`**: TOML, deterministic, and git-diffable. Current installs use this file for exact graph replay.

Older lockfile schemas can also have &#x2A;*`lpm.lockb`**, a generated binary companion. Lockfile version 13 does not write this file.

This page is the conceptual overview — when each file gets read, what the dual format buys you, and how the warm-install fast paths work. For the on-disk schema, see [`lpm.lock` format](/docs/reference/lockfile-format) and [`lpm.lockb` format](/docs/reference/lockb-format).

## Why two files [#why-two-files]

| File        | Optimized for                                           | When it's read                                                                         |
| ----------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `lpm.lock`  | Humans, git diffs, authoritative install input          | Read by install, audit, graph, and review workflows                                    |
| `lpm.lockb` | Generated binary companion for the representable subset | Used for binary validation/writeback and cheap generated-cache inspection when present |

The TOML lockfile is the canonical source. The binary companion does not contain exact package-instance identities.

Current installs skip `lpm.lockb` and remove stale binary files. You do not select this behavior manually.

## What's pinned [#whats-pinned]

Each entry in the lockfile records:

* **Exact resolved version** — never a range
* **Source registry** (e.g. `registry+https://registry.npmjs.org`)
* **SRI integrity hash** (`sha512-…`) — verified on every install
* **Platform metadata** (`os`, `cpu`, `libc`) and optional reachability
* **Registry signature evidence** (`registry-signatures`, `registry-published-at`) when npm metadata provides it
* **Verified provenance evidence** — publisher/workflow identity, exact npm package URL, tarball SHA-512 binding, transparency-log coordinates, certificate digest, and original bundle digest
* **Patch records** (`path`, patch-file `sha256`, `original-integrity`) when `lpm.patchedDependencies` changes installed bytes
* **Direct dep names + versions** — for transitive walking
* **Tarball URL hint** — cached resolved URL that lets warm installs skip the per-package metadata round-trip
* **Importer snapshot** — the package.json dependency sections and resolver-affecting settings that frozen installs compare before replaying the lockfile
* **Workspace importer projection** — content-addressed package IDs plus importer-local root selections, peers, aliases, patches, catalogs, provenance, and automatic linker state
* **Exact package-instance graph** — each package row, dependency, peer, and root selection identifies one graph instance
* **(if any)** npm-alias edges — `local_name → target_canonical_name` pairs

After a standalone `lpm install`:

* `lpm.lock` — commit it
* `lpm.lockb` — remove a stale copy after LPM CLI upgrades the lockfile to version 13

After a recursive workspace install, commit the single root `lpm.lock`. LPM CLI migrates legacy member lockfiles into that root union after a successful install, then removes the obsolete member `lpm.lock` / `lpm.lockb` files.

## Workspace lockfiles [#workspace-lockfiles]

A workspace lockfile separates storage from importer views:

* `workspace-packages` stores each distinct locked package row under a SHA-256 content address.
* `importers` maps `"."` and member-relative paths such as `"packages/web"` to the package IDs and root state visible to that project.
* Member commands locate the nearest owning root lockfile and materialize only their projection. A conflicting version or peer context in another member is not visible unless it is also reachable from the current importer.

Cold recursive installs fuse eligible importers into one synthetic-root greedy-fusion traversal, then project the result per importer. Conflicting roots, aliases, optional reachability, peer bindings, overrides, and release policy remain importer-specific. If one importer cannot be projected safely, only that importer runs an isolated resolver fallback.

Workspace mutations use the root lockfile as their commit point. Filtered add, uninstall, upgrade, audit fix, patch, and tidy changes do not commit their manifests or patches until the corresponding importer projection has been written successfully. A failure restores the previous manifests, patches, and lockfiles, then deletes affected install hashes so the next install repairs any provisional `node_modules` changes.

## Install fast paths [#install-fast-paths]

The install pipeline has a tiered fast-path hierarchy. Each tier skips more work than the next.

### Tier 1: Up-to-date install (\~14 ms) [#tier-1-up-to-date-install-14-ms]

If the install-hash file (`.lpm/install-hash`) matches the current state of `package.json`, its authoritative lockfile projection, and the resolved linker mode, install exits immediately. Nothing to do.

This is the "I just ran `lpm install` and now I'm running it again" case. Hits in CI between consecutive `lpm install` calls in a script. The hash check is sync — file stat + timestamp comparison — so the entire `lpm install` invocation finishes in a few milliseconds on small projects and around 14 ms on the VitePress docs benchmark fixture.

`lpm install <pkg>` finalizes its saved range (for example, the bare `zod` request becomes `^4.3.6`) and reconciles the importer snapshot plus install hash inside the same manifest transaction. An immediate bare `lpm install` therefore sees the finalized `package.json` and lockfile as one completed state and can take this tier instead of repairing a transient `*` importer.

### Tier 2: Warm install (\~387 ms) [#tier-2-warm-install-387-ms]

If the current standalone lockfile or workspace importer projection matches `package.json` (no drift), but `node_modules/` is missing or stale, the install rebuilds `node_modules/` directly from that projected graph + the [global content-addressable store](/docs/packages/content-addressable-store).

No metadata fetches. No tarball downloads. Project `node_modules/<pkg>` is created as a symlink into the matching link entry under the selected store version's `links/<graph-key>/` directory (v2 by default). The link entries live in the **global** store, not in the project, so `rm -rf node_modules` between iterations only loses the cheap project-side symlinks — the canonical extracted bytes and the per-graph wrappers stay put. Warm install is a symlink rebuild over already-materialized link entries.

[`lpm fetch`](/docs/packages/fetch) is the lockfile-only way to create that warm store state ahead of time. It reads `lpm.lock`, downloads compatible remote tarballs into the store, and leaves `package.json`, `node_modules`, and the lockfile untouched.

### Tier 3: Lockfile-only install (offline-able) [#tier-3-lockfile-only-install-offline-able]

`lpm install --offline` forces this path. The resolver does not run. LPM CLI reads each exact package instance from `lpm.lock`.

Replay also checks the source against the current manifest. A registry range cannot replay a Git, tarball, or local-path package.

Local `file:` and `link:` entries must resolve to a declared path or a reachable transitive source. Offline mode keeps this rule.

Lockfile versions 1–12 do not contain an exact package-instance graph. Offline and frozen installs reject these lockfiles because replay can select the wrong peer context.

`lpm migrate --no-install` intentionally writes a version 12 staging lockfile. Run an online `lpm install` to create version 13 before offline use.

For a standalone project, run an online mutable install once:

```bash
lpm install
git add lpm.lock
git commit
```

For a workspace, run a recursive install from the workspace root:

```bash
lpm install --recursive
git add lpm.lock
git commit
```

The online install resolves the graph again and writes lockfile version 13. Then retry the offline or frozen install.

This is the CI-recommended path:

```bash
lpm install --offline --strict-integrity
```

`--strict-integrity` tightens the contract further: tarball-URL deps must declare their SRI inline. Trust-on-first-use is disabled.

### Unavailable pinned artifacts [#unavailable-pinned-artifacts]

An online lockfile replay keeps the exact package version, source, and integrity pinned in `lpm.lock`. If a cached tarball URL returns 404, LPM CLI can refresh registry metadata once. It retries a moved URL only when the package contract stays unchanged. It never uses that recovery to select a newer version or another registry.

If the pinned artifact remains unavailable, install fails and preserves the existing bytes of both `lpm.lock` and `lpm.lockb`. This applies to ordinary mutable replay, explicit `--frozen-lockfile`, plain `lpm install` automatically frozen by CI, and `lpm ci`. A mutable replay does not silently replace the pin.

For a direct dependency, run the reported command using its `package.json` key, then commit the result:

```bash
lpm upgrade <manifest-key>
git add lpm.lock
git commit
```

Aliases use their local manifest key. For example, `"local": "npm:canonical@1.0.0"` is upgraded with `lpm upgrade local`. An unavailable transitive package has no machine-actionable upgrade command because its canonical name is not a direct `package.json` entry. Restore the artifact, or update the owning direct dependency or an override in a mutable development environment.

If the upgrade removes `lpm.lockb`, stage that removal with `lpm.lock`.

The failure reports the pinned package and version with a sanitized source identity. Registry and remote-tarball diagnostics omit credentials and secret URL components.

Authentication, rate-limit, timeout, transport, and registry 5xx failures remain distinct errors. LPM CLI does not report them as unavailable artifacts.

### Tier 4: Resolve-and-install (cold) [#tier-4-resolve-and-install-cold]

If the lockfile drifts from `package.json` (a new dep was added, a range was bumped), the resolver runs. The streaming dispatcher fetches metadata, picks versions, downloads tarballs, populates the store, links into `node_modules/`, and writes a fresh lockfile. In a cold recursive workspace install, eligible importers share one union resolution, with shared expansion passes as needed, and one root lockfile commit.

This is the slow path, but "slow" is relative — cold install on the 535-package VitePress docs fixture is **2,945 ms** in our benchmarks (vs npm 17,354 ms / pnpm 6,125 ms).

## Lockfile-version [#lockfile-version]

The TOML lockfile is **schema-versioned** (current: `13`), not tool-versioned. A version changes only when the file structure changes.

Version 11 added local manifest fingerprints. Version 12 added structured peer edges. Version 13 added exact package-instance identities for rows, edges, and roots.

An older LPM CLI rejects a newer schema. A current LPM CLI rejects unsafe replay of lockfile versions 1–12 in offline and frozen modes.

The binary lockfile has its own wire-format version (current: `3`). Version `3` added a sparse provenance section and an 8-byte footer.

If the binary version does not match, the reader rejects it and reads TOML. Lockfile version 13 does not rebuild the binary file.

LPM CLI reports a binary writeback only when it actually writes a representable `lpm.lockb`. TOML-only maintenance such as reconciling an importer snapshot, patch record, or lockfile schema does not produce a misleading binary-format upgrade message.

## When the binary lockfile is skipped [#when-the-binary-lockfile-is-skipped]

`lpm.lockb` is intentionally smaller than `lpm.lock`. It does not contain the exact package-instance graph from lockfile version 13.

As a result, current installs write &#x2A;*only `lpm.lock`**. LPM CLI also removes a stale binary companion during a lockfile version 13 write.

An incomplete binary file can disagree with the reviewer-visible TOML lockfile. Therefore, LPM CLI skips the write and removes stale binary state.

If your project only has `lpm.lock`, that's expected when the graph needs TOML-only metadata.

## Determinism [#determinism]

The lockfile is deterministic by construction:

* Entries sorted by name
* Workspace package rows keyed by a SHA-256 digest of their serialized locked content
* Importer package IDs sorted and deduplicated
* Per-entry `dependencies` arrays sorted
* Optional fields omitted (never written as `null`)
* Schema-versioned

This is what makes `git diff lpm.lock` actionable. A new dep adds entries. A version bump rewrites a single package's fields. Supply-chain surprises (a transitive integrity change without a version bump, a new package appearing without a corresponding `package.json` change) are visible without parsing.

## Safe atomic rewrites [#safe-atomic-rewrites]

When LPM CLI replaces `lpm.lock` or `lpm.lockb`, it writes through an exclusively created, collision-resistant file in the same directory and then atomically replaces the destination. An attacker-preplanted temporary symlink, hardlink, junction, or reparse entry is not followed, so an ordinary lockfile update cannot use a predictable staging name to overwrite its external target.

Metadata and artifact fetch failures happen before lockfile replacement and leave any existing `lpm.lock` and `lpm.lockb` byte-for-byte unchanged.

This guarantee covers LPM-managed atomic rewrites and their temporary leaf entries. It does not confine an attacker-controlled parent directory, apply to every filesystem write made by LPM CLI or package scripts, or by itself promise that a completed write survives sudden power loss.

## Should I commit `lpm.lockb`? [#should-i-commit-lpmlockb]

Commit `lpm.lock` always. In a workspace, commit only the root `lpm.lock`. Member lockfiles are obsolete after migration.

Lockfile version 13 is TOML-only. If an upgrade removes `lpm.lockb`, commit the removal. Do not create a binary file manually.

The only special handling: mark `lpm.lockb` as binary in `.gitattributes`:

```text
lpm.lockb binary
```

`lpm init` adds this line for you.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — what reads the lockfile, and how `--offline` / `--strict-integrity` work
* [`lpm.lock` format](/docs/reference/lockfile-format) — full TOML schema reference
* [`lpm.lockb` format](/docs/reference/lockb-format) — binary format wire spec
* [Resolver](/docs/packages/resolver) — what produces a lockfile
* [Content-addressable store](/docs/packages/content-addressable-store) — where the bytes the lockfile points at live


# lpm migrate (/docs/packages/migrate)



```bash
lpm migrate
```

Detects the current package manager and converts its lockfile to a version 12 LPM staging lockfile. The staging graph can include `lpm.lockb`.

By default, the command then runs an online `lpm install`. This install resolves exact package-instance identities and writes the current version 13 TOML lockfile.

`lpm migrate` is non-destructive: every file it touches is backed up first (`package-lock.json.backup`, `.npmrc.backup`, etc.), and `--rollback` restores them.

## Examples [#examples]

```bash
lpm migrate                  # default — non-interactive
lpm migrate --dry-run        # parse + convert, write nothing
lpm migrate --force          # overwrite an existing lpm.lock
lpm migrate --no-install     # convert lockfile only, don't install
lpm migrate --skip-verify    # skip the build/test verification step
lpm migrate --no-npmrc       # don't touch .npmrc
lpm migrate --ci             # also generate a CI workflow template
lpm migrate --rollback       # restore from .backup files
```

The flow is non-interactive — there are no prompts to skip and `-y` is reserved for a future interactive mode. Pass `--force` if you want to overwrite an existing `lpm.lock`.

## Input limits and supported formats [#input-limits-and-supported-formats]

`lpm migrate` reads one fixed snapshot of the selected source lockfile. It rejects malformed files and unsupported versions.

The command supports npm lockfile versions 1, 2, and 3. It supports pnpm lockfile major versions 5, 6, and 9.

Each source lockfile has these limits:

* The file size limit is 64 MiB.
* The package-entry limit is 200,000.

npm lockfiles have these additional limits:

* The package-path limit is 32 KiB.
* The package nesting limit is 256 levels.
* The dependency limit for one package is 10,000.
* The total dependency-edge limit is 1,000,000.

For `bun.lockb`, LPM CLI copies the selected file before conversion. The Bun converter has these limits:

* The time limit is 30 seconds.
* The converted output limit is 64 MiB.
* The diagnostic-output limit is 64 KiB.

If a file or conversion exceeds a limit, migration stops before it changes the project.

## What runs, in order [#what-runs-in-order]

1. **Pre-flight** — confirm `package.json` exists; refuse if `lpm.lock` already exists (unless `--force` or `--dry-run`).
2. **Detect + parse + convert** — identify source PM (npm / pnpm / yarn / bun), read its lockfile, convert to LPM CLI format.
3. **Translate `pnpm.*` blocks** — auto-translate `pnpm.overrides`, `pnpm.patchedDependencies`, and `pnpm.peerDependencyRules` into the matching `lpm.*` fields. Patch translations also add `lpm.lock > [patches]` checksum records. Validation runs up-front: range parse errors, conflicting keys / ranges, unsupported value shapes, missing patch integrity bindings — all surface BEFORE any disk mutation. The original `pnpm.*` block stays in place after migration so a parallel `pnpm install` keeps working during the transition. See [Migrating from pnpm](/docs/guides/migrating-from-pnpm) for per-block details.
4. **Write** — emit a version 12 staging `lpm.lock`. Emit `lpm.lockb` only when the graph fits the binary format. Back up all files before replacement.
5. **`.npmrc`** — optionally append `@lpm.dev:registry=https://lpm.dev/api/registry/` to `.npmrc` (or create one). Backs up the original on touch. **No-op** if the file already declares an `@lpm.dev:registry` scope. Skip the whole step with `--no-npmrc`.
6. **Install** — run an online `lpm install`. This step resolves exact identities, writes version 13, and removes the staging binary file. Skip with `--no-install`.
7. **Verify** — run `build` + `test` scripts to confirm nothing broke. Skip with `--skip-verify`.
8. **CI template** — print a hint pointing at a CI workflow template, or generate one with `--ci`. Suppress with `--no-ci`.
9. **Completion** — print a compact status transcript: detected source + backup reminder, converted lockfile outputs, any install/verify phases, then `Done · migration completed successfully`.

## Rollback [#rollback]

If anything goes wrong (or the migration was a mistake), restore in one command:

```bash
lpm migrate --rollback
```

Walks the backups created by the previous migration and restores them in place. Safe to run repeatedly.

## Flags [#flags]

| Flag            | Effect                                                                                             |
| --------------- | -------------------------------------------------------------------------------------------------- |
| `--dry-run`     | Parse and convert only — write nothing                                                             |
| `--force`       | Overwrite an existing `lpm.lock`                                                                   |
| `-y`, `--yes`   | Reserved. The flow is non-interactive today, so this flag is a no-op. It does NOT imply `--force`. |
| `--no-install`  | Don't run `lpm install` after conversion                                                           |
| `--skip-verify` | Skip the build/test verification step                                                              |
| `--no-npmrc`    | Don't touch `.npmrc`                                                                               |
| `--ci`          | Generate a CI workflow template for the detected platform                                          |
| `--no-ci`       | Suppress the CI template hint                                                                      |
| `--rollback`    | Restore files from `.backup` copies created by a previous migration                                |

Plus the [global flags](/docs/commands#global-flags).

`--no-install` leaves the version 12 staging lockfile in place. Run `lpm install` before an offline or frozen install.

## See also [#see-also]

* [Migrating from npm](/docs/guides/migrating-from-npm)
* [Migrating from pnpm](/docs/guides/migrating-from-pnpm)
* [Migrating from Yarn](/docs/guides/migrating-from-yarn)
* [Migrating from Bun](/docs/guides/migrating-from-bun)
* [`lpm install`](/docs/packages/install) — runs after conversion to materialize `node_modules`
* [Lockfile](/docs/packages/lockfile) — `lpm.lock` and `lpm.lockb` formats


# npm compatibility (/docs/packages/npm-compatibility)



LPM CLI is **drop-in compatible** with the npm ecosystem. Reads `package.json`. Resolves npm-flavored semver. Materializes `node_modules/`. Installs from `registry.npmjs.org` by default. Honors `.npmrc` for routing and auth. Forwards subprocess exit codes. Almost any npm-shaped project works without conversion — `lpm install` produces a runnable tree.

This page is the explicit scope: what carries over verbatim, what's different, and what LPM CLI adds.

## Carries over verbatim [#carries-over-verbatim]

| Surface                                                                                         | Status                                                                                                                                                                                                                                                                                                                                                                                                                            |   |                                                                                 |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | ------------------------------------------------------------------------------- |
| `package.json > dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies` | Read identically                                                                                                                                                                                                                                                                                                                                                                                                                  |   |                                                                                 |
| `package.json > scripts`                                                                        | Run via `lpm run <name>` (or bare `lpm <name>` as shorthand)                                                                                                                                                                                                                                                                                                                                                                      |   |                                                                                 |
| `package.json > bin` (string or map form)                                                       | Both shapes accepted                                                                                                                                                                                                                                                                                                                                                                                                              |   |                                                                                 |
| `package.json > files`                                                                          | Honored at publish time                                                                                                                                                                                                                                                                                                                                                                                                           |   |                                                                                 |
| `package.json > engines.node`                                                                   | Enforced as a compatibility constraint for the root and selected dependency versions; it never selects or installs Node, required mismatches abort, and optional-only incompatible packages are skipped                                                                                                                                                                                                                           |   |                                                                                 |
| `package.json > engines.lpm`                                                                    | Enforced against the running CLI version (npm only enforces with `engine-strict=true`; LPM CLI enforces by default)                                                                                                                                                                                                                                                                                                               |   |                                                                                 |
| `package.json > workspaces` (array or object form)                                              | Both shapes accepted                                                                                                                                                                                                                                                                                                                                                                                                              |   |                                                                                 |
| `package.json > overrides`                                                                      | npm-style overrides honored (lower precedence than `lpm.overrides`)                                                                                                                                                                                                                                                                                                                                                               |   |                                                                                 |
| `package.json > resolutions`                                                                    | yarn-style resolutions honored                                                                                                                                                                                                                                                                                                                                                                                                    |   |                                                                                 |
| `package.json > catalogs` and `pnpm-workspace.yaml > catalog` / `catalogs`                      | Centralized version catalogs                                                                                                                                                                                                                                                                                                                                                                                                      |   |                                                                                 |
| `.npmrc`                                                                                        | Read for `registry=`, `@scope:registry=`, and `//host/:_authToken=` lines                                                                                                                                                                                                                                                                                                                                                         |   |                                                                                 |
| Semver dialect                                                                                  | `^`, `~`, \`                                                                                                                                                                                                                                                                                                                                                                                                                      |   | `, `\*`, `x`, hyphen ranges, prereleases, dist-tags (`latest`, `beta`, `next\`) |
| Lifecycle script names                                                                          | Bare root installs run `pnpm:devPreinstall`, then `preinstall`, `install`, `postinstall`, `preprepare`, `prepare`, and `postprepare`. Dependency packages execute only `preinstall`, `install`, and `postinstall` through the trust-gated rebuild path (see [Differences](#differences)); dependency `prepare`, `prepublishOnly`, `preuninstall`, `uninstall`, and `postuninstall` are recognized for detection but not executed. |   |                                                                                 |

If a project's only contract with npm is "I have a `package.json` with deps and scripts," that project works unchanged.

## Differences [#differences]

The differences are deliberate — LPM CLI defaults differ where npm's defaults aren't safe, and a handful of legacy npm behaviors are simplified.

### Dependency lifecycle scripts: deny by default [#dependency-lifecycle-scripts-deny-by-default]

npm runs `preinstall` / `install` / `postinstall` for every dependency package by default. LPM CLI **doesn't**. Bare `lpm install` still runs the root project's install lifecycle, including `prepare`; dependency package scripts stay behind the approval gate.

```bash
lpm install        # root lifecycle + downloads + links; dependency scripts blocked
lpm rebuild        # manually runs scripts for trusted packages
```

To run dependency scripts during install:

```bash
lpm install --policy=allow      # or --yolo
```

To approve packages for the default deny path: [`lpm approve-scripts`](/docs/packages/approve-scripts) walks the blocked set interactively.

This is the single biggest behavioral difference. See [Security & audit](/docs/packages/security-audit) for why.

### `node_modules` layout: hoisted by default, isolated for workspaces and peer conflicts [#node_modules-layout-hoisted-by-default-isolated-for-workspaces-and-peer-conflicts]

LPM CLI starts single-package projects in its v2 hoisted virtual-store layout: declared root deps are exposed at project `node_modules/<dep>`, and package-local dependency links live inside shared store link entries. This is compatible with normal Node resolution, but it is not npm-v3-style "flatten every transitive to the project root." **Workspaces auto-flip to isolated** (pnpm-style symlinks into the global virtual store) — phantom-dep bugs are most expensive in monorepos, so the safer layout becomes the default there. If a default-hoisted resolve finds incompatible peer requirements, LPM CLI also auto-switches that project to isolated and records the decision in `lpm.lock`. Force a specific mode:

```bash
lpm install --linker=isolated      # opt into pnpm-style for non-workspaces
lpm install --linker=hoisted       # use LPM CLI's hoisted virtual-store layout
```

Or persist it in `package.json > lpm > linker` / `~/.lpm/config.toml > linker`. Explicit linker settings opt out of peer-conflict auto-switching.

Under isolated layout, code that imports a dep it didn't declare in `dependencies` (phantom-dep access) breaks. npm's hoisting silently allowed it; isolated doesn't. Add the missing entries to `dependencies` — the fix is one-line per package.

### Lockfile: `lpm.lock` + optional `lpm.lockb` [#lockfile-lpmlock--optional-lpmlockb]

npm produces `package-lock.json`. LPM CLI always produces `lpm.lock` (TOML, git-diffable, authoritative). It also produces `lpm.lockb` when the current binary wire format can represent the graph. Commit `lpm.lock` always; commit `lpm.lockb` when it exists.

Convert with:

```bash
lpm migrate
```

See [Migrating from npm](/docs/guides/migrating-from-npm).

### Save policy: never `*` [#save-policy-never-]

npm's `npm install <pkg>` saves `^resolvedVersion`. LPM CLI does the same — but LPM CLI also enforces that `*` can never become a default. Wildcards must be requested per-package via `pkg@*`. See [Save policy](/docs/packages/save-policy).

### Cooldown on recent versions [#cooldown-on-recent-versions]

The release cooldown is off by default. Enable it with `lpm config release-age --set 1d`. It checks direct dependencies unless you select `minimumReleaseAgePolicy: "strict"`. See [Recently published packages](/docs/packages/install#recently-published-packages).

### `engines` enforced by default [#engines-enforced-by-default]

npm reads `engines.node` / `engines.npm` and only enforces them when `engine-strict=true` is set in `.npmrc`. LPM CLI enforces by default: workspace-root `engines.lpm` and `engines.node` mismatches abort, as do incompatible required dependencies. An incompatible package reachable only through optional edges is skipped. Dependency Node ranges are stored in `lpm.lock` and revalidated on warm, frozen, and offline installs.

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

Or persistent in `package.json > lpm > engineStrict = false` (per-project) or `~/.lpm/config.toml > engine-strict = false` (per-user). See [`lpm install` § engines enforcement](/docs/packages/install#engines-enforcement).

### `npm publish --otp` → `lpm publish` with `otpRequired` [#npm-publish---otp--lpm-publish-with-otprequired]

npm prompts for OTP at publish time when 2FA is enabled. LPM CLI mirrors this; configure via `lpm.json > publish.npm.otpRequired = true` to prompt up-front (saves a round-trip). See [`lpm.json` publish.npm](/docs/reference/lpm-json#publishnpm).

## What LPM CLI adds [#what-lpm-cli-adds]

Things LPM CLI offers on top of the npm-compatible baseline:

| Feature                                                                               | Where                                                                                                                           |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Strict trust binding for lifecycle scripts (`{name, version, integrity, scriptHash}`) | [`lpm approve-scripts`](/docs/packages/approve-scripts)                                                                         |
| Behavioral analysis tags                                                              | [`lpm audit`](/docs/packages/audit), [`lpm query`](/docs/packages/query)                                                        |
| Registry signature audit and optional install-time verification                       | [`lpm audit signatures`](/docs/packages/audit#registry-signatures), [`lpm config signatures`](/docs/infra/config#setup-wizards) |
| npm publish-trust and verified-provenance no-downgrade policy                         | [`lpm config trust-policy`](/docs/infra/config#setup-wizards)                                                                   |
| Provenance drift detection                                                            | [Security & audit § Layer 4](/docs/packages/security-audit#layer-4-provenance--cooldown)                                        |
| Triage gate                                                                           | [Security & audit § Layer 5](/docs/packages/security-audit#layer-5-triage)                                                      |
| Local patches with integrity binding                                                  | [`lpm patch`](/docs/packages/patch)                                                                                             |
| Dual-format lockfile                                                                  | [Lockfile](/docs/packages/lockfile)                                                                                             |
| Workspace `--affected` + topology-aware caching                                       | [Workspaces](/docs/packages/workspaces), [Task runner](/docs/dev/task-runner)                                                   |
| `workspace:*` protocol                                                                | [Workspaces](/docs/packages/workspaces#workspace-protocol)                                                                      |
| Catalog protocol                                                                      | [Workspaces](/docs/packages/workspaces#catalogs)                                                                                |
| Native `jsr:` dependency specs                                                        | [`lpm install`](/docs/packages/install#jsr-dependencies), [Registries](/docs/registries#jsr-packages)                           |
| Path-selector overrides (`baz>foo`)                                                   | [`package.json` lpm.overrides](/docs/reference/package-json-lpm#overrides)                                                      |
| Multi-registry routing via `.npmrc` (LPM.dev Registry + npm + private in one project) | [Registries](/docs/registries)                                                                                                  |
| Source delivery (`lpm add`)                                                           | [`lpm add`](/docs/packages/add)                                                                                                 |
| Built-in lazy-downloaded tools (oxlint, biome)                                        | [`lpm lint`](/docs/dev/lint), [`lpm fmt`](/docs/dev/fmt)                                                                        |
| Zero-config dev server with HTTPS, tunnel, services                                   | [`lpm dev`](/docs/dev/dev)                                                                                                      |
| SE-0292 Swift Package Registry support                                                | [Swift Package Registry](/docs/packages/swift-package-registry)                                                                 |

## What LPM CLI does not do [#what-lpm-cli-does-not-do]

Honest scope:

* **Lockfile-only PR review** — no first-class diff tool yet beyond `git diff lpm.lock`. Use the deterministic format and your PR review tool of choice.
* **Reading the `pnpm.*` namespace at install time** — plain `lpm install` does not consume `pnpm.overrides`, `pnpm.patchedDependencies`, or `pnpm.peerDependencyRules`. `lpm migrate` translates all three into their `lpm.*` equivalents in one pass; install-time warnings (and stable `lpm doctor --json` codes `pnpm_overrides_drift`, `pnpm_patches_drift`, `pnpm_peer_rules_drift`) flag any post-migration drift. See [Migrating from pnpm](/docs/guides/migrating-from-pnpm).
* **Yarn berry's `.pnp.cjs`** — no Plug'n'Play support. LPM CLI materializes `node_modules/` (isolated or hoisted).
* **GUI** — there's no `lpm-vault`-style desktop app shipped today. The CLI is the surface.

## Compatibility with the broader ecosystem [#compatibility-with-the-broader-ecosystem]

* **Tools that read `package.json`** (TypeScript, ESLint, build tools): work unchanged.
* **Tools that walk `node_modules/`** (most bundlers, test runners): work under both isolated and hoisted layouts.
* **Tools that hard-code `package-lock.json`**: don't see LPM CLI's lockfile. Either run `lpm migrate` to add `lpm.lock` alongside, or use `lpm install --linker=hoisted` for a closer-to-npm shape.

## See also [#see-also]

* [`lpm migrate`](/docs/packages/migrate) — convert an npm/pnpm/yarn/bun project
* [Migrating from npm](/docs/guides/migrating-from-npm) — walkthrough
* [Registries](/docs/registries) — `.npmrc` routing
* [Save policy](/docs/packages/save-policy) — npm-compatible defaults with stricter rails
* [Security & audit](/docs/packages/security-audit) — what's different about LPM CLI's trust model


# lpm outdated (/docs/packages/outdated)



```bash
lpm outdated
```

Compares every entry in `package.json > dependencies` and `package.json > devDependencies` against the registry and prints the ones with newer versions available. Read-only — never modifies anything.

## Example output [#example-output]

```text
Section            Package                            Current      Wanted       Latest
dependencies       react                              18.2.0       18.3.1       19.0.0
dependencies       zod                                4.3.5        4.3.6        4.4.0
devDependencies    typescript                         5.4.0        5.6.2        5.7.0
```

A row appears when the newest installable version is greater than the installed direct dependency version from `lpm.lock`. `wanted` is the newest installable version that still satisfies the declared range. `latest` is the newest installable version overall.

If your installed version is newer than the registry version, the command does not report an available update. This behavior prevents a registry rollback from appearing as an upgrade.

Fresh direct dependency versions inside the cooldown window are ignored until they mature. This behavior matches [`lpm upgrade`](/docs/packages/upgrade) and [`lpm install`](/docs/packages/install).

## Ecosystem scope [#ecosystem-scope]

By default, `lpm outdated` checks both `@lpm.dev/*` packages and npm packages whose lockfile source can be checked without disclosing a private name to a new registry.

```bash
lpm outdated                     # both ecosystems (default)
lpm outdated --registry-only=lpm # @lpm.dev only — useful when npm is rate-limiting or offline
```

`--registry-only=all` is the explicit form of the default.

## Acting on the report [#acting-on-the-report]

[`lpm upgrade`](/docs/packages/upgrade) applies the report for `@lpm.dev/*` packages and for npm packages whose existing `lpm.lock` entry records either public npm or the configured LPM.dev Registry proxy as the source. That keeps the common flow as:

```bash
lpm outdated
lpm upgrade -y
```

For one-off manual bumps, use `lpm install` with the spec you want:

```bash
lpm install zod@latest
lpm install react@^19            # explicit range
```

Packages without a recorded public npm or LPM.dev Registry-proxy source are still skipped instead of being queried against `registry.npmjs.org`. Run `lpm install` first so the source is captured in `lpm.lock`, then rerun `lpm outdated` or `lpm upgrade`.

## JSON output [#json-output]

```bash
lpm outdated --json
```

Emits a schema-versioned envelope with `schema_version`, `success`, `count`, `outdated_count`, and a `packages[]` array. Each row carries:

| Field            | Meaning                                                                                                         |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `schema_version` | JSON contract version for the envelope. Current value: `2`                                                      |
| `name`           | Package name                                                                                                    |
| `current`        | Version resolved in `lpm.lock`, or `"?"` if the lockfile is missing                                             |
| `wanted`         | The newest installable version that satisfies the declared range, or `null` when nothing published satisfies it |
| `wanted_range`   | The raw declared spec from `package.json` (for example `"^4.3.0"`)                                              |
| `latest`         | The newest installable version on the registry, regardless of range                                             |
| `section`        | Which dependency map produced the row: `dependencies` or `devDependencies`                                      |
| `outdated`       | `true` when `latest` is greater than `current`                                                                  |

When `lpm outdated` skips non-`@lpm.dev/*` packages because they do not have recorded public npm or LPM.dev Registry-proxy source attribution in `lpm.lock`, the envelope also includes `skipped_private` and `skipped_private_reason`.

LPM CLI reports registry lookup failures and malformed metadata. In human mode, LPM CLI lists the unresolved packages and exits non-zero.

In `--json` mode, the envelope keeps successfully checked rows and sets `success: false`. It also adds `unresolved_count` and `unresolved[]`. Then `lpm outdated` exits with code 1.

If `lpm.lock` exists but is malformed or unsupported, the command stops and reports the lockfile error. It does not treat the file as missing.

## Flags [#flags]

| Flag                         | Effect                                              |
| ---------------------------- | --------------------------------------------------- |
| `--registry-only <all\|lpm>` | Limit checks to a single ecosystem (default: `all`) |

Plus the [global flags](/docs/commands#global-flags) — `--json` is especially useful for scripting.

## See also [#see-also]

* [`lpm upgrade`](/docs/packages/upgrade) — apply the updates surfaced by `lpm outdated`
* [`lpm audit`](/docs/packages/audit) — check for vulnerable versions you should bump


# lpm patch / patch-commit / patch-remove (/docs/packages/patch)



```bash
lpm patch <selector>                  # step 1 — extract for editing
lpm patch-commit <staging_dir>        # step 2 — finalize as a .patch file
lpm patch-remove <selector>           # remove a registered patch
```

Two-step workflow for fixing a bug in a dependency without forking it. The patch travels with your repo under `patches/<name>@<version>.patch` and re-applies automatically on every subsequent `lpm install`.

Same shape as [`patch-package`](https://www.npmjs.com/package/patch-package) — but bound to the original store integrity, so a silently-republished version of the dep can never substitute for the version the patch was authored against.

## The workflow [#the-workflow]

```bash
# 1. Extract a clean copy of the store entry to a staging dir
lpm patch lodash@4.17.21
# → /tmp/lpm-patch-XXXXXX/node_modules/lodash/  (path printed)

# 2. Edit files in the staging dir however you want
vim /tmp/lpm-patch-XXXXXX/node_modules/lodash/index.js

# 3. Produce the patch file and register it in package.json
lpm patch-commit /tmp/lpm-patch-XXXXXX
# → patches/lodash@4.17.21.patch
# → package.json :: lpm.patchedDependencies updated
# → lpm.lock :: [patches] checksum updated when a lockfile exists
```

After `patch-commit`, &#x2A;*commit `patches/`, `package.json`, and `lpm.lock`** to your repo. The next `lpm install` (yours, your teammates', CI's) verifies the lockfile-recorded patch checksum, then auto-applies the patch after linking.

## `lpm patch <selector>` [#lpm-patch-selector]

```bash
lpm patch lodash                # bare name — resolves against the project lockfile
lpm patch lodash@4.17.21        # exact pin — works without a lockfile
lpm patch lodash@^4.0.0         # semver range — resolves against the project lockfile
```

| Selector                       | Notes                                                                                               |
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
| Bare name (`lodash`)           | Resolves to whatever exact version is in `lpm.lock`. Requires a lockfile (run `lpm install` first). |
| Exact pin (`lodash@4.17.21`)   | Works without a lockfile. The version must already be in the global store.                          |
| Semver range (`lodash@^4.0.0`) | Resolves to the lockfile's pinned version. Requires a lockfile.                                     |
| Dist-tags (`latest`, `next`)   | Not accepted. Pin to an exact version or range.                                                     |

Steps:

1. Resolves the selector to an exact `(name, version)` pair (using the lockfile for non-exact inputs).
2. Looks up `<name>@<version>` in the global store. Errors with a `lpm install <name>@<version>` hint if not present.
3. Copies the **pristine** store bytes into a unique staging directory under the OS temp root (`/tmp/lpm-patch-XXXXXX/node_modules/<name>/`). "Pristine" matters: re-running `lpm patch` on a project where this dep is already patched still seeds the staging dir from the upstream bytes, so the resulting diff is delta-from-upstream, not delta-from-previously-patched.
4. Writes a `.lpm-patch.json` breadcrumb in the staging root recording the resolved `name`, `version`, exact-pin `key`, and the store source path.
5. Prints the staging path. The dir is intentionally NOT auto-deleted — it must outlive the process so you can edit it.

With `--json`, `lpm patch` emits `success`, `name`, `version`, `key`, `staging_dir`, `package_dir`, and `next_steps[]`. The next step is an object with `description` and a runnable `lpm patch-commit <staging_dir>` command after you edit `package_dir`.

## `lpm patch-commit <staging_dir>` [#lpm-patch-commit-staging_dir]

```bash
lpm patch-commit /tmp/lpm-patch-XXXXXX
```

Steps:

1. Reads the breadcrumb to recover `(name, version, key)`.
2. Re-locates the store baseline from the live store (not the breadcrumb's recorded path), so a store relocation between `patch` and `patch-commit` doesn't break the diff.
3. Generates a unified diff between the staging directory and the pristine baseline. Aborts if the diff is **empty** ("no changes detected") or contains **binary file edits** (patches must be text-only — `originalIntegrity` already covers binary drift on install).
4. Writes `<project>/patches/<key>.patch` atomically (`.tmp` then rename). Scoped names have `/` translated to `__` for cross-platform portability — `@scope/pkg@1.0.0` lands at `patches/@scope__pkg@1.0.0.patch`. Mirrors pnpm.
5. Updates `package.json > lpm > patchedDependencies` with an entry recording the patch path and the original SRI integrity hash.
6. Updates `lpm.lock > [patches]` when a lockfile exists, recording the patch path, patch-file SHA-256, and original integrity.
7. Cleans up the staging directory (best-effort).

The on-disk `package.json > lpm > patchedDependencies` entry looks like:

```json
{
  "lpm": {
    "patchedDependencies": {
      "lodash@4.17.21": {
        "path": "patches/lodash@4.17.21.patch",
        "originalIntegrity": "sha512-..."
      }
    }
  }
}
```

## `lpm patch-remove <selector>` [#lpm-patch-remove-selector]

```bash
lpm patch-remove lodash@4.17.21
lpm patch-remove lodash --dry-run
lpm patch-remove lodash@4.17.21 --keep-file
```

Removes entries from `package.json > lpm > patchedDependencies` and removes matching `lpm.lock > [patches]` records when a lockfile exists. Exact pins (`lodash@4.17.21`) match one manifest entry. Bare names (`lodash`) work only when they uniquely match a single patched version; if multiple patched versions exist, use the exact key.

By default, LPM CLI also deletes the patch file when it is safely inside the project and no remaining patch entry references it. Use `--keep-file` to leave the file on disk. Use `--dry-run` to preview the manifest and file changes without writing.

After removing a patch, run `lpm install` to refresh `node_modules`.

## Drift safety [#drift-safety]

On every install, LPM CLI verifies both patch inputs before applying anything:

* The current patch file's SHA-256 must match `lpm.lock > [patches].sha256`.
* The store entry's integrity must match `originalIntegrity`.

**Drift is a hard error** — your install will refuse to apply the patch if the patch file changed outside `lpm patch-commit`, or if the underlying package's content changed (re-publish under the same version, tampering, etc.). Restore the recorded patch file, or re-author it through `lpm patch` + `lpm patch-commit`.

This is the key safety property versus stock `patch-package`: a republished `lodash@4.17.21` with different bytes can't silently slip past your patch.

## When to use [#when-to-use]

* A dependency has a known bug; the upstream PR isn't merged yet.
* A peer-dep mismatch needs a one-line fix to make installation work.
* A vendored package needs a tiny tweak that doesn't justify forking.

For larger or longer-lived divergence, prefer forking + publishing your own scoped variant.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — auto-applies patches after linking
* [`lpm sbom`](/docs/packages/sbom) — includes patch metadata in generated SBOMs
* [`package.json` "lpm.patchedDependencies"](/docs/reference/package-json-lpm#patcheddependencies) — the file format
* [Content-addressable store](/docs/packages/content-addressable-store) — what the patch is generated against


# lpm pool (/docs/packages/pool)



```bash
lpm pool
```

Prints your Pool revenue stats for the current billing period — total weighted downloads, estimated earnings, and a per-package breakdown. Requires an authenticated LPM.dev Registry session (`lpm login` first).

## Example [#example]

```text
$ lpm pool

  Pool Revenue Stats

  billing period         2026-05
  weighted downloads     142,830
  estimated earnings     $18.42

  packages (3):
    @lpm.dev/acme.toolkit       (84,210 downloads)
    @lpm.dev/acme.cli           (51,003 downloads)
    @lpm.dev/acme.icons         (7,617  downloads)
```

The current period shows an estimate. Final revenue uses completed installs and the recorded payments for that billing period.

## How weighting works [#how-weighting-works]

The LPM.dev Registry calculates Pool revenue. Each subscriber's paid author share goes to the eligible packages that subscriber installed during the billing period.

After a successful install, LPM CLI reports the resolved dependency graph. This includes installed versions, overrides, optional dependencies, peer dependencies, and canonical alias targets. A failed download or extraction does not create Pool credit.

Each `@lpm.dev/*` dependency adds one level. npm packages do not add levels:

```text
project → @lpm.dev/owner.a → @lpm.dev/owner.b → @lpm.dev/owner.c
          depth 0          depth 1          depth 2

project → npm-wrapper → @lpm.dev/owner.a → npm-helper → @lpm.dev/owner.b
                        depth 0                        depth 1
```

| Depth       | Base weight |
| ----------- | ----------- |
| 0           | 1.00        |
| 1           | 0.70        |
| 2           | 0.49        |
| 3           | 0.34        |
| 4 or deeper | No credit   |

The LPM.dev Registry applies these rules:

* Each subscriber contributes at most one observation per package per month. Repeated installs and retries do not add observations.
* The shallowest observed path wins. At equal depth, a path without the same-owner penalty wins before a stable tie-break.
* A dependency with the same owner as its immediate `@lpm.dev/*` parent receives 30% of its base weight.
* A transitive dependency needs at least two unique installers before its weight counts.
* Publisher access does not create credit, including personal packages and packages owned by the subscriber's organizations.
* Optional packages omitted on the current platform and dependencies absent from the installed graph receive no credit.

For example, `a` from Alice depends on `b` from Bob, which depends on `c` from Bob. The base weights are `1`, `0.7`, and `0.49`. The same-owner rule reduces `c` to `0.147`, before other eligibility rules and rounding.

Unmanaged clients do not report a completed dependency graph. Their authenticated Pool downloads count as direct observations for the fetched packages only.

Older LPM CLI versions that report only package roots can still complete installs successfully. The LPM.dev Registry credits eligible reported roots after it verifies their versions and authenticated download evidence. It does not infer transitive dependencies from these reports. Transitive packages can receive less credit until subscribers update LPM CLI. Access checks and payment rules still apply.

## Payments and refunds [#payments-and-refunds]

The author share is 80% of the paid Pool amount, excluding tax. Discounts reduce that amount. A $6 discounted payment contributes $4.80 to author revenue.

Refunds reduce the recorded contribution. Pending refund adjustments hold new payouts, retries, and payout setup recovery until revenue calculation handles those adjustments.

## Recover an unconfirmed install report [#recover-an-unconfirmed-install-report]

If LPM CLI reports `pool_attribution_unconfirmed`, retry the same install command. LPM CLI can reuse cached packages and retry the report without duplicate credit.

Full graph reports have limits of 10,000 graph nodes, 50,000 edges, and 1,000 `@lpm.dev/*` package instances. LPM CLI includes only paths relevant to `@lpm.dev/*` packages. An oversized or incomplete graph returns an error instead of partial credit. Older clients can send up to 200 roots per report.

## `--json` output [#--json-output]

```json
{
  "success": true,
  "billingPeriod": "2026-05",
  "totalWeightedDownloads": 142830,
  "estimatedEarningsCents": 1842,
  "packages": [
    {
      "name": "@lpm.dev/acme.toolkit",
      "owner": "acme",
      "packageName": "toolkit",
      "installCount": 91500,
      "weightedDownloads": 84210,
      "sharePercentage": 58.96,
      "estimatedEarningsCents": 1086
    }
  ]
}
```

| Field                               | Meaning                                                |
| ----------------------------------- | ------------------------------------------------------ |
| `billingPeriod`                     | ISO month (`YYYY-MM`) the stats cover                  |
| `totalWeightedDownloads`            | Sum across all your pool packages                      |
| `estimatedEarningsCents`            | Accruing total in cents (`/100` for dollars)           |
| `packages[].installCount`           | Unique subscriber observations before weighting        |
| `packages[].weightedDownloads`      | Post-weighting count that determines share             |
| `packages[].sharePercentage`        | Your share of this package within the pool, in percent |
| `packages[].estimatedEarningsCents` | Per-package accrual in cents                           |

Any field can be absent (server omits null entries). Treat the envelope as forward-compatible — additional fields may appear in future releases.

## Flags [#flags]

`lpm pool` takes no specific flags besides the global flags. `--json` switches the output to the structured envelope above.

## See also [#see-also]

* [Distribution mode](/docs/reference/glossary#distribution-mode) — what `private` / `pool` / `marketplace` mean and why mode changes are irreversible
* [`lpm publish`](/docs/packages/publish) — how a package opts into Pool (the dashboard-driven flip)
* [Publishing a package](/docs/guides/publishing-a-package) — walkthrough including the pool/marketplace flip


# lpm publish (/docs/packages/publish)



```bash
lpm publish [--wait [--wait-timeout <SECONDS>]]   # alias: lpm p
```

Packs the current directory and uploads it to a registry. By default, the target is **LPM.dev Registry** (where every package is private until you flip distribution to Pool or Marketplace). Publish elsewhere with `--npm`, `--github`, `--gitlab`, or `--publish-registry`.

The final artifact for every target is scanned for hardcoded secrets. When LPM.dev Registry is one of the targets, LPM CLI also computes the local quality score and enforces `--min-score`. Normal npm, GitHub Packages, GitLab Packages, and custom-registry publishes do not run that quality gate.

## Examples [#examples]

```bash
lpm publish                                  # publish to LPM.dev Registry
lpm publish --dry-run                        # prepare and show the publish plan
lpm publish --check                          # local preparation, no registry calls
lpm publish --npm                            # publish to registry.npmjs.org
lpm publish --github                         # publish to GitHub Packages
lpm publish --publish-registry https://r.example.com   # custom npm-compatible
lpm publish --provenance                     # require Sigstore provenance (CI)
lpm publish --npm --provenance               # npm-compatible publish provenance
lpm publish --npm --provenance-file bundle.sigstore
lpm publish --no-provenance                  # override provenance config
lpm publish --min-score 80                   # gate an LPM.dev publish on local quality
lpm publish --lpm --otp 123456               # supply LPM.dev MFA without a prompt
lpm publish --wait                           # wait for LPM.dev lifecycle activation
lpm publish --wait --wait-timeout 1200       # wait for up to 20 minutes
lpm stage publish --tag next                 # stage a version on npm
lpm stage list @scope/pkg                    # list staged npm versions
lpm stage approve <stage-id> --otp 123456    # promote the staged version
```

## What runs before upload [#what-runs-before-upload]

In order:

1. **Skills validation** (LPM.dev Registry target only) — when the project ships authored files under `.lpm/skills/`, validate their format and run the skill security-pattern check.
2. **Pack** — assemble an in-memory tarball from the package manifest. Without `package.json > files`, the implicit set excludes project-local `.lpm` state except direct publisher-authored `.lpm/skills/*.md` candidates. LPM.dev Registry publishes validate those candidates first; npm, GitHub Packages, GitLab Packages, and custom-registry-only publishes include them by path shape without skill-schema validation. Installed dependency skill sets remain excluded. An explicit `files` list controls deliberate inclusion, but cannot re-include installed dependency skills. Declared `bundledDependencies` (and the legacy `bundleDependencies` alias) are copied from verified hoisted or isolated install layouts, including required transitives. Bundled bytes are part of the same size, secret-scan, digest, and provenance boundaries. The [archive limits](#archive-limits) apply before any upload.
3. **Workspace protocol rewrite** — in monorepos, replace publishable `workspace:` and `catalog:` dependency specifiers inside the in-memory tarball. The source manifest stays unchanged.
4. **Provenance-file validation** — when `--provenance-file` is used for an npm-compatible target, verify the bundle and confirm its subject and SHA-512 digest match the final target artifact.
5. **Secret scan** — scan supported text files in the actual final artifact for every target, including target-specific rewrites and bundled dependency bytes. A file is not exempt because Git ignores it: if it is explicitly included in an artifact, it is scanned. Skip with `--allow-secrets` only when you have reviewed every target artifact carefully.
6. **Quality score** (LPM.dev Registry target only) — compute the local readme/license/types/tests/maintenance signals. The local preflight score is normalized to 0–100 over checks the CLI can actually evaluate; server-only checks remain `not_evaluated` and do not depress the local denominator. With `--min-score N`, abort if the normalized local score is below the threshold.
7. **Confirm** — ask before a real interactive publish. Skip with `-y`.
8. **Authenticate, generate requested provenance, and upload** — resolve auth for each target, generate Sigstore provenance when requested, then upload sequentially.

## Archive limits [#archive-limits]

LPM CLI applies these limits when it creates or rewrites a publish artifact:

* 100,000 archive entries.
* 100,000 GNU or PAX metadata entries.
* 256 path components, including the top-level `package/` component.
* 32 KiB for each encoded archive path.
* 1 MiB for each GNU or PAX metadata record.
* 500 MiB for the uncompressed tar archive, including headers, padding, and end markers.
* 500 MiB for the compressed tarball.

The `package/` prefix uses one component. Thus, a project-relative source path can use at most 255 components.

If an artifact exceeds a limit, LPM CLI stops during local preparation and does not upload it. Target-specific name and workspace rewrites use the same limits.

LPM.dev Registry accepts uploads of at most 100 MiB. Other registries can use smaller limits.

Successful human output is a compact status stream:

```text
✓ Secret scan passed
✓ Quality score: 64/100
› Uploading tarball to LPM.dev Registry
    target     @lpm.dev/owner.pkg@1.2.3
    visibility private
    dist-tag   latest
✓ Done · published @lpm.dev/owner.pkg@1.2.3 in 2.41s
```

If quality checks fail or are missing, only those rows are shown beneath the score. The full per-category breakdown lives in [`lpm quality`](/docs/packages/quality).

For LPM.dev Registry publishes, the package link uses the configured registry host and port.
If you publish to a local development registry, the link opens that registry.

## Interrupted publishing [#interrupted-publishing]

These recovery checks apply to publishes to LPM.dev Registry.

If an upload stops before the registry accepts it, repeat the publish command:

```bash
lpm publish
```

If the connection fails after upload, LPM CLI checks the stored version and artifact integrity before it retries. A matching artifact can complete successfully without another upload.

If you terminate the process after the registry accepts the version, a repeat publish reports that the version already exists. It also explains how to proceed.

Verify the existing release in the registry. To publish different contents, choose a new version. An existing version cannot be overwritten.

## Publication review and `--wait` [#publication-review-and---wait]

An LPM.dev Registry version can enter a processing state after upload. A private version can enter a short security cooldown after a publish-source change.

Pool and Marketplace versions can require publication review before they become public.

For a pending version, LPM CLI shows these separate values:

* The uploaded version.
* The publication state.
* The current latest version available under the package's current distribution rules, if one exists.

When automation must wait for the uploaded version, use `--wait`. The command waits until the version is available under its distribution rules:

```bash
lpm publish --wait
lpm publish --wait --wait-timeout 1200
```

LPM CLI polls every three seconds. The default timeout is 1200 seconds. You can set a timeout from 10 through 3600 seconds.

For multiple targets, LPM CLI completes all uploads before it starts the wait. An LPM.dev Registry target is required.

The command stops successfully when the version becomes active. It stops with a nonzero exit code for these results:

* The wait times out.
* A polling request fails.
* The version requires manual review.
* The review rejects the version.
* The version becomes quarantined or unpublished.
* The Registry returns an unrecognized publication state.

These results do not undo the upload. If the wait fails, do not publish the same version again.

JSON keeps the upload result in each target's `success` field. It reports the separate wait result in `publication_wait`.

`--wait` is not valid with `--dry-run` or `--check`. The `--wait-timeout` flag requires `--wait`.

In `--json` output, the `quality` object keeps `score`, `max_score`, and `checks`, and also reports the raw local denominator as `earned_points` and `applicable_points`. Every check has an `evaluation` value of `"passed"`, `"failed"`, or `"not_evaluated"`; the last value identifies server-only checks while preserving their existing `passed: false` representation.

`--check` is the network-free preparation mode. It resolves the target configuration and names, validates authored LPM.dev skills, assembles the tarball, validates any supplied provenance file, runs the secret scan, and computes the local quality score when LPM.dev Registry is a target. It stops before OIDC exchange, target authentication, generated provenance, confirmation, and upload.

`--dry-run` runs the same local preparation. For an LPM.dev Registry target, it resolves the real available authentication (including Trusted Publisher/OIDC), requires any authored-skills staleness lookup to succeed, and calls a non-mutating Registry preflight that validates the resolved `publish.lpm.name`, publish permission, token restrictions, owner authorization, account limits, and version availability. Any failure exits non-zero; JSON mode emits one `success: false` document. The preflight never uploads, reserves a version, creates package state, or claims publication succeeded. npm/GitHub/GitLab dry runs retain their existing local-only behavior. A supplied `--provenance-file` is still validated locally.

Both preview modes leave the working tree untouched. When authored skills exist and `package.json > files` does not include `.lpm/skills`, preparation adds that entry only to the effective manifest used in memory. A real publish may persist the entry.

For LPM.dev Registry targets, `lpm.config.json > type` controls Registry classification and accepts only `package` or `source`. Omitting it leaves classification to Registry inference. The file itself remains an LPM CLI `lpm add` contract: any valid `lpm.config.json` activates the configured add path, including when `type` is `package`.

## Bundled dependencies [#bundled-dependencies]

LPM CLI honors npm-compatible `bundledDependencies` and `bundleDependencies` values. Arrays bundle the named runtime dependencies; `true` expands to every declared `dependencies` entry; `false` bundles none. Once a bundle root is selected, its installed production and optional transitive dependencies are included when available. Ordinary `node_modules` content remains excluded.

Run `lpm install` before publishing. A requested bundle that is missing, has the wrong package identity, escapes the project/LPM store roots, or contains an internal symlink fails preparation instead of producing an incomplete or unsafe archive. Archive paths use portable `node_modules/<name>/...` paths and are sorted deterministically for every publish target.

## npm Trusted Publishing [#npm-trusted-publishing]

`lpm publish --npm` and `lpm stage publish` automatically use npm Trusted Publishing when a supported CI OIDC token is available. LPM CLI exchanges the CI identity token with npm for a short-lived registry token, then uses that token only for the publish or stage-publish upload.

Supported inputs:

| CI source                                        | Configure                                                                                                |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| GitHub Actions                                   | `permissions: id-token: write`; LPM CLI fetches the runtime token with audience `npm:registry.npmjs.org` |
| GitLab CI / CircleCI / other supported providers | Set `NPM_ID_TOKEN` to an OIDC ID token minted with audience `npm:registry.npmjs.org`                     |

Trusted Publishing is attempted only for npm's public registry (`https://registry.npmjs.org`; HTTP loopback is accepted for local test registries). Custom npm-compatible registries, GitHub Packages, GitLab Packages, and `--publish-registry` targets keep using token auth. If npm's OIDC exchange fails and a normal npm token is available, LPM CLI falls back to the token path; otherwise the OIDC error is surfaced.

OIDC is publish-only. `lpm stage list`, `view`, `download`, `approve`, and `reject` still require normal npm auth because npm limits Trusted Publishing authentication to publish operations.

## npm staged publish [#npm-staged-publish]

Full command reference: [`lpm stage`](/docs/packages/stage).

```bash
lpm stage publish [--tag next] [--access public] [--provenance | --provenance-file <PATH> | --no-provenance] [-y]
lpm stage list [package]
lpm stage view <stage-id>
lpm stage download <stage-id>
lpm stage approve <stage-id> [--otp 123456]
lpm stage reject <stage-id> [--otp 123456]
```

`lpm stage` wraps npm staged publishing for the current project. It is npm-only in this release: there is no LPM.dev Registry, GitHub Packages, GitLab Packages, custom publish registry, workspace-recursive, or tarball/package-spec staging mode yet.

Stage publish reuses the normal npm publish preparation path: pack, workspace dependency rewrite, secret scan, quality gate, npm name/access/tag config, optional npm-name tarball rewrite, and optional `--provenance` / `--provenance-file` over the final tarball. The upload goes to npm's staging endpoint instead of the live publish endpoint, so the version is not installable until it is approved.

Auth for `lpm stage publish` against the default npm registry uses npm Trusted Publishing first when available, then the normal npm token sources:

| Source order                                                                                      |
| ------------------------------------------------------------------------------------------------- |
| npm Trusted Publishing (`NPM_ID_TOKEN` or GitHub Actions OIDC, audience `npm:registry.npmjs.org`) |
| `NPM_TOKEN`                                                                                       |
| token stored by `lpm login --npm`                                                                 |
| locked `.npmrc` token                                                                             |

Publishing to staging does not prompt for OTP. npm defers proof-of-presence to `lpm stage approve` and `lpm stage reject`, where `--otp` or the usual npm web-auth retry flow is used. Those approval commands are token-authenticated; `NPM_ID_TOKEN` alone is not enough.

`lpm stage publish` validates npm's version/tag safety rules before the real upload: the package must already exist on npm, an already-published version is blocked, prerelease versions require an explicit tag, and implicit `latest` is blocked when npm already has a higher stable version. `--dry-run` stays local-only and does not contact npm, so those remote version checks run only for a real stage publish.

Use `--npm-registry <URL>` to target a compatible npm staging registry. The global `--registry` flag still means LPM.dev Registry and is rejected on `lpm stage` commands with a message to use `--npm-registry`.

If `lpm.json > publish.npm.registry` points at a non-default staging registry, LPM CLI requires an exact registry-scoped token from `lpm login --login-registry <URL> --token <T>`. Generic `NPM_TOKEN` and tokens stored by `lpm login --npm` are only used for `https://registry.npmjs.org`.

Every `lpm stage` subcommand supports `--json`. JSON output uses LPM CLI envelopes with `success`, `target: "npm"`, `registry`, `auth` on stage publish (`"oidc"` or `"token"`), `stageId` when a single staged package is involved, and `data` for the registry response.

## Provenance [#provenance]

```bash
lpm publish --provenance
lpm publish --npm --provenance
lpm publish --npm --provenance-file ./pkg-1.2.3.sigstore
lpm publish --no-provenance
```

`--provenance` generates a Sigstore-signed attestation over the final tarball for each publish target. For LPM.dev Registry, the bundle is stored in package metadata. For npm-compatible targets (`--npm`, `--github`, `--gitlab`, or `--publish-registry`), LPM CLI also attaches npm's expected `{name}-{version}.sigstore` bundle in the publish payload.

Generated npm-compatible attestations use npm's current `libnpmpublish` SLSA statement shapes for GitHub Actions and GitLab CI, so the bundle matches the provenance contract npm-compatible registries and registry UIs expect.

Generated provenance requires an audience-`sigstore` OIDC token from a supported CI environment:

* **GitHub Actions** — enable `permissions: id-token: write` on the job; the runtime mints the JWT.
* **GitLab CI** — mint `SIGSTORE_ID_TOKEN` via the `id_tokens` block with `aud: sigstore` (the legacy `LPM_GITLAB_OIDC_TOKEN` env var is also accepted).

Other platforms aren't supported — Sigstore needs the OIDC issuer to sign. The flag fails loud (non-zero exit) if no usable token is found or if Sigstore signing/Rekor recording fails; it never silently falls back to publishing without provenance.

Generated npm-compatible provenance and provenance-file attachments require effective npm access to be `public`. LPM CLI's npm access default is `public` for scoped and unscoped packages. If you set `publish.npm.access`, `publish.github.access`, `publish.gitlab.access`, or `--access restricted` on stage publish, LPM CLI fails before npm auth, Sigstore signing, file validation, or upload.

`--provenance-file <PATH>` attaches a pre-generated Sigstore bundle to npm-compatible targets. The file is parsed, verified, and checked against the final tarball subject and SHA-512 digest before upload. It is not valid for LPM.dev Registry targets, so `lpm publish --lpm --npm --provenance-file ...` fails before validation or upload. `--provenance`, `--provenance-file`, and `--no-provenance` are mutually exclusive.

Provenance can also be enabled through npm-compatible config, in this order:

1. CLI: `--no-provenance`, `--provenance`, `--provenance-file <PATH>`
2. `package.json > publishConfig.provenance`
3. Environment: `NPM_CONFIG_PROVENANCE_FILE`, `NPM_CONFIG_PROVENANCE`
4. Project `.npmrc`: `provenance=true` or `provenance-file=...`
5. Home `.npmrc`

`provenance` and `provenance-file` are mutually exclusive in env and `.npmrc` config. Use `--no-provenance` when a repo-level npm config enables provenance but a particular publish or staged publish should skip it.

Consumers can later verify provenance via the audit pipeline or registry UI.

## Targets [#targets]

```bash
lpm publish --lpm --npm                 # publish to both in one run
lpm publish --github --gitlab           # cross-publish to both git-host registries
```

Multiple target flags compose — `lpm publish` runs through them in order and reports per-target success / failure. Up to **5** target registries per invocation.

| Flag                       | Registry                                                    | Requirements                                                                                                                                                               |
| -------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| (default) / `--lpm`        | LPM.dev Registry                                            | Package name must be `@lpm.dev/owner.pkg`. Override with `publish.lpm.name` in `lpm.json`                                                                                  |
| `--npm`                    | `registry.npmjs.org` (or custom via `publish.npm.registry`) | Override name with `publish.npm.name`; non-default registries require exact registry-scoped auth                                                                           |
| `--github`                 | GitHub Packages                                             | Package name must be **scoped** (`@owner/pkg`). Falls back to `publish.github.name` → `publish.npm.name` → `package.json#name` — any of them being a scoped name is enough |
| `--gitlab`                 | GitLab Packages                                             | `publish.gitlab.projectId` is required. Optional `publish.gitlab.registry` overrides the default `https://gitlab.com` instance                                             |
| `--publish-registry <URL>` | Any npm-compatible registry                                 | URL must be `https://...`                                                                                                                                                  |

When no target flag is set on the CLI, the active set comes from `lpm.json > publish.registries` (default: `["lpm"]`).

You must have auth for the chosen target — see [Authentication](/docs/infra/authentication) for `lpm login --npm`, `--github`, `--gitlab`, or `lpm login --login-registry <URL>` for custom registries.

Custom publish registry URLs are HTTPS-only whether they come from `--publish-registry` or `lpm.json > publish.registries`. LPM CLI rejects `http://` before dry-run planning or upload.

Repo-configured custom npm registries are routing data only. They do not redirect `NPM_TOKEN`, tokens stored by `lpm login --npm`, or locked default-npm `.npmrc` auth. Store a token for the exact registry URL with `lpm login --login-registry <URL> --token <T>`.

## Authentication and OTP [#authentication-and-otp]

If LPM.dev Registry requires MFA, an interactive publish asks for a six-digit authenticator code. Use `--otp <CODE>` when a terminal prompt is unavailable:

```bash
lpm publish --lpm --otp 123456 --json
```

The flag applies only to LPM.dev Registry uploads. LPM CLI rejects a value that is not exactly six ASCII digits.

Token resolution for npm-compatible targets:

| Target                             | Resolution order                                                                                                                          |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| npm (`https://registry.npmjs.org`) | npm Trusted Publishing for `publish --npm` / `stage publish` -> `NPM_TOKEN` -> token stored by `lpm login --npm` -> locked `.npmrc` token |
| GitHub Packages                    | `GITHUB_TOKEN` → valid `gh` auth for `github.com` → LPM CLI-stored fallback token                                                         |
| GitLab.com Packages                | `GITLAB_TOKEN` / `CI_JOB_TOKEN` → valid `glab` auth → LPM CLI-stored fallback token                                                       |
| Self-managed GitLab                | `GITLAB_TOKEN` / `CI_JOB_TOKEN` → LPM CLI-stored fallback token                                                                           |
| Custom registries                  | Exact LPM CLI-stored custom-registry token from `lpm login --login-registry <URL> --token <T>`                                            |

LPM CLI advertises npm web auth on npm-compatible publish requests (`npm-auth-type: web`, `npm-command: publish`). If npm returns a browser-based OTP challenge (`authUrl` + `doneUrl`), LPM CLI opens the browser flow, polls for completion, and retries with the returned `npm-otp`. Classic `www-authenticate: OTP` prompts still work in an interactive terminal; set [`lpm.json > publish.npm.otpRequired`](/docs/reference/lpm-json#publishnpm) to prompt before the first publish attempt. Non-interactive publish should use an automation token.

## Flags [#flags]

| Flag                                        | Effect                                                                                                                  |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--dry-run`                                 | Prepare and show the plan; for LPM.dev, perform non-mutating remote auth/permission/version preflight without uploading |
| `--check`                                   | Run network-free local publish preparation and stop before upload                                                       |
| `--wait`                                    | Wait until an LPM.dev Registry upload becomes active under its distribution rules                                       |
| `--wait-timeout <SECONDS>`                  | Set the wait limit from 10 through 3600 seconds. The default is 1200 seconds                                            |
| `--otp <CODE>`                              | Send a six-digit authenticator code for an MFA-protected LPM.dev publish                                                |
| `-y`, `--yes`                               | Skip interactive confirmation                                                                                           |
| `--provenance`                              | Require Sigstore provenance (CI with OIDC only)                                                                         |
| `--no-provenance`                           | Disable provenance even when npm config enables it                                                                      |
| `--provenance-file <PATH>`                  | Attach a pre-generated Sigstore bundle to npm-compatible targets                                                        |
| `--min-score <N>`                           | Minimum local quality score required when LPM.dev Registry is a target                                                  |
| `--allow-secrets`                           | Skip pre-publish secret scanning (not recommended)                                                                      |
| `--npm` / `--lpm` / `--github` / `--gitlab` | Target a specific registry                                                                                              |
| `--publish-registry <URL>`                  | Custom npm-compatible registry (`https://` only)                                                                        |
| `stage --npm-registry <URL>`                | npm staging registry for `lpm stage` subcommands                                                                        |
| `stage publish --provenance`                | Generate npm-compatible Sigstore provenance for a staged publish                                                        |
| `stage publish --no-provenance`             | Disable staged-publish provenance even when npm config enables it                                                       |
| `stage publish --provenance-file <PATH>`    | Attach a pre-generated Sigstore bundle to a staged publish                                                              |
| `stage --otp <CODE>`                        | One-time password for `lpm stage approve` / `reject`                                                                    |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [Authentication](/docs/infra/authentication) — login, tokens, CI setup
* [`lpm stage`](/docs/packages/stage) — npm staged publish command reference
* [Save policy](/docs/packages/save-policy) — how versions get saved on the consumer side
* [`lpm quality`](/docs/packages/quality) — preview the quality report
* [Pool](/docs/packages/pool) — flipping a published package to pool distribution


# lpm quality (/docs/packages/quality)



```bash
lpm quality <package>
```

Fetches the LPM.dev Registry quality report for an `@lpm.dev/*` package and prints it: a score out of 100, a tier label, and a per-check breakdown grouped by category (readme, license, types, tests, maintenance, etc.).

**Scope: `@lpm.dev/*` packages only.** The quality endpoint belongs to LPM.dev Registry; npm-registry packages aren't covered. For broad quality-shaped signals on installed npm packages, [`lpm audit`](/docs/packages/audit) covers vulnerabilities + behavioral analysis.

[`lpm publish`](/docs/packages/publish) runs the locally available part of the rubric when LPM.dev Registry is a target. Checks that require Registry analysis remain pending until upload; `lpm quality` returns the completed report for an already-published package without installing it.

## Examples [#examples]

```bash
lpm quality @lpm.dev/owner.pkg
lpm quality owner.pkg                                           # short form
lpm quality @lpm.dev/owner.pkg --json | jq '.checks[] | select(.passed == false)'
```

## What's shown [#whats-shown]

* **Score** — points out of 100 (color-coded: green ≥80, yellow ≥50, red below)
* **Tier** — human label: `excellent`, `good`, `fair`, or `needs-work`
* **Ecosystem** — `js`, `swift`, etc.
* **Checks** — per-category list with pass/fail icons, label, and points awarded

## Flags [#flags]

`lpm quality` takes no specific flags besides the [global flags](/docs/commands#global-flags). `--json` returns the full report including check IDs, descriptions, and remediation hints.

## See also [#see-also]

* [`lpm publish`](/docs/packages/publish) — local publish preparation, pending server checks, and the LPM.dev `--min-score` gate
* [`lpm info`](/docs/packages/info) — package metadata
* [`lpm audit`](/docs/packages/audit) — installed-package security audit (different system)


# lpm query (/docs/packages/query)



```bash
lpm query <selector> [flags]
```

Selector engine that targets behavioral tags, package state, and dependency relationships across the installed set. Pairs with [`lpm audit`](/docs/packages/audit) — `audit` is the broad report; `query` is the precision tool.

Use it in CI to gate on specific risky combinations, in shells to grep through installed packages by behavior, or in agents to ask structured questions about the dep tree.

For exact license inventory and compliance policy, use [`lpm licenses`](/docs/packages/licenses). `lpm query :copyleft` and `:no-license` are selector tags, not a full license report.

## Examples [#examples]

```bash
lpm query :eval                                    # any package that uses eval()
lpm query :network                                 # any package making outbound HTTP
lpm query :info                                    # common capabilities and artifact signals
lpm query :scripts:not(:built)                     # has lifecycle scripts but they haven't run
lpm query ":root > :network"                       # direct deps that hit the network
lpm query "#lodash"                                # find lodash in the tree
lpm query :critical --assert-none                  # CI gate: fail if any critical-tagged pkg
lpm query --count                                  # tag counts across all packages
lpm query :eval --format mermaid > eval-graph.mmd  # subgraph diagram
```

## Selectors [#selectors]

Each selector matches packages by **behavioral tag**, **state**, or **identity**. Tags are computed by the same static analysis pipeline that backs [`lpm audit`](/docs/packages/audit).

### Behavioral tags [#behavioral-tags]

| Tag                     | Matches packages that                                             |
| ----------------------- | ----------------------------------------------------------------- |
| `:eval`                 | Use `eval`, `Function()`, or `vm.runInThisContext`                |
| `:network`              | Make outbound HTTP / WS connections                               |
| `:fs`                   | Touch the filesystem outside their own directory                  |
| `:shell`                | Spawn shells (`spawn`, `exec`, `execSync`)                        |
| `:child-process`        | Use `child_process` (any form)                                    |
| `:native`               | Ship native modules (`.node`, `.wasm`)                            |
| `:crypto`               | Use cryptographic primitives                                      |
| `:dynamic-require`      | Use dynamic `require()` (variable arg)                            |
| `:env`                  | Read `process.env`                                                |
| `:ws`                   | Use WebSockets                                                    |
| `:obfuscated`           | Show signs of code obfuscation                                    |
| `:possible-obfuscation` | Show possible code obfuscation at moderate confidence             |
| `:high-entropy`         | Contain high-entropy string blobs                                 |
| `:minified`             | Ship minified-only source                                         |
| `:telemetry`            | Make telemetry / analytics calls                                  |
| `:url-strings`          | Contain URL string literals                                       |
| `:trivial`              | Tiny — measured by AST node count                                 |
| `:protestware`          | Match a curated list of known protest-license / sabotage packages |

### Dependency state [#dependency-state]

| Tag             | Matches                                                                      |
| --------------- | ---------------------------------------------------------------------------- |
| `:git-dep`      | Installed from a git URL                                                     |
| `:http-dep`     | Installed from an HTTP tarball URL                                           |
| `:wildcard-dep` | Declared with `*` or `latest`                                                |
| `:copyleft`     | Copyleft license (GPL family)                                                |
| `:no-license`   | No `license` field                                                           |
| `:scripts`      | Declares lifecycle scripts (`preinstall`/`postinstall`/etc.)                 |
| `:built`        | Lifecycle scripts have been run (the package is in the trust set + executed) |
| `:vulnerable`   | Listed in OSV / registry advisories                                          |
| `:deprecated`   | Marked deprecated by the publisher                                           |

### Origin [#origin]

| Tag    | Matches               |
| ------ | --------------------- |
| `:lpm` | `@lpm.dev/*` packages |
| `:npm` | npm packages          |

### Severity [#severity]

Severity selectors are **aliases**. Each selector expands to an OR of the behavioral tags assigned to that level. Install summaries and audits use the same assignments.

Info tags are behavioral metadata, not confirmed vulnerabilities. They identify common capabilities and artifact traits that can be useful during investigation.

| Tag         | Expands to                                                                                                                                                        |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `:critical` | `:obfuscated` OR `:protestware`                                                                                                                                   |
| `:high`     | `:eval` OR `:child-process` OR `:shell` OR `:dynamic-require` OR `:scripts` OR `:vulnerable`                                                                      |
| `:medium`   | `:network` OR `:git-dep` OR `:http-dep` OR `:wildcard-dep` OR `:no-license` OR `:native`                                                                          |
| `:info`     | `:fs` OR `:crypto` OR `:env` OR `:ws` OR `:possible-obfuscation` OR `:high-entropy` OR `:telemetry` OR `:trivial` OR `:copyleft` OR `:minified` OR `:url-strings` |

### Structural [#structural]

| Tag               | Matches                                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `:root`           | The invocation project. Use `:root > <selector>` to match its direct dependency instances.                   |
| `:workspace-root` | The owning workspace container. Use `:workspace-root > <selector>` to match its direct dependency instances. |

When you run the command in a workspace member, `:workspace-root` still uses the owning container projection. It does not use a same-name instance from the member.

The behavior is the same from a nested directory inside that member. Outside a workspace, `:workspace-root` and `:root` use the same project.

If the workspace lockfile has no root projection, `:workspace-root` matches no direct dependencies. It does not fall back to the current member.

### Combinators [#combinators]

| Combinator       | Meaning                                                 |
| ---------------- | ------------------------------------------------------- |
| `:a:b`           | AND — package matches both tags                         |
| `:a, :b`         | OR — package matches either tag                         |
| `:not(:a)`       | NOT — package doesn't match `:a`                        |
| `#name`          | Identity — match a specific package by name             |
| `:root > :child` | Direct-dep — `:child` is a direct dependency of `:root` |

## CI gating [#ci-gating]

```bash
lpm query :critical --assert-none
```

Exits non-zero if any package matches the selector. Use to fail the build on specific risky combinations:

```bash
# fail on a vulnerable package that hasn't been patched
lpm query ":vulnerable:not(:built)" --assert-none

# fail on a transitively-pulled package that uses eval AND has lifecycle scripts
lpm query ":eval:scripts" --assert-none
```

`--assert-none` is the canonical CI-gate flag. For broader gating with severity policies, [`lpm audit --fail-on`](/docs/packages/audit) is usually a better fit.

## Counts and details [#counts-and-details]

```bash
lpm query --count
```

Tabulates tag counts across every installed package, grouped by severity. Useful for high-level "how risky is my tree right now" surveys.

```bash
lpm query :network --query-verbose
```

Human list output includes a second `tags:` line for each matched package that carries behavioral tags. `--query-verbose` keeps the same human shape and includes the full analysis fields in JSON output.

## Output formats [#output-formats]

```bash
lpm query :eval --format list      # default — names, one per line
lpm query :eval --format mermaid   # Mermaid subgraph diagram
```

| Format           | Output                                                                   |
| ---------------- | ------------------------------------------------------------------------ |
| `list` (default) | Package names plus an indented `tags:` detail line when tags are present |
| `mermaid`        | Mermaid graph block showing exact package instances and dependency edges |

For broader graph rendering (full tree, DOT, JSON, HTML), use [`lpm graph`](/docs/packages/graph).

## Exact instances and paths [#exact-instances-and-paths]

```bash
lpm --json query :eval
```

Each JSON result includes `name` and `version`. An exact LPM CLI-managed result also includes `instanceId`.

The optional `path` value identifies the directory that the query analyzed. It points to the exact installed package materialization when that path is available.

For a `file:` or `link:` dependency, `path` identifies the original live source directory. Foreign lockfile formats can omit `instanceId`.

Results with the same `name` and `version` remain separate when their instance or path differs. Use `instanceId` as the row key when it exists.

Mermaid output also keeps these instances separate. A node label includes the first 12 characters of its instance ID when exact identity is available.

## Flags [#flags]

| Flag                       | Effect                                                   |
| -------------------------- | -------------------------------------------------------- |
| `<selector>`               | Optional selector expression. Required unless `--count`. |
| `--count`                  | Show tag counts across all packages, grouped by severity |
| `--query-verbose`          | Include full per-match analysis details in JSON output   |
| `--assert-none`            | Exit non-zero if any package matches (CI gate)           |
| `--format <list\|mermaid>` | Output format (default `list`)                           |

Plus the [global flags](/docs/commands#global-flags). `--json` emits `name`, `version`, optional `instanceId`, and optional `path` for each match.

With `--query-verbose`, each match also includes `analysis`, `hasScripts`, `isBuilt`, and `isVulnerable`.

## See also [#see-also]

* [`lpm audit`](/docs/packages/audit) — broader report with vulnerability + behavior data
* [`lpm graph`](/docs/packages/graph) — render the full dep tree
* [Security audit](/docs/packages/security-audit) — design overview of the tag system


# lpm rebuild (/docs/packages/rebuild)



```bash
lpm rebuild [packages...]
```

Bare default-policy `lpm install` downloads and links dependencies while blocking dependency lifecycle scripts. `lpm rebuild` is the manual and retry surface for those scripts: it selectively runs `preinstall`, `install`, and `postinstall` based on the active [script policy](/docs/packages/install#lifecycle-scripts) and the project's `trustedDependencies`. Installs that opt into allow, auto-build, or trusted-package builds use the same pipeline automatically.

Matches `npm rebuild` / `pnpm rebuild` semantics.

## What runs [#what-runs]

`lpm rebuild` executes exactly three phases, in order:

1. `preinstall`
2. `install`
3. `postinstall`

Other lifecycle names — `prepare`, `prepublishOnly`, `preuninstall`, `uninstall`, `postuninstall` — are **recognized** by LPM CLI for detection (a package declaring them shows up in [`lpm audit`](/docs/packages/audit) and the [`lpm approve-scripts`](/docs/packages/approve-scripts) review queue) but **never executed** by the install pipeline. Approving a package whose only declared script is `prepare` is harmless: nothing runs.

## Examples [#examples]

```bash
lpm rebuild                                  # rebuild every trusted package
lpm rebuild esbuild                          # rebuild a specific package
lpm rebuild esbuild sharp                    # multiple
lpm rebuild --dry-run                        # preview, run nothing
lpm rebuild --force                          # re-run scripts even for already-built packages
lpm rebuild --all                            # bypass trust policy — DANGEROUS
lpm rebuild --policy=allow                   # allow every scripted package this run
lpm rebuild --triage                         # tiered gate (greens auto, ambers/reds blocked)
lpm rebuild --timeout 600                    # 10-minute per-script timeout
```

Human output reports each completed lifecycle phase as one compact row and keeps the script command body out of the transcript:

```bash
› Rebuilding lifecycle scripts for trusted packages
  ✓ esbuild@0.25.1  postinstall
  ✓ sharp@0.34.2    install

✓ Completed 2 scripts
  skipped: 1 blocked package
  hint: run lpm approve-scripts

✓ Done · rebuild finished in 1.04s
```

When you name packages explicitly, every requested package must be installed and must have an executable lifecycle script. A typo or an installed package with no `preinstall` / `install` / `postinstall` exits non-zero instead of silently succeeding as an empty rebuild.

## Default selection [#default-selection]

With no arguments and no `--all`, `lpm rebuild` runs scripts for packages whose name appears in:

* `package.json > lpm > trustedDependencies` (legacy array form), OR
* `package.json > lpm > trustedDependencies` (rich-map form, with matching integrity + script hash), OR
* A scope glob in `package.json > lpm > scripts.trustedScopes` (e.g. `@myorg/*`)

Manage strict package approvals with [`lpm approve-scripts`](/docs/packages/approve-scripts). Manage project scope trust with [`lpm trust lifecycle-scope`](/docs/packages/trust#lpm-trust-lifecycle-scope).

## Policy override [#policy-override]

The CLI policy flags share semantics with [`lpm install`](/docs/packages/install#lifecycle-scripts) — they govern which scripted packages are eligible to run.

| Flag                           | Behavior                                                                         |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `--policy=deny` (default)      | Filter to `trustedDependencies` only                                             |
| `--policy=allow` / `--yolo`    | Include every scripted package regardless of trust                               |
| `--policy=triage` / `--triage` | Filter to trusted-only, but tiered greens are auto-promoted into the rebuild set |

`--all` overrides the filter under every policy.

`--policy`, `--yolo`, and `--triage` are mutually exclusive.

## Sandbox [#sandbox]

Lifecycle scripts run inside a **filesystem sandbox** by default — Seatbelt on macOS, landlock on Linux, AppContainer + Job Object on Windows. Three modes:

| Mode         | Filesystem                                                                                                     | Env                                                                       | Outbound network | Project secret files                                                                     | How to engage                                                                                                                                                |
| ------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Default**  | Write-containment to the package's own directory + anything in `package.json > lpm > scripts.sandboxWriteDirs` | Credential vars stripped (`LPM_TOKEN`, `NPM_TOKEN`, `GITHUB_TOKEN`, etc.) | Allowed          | **Denied** — `.env*`, `.npmrc`, `.aws/`, `*.pem`, … (see [below](#project-secret-files)) | Nothing — this is the default                                                                                                                                |
| **Strict**   | Same as default                                                                                                | Same as default                                                           | **Denied**       | **Denied**                                                                               | `--strict-sandbox` (or alias `--paranoid`), persistent via `[sandbox] mode = "strict"` in `~/.lpm/config.toml` / `./lpm.toml`, or env `LPM_STRICT_SANDBOX=1` |
| **Disabled** | No containment                                                                                                 | **Not scrubbed** — `LPM_TOKEN`, `NPM_TOKEN`, etc. all pass through        | Allowed          | **Readable** — every file in `project_dir` reachable                                     | `--no-sandbox` (per-command, loud banner), or `lpm config sandbox --set none` (persistent)                                                                   |

```bash
lpm rebuild --strict-sandbox     # filesystem + env + network containment for this run
lpm rebuild --paranoid           # alias — same behavior
```

Strict network denial coverage is platform-asymmetric: macOS Seatbelt denies every socket family; Linux landlock denies TCP `connect(2)`/`bind(2)` and seccomp-bpf denies direct UDP / raw / AF\_PACKET / AF\_NETLINK sockets; Windows uses AppContainer + Windows Filtering Platform. On platforms where strict isn't fully implementable, the sandbox refuses to start unless the `allow_degraded` config is set.

## Native build cache [#native-build-cache]

On macOS and Linux, strict rebuilds automatically reuse successful native
dependency builds. The first eligible build publishes the completed package
tree under `~/.lpm/store/v2/builds/<build-key>/`; an identical reinstall can
restore that tree instead of rerunning `node-gyp`, `prebuild-install`,
esbuild's installer, or sharp's installer. There is no cache flag to enable.

```bash
lpm install
lpm rebuild --strict-sandbox --all   # miss: build and publish

rm -rf node_modules
lpm install                          # pristine rematerialization
lpm rebuild --strict-sandbox --all   # hit: restore the cached build
```

Cache lookup happens **after** normal trust and policy selection. A cached
artifact never makes an untrusted package eligible to execute or restore.

### Eligibility and isolation [#eligibility-and-isolation]

An artifact is cacheable only when all of these are true:

* The package uses the v2 content-addressable store.
* The lifecycle command belongs to a recognized native-build family:
  `node-gyp`, `node-gyp-build`, `node-gyp-build-optional-packages`,
  `prebuild-install`, `electron-rebuild`, `cmake-js`, esbuild's `install.js`,
  or sharp's installer. Recognition is based on shell command position and
  package identity; mentions in comments or unrelated arguments do not
  qualify.
* The effective sandbox posture is fully strict and outbound network access is
  denied.
* The project did not widen sandbox read/write paths or lifecycle
  capabilities.
* The host is macOS or Linux. Windows rebuilds still run normally, but native
  artifact reuse stays disabled until the AppContainer backend has the same
  cacheable input boundary.

During an eligible miss, persistent writes are narrowed to the package tree;
temporary writes use a stable private directory inside the locked graph entry,
so its path is deterministic for equivalent builds. The dependency graph,
system toolchain, and local Node header cache remain readable. User npm caches,
project-wide write roots, and the broader default-sandbox write set are not
part of the cacheable build.

If any eligibility check fails, LPM CLI runs the lifecycle command through the
normal rebuild path and reports the package as bypassed in JSON metrics.

### Cache key and invalidation [#cache-key-and-invalidation]

The build key includes:

* package tarball integrity and v2 graph identity
* canonical dependency edges, aliases, peer targets, source identities, and
  patch fingerprints across the installed graph
* ordered lifecycle phases and exact command bodies
* OS, architecture, libc, and CPU-feature compatibility
* Node version, modules ABI, N-API, V8 version, and runtime executable identity
* strict sandbox posture and allowed-input policy
* the complete environment exposed to the lifecycle script, including custom
  variables plus LPM CLI's effective `PATH`, `INIT_CWD`, `TMPDIR`, `TMP`, and
  `TEMP` values
* compiler, linker, SDK, CMake, Ninja, invoked build-executable, operating
  system, dynamic-linker cache, and Node-header fingerprints when discoverable
* compiler sysroot/search configuration, pkg-config databases, and
  discoverable OS package-manager, Homebrew, macOS receipt, and Xcode state

The complete host-toolchain fingerprint is persisted under
`~/.lpm/cache/metadata/native-toolchains/v1/`. On later rebuilds, LPM CLI first
validates compiler identities, package-database generations, pkg-config
directories, SDK selection, Homebrew/macOS receipts, Rust toolchain selection,
and the selected Node header cache. An unchanged snapshot avoids rerunning the
compiler and package-manager probes. Changed, expired, oversized, or malformed
state fails closed to the complete fingerprint pass; snapshots have a one-hour
maximum lifetime and `lpm cache clean metadata` removes them. Project-visible
executables are still fingerprinted for every key and are never executed by
the host probe path.

Because absolute lifecycle paths are observable inputs, reuse across separate
checkouts requires those paths to match. This is normally true for repeated CI
runs that restore the LPM CLI store into the same workspace path; a differently
located checkout receives a different key rather than reusing an artifact that
could contain the previous path. The complete environment is also exact: CI
values such as `GITHUB_RUN_ID`, `GITHUB_RUN_NUMBER`, and
`GITHUB_RUN_ATTEMPT` intentionally invalidate reuse unless the workflow
normalizes them before invoking LPM CLI.

The host fingerprint is conservative, not a claim that native builds are
fully hermetic. It detects normal compiler, SDK, package-manager, header-cache,
and pkg-config changes without hashing every readable byte under `/usr` or a
macOS SDK on every lookup. An administrator can still replace an unmanaged
system header or library in place without updating discoverable package state.
After such a manual host mutation, use
`lpm rebuild --strict-sandbox --all --force` to rebuild from pristine package
contents; reference-aware cache pruning can remove the older artifact
afterward.

Changing any keyed input invalidates both the artifact and the package's local
`.lpm-built` state. LPM CLI rematerializes the pristine package object before the
new command runs, so stale outputs cannot leak into the replacement artifact.
Corrupt or incomplete artifacts are treated as misses.

`--force` always bypasses lookup, rebuilds from pristine source, and replaces
the artifact for the resulting key.

### Measured performance [#measured-performance]

On macOS arm64 with Node 22.22.1 and the sharp `NativeToolchain` fixture (10
release samples), a local hit completed in **59.07 ms median** versus a
**173.10 ms** strict artifact miss: **2.93× faster** and **65.88% lower wall
time**. Recreating a CI checkout at the same stable path while retaining the
warm LPM CLI store completed in **58.49 ms median** (**2.96× faster**, **66.21%
lower**). Warm host-key derivation took 15–24 ms. The first complete host
fingerprint took 126 ms before its validated snapshot existed, so these
steady-state numbers do not hide the cold security cost. Every scenario used
the same strict sandbox, and the median retained artifact used 503,469 bytes.
The benchmark and raw methodology live in the rust-client repository under
`bench/scripts/native-build-cache-benchmark.mjs` and
`bench/perf-results/native-build-cache-20260712-macos-arm64-native-toolchain.*`.

### Project secret files [#project-secret-files]

**On macOS and Linux, the sandbox blocks lifecycle script reads of project secrets by default; opt in via `script-read-allow` if your build needs a specific secret file.**

Lifecycle scripts have read access to most of `project_dir` so they can locate sources, configs, generated files, etc. But certain conventional secret-file locations are denied even though they sit inside the project tree — a malicious or compromised script has no legitimate reason to read your `.env`, `.aws/credentials`, or a `*.pem` private key.

The deny list covers:

* **dotenv conventions** — `.env`, `.env.local`, `.env.production(.local)`, `.env.development(.local)`, `.env.staging(.local)`, `.env.test(.local)`, `.envrc`
* **package-manager auth files** — `.npmrc`, `.yarnrc`, `.yarnrc.yml`, `.pypirc`
* **shell / HTTP auth files** — `.netrc`, `_netrc`, `.git-credentials`, `.htpasswd`
* **git credential URLs** — `.git/config`, `.git/credentials`
* **SSH keys at project root** — `id_rsa`, `id_ecdsa`, `id_ed25519`, `id_dsa` (and `.pub` variants)
* **cloud / credential dirs** — `.ssh/`, `.aws/`, `.kube/`, `.gcp/`, `.config/gcloud/`, `.terraform/`, `secrets/`, `secret/`
* **filetype patterns in the bounded project scan** — `*.pem`, `*.key`, `*.pfx`, `*.p12`, `*.tfstate`, `*.tfvars`, `*.tfvars.json`

On macOS the denial fires as a Seatbelt `(deny file-read* ...)` rule layered after the project-dir allow (SBPL last-match-wins). On Linux the sandbox enters a fresh user and mount namespace, makes mount propagation private, and bind-mounts `/dev/null` over each enumerated secret file before the command executes. Reads then return zero bytes.

Linux treats that overlay as required whenever at least one protected file needs masking. Namespace creation, ID mapping, propagation setup, and every bind mount must all succeed. If a hardened host blocks any step — for example through `user.max_user_namespaces=0`, AppArmor or SELinux policy, or container restrictions — LPM CLI refuses to execute the lifecycle script. It does not fall back to the broad Landlock project-read access. Projects with no matching secret files, or whose matches are all explicitly allowlisted, skip the namespace setup and continue normally.

The protection is a bounded pre-spawn snapshot, not a dynamic filesystem monitor. Files created after enumeration are not added to the overlay. The extension scan stops below four nested project levels and prunes generated or dependency-heavy directories such as `node_modules`, `.git`, `target`, `dist`, and `build`; explicit literal paths such as `.git/config` are still checked separately. Linux does not follow symlink entries while enumerating protected files. Windows AppContainer has no equivalent project-secret overlay today.

#### Opting specific files back in [#opting-specific-files-back-in]

Some builds genuinely need access — `prisma generate` reads `DATABASE_URL` from `.env`, a custom build script may read a project-rooted CA bundle, etc. Two surfaces let you exempt named files:

```json title="package.json (per-project)"
{
  "lpm": {
    "scripts": {
      "sandboxReadAllow": [".env", "services/api/.env"]
    }
  }
}
```

```toml title="~/.lpm/config.toml (per-user, applied to every project)"
script-read-allow = [".env", ".npmrc"]
```

Per-project (`package.json > lpm > scripts > sandboxReadAllow`) and per-user (`~/.lpm/config.toml > script-read-allow`) lists are **unioned** at install time. Every entry must resolve to a path **inside** `project_dir` — traversal escapes (`..`) and absolute paths pointing outside the project are rejected with an actionable error naming the config source and key. Duplicate entries across the two lists are deduplicated. These entries are the explicit read opt-in: an allowlisted secret remains readable, and a project whose matching secrets are all allowlisted does not need the Linux namespace overlay.

### Disabling the sandbox [#disabling-the-sandbox]

```bash
lpm rebuild --no-sandbox
```

A **single flag** drops both containment AND env scrubbing — scripts run with full host access including credential-bearing env (`LPM_TOKEN`, `NPM_TOKEN`, `GITHUB_TOKEN`). Reserve for debugging a sandbox false-positive that `sandboxWriteDirs` can't express.

`--no-sandbox` is mutually exclusive with `--strict-sandbox`, `--paranoid`, and `--sandbox-log`. Persistent off-mode goes through `lpm config sandbox --set none` instead — the CLI flag is the per-command escape.

### Diagnostic mode [#diagnostic-mode]

```bash
lpm rebuild --sandbox-log
```

macOS-only. Rule triggers are **logged** via `sandboxd` but **not enforced**. View accesses via `log show --last 5m --predicate 'senderImagePath CONTAINS "Sandbox"'` and grep for the script's pid. **Not a safety signal** — a clean run under `--sandbox-log` does not mean the script would pass under the full sandbox; it only means the logged accesses were visible for review.

On Linux and Windows, `--sandbox-log` errors at sandbox init (no native observe-only primitive) and points at `--no-sandbox` for the same debug case.

## `--force` flag [#--force-flag]

```bash
lpm rebuild --force esbuild
```

By default, packages whose scripts have already run (and whose `script_hash` hasn't changed) are skipped. `--force` ignores that record and re-runs them. Useful after deleting `node_modules` if you want fresh `postinstall` work without a full reinstall.

## `--deny-all` [#--deny-all]

```bash
lpm rebuild --deny-all
```

Refuses to run **any** scripts, even trusted ones. Useful as a CI smoke test: "would my install want to execute scripts?" Or paired with `package.json > lpm > scripts.denyAll: true` for a project-wide kill switch.

## JSON output [#json-output]

`lpm rebuild --json --dry-run` emits a dry-run envelope with `dry_run: true` and `packages[]`, including when the set is empty. A live `lpm rebuild --json` emits `success`, `built`, `failed`, and `build_cache`; an empty rebuild reports `success: true`, `built: 0`, `failed: 0` with zeroed cache counters.

`build_cache` contains `eligible`, `hits`, `misses`, `bypassed`,
`local_state_hits`, `scripts_avoided`, `restored_bytes`, and
`lifecycle_ms_avoided`. Its `timings_ms` object breaks out fingerprint
preparation, keying, lookup, restore, pristine rematerialization, and
publication time. A `hit` means an artifact was restored; a
`local_state_hit` means the already-materialized package's keyed build marker
still matched and no restore was needed.

Errors such as a missing `lpm.lock`, a failing lifecycle script, or an explicitly requested package that is not rebuildable emit `success: false` in the top-level JSON error envelope and exit non-zero.

## Flags [#flags]

| Flag                             | Effect                                                                                                                                    |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `--all`                          | Rebuild every scripted package (bypasses trust)                                                                                           |
| `--dry-run`                      | Preview without executing scripts                                                                                                         |
| `--force`                        | Re-run scripts even for already-built packages                                                                                            |
| `--timeout <SECS>`               | Per-script timeout (default `300` = 5 min)                                                                                                |
| `--deny-all`                     | Refuse to run any scripts                                                                                                                 |
| `--policy <deny\|allow\|triage>` | Override the policy for this invocation                                                                                                   |
| `--yolo`                         | Alias for `--policy=allow`                                                                                                                |
| `--triage`                       | Alias for `--policy=triage`                                                                                                               |
| `--strict-sandbox`               | Engage strict sandbox (filesystem + env + outbound network denial)                                                                        |
| `--paranoid`                     | Alias for `--strict-sandbox`                                                                                                              |
| `--no-sandbox`                   | Drop all containment for this run — also drops env scrubbing. Mutually exclusive with `--strict-sandbox` / `--paranoid` / `--sandbox-log` |
| `--sandbox-log`                  | macOS only: observe-only sandbox                                                                                                          |
| `--no-engine-strict`             | Use warning-only workspace-root engine checks (see [`lpm install`](/docs/packages/install#engines-enforcement))                           |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm install`](/docs/packages/install#lifecycle-scripts) — installs packages and can auto-run this build pipeline when policy permits
* [`lpm approve-scripts`](/docs/packages/approve-scripts) — manage the trust allowlist
* [`lpm trust`](/docs/packages/trust) — inspect drift in the allowlist
* [`package.json` "lpm.scripts"](/docs/reference/package-json-lpm#the-lpmscripts-block) — capability and sandbox config


# lpm release (/docs/packages/release)



```bash
lpm release plan --affected --base main --bump patch
```

Coordinates releases across a workspace. `lpm release plan` computes the package/version/dependency update plan, `lpm release apply` writes it to manifests, and `lpm release publish` publishes selected members in dependency order.

`lpm release` is intentionally separate from [`lpm version`](/docs/packages/version): single-package version bumps stay simple, while workspace releases get explicit planning and publish orchestration.

## Examples [#examples]

```bash
lpm release plan --all --bump patch
lpm release plan --affected --base origin/main --bump minor --json
lpm release apply --filter core --bump major
lpm release apply --affected --base main --dry-run --json
lpm release publish --all --dry-run --json
lpm release publish --affected --base origin/main --npm -y
lpm release publish --all --lpm --otp 123456 --json
```

## Planning [#planning]

A release plan reads every selected workspace member's `package.json`, computes the new versions, and then checks workspace-internal dependents.

Dependency ranges are updated only when the new version no longer satisfies the existing range and the range is safely rewriteable:

| Existing spec            | Example update                           |
| ------------------------ | ---------------------------------------- |
| exact old version        | `1.2.3` -> `2.0.0`                       |
| caret old version        | `^1.2.3` -> `^2.0.0`                     |
| tilde old version        | `~1.2.3` -> `~2.0.0`                     |
| explicit workspace range | `workspace:^1.2.3` -> `workspace:^2.0.0` |

Dynamic workspace specs such as `workspace:*`, `workspace:^`, and `workspace:~` are left unchanged. `catalog:` specs are also left unchanged because the catalog owns the concrete version.

If any dependent has a range that would stop accepting a bumped internal package and LPM CLI cannot rewrite it safely, planning fails before anything is written.

## Bump sources [#bump-sources]

Pass `--bump <level>` to use one bump for every selected package:

```bash
lpm release plan --affected --bump patch
```

Or add lightweight change files under `.lpm/changes/`. Each non-empty, non-comment line is:

```text
<package-name> <bump>
```

Example:

```text
core minor
app patch
```

Change-file bumps override the command's `--bump` fallback for matching packages. If a selected package has no change-file entry and no `--bump`, the command fails instead of guessing. Use `--bump patch` when you want the conservative patch fallback.

## Selection [#selection]

Every `lpm release` subcommand requires an explicit workspace selection:

Selection modes are mutually exclusive: `--all` cannot be combined with `--affected`, `--filter`, or `--filter-prod`.

| Flag                                    | Description                                                                                              |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `--all`                                 | Select every workspace member.                                                                           |
| `--affected`                            | Select packages changed since `--base` plus their transitive dependents.                                 |
| `--base <REF>`                          | Git base ref for `--affected`. Defaults to `main`.                                                       |
| `--filter <EXPR>`                       | Select workspace members with the same filter grammar as [`lpm run --filter`](/docs/dev/run#workspaces). |
| `--filter-prod <EXPR>`                  | Like `--filter`, but dependency closures follow production edges only.                                   |
| `--changed-files-ignore-pattern <GLOB>` | Ignore matching changed files when evaluating affected packages.                                         |
| `--test-pattern <GLOB>`                 | Treat matching changed files as tests so affected fanout does not include dependents from them.          |
| `--fail-if-no-match`                    | Exit non-zero when the selection resolves to no members.                                                 |

## Subcommands [#subcommands]

### `lpm release plan` [#lpm-release-plan]

Prints the release plan and never writes files.

```bash
lpm release plan --all --bump patch
lpm release plan --affected --base origin/main --bump minor --json
```

### `lpm release apply` [#lpm-release-apply]

Writes the plan to the selected package manifests and any workspace dependents that need range updates.

```bash
lpm release apply --filter core --bump major
lpm release apply --affected --bump patch --dry-run --json
```

LPM CLI records the original manifests before it replaces any file. It also coordinates these writes with installs, version changes, and publishing.

If the process stops during the write, run a mutating `release apply`, `release publish`, or `version` command. The command restores an incomplete update before it continues. If the original operation completed, an identical retry reports the completed operation without another version bump.

Read-only commands do not change recovery state. `release plan`, `release apply --dry-run`, and `release publish --dry-run` stop when an incomplete update exists. A concurrent install also stops instead of using a partially updated workspace.

### `lpm release publish` [#lpm-release-publish]

Publishes selected members in topological order, dependencies before dependents. Before the first upload it validates current workspace-internal ranges so a stale manifest fails before a partial publish.

`release publish` reuses the single-package [`lpm publish`](/docs/packages/publish) implementation for each member. It supports the same target flags that make sense for a workspace publish:

| Flag                       | Description                                                              |
| -------------------------- | ------------------------------------------------------------------------ |
| `--dry-run`                | Print publish order and skip decisions without uploading.                |
| `-y, --yes`                | Skip per-package confirmation prompts.                                   |
| `--otp <CODE>`             | Send a six-digit authenticator code for MFA-protected LPM.dev publishes. |
| `--min-score <N>`          | Require a minimum quality score.                                         |
| `--allow-secrets`          | Skip the pre-publish secret scan.                                        |
| `--npm`                    | Publish to npm.                                                          |
| `--lpm`                    | Publish to LPM.dev Registry.                                             |
| `--github`                 | Publish to GitHub Packages.                                              |
| `--gitlab`                 | Publish to GitLab Packages.                                              |
| `--publish-registry <URL>` | Publish to a custom npm-compatible registry.                             |
| `--provenance`             | Require generated Sigstore provenance.                                   |
| `--no-provenance`          | Disable provenance for this run.                                         |
| `--provenance-file <PATH>` | Attach a pre-generated Sigstore bundle for npm-compatible targets.       |

For an LPM.dev workspace release, `--otp` supplies the same code to each selected package. Use a fresh code before it expires.

For LPM.dev Registry and npm targets, LPM CLI checks metadata before upload. If every checked target already has the selected version, the package is skipped. If only some checked targets already have it, the command fails before uploading so it does not partially publish.

Custom, GitHub, and GitLab targets are still validated for local target-name/config correctness, but remote version existence is left to the target publish response.

Workspace publishing is not transactional across packages. If a later package fails, LPM CLI does not remove earlier successful uploads.

Human output names the failed package and shows its preparation or upload error. JSON output reports each selected package with one of these states:

| Status          | Meaning                                                   |
| --------------- | --------------------------------------------------------- |
| `published`     | Every selected target for the package succeeded.          |
| `failed`        | Preparation or at least one selected target failed.       |
| `not_attempted` | An earlier package failed, so this package did not start. |
| `skipped`       | The checked targets already contain this version.         |
| `planned`       | A dry run prepared this package without an upload.        |

If a publish fails, keep the successful uploads. Then retry only the failed and `not_attempted` packages with a narrower selection.

## JSON [#json]

`--json` is available on every subcommand. `plan` and `apply` emit the same envelope shape:

```json
{
  "success": true,
  "dry_run": true,
  "packages": [],
  "dependency_updates": [],
  "files": []
}
```

`publish --json` emits:

```json
{
  "success": true,
  "dry_run": true,
  "packages": 2,
  "results": []
}
```

On failure, `success` is `false`. The `results` array keeps completed, failed, and unattempted packages, and `warning` explains the retry boundary.

## See also [#see-also]

* [`lpm version`](/docs/packages/version) — single-package version bumps
* [`lpm publish`](/docs/packages/publish) — single-package publish behavior and provenance
* [`lpm workspaces`](/docs/packages/workspaces) — workspace declarations, protocols, catalogs, and filters


# lpm remove (/docs/packages/remove)



```bash
lpm remove <package>   # alias: lpm rm
```

Cleans up the on-disk artifacts of an [`lpm add`](/docs/packages/add) invocation by reading the project-local `.lpm/added-sources.json` manifest that `lpm add` writes. Exact tracked files are removed from whatever path they were copied to, then LPM CLI cleans up any package skills it installed.

This is the &#x2A;*reverse of [`lpm add`](/docs/packages/add)** for source-delivered packages on LPM.dev Registry, npm, or a private registry routed via `.npmrc`. To remove a runtime dependency from `package.json` and `node_modules`, use [`lpm uninstall`](/docs/packages/uninstall).

## Scope [#scope]

`lpm remove` accepts the same package identity you passed to [`lpm add`](/docs/packages/add): bare npm names (`lodash.merge`), scoped registry packages (`@scope/private-pkg`), and LPM.dev Registry packages (`@lpm.dev/owner.ui-kit`). For legacy LPM.dev Registry cleanup, the short remove-only form (`owner.ui-kit`) still works too.

```bash
lpm remove lodash.merge
lpm remove @scope/private-pkg
lpm remove @lpm.dev/owner.ui-kit
lpm rm owner.ui-kit                 # short form
```

## What it removes [#what-it-removes]

For a tracked package, the command:

1. Removes the exact file paths recorded in `.lpm/added-sources.json` when `lpm add` copied the source files.
2. Prunes now-empty parent directories under the project root.
3. Removes `.lpm/skills/<owner>.<name>/` if the add installed package-published skills. Current installs do not create editor links for package skills; cleanup also removes links left by older LPM CLI versions.

That manifest-backed path means custom `--path` installs are reversible now — if you ran `lpm add source-pkg --path ./src/vendor`, `lpm remove source-pkg` removes the tracked files from `src/vendor` rather than guessing from framework defaults.

For older projects added before `.lpm/added-sources.json` existed, `lpm remove` falls back to the legacy heuristic scan of canonical directories such as `components/`, `src/components/`, `lib/`, `src/lib/`, `Sources/`, and `Packages/LPMComponents/Sources/`. That fallback can still miss custom pre-manifest installs.

There is still no dedicated `--dry-run` or "show me the tracked paths" flag. If you need to inspect what a manifest-backed remove will target, read `.lpm/added-sources.json` in the project root.

The command exits successfully even when no files were found, with a warning naming the package — useful for idempotent CI cleanup, but it also means a typo'd name fails silently.

## See also [#see-also]

* [`lpm add`](/docs/packages/add) — the forward direction
* [`lpm uninstall`](/docs/packages/uninstall) — remove a runtime dependency


# lpm resolve (/docs/packages/resolve)



```bash
lpm resolve <packages...>
```

Runs the resolver against the given specs and prints the dependency tree it would install. **Read-only** — no filesystem changes, no lockfile, no downloads, no `package.json` mutation.

Useful for previewing the impact of a candidate dep, debugging unexpected transitive selections, or scripting dep-graph analysis (`--json` makes the output structured).

## Examples [#examples]

```bash
lpm resolve react                            # latest, full tree
lpm resolve react@^19                        # a specific range
lpm resolve react vue zod                    # multiple at once
lpm resolve @lpm.dev/owner.pkg               # @lpm.dev/* through LPM.dev Registry
lpm resolve @my-co/internal                  # through the current project's .npmrc scoped registry
lpm resolve react --json | jq '.packages'    # structured output for scripts
```

## Spec format [#spec-format]

Each spec is `<name>` or `<name>@<range>`. Bare names default to `*` (latest matching anything). Scoped names work as expected — the parser handles the leading `@` correctly so `@lpm.dev/owner.pkg@^1.0.0` parses as `(name=@lpm.dev/owner.pkg, range=^1.0.0)`.

| Spec                    | Resolves to                       |
| ----------------------- | --------------------------------- |
| `react`                 | Latest react                      |
| `react@^19`             | Latest react matching `^19`       |
| `react@19.0.0`          | Exact `19.0.0`                    |
| `react@beta`            | Latest tag `beta`                 |
| `@lpm.dev/owner.pkg@^1` | Latest `^1` from LPM.dev Registry |

## How it works [#how-it-works]

1. Parses each spec into `(name, range)`.
2. Calls the resolver with the combined dep map. `@lpm.dev/*` names route to LPM.dev Registry; `.npmrc`-declared scoped or default registries are honored for npm/private packages; everything else resolves against npmjs.org.
3. Prints the resolved tree as `name@version` rows, nesting transitives under their parent with tree edges.

No tarballs are downloaded. The resolver only needs metadata.

Human output is written as a readable tree:

```text
react@19.0.0
  └─ scheduler@0.25.0
```

## JSON output [#json-output]

```bash
lpm resolve react --json
```

```json
{
  "success": true,
  "count": 47,
  "elapsed_secs": 0.83,
  "packages": [
    { "package": "react", "version": "19.0.0" },
    { "package": "scheduler", "version": "0.25.0" }
  ]
}
```

| Field                | Type     | Notes                                        |
| -------------------- | -------- | -------------------------------------------- |
| `count`              | `number` | Total resolved packages (root + transitive). |
| `elapsed_secs`       | `number` | Resolver wall-clock time.                    |
| `packages[].package` | `string` | Package name.                                |
| `packages[].version` | `string` | Resolved exact version.                      |

Integrity hashes aren't part of the resolve envelope — pair with [`lpm download`](/docs/packages/download) to fetch the tarball + SRI for a specific resolved version.

## When to reach for this [#when-to-reach-for-this]

* Preview the transitive impact of adding a new dep before touching `package.json`.
* Investigate why a specific transitive version got picked. Pair with `--json` and `jq` for ad-hoc analysis.
* Generate dep-graph fixtures for testing.
* Check whether a candidate version has unexpected peer-dep conflicts.

For installing the result, run `lpm install <spec>`. For just downloading the tarball, use [`lpm download`](/docs/packages/download).

## Flags [#flags]

`lpm resolve` takes no specific flags. Use the [global flags](/docs/commands#global-flags) — `--json` is especially useful here, and `--registry` / `--token` route the lookup against a specific endpoint or session.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — installs the resolved tree
* [`lpm download`](/docs/packages/download) — fetch + extract the tarball without installing
* [`lpm graph`](/docs/packages/graph) — visualize the dep tree of an already-installed project
* [Resolver](/docs/packages/resolver) — design overview


# Resolver (/docs/packages/resolver)



The resolver decides which version of every transitive dependency lands in your `node_modules/`. It walks `package.json`, talks to the appropriate registry for each canonical package, and computes a `(name, version)` set that satisfies every declared semver range. This page is the conceptual deep-dive.

## Two resolvers [#two-resolvers]

LPM CLI ships two resolvers in the same binary. Both speak the same npm-compatible semver dialect (`^`, `~`, `||`, `*`, `x`, hyphen ranges, prereleases, `latest`/`beta`/`next` dist-tags) and produce the same lockfile shape. They differ in how they pick versions when multiple ranges target the same canonical package.

| Resolver                     | Default?    | Algorithm                                                                | Notes                                                                                                |
| ---------------------------- | ----------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| **Greedy-fusion**            | **Yes**     | First-match version pick, reuse-on-compatible / allocate-on-incompatible | Faster. Bun-style. The fused dispatcher IS the metadata fetch dispatcher — no separate walker spawn. |
| **PubGrub-with-split-retry** | No (opt-in) | Conflict-driven backtracking                                             | Slower but exhaustive.                                                                               |

Pick the alternative for one invocation:

```bash
LPM_RESOLVER=pubgrub lpm install
```

For deeper debugging of the dispatcher, force the walker-arm orchestration:

```bash
LPM_GREEDY_FUSION=0 lpm install
```

Both are stable. The default is greedy-fusion because it's faster and matches bun/npm/pnpm semantics for the multi-version case below.

## How greedy-fusion picks versions [#how-greedy-fusion-picks-versions]

The resolver maintains a graph keyed by `(canonical, version)`. When it processes an edge:

1. **Reuse-on-compatible.** If the canonical already has a node and that node's version satisfies the new edge's range, the new edge points at the existing node. First-version-wins inside any single satisfying range bucket — same as bun, npm, and pnpm.
2. **Allocate-on-incompatible.** If no existing node satisfies the new range, the resolver picks the best available version for the new range and allocates a new `(canonical, version)` node. Both versions live independently in the resolved tree.

Example: edge A wants `lodash@^4`. The resolver picks `lodash@4.17.21` and allocates one node. Edge B then wants `lodash@^4` — same range bucket, reuses A's node. Edge C wants `lodash@^3` — `4.17.21` doesn't satisfy `^3`, so the resolver allocates a new `lodash@3.10.1` node. The tree now has two `lodash` versions, each shared by everyone whose range pointed at it.

This is the "multi-version per canonical" property. npm has it. pnpm has it. Yarn-classic (with hoisting) approximated it. LPM CLI follows the bun-shaped recipe.

## Candidate policy [#candidate-policy]

The resolver walks available versions newest-first. Semver range satisfaction is the first filter, then LPM CLI applies install policy:

* **Minimum release age** is off by default. When enabled, it skips too-new matching candidates for direct/root package ranges and keeps walking to the newest mature candidate. Strict release-age policy extends the same check to transitive candidates and lockfile replays. Exact checked pins to a too-new version fail.
* **`trust-policy = "no-downgrade"`** skips candidates that reduce npm trusted-publisher or staged-publish evidence compared with an earlier published version. Registry attestation pointers do not count before verification. Ranges can fall back to an older allowed candidate; exact blocked pins fail with the trust-policy reason. After selection, the install gate separately requires new verified provenance when the lockfile contains verified history for that package name.
* **Platform metadata** (`os`, `cpu`, `libc`) is preserved in the lockfile and applied at reify/filter time, not used to hide the newest semver-satisfying version from the lockfile. Incompatible optional packages are skipped on the current host; incompatible required packages fail with a platform error.

This split keeps lockfiles reviewable and portable across hosts while still avoiding known-bad candidates before the graph is committed.

## Resolution failures [#resolution-failures]

When no compatible package version exists, LPM CLI fails before writing the lockfile or touching `node_modules/`. The error names the dependency edge that failed, the package that required it when that context is available, and the newest published version LPM CLI saw.

```text
✗ Could not resolve dependencies
  package missing-leaf@^2.0.0
  required by parent-pkg@1.0.0
  reason no published version satisfies ^2.0.0
  available 1 version, newest 1.0.0
```

`--json` uses the stable top-level `error_code: "resolution_failed"` and puts resolver-specific fields in the `error` object:

```json
{
  "success": false,
  "error_code": "resolution_failed",
  "error": {
    "code": "RESOLUTION_FAILED",
    "message": "failed to resolve missing-leaf@^2.0.0 required by parent-pkg@1.0.0: no published version satisfies ^2.0.0",
    "package": "missing-leaf",
    "requested": "^2.0.0",
    "dependency": "missing-leaf",
    "kind": "no_matching_version",
    "reason": "no published version satisfies ^2.0.0",
    "required_by": "parent-pkg@1.0.0",
    "available_versions": 1,
    "newest_version": "1.0.0"
  }
}
```

`kind` distinguishes the class of resolver failure (`no_matching_version`, `platform_incompatible`, `policy_blocked`, `fetch_failed`, `no_solution`, or `peer_conflict`). Use that field for automation that needs to decide whether to relax a range, change a platform target, approve a policy exception, or retry a registry lookup.

## Streaming dispatch [#streaming-dispatch]

The resolver loop is single-threaded — bun's PackageManager event loop runs on one thread, and parallelism comes from I/O fan-out. Each iteration:

1. Pop a pending edge off the task queue.
2. Resolve the canonical's manifest. Fast path: the shared cache hit — the BFS walker has been prefetching concurrently. Slow path: wait on the per-canonical `Notify` for an in-flight fetch to complete.
3. Pick a version per the rules above.
4. Enqueue the chosen version's deps for the next iteration.

The dispatcher and metadata fetcher are **fused** in the default resolver — the same loop drives both, so the resolver sees in-flight metadata land as it streams in rather than waiting for level-step batch fetches.

## peerDependencies and optionalDependencies [#peerdependencies-and-optionaldependencies]

* **Peer deps are auto-installed by default.** After the main resolution pass, an eager peer-drain step synthesizes installs for every non-optional `peerDependency` not already in the resolved tree. These "ambient" installs surface at `node_modules/<name>/` even though they aren't in `package.json`. `check_unmet_peers` then runs a post-pass to warn on anything that couldn't be satisfied (e.g., peer-range conflicts between consumers).
* Toggle the auto-install with `package.json > lpm > autoInstallPeers` (default `true`) → `~/.lpm/config.toml > auto-install-peers` → built-in default. Set to `false` for npm-classic / pnpm-classic semantics (no synthesis, peer warnings only).
* **Optional peers** (`peerDependenciesMeta.<name>.optional = true`) are **never** auto-installed regardless of the flag — the manifest author opted out of the dependency entirely.
* **Optional deps** are tried; failure to resolve or download is logged but doesn't fail the install.

## Overrides and resolutions [#overrides-and-resolutions]

The resolver consumes three sources of override declarations, with `lpm.overrides` winning on conflict:

| Source                                    | Priority |
| ----------------------------------------- | -------- |
| `package.json > lpm > overrides`          | Highest  |
| `package.json > overrides` (npm-style)    | Middle   |
| `package.json > resolutions` (yarn-style) | Lowest   |

Selectors:

* `"foo"` — every instance of `foo`
* `"foo@<1.0.0"` — instances whose natural version satisfies a range
* `"baz>foo"` / `"baz>foo@1"` — path-selector: instances reached **through** `baz`

Multi-segment paths (`a>b>c`) are rejected at parse time. See [`package.json` overrides](/docs/reference/package-json-lpm#overrides).

An override replaces the consumer range for direct and transitive dependencies. Release-age, trust, and platform policy still apply. Override targets must be strings. LPM CLI warns about nested-object values and does not apply them.

## npm aliases [#npm-aliases]

Deps declared as `"my-pkg": "npm:other-pkg@^1.0.0"` (npm-alias edges) are passed through the resolver verbatim. The cache populates an `aliases` map; the resolved tree records each alias edge. The lockfile's `alias-dependencies` and `root-aliases` blocks preserve the metadata for warm installs.

Projects with alias edges, peer pinning, dependency engine constraints, platform metadata, optional-reachability state, catalogs, or registry signatures may write &#x2A;*only `lpm.lock`** — the binary lockfile (`lpm.lockb`) is skipped whenever its v3 wire format cannot represent the TOML metadata. See [`lpm.lockb` format](/docs/reference/lockb-format#whats-not-stored-in-the-binary-lockfile).

## Tunables [#tunables]

| Env var                | Effect                                                          |
| ---------------------- | --------------------------------------------------------------- |
| `LPM_RESOLVER=pubgrub` | Use PubGrub-with-split-retry instead of greedy-fusion           |
| `LPM_GREEDY_FUSION=0`  | Force walker-arm orchestration (debug)                          |
| `LPM_NPM_FANOUT=N`     | Concurrent npm metadata fetches (default `256`)                 |
| `LPM_WALKER=stream`    | Use the continuous-stream walker; default is the level-step BFS |

## When to look at the resolver [#when-to-look-at-the-resolver]

* An install picked a transitive version you didn't expect → use [`lpm graph --why <pkg>`](/docs/packages/graph) to trace the path, then add an `lpm.overrides` entry.
* Resolution is slow → check whether your tree exercises the multi-version path repeatedly, or try `LPM_NPM_FANOUT` higher.
* A new release breaks a peer dep → check `lpm install` output for unmet peer warnings; add the peer to `dependencies` to opt into the resolver's allocation.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — runs the resolver as part of the install pipeline
* [`lpm resolve`](/docs/packages/resolve) — runs the resolver standalone, prints the tree, no install
* [Lockfile](/docs/packages/lockfile) — what gets persisted after resolution
* [`package.json` overrides](/docs/reference/package-json-lpm#overrides) — selector grammar
* [Environment variables](/docs/reference/env-vars#resolver-advanced) — every tunable


# Save policy (/docs/packages/save-policy)



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`](/docs/packages/install). For the file format, see [`lpm.toml`](/docs/reference/lpm-toml) and [`~/.lpm/config.toml`](/docs/reference/config-toml).

## The decision table [#the-decision-table]

| You ran                        | `package.json` ends up with | Why                                                                                            |
| ------------------------------ | --------------------------- | ---------------------------------------------------------------------------------------------- |
| `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 [#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 CLI to save a catalog reference after verifying the selected catalog matches the resolved version.

## CLI flags [#cli-flags]

```bash
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](#bare-reinstall-doesnt-rewrite) 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 CLI rolls `package.json` back and exits with an error.

## Persistent config [#persistent-config]

Pin a default for one project:

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

Or for every project on this machine:

```toml title="~/.lpm/config.toml"
save-prefix = "^"
save-exact  = false
```

| Key           | Type                   | Default | Notes                                         |
| ------------- | ---------------------- | ------- | --------------------------------------------- |
| `save-prefix` | `"^"` \| `"~"` \| `""` | `"^"`   | Prefix used when nothing else applies         |
| `save-exact`  | bool                   | `false` | Force exact saves regardless of `save-prefix` |

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

```bash
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 [#prerelease-safety]

```bash
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:

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

## Bare reinstall doesn't rewrite [#bare-reinstall-doesnt-rewrite]

```bash
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 [#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 [#workspace-protocol]

```bash
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` [#why-this-is-in-lpmtoml-not-packagejson]

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 [#see-also]

* [`lpm install`](/docs/packages/install#save-policy) — CLI flag reference
* [`lpm.toml`](/docs/reference/lpm-toml) — project-level config file
* [`~/.lpm/config.toml`](/docs/reference/config-toml) — user-level config file
* [`lpm config`](/docs/infra/config) — manage user-level keys from the CLI


# lpm sbom (/docs/packages/sbom)



```bash
lpm sbom
lpm sbom --format spdx
lpm sbom --output bom.cdx.json
lpm sbom --registry-metadata
```

`lpm sbom` exports a Software Bill of Materials for the current project. An SBOM is a machine-readable inventory of the packages in a build: names, versions, package URLs, dependency edges, sources, integrity hashes, licenses when local metadata is available, patch metadata, and provenance metadata when cached or fetched.

Security teams, enterprise customers, release pipelines, and vulnerability scanners use SBOMs to answer "what shipped?" after a build. Generate one in CI and attach it to releases or container images.

## Formats [#formats]

Default output is CycloneDX 1.7 JSON:

```bash
lpm sbom > bom.cdx.json
```

SPDX 2.3 JSON is available with `--format spdx`:

```bash
lpm sbom --format spdx > bom.spdx.json
```

Both formats are generated from `lpm.lock`, so run `lpm install` first.

## Enrichment [#enrichment]

By default, SBOM generation is local-first and does not depend on the network. It reads:

* `lpm.lock` for the resolved package graph
* `package.json` for root metadata and direct dependency scopes
* installed package manifests from `node_modules/` and the LPM CLI store when present
* `lpm.lock > [patches]` for patch paths, original integrity bindings, and patch-file SHA-256 records
* verified provenance evidence from `lpm.lock`
* cached Sigstore bundles under `~/.lpm/cache/metadata/attestations`, only when the package has integrity that can bind the bundle to its exact tarball

Use `--registry-metadata` when you want live registry metadata and provenance attestation checks during SBOM generation:

```bash
lpm sbom --registry-metadata --output bom.cdx.json
```

The flag respects the same registry routing and `.npmrc` behavior as metadata-reading commands. Plain `lpm sbom` stays deterministic and offline-friendly.

Lockfile evidence is preferred because it is already bound to the package's exact name, version, source, npm package URL, and SHA-512 integrity. A cache entry is never treated as a writable “verified” snapshot: the cache stores the original bundle bytes, and every use reruns certificate, transparency-log, identity, subject, and tarball-digest verification. A cache entry cannot contribute provenance when the package has no integrity to bind it.

## Output file [#output-file]

```bash
lpm sbom --output bom.cdx.json
lpm sbom --format spdx --output bom.spdx.json
```

When `--output` is set, LPM CLI writes the SBOM to that path and does not duplicate the JSON to stdout.

Progress is still printed to stderr, so stdout stays reserved for the SBOM document when you do not pass `--output`:

```bash
› Generating CycloneDX SBOM from lpm.lock
    packages  207
    format    cyclonedx
    output    /path/to/bom.cdx.json

✓ Included patch and provenance metadata
✓ Done · wrote SBOM in 236ms
```

## Patch and provenance fields [#patch-and-provenance-fields]

CycloneDX output uses the native `component.pedigree.patches` field for patched dependencies. LPM CLI also carries checksum and integrity details as component `properties`, for example:

```json
{
  "pedigree": {
    "patches": [
      {
        "type": "unofficial",
        "diff": {
          "url": "patches/lodash@4.17.21.patch"
        }
      }
    ]
  },
  "properties": [
    {
      "name": "lpm:patch:sha256",
      "value": "sha256-..."
    }
  ]
}
```

SPDX output carries the same LPM CLI details in package `attributionTexts`. This keeps the documents valid while preserving the information needed to audit patched dependencies.

## Flags [#flags]

| Flag                         | Effect                                                   |
| ---------------------------- | -------------------------------------------------------- |
| `--format <cyclonedx\|spdx>` | Choose the SBOM format. Default: `cyclonedx`             |
| `-o`, `--output <FILE>`      | Write to a file instead of stdout                        |
| `--registry-metadata`        | Fetch live registry metadata and provenance attestations |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — produces the lockfile SBOMs read
* [`lpm patch`](/docs/packages/patch) — patch metadata included in SBOM output
* [`lpm licenses`](/docs/packages/licenses) — human/JSON license inventory and CI policy gates
* [`lpm audit`](/docs/packages/audit) — vulnerability and behavioral analysis
* [Lockfile](/docs/packages/lockfile) — resolved graph source of truth


# lpm search (/docs/packages/search)



```bash
lpm search <query> [--limit <N>]
```

Searches the registry that matches your query:

* `@lpm.dev/...` queries the LPM.dev Registry catalogue.
* `@scope/...` queries follow the current project's `.npmrc` `@scope:registry=...` mapping when one exists.
* Everything else searches the current project's default `.npmrc` registry, or npmjs.org when no override is present.

`lpm search` still returns a single normalized JSON envelope, but the backing search API depends on where the query routes.

Human output is compact: each result prints the package name, an indented description when one exists, and a metadata line such as `latest 1.2.3 · quality 91 · ecosystem js`. Use `--json` when you need the full routed search envelope, including fields such as `downloadCount` and distribution mode.

## Examples [#examples]

```bash
lpm search react                    # npmjs.org (or project .npmrc default registry)
lpm search @my-co/internal          # project .npmrc scoped registry
lpm search @lpm.dev/highlight       # force LPM.dev Registry catalogue search
lpm search "react hooks" --json     # structured output
```

## Flags [#flags]

| Flag          | Effect                                         |
| ------------- | ---------------------------------------------- |
| `--limit <N>` | Maximum results to return (1–20, default `20`) |

Plus the [global flags](/docs/commands#global-flags) — `--json` is useful for piping into other tools.

## See also [#see-also]

* [`lpm info`](/docs/packages/info) — full metadata for a single package through the same routing rules
* [`lpm quality`](/docs/packages/quality) — quality report


# Security & audit (/docs/packages/security-audit)



LPM CLI's security model is **layered**. No single check is the answer; instead, several independent gates each catch a different class of threat. This page is the conceptual overview — what each layer does, how they compose, and where to dig deeper.

The CLI surfaces are [`lpm audit`](/docs/packages/audit) (broad report), [`lpm query`](/docs/packages/query) (precision selectors), [`lpm approve-scripts`](/docs/packages/approve-scripts) (script trust), and [`lpm trust`](/docs/packages/trust) (drift inspection). All of them read from the same underlying analysis pipeline.

## The five layers [#the-five-layers]

| Layer                          | What it catches                                                                                                | Where it runs                                             |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| **Lifecycle script gate**      | Postinstall script supply-chain attacks                                                                        | At install time, blocks until approval                    |
| **Behavioral analysis**        | Risky API usage, obfuscation, telemetry, license issues                                                        | At extract time when enabled, or during an explicit audit |
| **Vulnerability data**         | Known CVEs from OSV.dev + registry advisories                                                                  | At `lpm audit` time                                       |
| **Publisher trust + cooldown** | Recently-published direct versions, registry-signature failures, trust downgrades, or publisher-identity drift | At install time and via `lpm audit signatures`            |
| **Triage**                     | Tiered automation for the script gate                                                                          | At install time, per-package                              |

Each layer is independent — a package can pass one and fail another. Most attacks in the wild trip more than one.

## Layer 1: Lifecycle script gate (default-deny) [#layer-1-lifecycle-script-gate-default-deny]

Bare `lpm install` runs the root project's install lifecycle, including `prepare`. Dependency package lifecycle scripts (`preinstall`, `install`, `postinstall`) do **not** run by default. To execute dependency scripts, you either:

* **Approve them explicitly** via [`lpm approve-scripts`](/docs/packages/approve-scripts) — the package gets recorded in `package.json > lpm > trustedDependencies` (project) or `~/.lpm/global/trusted-dependencies.json` (`--global`) with its integrity + script hash.
* **Opt out per-invocation** with `lpm install --policy=allow` (or `--yolo`) — runs everything, no gate. Works on `lpm install -g` too.
* **Opt out persistently** with `package.json > lpm > scriptPolicy: "allow"` or `~/.lpm/config.toml > script-policy = "allow"`. The user-config tier applies to both project and global installs.

Approvals are **strict**: bound to `{name, version, integrity, scriptHash}`. A republished version with different bytes invalidates the approval and re-opens the package for review. This is what makes the deny-by-default model usable across long-running projects — trust is for a specific build, not a name.

The dependency lifecycle phases that actually run during `lpm install`'s build pipeline are `preinstall`, `install`, `postinstall`. The broader BLOCKED set (which detection scans for) also includes `preuninstall`, `uninstall`, `postuninstall`, `prepare`, and `prepublishOnly` — dependency packages that declare any of these get flagged even if the current dependency pipeline doesn't execute them.

## Layer 2: Behavioral analysis [#layer-2-behavioral-analysis]

Install-time source analysis is off by default. When enabled, LPM CLI analyzes package source as extracted bytes enter the store. Results are cached in a schema-versioned `.lpm-security.json` next to the package files. Cache identity includes package source and integrity, so the same `name@version` from different registries does not share findings.

Use `lpm config source-analysis` to manage install-time analysis. The disabled setting skips new install-time scans and cache writes without deleting existing caches. Disabling a previously approved source-analysis setting requires security approval. [`lpm audit`](/docs/packages/audit) still scans installed source when it cannot use a cache. After analysis is re-enabled, the next install backfills any missing, malformed, or outdated store cache from the already-extracted bytes without downloading the tarball again.

Three groups of tags — source behavior (what the code does), supply-chain signals (what the artifact looks like), and manifest declarations (what `package.json` declares):

**Source tags** — what the source code does:

| Tag                | Detects                                            |
| ------------------ | -------------------------------------------------- |
| `:eval`            | Use `eval`, `Function()`, or `vm.runInThisContext` |
| `:network`         | Make outbound HTTP / WS connections                |
| `:fs`              | Touch the filesystem outside their own directory   |
| `:shell`           | Spawn shells (`spawn`, `exec`, `execSync`)         |
| `:child-process`   | Use `child_process` (any form)                     |
| `:native`          | Ship native modules (`.node`, `.wasm`)             |
| `:crypto`          | Use cryptographic primitives                       |
| `:dynamic-require` | Use dynamic `require()` (variable arg)             |
| `:env`             | Read `process.env`                                 |
| `:ws`              | Use WebSockets                                     |

**Supply-chain tags** — what the artifact looks like:

| Tag                     | Detects                                                           |
| ----------------------- | ----------------------------------------------------------------- |
| `:obfuscated`           | Show signs of code obfuscation                                    |
| `:possible-obfuscation` | Show possible code obfuscation at moderate confidence             |
| `:high-entropy`         | Contain high-entropy string blobs                                 |
| `:minified`             | Ship minified-only source                                         |
| `:telemetry`            | Make telemetry / analytics calls                                  |
| `:url-strings`          | Contain URL string literals                                       |
| `:trivial`              | Tiny — measured by AST node count                                 |
| `:protestware`          | Match a curated list of known protest-license / sabotage packages |

**Manifest tags** — what the `package.json` declares:

| Tag             | Detects                            |
| --------------- | ---------------------------------- |
| `:git-dep`      | Installed from a git URL           |
| `:http-dep`     | Installed from an HTTP tarball URL |
| `:wildcard-dep` | Declared with `*` or `latest`      |
| `:copyleft`     | Copyleft license (GPL family)      |
| `:no-license`   | No `license` field                 |

Plus state tags driven by other layers (`:scripts`, `:built`, `:vulnerable`, `:deprecated`, `:lpm`, `:npm`, `:critical`/`:high`/`:medium`/`:info`).

Install, audit, and query use one severity policy for these tags:

| Severity | Behavioral tags                                                                                                                               |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Critical | `:obfuscated`, `:protestware`                                                                                                                 |
| High     | `:eval`, `:child-process`, `:shell`, `:dynamic-require`                                                                                       |
| Medium   | `:network`, `:native`, `:git-dep`, `:http-dep`, `:wildcard-dep`, `:no-license`                                                                |
| Info     | `:fs`, `:crypto`, `:env`, `:ws`, `:possible-obfuscation`, `:high-entropy`, `:telemetry`, `:trivial`, `:copyleft`, `:minified`, `:url-strings` |

Info tags are behavioral metadata. They describe common capabilities or artifact traits, not confirmed vulnerabilities. LPM CLI detects and caches them during installation.

Normal install output hides Info tags. Use `lpm --verbose install`, `lpm audit`, or `lpm query :info` to inspect them. Verbose install hints contain the selectors for the signals that were found.

### What Critical means [#what-critical-means]

The local behavioral scanner assigns Critical to two signals: high-confidence obfuscation and protestware. Obfuscation uses these confidence bands:

| Confidence          | Result                         |
| ------------------- | ------------------------------ |
| `≤ 0.3`             | No obfuscation tag             |
| `> 0.3` and `≤ 0.7` | `:possible-obfuscation` (Info) |
| `> 0.7`             | `:obfuscated` (Critical)       |

The protestware detector targets sabotage patterns. Examples include deliberate infinite loops and locale or network-interface checks near process exits.

Behavioral tags report code patterns and package capabilities. They do not prove data flow, user intent, or malicious execution.

For example, `:env` and `:network` are independent signals. Their presence does not prove that a package sends environment variables to a remote host.

A shell command such as `rm -rf` matches `:shell` and `:child-process`. These High signals do not become Critical from the command text alone.

When `lpm audit` or audit-after-install reports a known vulnerability or LPM.dev Registry security finding, it uses the published severity. A Critical advisory or Registry finding remains Critical.

The secret scanner is separate. Critical hardcoded-secret findings appear only when you run `lpm audit --secrets`.

### Performance design [#performance-design]

* `RegexSet` + `OnceLock` for compile-once, single-pass matching across all behavioral patterns.
* All regex uses Rust's `regex` crate (Thompson NFA, **linear-time guarantees**). The `fancy-regex` crate is **banned** for security analysis — backtracking against untrusted input is a ReDoS vector.
* File extension filter before I/O (skip non-source files).
* Per-file size cap: 2MB. Per-package total: 50MB scanned. Per-package file count: 5,000.
* Shannon entropy pre-filter skips \~95% of files before the expensive extraction pass.

Target: \< 100ms per typical 100-file package.

### Querying the tags [#querying-the-tags]

```bash
lpm audit                         # broad report — all tags grouped
lpm query :eval                   # one tag
lpm query :scripts:not(:built)    # combinator: scripted but never built
lpm query :critical --assert-none # CI gate: fail if any critical-tagged package
```

[`lpm query`](/docs/packages/query) is the precision tool — exposes the same data the audit pipeline produces, with combinators for ad-hoc gates.

## Layer 3: Vulnerability data (OSV) [#layer-3-vulnerability-data-osv]

`lpm audit` cross-references every installed package against [OSV.dev](https://osv.dev) (for npm packages) and any LPM.dev Registry-side advisories (for `@lpm.dev` packages). Findings are surfaced inline with the behavioral report. Severity ladder: `critical` > `high` > `moderate` (alias `medium`) > `info` (alias `low`).

```bash
lpm audit                       # all findings
lpm audit --level high          # only high-severity vulnerabilities
lpm audit --fail-on vuln        # CI: fail on confirmed vulns only (not behavior)
```

The vulnerability check is **online** — it hits OSV/registry. Cache hits keep the cost low on repeat runs.

## Layer 4: Provenance + cooldown [#layer-4-provenance--cooldown]

Install-time and audit-time gates that catch attacks against the publisher identity and registry metadata itself. They apply to both `lpm install` and `lpm install -g` unless noted.

**Minimum release age** — The release cooldown is off by default. Enable it with `lpm config release-age --set 1d`. When enabled, project installs check direct dependencies by default. Strict mode also checks transitive dependencies and lockfile replays.

For ranges, the resolver skips new candidates and selects the newest mature candidate that satisfies the range. An exact pin fails during its cooldown.

`lpm install -g`, `lpm dlx`, `lpm upgrade`, and `lpm outdated` apply the policy to the selected package version.

Configure the duration with `--min-release-age`, `minimumReleaseAge`, or `minimum-release-age-secs`. Configure strict mode with `minimumReleaseAgePolicy` or `release-age-policy`.

Use `--allow-new` to bypass the cooldown for one install. Use `--min-release-age-exclude <SELECTOR>` to exempt a package name, exact version, or `@scope/*`.

Use [`lpm trust release-age-exclude`](/docs/packages/trust#lpm-trust-release-age-exclude) for project selectors. Use [`lpm config release-age-exclude`](/docs/infra/config#exclude-selected-packages-from-the-release-cooldown) for user selectors.

CLI, project, and user lists merge. Global installs do not use the project list. They use the CLI, user, and default layers.

**Registry signatures** — npm packages can carry detached registry signatures in `dist.signatures`. Run [`lpm audit signatures`](/docs/packages/audit#registry-signatures) to verify the installed tree on demand. Install-time verification is disabled by default; enable it with:

```bash
lpm config signatures --set true
```

or for one process:

```bash
LPM_VERIFY_REGISTRY_SIGNATURES=1 lpm install
```

When enabled on install, LPM CLI verifies registry signatures after resolution and before the install commits. A missing signature, missing integrity hash, missing signing key, expired key, or invalid signature fails the install for npm registry packages. `@lpm.dev/*` packages and non-registry sources are skipped by this specific check and still pass through the other layers.

**Trust policy** — optional no-downgrade policy for npm trust evidence:

```bash
lpm config trust-policy --set no-downgrade
```

When enabled, LPM CLI enforces two history floors. During resolution, it refuses a release whose npm trusted-publisher or staged-publish evidence is weaker than an earlier published release; ranges can select an older acceptable version, while an exact downgraded pin fails. After selection, verified lockfile history adds an artifact-level floor: once any locked version has artifact-bound Sigstore evidence, a later selected version must also verify. An absent attestation, unavailable attestation, invalid bundle, frozen lockfile without matching evidence, or disabled/skipped verifier fails instead of allowing provenance to disappear. This artifact-level rule takes precedence over `verify=warn`, `verify=off`, per-package verification skips, and best-effort availability; disable `trust-policy` itself if you intentionally want to permit a downgrade. Registry attestation pointers alone count toward neither floor. The default remains `off`.

**Provenance drift** — when a previously-approved package is re-resolved, LPM CLI compares the candidate version's Sigstore provenance against the snapshot captured at approval time:

```text
(now,      approved)
(Some(n),  Some(a))   if n == a   → pass
(Some(_),  Some(_))               → drift! identity changed
(None,     Some(_))               → drift! provenance dropped
(Some(_),  None)                  → OK — present now, wasn't at approval
(None,     None)                  → never had it; layers 1/2/4 decide
```

A drift event blocks the install. Re-approve via [`lpm approve-scripts`](/docs/packages/approve-scripts) (or `lpm approve-scripts --global`) to capture the new identity, or opt out per-package with `--ignore-provenance-drift <pkg>` / blanket-waive with `--ignore-provenance-drift-all`. The reference snapshot lives in `package.json > lpm > trustedDependencies > <pkg>@<ver> > provenanceAtApproval` for project trust and in `~/.lpm/global/trusted-dependencies.json` for global trust.

The verifier binds the signed in-toto subject to the exact npm package URL (`pkg:npm/<name>@<version>`) and the package tarball's SHA-512 integrity. It also verifies the Sigstore certificate chain, transparency-log inclusion, publisher/workflow identity, and bundle digest. Registry metadata that merely points at an attestation is not proof.

Attestation URLs must be HTTPS and match the package registry origin. Redirects are checked at every hop and cannot change origin or downgrade HTTPS. The provenance cache stores the original bundle bytes, not a writable “verified” result; every cache hit reruns cryptographic verification and artifact binding. A malformed or locally modified cache entry is discarded and fetched again.

Verified evidence is written sparsely to `lpm.lock`, including the publisher/workflow snapshot, subject purl and SHA-512, transparency-log identity/index/time, and bundle SHA-256. Frozen and offline installs validate that evidence against the locked package name, version, source, and integrity before replay. The lockfile contains derived evidence rather than the original Sigstore bundle, so frozen replay cannot independently re-prove the publisher and transparency-log claims after a malicious lockfile edit. Treat `lpm.lock` as trusted, reviewable project input; online cache hits are different because the cache retains the original bundle and reruns verification.

To broaden or tighten verification explicitly:

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

`scope=all` checks every resolved package; the default `scope=approved` limits checks to packages with an approved provenance identity. `availability=strict` requires an attestation; the default `best-effort` preserves missing/temporarily unavailable attestations as absence or unknown without blocking. A present bundle that fails verification is still rejected by the default `verify=deny` posture.

`--allow-new`, registry signatures, trust-policy, and `--ignore-provenance-drift[-all]` are independent. `--allow-new` only skips the cooldown; signature verification and drift still apply unless their own controls are disabled or waived.

The cooldown threshold also gates the Layer 5 [identity widening](#layer-5-triage) — a recent-publish package whose script body matches an identity-Green shape still stays Amber until the publish age clears the cooldown. The two axes are independently bypassable: `--allow-new` skips the cooldown halt, `--policy=allow` skips the script-tier review.

## Layer 5: Triage [#layer-5-triage]

The tiered gate that lets some scripted packages auto-approve. Active under `script-policy: "triage"` (or `lpm install --triage` / `--policy=triage`).

| Tier      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Green** | A curated catalog of shape patterns (`node-gyp rebuild`, `tsc`, `prisma generate`, `husky install`, etc.) plus identity-matched delegating installers — a `node install.js` / `node postinstall.js` / `node scripts/install.js` body (+ `.cjs`/`.mjs` variants) Greens when the package's `repository` URL or a `bin` entry plainly names the package. Auto-approves and runs in the [filesystem sandbox](/docs/reference/package-json-lpm#the-lpmscripts-block). |
| **Amber** | Doesn't fit a green pattern, doesn't match a red. Defers to layers 2/3/4 (trust manifest, provenance + cooldown, optional LLM advisor). Network binary downloaders (`puppeteer install`, `playwright install`, `cypress install`) live here by design.                                                                                                                                                                                                            |
| **Red**   | Hand-curated blocklist (pipe-to-shell, base64-decode-to-execute, nested package-manager installs, etc.). **Blocks unconditionally**. Never reaches the advisor.                                                                                                                                                                                                                                                                                                   |

Worst-wins reducer across phases — a package whose `preinstall` is green but `postinstall` is red is classified as red.

Identity widening is shape-keyed, not name-keyed: there is no per-package allowlist. The classifier looks at the script body's structure (exactly `node <reserved>.{js,cjs,mjs}`), reads the manifest's `repository` URL and `bin` entries, and matches by the package's base name (case-insensitive, with `.git` suffixes stripped). Generic / too-short base names (anything ≤ 2 chars, plus `js`, `lib`, `core`, `node`, `src`, `util`, `utils`) carry no identity payload and never widen.

**Cooldown defense-in-depth.** Identity widening refuses to fire when the configured [minimum release age](#layer-4-provenance--cooldown) is above zero AND the package's publish age is below the threshold (or unknown). This classifier gate is separate from the direct/root install halt — `lpm install --allow-new <pkg>` bypasses the cooldown halt but a recent-publish package whose identity matches still stays Amber and surfaces in the script-tier review. To opt out of both, set `minimum-release-age` to `0` (universally disabling cooldown also disables the defense-in-depth check) or use `--policy=allow`.

The sandbox itself is Seatbelt on macOS, landlock on Linux, AppContainer on Windows. Scripts can read the project, write to their own package directory, and write to anything declared in `package.json > lpm > scripts.sandboxWriteDirs`. Anything else is denied. Capability-widening (extra env vars, full project read, looser rlimits) requires explicit approval.

## Putting it together — `script-policy` × `sandbox-mode` [#putting-it-together--script-policy--sandbox-mode]

The two axes are **independent**. `script-policy` decides whether a script runs; `sandbox-mode` decides what it can do once running. Every combination is valid and configured through separate precedence chains.

| script-policy                                         | sandbox-mode | What happens to a scripted package                                                                                                                                                            |
| ----------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`deny`](#layer-1-lifecycle-script-gate-default-deny) | `default`    | Script blocked at the script-policy gate. After [`lpm approve-scripts`](/docs/packages/approve-scripts): runs with filesystem + env containment, outbound network allowed.                    |
| `deny`                                                | `strict`     | Same gate. After approval: runs with containment **+ outbound network denied**.                                                                                                               |
| `deny`                                                | `none`       | Same gate. After approval: runs unsandboxed (full host access including credential env).                                                                                                      |
| [`triage`](#layer-5-triage)                           | `default`    | L1-L3 tier decides per package. Greens auto-run with containment + network allowed. Ambers go to the [`lpm approve-scripts`](/docs/packages/approve-scripts) queue. Reds blocked permanently. |
| `triage`                                              | `strict`     | Same tiering. Whatever runs runs with containment **+ outbound network denied**.                                                                                                              |
| `triage`                                              | `none`       | Same tiering. Whatever runs runs unsandboxed.                                                                                                                                                 |
| `allow`                                               | `default`    | Every script runs with containment + network allowed.                                                                                                                                         |
| `allow`                                               | `strict`     | Every script runs with containment **+ outbound network denied**. Trust-the-lockfile + deny-install-time-network shape.                                                                       |
| `allow`                                               | `none`       | Every script runs unsandboxed — equivalent to `npm install`.                                                                                                                                  |

Each axis is configured independently:

* **`script-policy`** — `package.json > lpm > scriptPolicy` (project), `~/.lpm/config.toml > script-policy` (user), CLI flag (`--policy=<v>` / `--yolo` / `--triage`), or the [`lpm config scripts`](/docs/infra/config#setup-wizards) wizard.
* **`sandbox-mode`** — `./lpm.toml > [sandbox] mode` (project) or `~/.lpm/config.toml > [sandbox] mode` (user), CLI flag (`--strict-sandbox` / `--paranoid` / `--no-sandbox`), `LPM_STRICT_SANDBOX=1` env, or the [`lpm config sandbox`](/docs/infra/config#setup-wizards) wizard.

[`lpm approve-scripts`](/docs/packages/approve-scripts) grants permission to **run** — it never escalates the sandbox. An approved script under `sandbox-mode = strict` still runs without network access; an approved script under `sandbox-mode = none` runs with full host access.

### Optional LLM advisor [#optional-llm-advisor]

Layer 5 includes an opt-in advisor that uplifts Amber packages to AutoRun for the current install only. Configure via `triage-advisor`:

| Value              | Provider                                                                         |
| ------------------ | -------------------------------------------------------------------------------- |
| `"none"` (default) | No advisor — portable layers 1-4 only.                                           |
| `"claude-cli"`     | [Claude CLI](https://github.com/anthropics/claude-code) — local subprocess call. |
| `"codex"`          | OpenAI [Codex CLI](https://github.com/openai/codex) — local subprocess call.     |
| `"ollama"`         | Ollama-served local model — local HTTP call.                                     |

```toml title="~/.lpm/config.toml"
script-policy   = "triage"
triage-advisor  = "claude-cli"
```

```json title="package.json"
{ "lpm": { "scriptPolicy": "triage", "triageAdvisor": "claude-cli" } }
```

The two keys are **independent**. `script-policy: "triage"` with `triage-advisor: "none"` is the portable L1-4 baseline. `script-policy: "deny"` with any advisor setting still blocks unconditionally — the advisor never runs outside `triage`.

Precedence for `triage-advisor` (highest first): `package.json > lpm > triageAdvisor` > `~/.lpm/config.toml > triage-advisor` > default `"none"`.

**Properties:**

* **Ephemeral.** Approvals never write to disk as trust state. A second `lpm install` invokes the advisor again — the verdict is non-deterministic and LPM CLI does not pretend otherwise. Strict bindings via [`lpm approve-scripts`](/docs/packages/approve-scripts) exist for the "approve once, remember forever" use case.
* **Source-aware identity.** Approvals key on `(name, version, integrity)` so a registry copy of a package can never inherit approval from a workspace copy of the same coordinates.
* **Degrade-and-warn.** Missing binary, timeout, unparseable response → install continues with one stderr warning. Never fails on the advisor itself.
* **Prompt-injection defense.** Untrusted script body is fenced with a per-call random nonce; the verdict parser strict-matches `Approve` and loose-matches only the safe-default verdicts.
* **Worst-of-phases per package.** Approve only if every amber phase classified Approve; one Manual or Abstain blocks the whole package.

### Prompt context [#prompt-context]

The advisor sees a fixed template — role framing, verdict-format reminder, and a structured context block:

| Field              | Source                                                                             | Notes                                                                                                                                                                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Package`          | `name@version`                                                                     | Always present.                                                                                                                                                                                                                                                              |
| `Repository:`      | `package.json > repository` (URL or shorthand)                                     | Emitted only when the manifest carries a non-empty value. Treated as untrusted hint — pairs with the body's actual behaviour, never approves on URL alone.                                                                                                                   |
| `Lifecycle phase`  | `preinstall` / `install` / `postinstall`                                           | Always present.                                                                                                                                                                                                                                                              |
| `Script body`      | The phase's exact `package.json` value                                             | Fenced with `<<UNTRUSTED-SCRIPT-BEGIN-{nonce}>>` markers using a per-call random nonce.                                                                                                                                                                                      |
| `Referenced files` | Files the body delegates to (e.g. `install.js` when the body is `node install.js`) | Each file fenced with its own per-file nonce. Caps: depth 1 (no recursive `require` following), ≤ 32 KB per file (truncated with explicit marker), safe-relative paths only (no `..`, abs paths, env-var expansion), text-only (binary files fall back to no-context Amber). |

### Verdict cache [#verdict-cache]

Advisor verdicts are persisted at `$LPM_HOME/cache/l4-verdicts.json`. The next install of the same `(name, version)` with byte-identical script bodies skips the advisor round-trip and replays the cached verdict.

The cache key folds in the script identity, the prompt template hash, the provider slug, and the model version. Any of those changing produces a different key and the next invocation re-classifies — there is no manual invalidation step. Cached verdicts expire after 30 days by default. Hits return the cached `Approve` / `Manual` / `Abstain` value verbatim; the cache does not promote anything.

Tunables (env vars):

* `LPM_L4_CACHE=0` — disable the cache entirely. Useful for measurement runs where the comparison must include the round-trip cost.
* `LPM_L4_CACHE_PATH=<path>` — override the cache file location.
* `LPM_L4_CACHE_TTL_SECS=<n>` — override the 30-day default TTL.

Set up interactively:

```bash
lpm config scripts                 # pick deny / triage / allow
lpm config triage                  # pick none / claude-cli / codex / ollama
```

Both wizards accept `--set <value>` for non-interactive use.

## Typosquatting [#typosquatting]

The typosquat guard is off by default. Enable it with `lpm config typosquat --set on`. Then `lpm install` and `lpm add` check new direct dependency names against popular npm packages. The detector covers edit-distance typos, adjacent transpositions (`axois` → `axios`), delimiter variants (`crossenv` → `cross-env`). Flag-shaped package names after `--` remain invalid even when the guard is off.

The check fires only when a suspicious direct package name is newly entering the project. Existing lockfile replays, including `CI=true` frozen installs, do not fail because the name was already reviewed into `lpm.lock`. Interactive terminals can switch to the suggested package or write a committed `lpm.toml` allow-list entry; `--json`, CI, non-TTY, and `--yes` runs fail closed with `error_code: "typosquat_suspected"`.

Operates entirely offline. See [`lpm.toml` typosquat policy](/docs/reference/lpm-toml#typosquat-policy) for intentional exceptions.

## npm firewall verdicts [#npm-firewall-verdicts]

LPM Firewall is an opt-in verdict check at `firewall.lpm.dev` for selected public npm package versions. It does not proxy npm metadata or tarballs: LPM CLI still fetches from the normal npm registry path, then batches selected `(name, version)` checks with LPM Firewall before tarballs are allowed through.

LPM Firewall is an LPM.dev Registry Pro/Org feature. Active `monitor` and `enforce` modes send LPM.dev Registry auth, so run `lpm login` first. Entitlement failures are warnings in `monitor` mode and blocking failures in `enforce` mode.

Configure it globally:

```bash
lpm config firewall --set monitor  # show what would block and continue
lpm config firewall --set enforce  # block malicious verdicts
```

`off` is the default. `monitor` is useful while evaluating coverage; `enforce` is the blocking mode. The legacy string `report` is still accepted as an alias for `monitor`. Once monitor or enforce mode is part of the approved machine posture, disabling or downshifting it is a guarded weakening through [`lpm security`](/docs/infra/security).

LPM Firewall also carries policy metadata for configurable enforcement. Recommended enforcement blocks trusted public malicious advisories and LPM Firewall AI-confirmed malware; it warns for LPM Firewall AI-agent control-surface policy, critical vulnerabilities, and static-only suspicious signals. See the [Firewall for npm guide](/docs/guides/firewall) for the full policy profile.

## Skill content (LPM.dev Registry only) [#skill-content-lpmdev-registry-only]

Packages on LPM.dev Registry that ship [agent skills](/docs/reference/ai-agent-skills) get a separate LLM security scan against the skill content at publish time. Consumers receive attested skill markdown — drift in the skill body invalidates the attestation.

This layer doesn't exist for non-LPM.dev Registry packages today (no per-tarball skills declaration yet).

## CI integration [#ci-integration]

The recommended CI gate stack:

```yaml
- run: lpm install --offline --strict-integrity
- run: lpm audit signatures
- run: lpm trust diff --assert-none
- run: lpm audit --fail-on vuln # confirmed vulns
- run: lpm audit --secrets --fail-on secrets # hardcoded credentials
- run: lpm licenses --fail-on copyleft --deny GPL-3.0 # license policy
- run: lpm query ":vulnerable:not(:built)" --assert-none
- run: lpm query ":eval:scripts" --assert-none # eval + scripts is a yellow flag
```

`lpm audit --fail-on` covers the broad-strokes gate; `lpm audit signatures` checks npm registry signatures; [`lpm licenses`](/docs/packages/licenses) gates license compliance; `lpm query --assert-none` covers your project's specific concerns; `lpm trust diff --assert-none` gates reviewed script-trust drift directly. Together they're cheap and they catch most real classes of supply-chain trouble.

## See also [#see-also]

* [`lpm audit`](/docs/packages/audit) — broad report CLI
* [`lpm query`](/docs/packages/query) — precision selectors
* [`lpm licenses`](/docs/packages/licenses) — license inventory and policy gates
* [`lpm approve-scripts`](/docs/packages/approve-scripts) — script-trust workflow
* [`lpm trust`](/docs/packages/trust) — drift inspection
* [`lpm install --policy`](/docs/packages/install#lifecycle-scripts) — script policy override
* [`package.json` "lpm.scripts"](/docs/reference/package-json-lpm#the-lpmscripts-block) — capability + sandbox config


# lpm skills (/docs/packages/skills)



`lpm skills` handles two skill systems: skills published with `@lpm.dev/*` packages and standalone skills added from GitHub or a local directory.

## Quickstart [#quickstart]

```bash
# Install guidance published with an LPM.dev package
lpm skills add @lpm.dev/owner.package --dry-run
lpm skills add @lpm.dev/owner.package --yes

# Validate guidance before publishing your own package
lpm skills validate

# Add a standalone skill from GitHub
lpm skills add owner/repository
```

## Choose a workflow [#choose-a-workflow]

| Goal                                           | Source                        | Main commands                                             |
| ---------------------------------------------- | ----------------------------- | --------------------------------------------------------- |
| Use guidance shipped by an LPM.dev package     | `@lpm.dev/owner.package`      | `lpm install`, `lpm skills add`, `lpm skills clean`       |
| Publish guidance with your own LPM.dev package | Flat `.lpm/skills/*.md` files | `lpm skills validate`, `lpm publish`                      |
| Manage a standalone agent skill                | GitHub or a local directory   | `lpm skills add`, `update`, `enable`, `disable`, `remove` |

For the ownership, storage, and security model behind these workflows, see [AI agent skills](/docs/reference/ai-agent-skills).

## Install skills from an LPM.dev package [#install-skills-from-an-lpmdev-package]

Direct `@lpm.dev/*` dependencies install their published skills automatically:

```bash
lpm install @lpm.dev/owner.package
```

The package-owned set is materialized in `.lpm/skills/owner.package/`. It stays separate from agent-specific skill directories and is reconciled when the package is installed, upgraded, or removed.

Fetch or preview one package explicitly with `lpm skills add`:

```bash
lpm skills add @lpm.dev/owner.package --list
lpm skills add @lpm.dev/owner.package --dry-run
lpm skills add @lpm.dev/owner.package --yes
```

Package sources are installed as one set. Standalone-only flags such as `--skill`, `--agent`, `--project`, `--global`, `--copy`, and `--full-depth` are rejected for `@lpm.dev/*` inputs.

### Configure automatic installation [#configure-automatic-installation]

Package skill reconciliation is enabled by default. Open the package-skills setting directly, or set it non-interactively:

```bash
lpm config lpm-skills
lpm config lpm-skills --set false
lpm config lpm-skills --set true
```

You can also open the full configuration wizard, choose **LPM.dev settings**, and toggle **LPM.dev package skills** in the grouped editor:

```bash
lpm config
```

Use `--no-skills` on [`lpm install`](/docs/packages/install), [`lpm add`](/docs/packages/add), or `lpm ci` to opt out once. Use `--skills` to override a disabled preference once. The setting does not delete existing package skills and does not affect explicit `lpm skills add` commands.

### Clean installed package skills [#clean-installed-package-skills]

Preview cleanup before applying it:

```bash
lpm skills clean --dry-run
lpm skills clean
lpm skills clean --yes
```

`clean` removes only package directories whose files and digests exactly match an LPM CLI ownership manifest. It preserves publisher-authored `.lpm/skills/*.md` files, untracked or modified content, invalid manifests, symlinks, standalone managed content, and agent links.

In a terminal, `clean` shows the package sets, file count, and size, then asks for confirmation with **No** as the default. Non-interactive cleanup requires `--yes` after reviewing `--dry-run`. Run [`lpm install`](/docs/packages/install) to restore removed package sets.

## Publish skills with an LPM.dev package [#publish-skills-with-an-lpmdev-package]

Package publishers author direct Markdown files under `.lpm/skills/`:

```text
.lpm/
└── skills/
    ├── usage.md
    └── migration.md
```

Nested publisher paths such as `.lpm/skills/guides/usage.md` are invalid. Each file needs YAML frontmatter and a Markdown body:

```md
---
name: package-usage
description: Use the package through its supported public API.
version: "1.4.0"
globs:
  - "src/**/*.ts"
---

# Package usage

Import the public client, initialize it once, and reuse that instance. Follow the package's documented error handling and configuration conventions when changing an existing integration.
```

Use the [LPM package-skill authoring guide](https://github.com/lpm-dev/lpm-guide) for help designing the files. You can install it as a standalone skill:

```bash
lpm skills add lpm-dev/lpm-guide
```

Validate locally, then publish:

```bash
lpm skills validate
lpm publish
```

`validate` checks the same publisher contract enforced by the LPM.dev Registry:

| Rule        | Requirement                                                                                    |
| ----------- | ---------------------------------------------------------------------------------------------- |
| Location    | Direct `.md` files under `.lpm/skills/`                                                        |
| Count       | At most 10 skills                                                                              |
| File size   | At most 15 KB per skill                                                                        |
| Total size  | At most 100 KB                                                                                 |
| Frontmatter | Required `name` and `description`; optional `version` and `globs`                              |
| Name        | Lowercase letters, numbers, and hyphens                                                        |
| Description | 10–500 characters                                                                              |
| Body        | At least 100 characters                                                                        |
| Security    | No blocked shell injection, exfiltration, prompt injection, or destructive filesystem patterns |

Validation exits non-zero when any rule fails. If the project also consumes LPM.dev package skills, manifest-owned `.lpm/skills/<owner.package>/` directories are ignored by publisher validation and excluded from the publish tarball. Other nested Markdown is rejected. When `package.json > files` is restrictive, `lpm publish` adds `.lpm/skills` before creating the tarball.

Set optional `version` to the package release the guidance was authored for. The LPM.dev Registry stores it independently from the release currently carrying the skill, and the LPM CLI preserves it when materializing installed guidance. Leaving an older value in place makes intentionally stale guidance visible instead of silently relabeling it as current.

## Add standalone skills from GitHub or local sources [#add-standalone-skills-from-github-or-local-sources]

Standalone inputs support GitHub URLs, GitHub `owner/repository` shorthand, and local directories:

```bash
lpm skills add vercel-labs/skills --skill find-skills
lpm skills add https://github.com/vercel-labs/agent-skills --skill web-design-guidelines
lpm skills add ./team-skills --skill release-notes --agent codex --project
```

Remote standalone inputs must resolve to GitHub. When a catalog helps you discover a skill, pass the underlying GitHub repository or tree URL to the LPM CLI.

In a terminal, `add` opens a wizard for source, skill selection, project or global scope, compatible agent targets, security review, and confirmation. For scripts, provide the selection flags and `--yes` after reviewing `--dry-run`:

```bash
lpm skills add owner/repository \
  --skill release-notes \
  --agent codex \
  --project \
  --dry-run

lpm skills add owner/repository \
  --skill release-notes \
  --agent codex \
  --project \
  --yes
```

`lpm skills install` remains a compatibility alias for `lpm skills add`. Use `add` in new scripts.

| Flag                     | Effect                                                                                                     |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `--skill <name>`         | Select a standalone skill. Repeat it, or use `--skill '*'` for all discovered skills.                      |
| `--agent <target>`       | Select `codex`, `claude-code`, `cursor`, `grok`, `opencode`, `pi`, or `kimi`. Repeat for multiple targets. |
| `--project` / `--global` | Choose standalone canonical storage scope.                                                                 |
| `--copy`                 | Copy standalone files to targets instead of linking them.                                                  |
| `--list`                 | Discover and preview a source without installing it.                                                       |
| `--full-depth`           | Search beyond standard skill directory layouts.                                                            |
| `--dry-run`              | Show planned filesystem changes without writing them.                                                      |
| `--yes`                  | Confirm a non-interactive mutation.                                                                        |

Warning-level security findings remain installable after explicit confirmation. Block-level findings stop `add` or `update` before any managed content or agent targets are written.

The interactive target step offers Codex, Claude Code, Cursor, Grok, OpenCode, Pi, and Kimi (Moonshot). Project installs use the agent-native `.agents`, `.claude`, `.cursor`, `.grok`, `.opencode`, `.pi`, or `.kimi` skill directory. Global installs honor `CODEX_HOME`, `GROK_HOME`, `XDG_CONFIG_HOME`, and `PI_CODING_AGENT_DIR` where the corresponding agent defines an override.

## Inspect all skills [#inspect-all-skills]

```bash
lpm skills list
lpm skills list --kind package
lpm skills list --kind managed --agent codex
lpm skills view owner.package/usage
lpm skills view release-notes
```

The unified inventory distinguishes ownership:

| Category   | Meaning                                                                     |
| ---------- | --------------------------------------------------------------------------- |
| `package`  | Skills installed from LPM.dev packages into `.lpm/skills/<owner.package>/`. |
| `managed`  | Standalone skills added through `lpm skills add`.                           |
| `external` | Existing agent-directory skills not managed by the LPM CLI.                 |

`view` reports source, integrity, estimated context size, security findings, and agent-target health. Add `--global` to include global managed and external locations.

## Manage standalone skills [#manage-standalone-skills]

```bash
lpm skills doctor
lpm skills update release-notes --dry-run
lpm skills update release-notes --yes
lpm skills disable release-notes --agent cursor --yes
lpm skills enable release-notes --yes
lpm skills remove release-notes --yes
lpm skills prune --dry-run
```

`update` re-resolves the source, shows a bounded content diff and new security findings, then refreshes canonical content and recorded targets. `disable` removes selected target links while retaining canonical content. `remove` removes targets and deletes canonical content when no targets remain. `prune` previews and removes orphaned managed links and stale state records.

`update`, `remove`, `enable`, and `disable` accept skill selectors or `--all`, plus `--global`, `--dry-run`, and `--yes`. `remove`, `enable`, and `disable` also accept repeated `--agent` filters. `prune` accepts `--global`, `--dry-run`, and `--yes`. These commands do not mutate package-installed skills.

## Local dashboard [#local-dashboard]

```bash
lpm skills dashboard
lpm skills dashboard --read-only
lpm skills dashboard --no-open
lpm skills dashboard --port 4401
```

`dashboard` opens an authenticated browser view of every discovered project and global skill. `lpm skills ui` is an alias. Use the source and **needs attention** tabs to filter the combined inventory without restarting the command with a scope flag.

Select a skill to inspect its rendered Markdown, exact raw source, bounded file tree, metadata, context estimate, integrity, and security status. Package and external skills are inspection-only. Managed standalone skills expose enable, disable, update, and remove through a reviewable preview-and-apply plan. `--read-only` disables all mutations.

When multiple agent directories link to the same external skill directory, the dashboard shows one skill with every agent target instead of duplicate rows. Compact badges identify Codex, Claude Code, Cursor, Grok, OpenCode, Pi, and Kimi targets; hover a badge for its agent name. The dashboard resolves the top-level agent alias for inspection but still rejects symlinks inside the skill tree. Broken agent aliases remain visible as `broken-link`, use explicit missing-target language, and appear under **needs attention**; `lpm skills doctor --global` reports their exact paths.

The server binds only to `127.0.0.1`. Omit `--port` to select a free port, or request an exact port. `--no-open` prints the authenticated URL. The URL fragment carries the local session token; the dashboard captures and removes it from the address bar after startup. Keep the command running while using the dashboard and press `Ctrl+C` to stop it.

## JSON and CI [#json-and-ci]

Global `--json` emits one structured result per command. Inventory includes total counts and `package`, `managed`, and `external` counts. Use `--dry-run --json` to capture a mutation plan, then repeat the command with `--yes`. No-op validation, cleanup, and prune operations still emit successful zero-count results.

## See also [#see-also]

* [AI agent skills](/docs/reference/ai-agent-skills) — ownership, storage, lifecycle, and security
* [`lpm install --no-skills`](/docs/packages/install) — opt out of package skill extraction
* [`lpm add --no-skills`](/docs/packages/add) — same for dependency installation
* [`lpm config lpm-skills`](/docs/infra/config#setup-wizards) — configure automatic package skill installation
* [MCP servers](/docs/reference/mcp-servers) — separate tool integration


# lpm stage (/docs/packages/stage)



```bash
lpm stage publish
lpm stage list [package]
lpm stage view <stage-id>
lpm stage download <stage-id>
lpm stage approve <stage-id>
lpm stage reject <stage-id>
```

`lpm stage` wraps npm staged publishing. It prepares the current package the same way [`lpm publish --npm`](/docs/packages/publish) does, uploads it to npm's staging endpoint, then lets you inspect, approve, reject, or download the staged version before it becomes installable.

`lpm stage` is npm-only in this release. It does not stage LPM.dev Registry, GitHub Packages, GitLab Packages, custom publish-registry targets, workspace-recursive publishes, or arbitrary tarball/package specs.

## Examples [#examples]

```bash
lpm stage publish --tag next
lpm stage publish --access public --provenance -y
lpm stage publish --dry-run

lpm stage list
lpm stage list @scope/pkg
lpm stage view <stage-id>
lpm stage download <stage-id>
lpm stage approve <stage-id> --otp 123456
lpm stage reject <stage-id> --otp 123456
```

## Stage A Version [#stage-a-version]

```bash
lpm stage publish --tag next
```

`stage publish` runs the normal npm publish preparation path:

1. Pack the current project.
2. Rewrite `workspace:*` and `catalog:` dependencies in the packed tarball.
3. Run the secret scan unless `--allow-secrets` is set.
4. Run the quality gate and enforce `--min-score` when present.
5. Resolve npm name, access, dist-tag, registry, auth, and provenance settings.
6. Upload to npm staging instead of the live publish endpoint.

The staged version is not installable until it is approved.

`--dry-run` stays local-only. It packs, scans, scores, and reports what would be staged, but it does not authenticate to npm and does not run npm's remote version/tag checks.

## Auth [#auth]

For `lpm stage publish` on the default npm registry, auth resolution is:

| Order | Source                                                                                                                |
| ----- | --------------------------------------------------------------------------------------------------------------------- |
| 1     | npm Trusted Publishing through CI OIDC (`NPM_ID_TOKEN` or GitHub Actions OIDC with audience `npm:registry.npmjs.org`) |
| 2     | `NPM_TOKEN`                                                                                                           |
| 3     | token stored by `lpm login --npm`                                                                                     |
| 4     | locked `.npmrc` token                                                                                                 |

OIDC is publish-only. `list`, `view`, `download`, `approve`, and `reject` require normal npm token auth.

For compatible staging registries, pass `--npm-registry <URL>`. The global `--registry` flag still means LPM.dev Registry and is rejected on `lpm stage` commands.

## Version And Tag Rules [#version-and-tag-rules]

Before a real stage publish, LPM CLI fetches npm package metadata and enforces npm-safe staging rules:

* The package must already exist on npm.
* The staged version must not already be published.
* Prerelease versions require an explicit `--tag`.
* If npm already has a higher stable version, implicit `latest` is blocked; pass an explicit tag.

## Review And Promote [#review-and-promote]

```bash
lpm stage list @scope/pkg
lpm stage view <stage-id>
lpm stage download <stage-id>
lpm stage approve <stage-id> --otp 123456
```

`list` shows staged package versions, optionally filtered by package name. `view` prints one staged entry. `download` retrieves the staged tarball for inspection. `approve` promotes the staged package to the live npm registry. `reject` discards it.

Publishing to staging does not prompt for OTP. npm defers proof-of-presence to `approve` and `reject`, where `--otp` or the usual npm web-auth retry flow is used.

Every subcommand supports the global `--json` flag. JSON envelopes include `success`, `target: "npm"`, `registry`, `stageId` when a single staged package is involved, and `data` for the registry response. `stage publish --json` also reports `auth` as `"oidc"` or `"token"`.

## `publish` Flags [#publish-flags]

| Flag                          | Effect                                                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--tag <TAG>`                 | npm dist-tag for the staged version. Prereleases require an explicit tag.                                    |
| `--access public\|restricted` | npm package access. Provenance requires effective `public` access.                                           |
| `--dry-run`                   | Preview locally without uploading or authenticating to npm.                                                  |
| `--provenance`                | Generate and require Sigstore provenance. Mutually exclusive with `--no-provenance` and `--provenance-file`. |
| `--no-provenance`             | Disable provenance even when npm config enables it.                                                          |
| `--provenance-file <PATH>`    | Attach a pre-generated Sigstore provenance bundle. Mutually exclusive with `--no-provenance`.                |
| `--min-score <N>`             | Minimum quality score required to stage, from `0` to `100`.                                                  |
| `--allow-secrets`             | Skip pre-publish secret scanning.                                                                            |
| `-y`, `--yes`                 | Skip the confirmation prompt.                                                                                |
| `--npm-registry <URL>`        | Override the npm staging registry URL.                                                                       |

## Other Subcommands [#other-subcommands]

| Command              | Arguments    | Flags                                  | Effect                                                                 |
| -------------------- | ------------ | -------------------------------------- | ---------------------------------------------------------------------- |
| `lpm stage list`     | `[package]`  | `--npm-registry <URL>`                 | List staged npm package versions, optionally filtered by package name. |
| `lpm stage view`     | `<stage-id>` | `--npm-registry <URL>`                 | Show one staged npm package version.                                   |
| `lpm stage download` | `<stage-id>` | `--npm-registry <URL>`                 | Download the staged tarball for inspection.                            |
| `lpm stage approve`  | `<stage-id>` | `--otp <CODE>`, `--npm-registry <URL>` | Promote the staged version to live npm.                                |
| `lpm stage reject`   | `<stage-id>` | `--otp <CODE>`, `--npm-registry <URL>` | Reject the staged version.                                             |

Plus the [global flags](/docs/commands#global-flags). Use global `--json` for structured output.

## See also [#see-also]

* [`lpm publish`](/docs/packages/publish) - normal publish flow and provenance details
* [Authentication](/docs/infra/authentication) - npm tokens and login flows
* [CI/CD setup](/docs/guides/ci-cd-setup) - OIDC and provenance in CI
* [`lpm quality`](/docs/packages/quality) - inspect quality score before publishing


# Swift Package Registry (SE-0292) (/docs/packages/swift-package-registry)



LPM.dev Registry implements the [SE-0292 Swift Package Registry API](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md). [`lpm install`](/docs/packages/install) configures Swift Package Manager (SPM) when needed, then resolves LPM.dev Registry-hosted Swift packages natively — no manual fork-as-git-dep and no separate setup command for the normal install flow.

This page is the conceptual reference. For the user-facing setup walkthrough, see [Using LPM CLI with Swift](/docs/guides/using-with-swift).

## What SE-0292 specifies [#what-se-0292-specifies]

SE-0292 defines a REST API for Swift package registries that SPM can talk to in lieu of a git URL. It covers:

* Listing versions for a package (`GET /{scope}/{name}`)
* Fetching a version's source archive (`GET /{scope}/{name}/{version}.zip`)
* Fetching a version's `Package.swift` manifest (`GET /{scope}/{name}/{version}/Package.swift`)
* Login (`POST /login`)
* Optional package signing via [CMS](https://datatracker.ietf.org/doc/html/rfc5652)

LPM.dev Registry implements all of the above plus signature verification metadata. SPM clients see a standard SE-0292 server.

## Identity mapping [#identity-mapping]

LPM.dev Registry packages are named `@lpm.dev/owner.pkg-name`. SE-0292 scopes can't contain `.`, so LPM.dev Registry maps:

| LPM.dev Registry name        | SE-0292 identity           |
| ---------------------------- | -------------------------- |
| `@lpm.dev/owner.pkg-name`    | `lpmdev.owner_pkg-name`    |
| `@lpm.dev/myorg.swift-utils` | `lpmdev.myorg_swift-utils` |

Rules:

* The LPM.dev Registry scope `@lpm.dev/` becomes the SE-0292 scope `lpmdev` (no dots, no `@`).
* The `_` between owner and package name is the unambiguous separator. LPM.dev Registry forbids `_` in both owner and package name (enforced at the storage layer), so the boundary is always clear even when either half contains hyphens. `@lpm.dev/swift-server.async-http-client` and `@lpm.dev/swift.server-async-http-client` map to two distinct identifiers (`lpmdev.swift-server_async-http-client` vs `lpmdev.swift_server-async-http-client`) — no ambiguity.

Identity translation is automatic — `lpm install @lpm.dev/owner.swift-pkg` writes the SE-0292 identity into `Package.swift`. You don't deal with the mapping by hand.

## Automatic setup during install [#automatic-setup-during-install]

```bash
lpm login
lpm install @lpm.dev/owner.swift-pkg
```

Before resolving the first Swift package in an unconfigured project, LPM CLI performs four setup steps. Scope, certificate, and signing-trust failures abort the install. Authentication setup warns when no token is available or SPM rejects the login, but LPM.dev Registry requests will return `401` until you sign in:

1. **Set the registry for the `lpmdev` scope.**
   ```bash
   swift package-registry set --scope lpmdev https://lpm.dev/api/swift-registry
   ```
2. **Log in.** Uses the LPM.dev Registry bearer token resolved through the standard LPM CLI session manager (`LPM_TOKEN` → keychain → OIDC → none).
3. **Install the signing certificate.** Downloads the CMS signing cert (DER) from `https://lpm.dev/api/swift-registry/certificate` and writes it to SPM's trust store at `~/.swiftpm/security/trusted-root-certs/lpm.der`. The DER body is size-checked before being written; an empty / truncated / HTML-error response aborts instead of leaving a broken cert on disk.
4. **Configure the SPM signing trust policy.** Writes `~/.swiftpm/configuration/registries.json` with a default `signing.onUnsigned = "warn"` + `signing.onUntrustedCertificate = "warn"` policy and a scope-specific override pinning `lpmdev` to `signing.onUntrustedCertificate = "silentAllow"` — see [Trust model](#trust-model) for the rationale.

[`lpm swift-registry`](/docs/packages/swift-registry) exposes this setup explicitly for repair and certificate rotation. Its `--force` flag re-downloads the certificate even when a valid file is present. A failed forced refresh is fatal rather than silently retaining the stale certificate.

## Trust model [#trust-model]

What the signature buys you, and what it doesn't — be precise about both:

**Current posture: detached package integrity + HTTPS transport authenticity.**

* **Detached package integrity.** SPM verifies the CMS signature against the source archive bytes on every install. A tampered tarball fails the integrity check before any code runs.
* **Transport authenticity via HTTPS.** The LPM.dev Registry origin is the trust anchor for "this came from LPM.dev Registry" — the system CA chain authenticates the connection that carried both the signing cert and the tarballs themselves.

**Not current posture: trusted signer identity verification.**

LPM.dev Registry's signing cert is self-signed, non-CA, and code-signing-only. SPM's root trust store rejects it (root certs must have basicConstraints `CA=true`). The `silentAllow` scope override in step 4 tells SPM to accept the signature without a chain to a system trust root for the `lpmdev` scope. The cryptographic CMS check still runs; what's bypassed is the "is this signer in my trust chain?" gate.

The signer today is LPM.dev Registry, not the package author — the signature attests "this came from LPM.dev Registry," not "this came from author X." Per-author attribution (Sigstore, transparency log, whatever shape it takes) is a deliberately separate, future track. This page will be updated when that lands; until then, treat any "signed by author X" mental model as not implemented.

## Installing a Swift package [#installing-a-swift-package]

```bash
lpm install @lpm.dev/owner.swift-pkg
lpm install @lpm.dev/owner.swift-pkg@1.2.0
```

Behind the scenes:

1. LPM CLI resolves the version against the SE-0292 endpoint.
2. Selects an eligible non-test target and updates `Package.swift` — appends the dependency into `dependencies:` and wires the product into that target's `targets:` array.
3. Triggers `swift package resolve` so SPM downloads the source archive, verifies the signature, and writes `Package.resolved`.

One eligible target is selected automatically. With multiple eligible targets, a normal install opens the selector; `-y` / `--yes` skips the prompt and selects the first eligible target, matching the selector's default.

> Use [`lpm install`](/docs/packages/install) for Swift, **not** `lpm add`. `lpm install` is the Registry-resolved SPM dependency path; `lpm add` is the legacy source-delivery path.

## Publishing a Swift package [#publishing-a-swift-package]

```bash
lpm publish
```

Detected as a Swift package when the repository has `Package.swift` at its root. The publish pipeline does the standard pack, secret scan, quality pass, and upload, plus three Swift-specific steps:

1. **Generate the SE-0292 source archive.** Wraps the pack output into a `{pkgName}-{version}/` top-level directory and creates the `.zip` SPM expects. The wrapper is a hard SE-0292 requirement (SPM uses `stripFirstLevel` semantics on the archive).
2. **Compute and record the SHA-256 checksum.** SPM verifies this on download. The hex is stored in the release metadata; the base64 form is sent in the `Digest` HTTP response header on download.
3. **Sign the package.** Generates a CMS-1.0.0 detached signature (ECDSA P-256 / SHA-256) using the LPM.dev Registry signing key. The signature is base64-encoded into release metadata and surfaced in three places per release:
   * The release metadata JSON (base64 CMS string)
   * The `Digest` HTTP response header on download
   * A comment block in the `Package.swift` manifest response

## Verifying signatures [#verifying-signatures]

SPM verifies the CMS detached signature on every package archive at install time. LPM CLI configures the certificate and signing policy during the normal install flow. Use `lpm swift-registry --force` only when a documented certificate rotation or local corruption requires an explicit refresh.

Two failure shapes a user can run into:

* **`onUnsigned: warn`** — a Swift package without a signature surfaces as a warning, not a hard error. LPM.dev Registry-published packages are always signed, so this typically only appears for packages outside LPM.dev Registry that SPM resolves alongside.
* **Cert missing or out of date** — SPM emits a "signer is not trusted" error. Re-run `lpm swift-registry` (or `lpm swift-registry --force` if you suspect a stale cert) to refresh both the cert file and the registries.json scope override.

If the cert rotates server-side, re-run `lpm swift-registry --force`. A failed `--force` re-download is fatal — `lpm` will not silently keep using the stale cert.

## Wire format details [#wire-format-details]

For the curious — the things SPM expects from the server side:

| Endpoint                                      | Returns                                                                              |
| --------------------------------------------- | ------------------------------------------------------------------------------------ |
| `GET /{scope}/{name}`                         | List of versions (JSON)                                                              |
| `GET /{scope}/{name}/{version}`               | Release metadata (JSON) — signature, checksum, dependencies                          |
| `GET /{scope}/{name}/{version}.zip`           | Source archive with `{name}-{version}/` top level, `Digest: SHA-256={base64}` header |
| `GET /{scope}/{name}/{version}/Package.swift` | Manifest with signature in a comment block                                           |
| `POST /login`                                 | Auth flow configured automatically during `lpm install`                              |
| `GET /certificate`                            | Public cert in DER format                                                            |

Implementation lives in the LPM.dev Registry origin server and registry worker. LPM CLI is a client of this API; it doesn't implement the server side itself.

## Limitations [#limitations]

* Automatic and explicit Swift Registry setup configure the `lpmdev` scope only. Custom-scope publishing (for example, `acme` for a private LPM.dev Registry tenant) is not surfaced in LPM CLI.
* Cert rotation is manual — no automatic refresh on schedule. The signing cert is long-lived; rotation events will be communicated via release notes.
* The SE-0292 `--allow-insecure-http` flag is honored only for `http://` registry URLs (i.e., local dev against `http://localhost`). The hosted LPM.dev Registry endpoint is HTTPS-only.

## See also [#see-also]

* [Using LPM CLI with Swift](/docs/guides/using-with-swift) — setup walkthrough + publish flow
* [`lpm swift-registry`](/docs/packages/swift-registry) — repair and certificate-refresh reference
* [`lpm install`](/docs/packages/install) — the right command for SPM deps
* [SE-0292 spec](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md) — the upstream protocol


# lpm swift-registry (/docs/packages/swift-registry)



```bash
lpm swift-registry              # repair or explicitly rerun setup
lpm swift-registry --force      # repair stale/corrupt state or refresh the certificate
```

Most users do not need to run this command before installing a package. [`lpm install`](/docs/packages/install) automatically configures Swift Package Manager (SPM) when it adds a Swift package from LPM.dev Registry.

Use `lpm swift-registry` when setup needs to be repaired or rerun explicitly. Use `--force` after a documented certificate rotation or when Registry configuration or the local certificate may be stale or corrupt. Sign in with [`lpm login`](/docs/infra/login) before resolving a Swift package; LPM.dev Registry requires authentication for every Swift package request.

## Normal install flow [#normal-install-flow]

```bash
lpm login
lpm install @lpm.dev/owner.swift-pkg
```

On the first install, LPM CLI sets the `lpmdev` Registry scope and installs the signing certificate. It also configures signing trust, updates `Package.swift`, and resolves the dependency.

During each HTTPS setup pass, LPM CLI signs in to SPM with the current Registry session. It does this even when the Registry scope already matches.

LPM CLI accepts the global `--token <TOKEN>` flag for explicit setup and automatic setup during install. It can also refresh an expired browser session before it calls SPM.

## What it does [#what-it-does]

Four setup steps. Scope, SPM login, certificate, and signing-trust failures abort an explicit run. A failed session refresh also aborts the command:

1. **Set the registry for the `lpmdev` scope.** Runs `swift package-registry set --scope lpmdev <registry-url>/api/swift-registry`. Adds `--allow-insecure-http` only for `http://` registry URLs (local dev); the hosted LPM.dev Registry endpoint is HTTPS-only.
2. **Log in.** Resolves the LPM.dev Registry bearer token through the standard LPM CLI session. It writes the token to a temporary file and calls `swift package-registry login --token-file <path> --no-confirm`. On Unix, the file mode is `0600`. LPM CLI removes the file after SPM exits, so the bearer does not appear in process arguments. This step runs only for HTTPS because SPM refuses to send authentication over HTTP. If no token is found, this step warns and is skipped. Setup can finish, but every Swift package request returns `401` until you run `lpm login`.
3. **Install and verify the signing certificate.** Downloads the CMS signing cert from `<registry-url>/api/swift-registry/certificate` with a bounded response reader, parses exactly one complete DER X.509 object, and compares its bytes with `~/.swiftpm/security/trusted-root-certs/lpm.der`. Missing, malformed, trailing-data, stale, or mismatched local bytes are atomically repaired; malformed or oversized Registry responses abort.
4. **Configure the SPM signing trust policy.** Writes `~/.swiftpm/configuration/registries.json` with a default `signing.onUnsigned = "warn"` + `signing.onUntrustedCertificate = "warn"` policy and a scope-specific override pinning `lpmdev` to `signing.onUntrustedCertificate = "silentAllow"`. See [Trust model](/docs/packages/swift-package-registry#trust-model) for the rationale.

The configuration operation is idempotent. LPM CLI retains matching scope, certificate, and trust configuration without a rewrite. It repairs stale or partial configuration with atomic file writes.

Automatic setup skips SPM login when no credential exists. If a credential exists, a refresh or login failure stops the install. A rejected refresh-backed bearer causes one session refresh and one login retry.

The existing `~/.swiftpm/configuration/registries.json` file has a 16 MiB limit enforced before JSON parsing. Missing retains the setup command's normal create behavior; an oversized file fails with its path and byte limit instead of being treated as absent or overwritten. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

## `--force` [#--force]

```bash
lpm swift-registry --force
```

Re-runs setup and re-downloads the signing certificate even when a valid one is on disk. Use it to repair stale or corrupt Registry configuration, or after a documented certificate rotation. A failed `--force` re-download is **fatal** even if a stale cert is still on disk — silently keeping the stale cert would defeat the explicit rotation the user asked for.

## JSON output [#json-output]

```bash
lpm swift-registry --json
```

Emits a structured envelope describing what was configured:

| Field                                                           | Meaning                                                                                           |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `registry_url`                                                  | The `<base>/api/swift-registry` URL the `lpmdev` scope now points at                              |
| `scope`                                                         | Always `"lpmdev"` today                                                                           |
| `https`                                                         | `true` when the registry URL is HTTPS; `false` for local-dev `http://`                            |
| `signing_certificate_installed` / `signing_certificate_outcome` | `"installed"` (new write) or `"already_installed"` (idempotent no-op)                             |
| `signing_trust_configured` / `signing_trust_outcome`            | `"configured"` or `"already_configured"`                                                          |
| `trust_anchor`                                                  | Always `"https"` — the trust anchor is the HTTPS connection to LPM.dev Registry, not a cert chain |
| `signer_trust_policy`                                           | Always `"silentAllow"` — see [Trust model](/docs/packages/swift-package-registry#trust-model)     |

## Flags [#flags]

| Flag      | Effect                                                               |
| --------- | -------------------------------------------------------------------- |
| `--force` | Re-download the signing certificate even when a valid one is on disk |

Plus the [global flags](/docs/commands#global-flags) — `--json` is documented above; `--registry <URL>` redirects the configured base URL.

## Prerequisites [#prerequisites]

* `swift` on `PATH` (steps 1 and 2 spawn it). Without Swift installed, `lpm swift-registry` errors with `Is Swift installed?` and exits.
* An LPM CLI session (`lpm login`). LPM.dev Registry requires a valid token for all Swift package resolution, including packages that would otherwise be public.

## See also [#see-also]

* [Swift Package Registry (SE-0292)](/docs/packages/swift-package-registry) — design overview, identity mapping, trust model, wire format
* [Using LPM CLI with Swift](/docs/guides/using-with-swift) — end-to-end setup + publish walkthrough
* [`lpm install`](/docs/packages/install) — the normal command for Swift dependencies; it configures SPM when needed
* [`lpm publish`](/docs/packages/publish) — publishes Swift packages with CMS signatures


# lpm tidy (/docs/packages/tidy)



```bash
lpm tidy [--fix]
```

`lpm tidy` scans the current package for two dependency hygiene issues:

* declared dependencies that are not imported, referenced by scripts, or found in common config files
* phantom imports: source imports for packages that are not declared in `package.json`

By default it only reports findings. It does not rewrite `package.json`, `lpm.lock`, or `node_modules`.

## Examples [#examples]

```bash
lpm tidy
lpm tidy --json
lpm tidy --fix
```

Use `lpm tidy` in CI when dependency drift should fail the build. Findings exit `1`; a clean package exits `0`.

## What `--fix` changes [#what---fix-changes]

`--fix` removes unused entries from:

* `dependencies`
* `devDependencies`
* `optionalDependencies`

After editing `package.json`, LPM CLI reconciles `lpm.lock`, `lpm.lockb`, and `node_modules` through the normal install and uninstall cleanup paths.

`peerDependencies` are report-only. They may describe a library contract for consumers even when the package does not import the peer itself.

Phantom imports are also report-only. `lpm tidy --fix` does not auto-add dependencies, because adding a package needs an explicit version/range and save-policy decision.

## What counts as used [#what-counts-as-used]

A dependency is treated as used when LPM CLI sees it in any of these places:

* source imports from `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, or `.cjs` files
* package scripts, including installed `bin` names when `node_modules` is present
* common tool config files such as `eslint.config.js`, `vite.config.ts`, `tsconfig.json`, `jest.config.js`, `tailwind.config.js`, and `webpack.config.js`
* matching TypeScript declaration packages, such as `@types/react` for `react`

Generated directories, nested packages, `node_modules`, build output, and local import aliases are skipped by the same source scanner used by install-time phantom dependency warnings.

## Ignore config [#ignore-config]

Use `[tidy]` in `lpm.toml` when a dependency is intentionally indirect or loaded by a framework convention the scanner cannot see.

```toml title="lpm.toml"
[tidy]
ignore-unused = ["eslint-config-next", "@types/node"]
ignore-phantom = ["virtual:*"]
ignore-paths = ["generated/**", "fixtures/**"]
```

| Key              | Type                   | Effect                                                     |
| ---------------- | ---------------------- | ---------------------------------------------------------- |
| `ignore-unused`  | string or string array | Suppress unused-dependency findings by package-name glob   |
| `ignore-phantom` | string or string array | Suppress phantom-import findings by package-name glob      |
| `ignore-paths`   | string or string array | Ignore source imports from matching project-relative paths |

## JSON output [#json-output]

```bash
lpm tidy --json
```

```json
{
  "success": false,
  "fixed": false,
  "manifest": "package.json",
  "counts": {
    "declared": 3,
    "imported": 2,
    "unused": 1,
    "phantoms": 1,
    "ignored": 0,
    "removed": 0,
    "remaining": 2
  },
  "unused": [
    {
      "name": "lodash",
      "section": "dependencies",
      "spec": "^4.17.21",
      "fixable": true,
      "reason": "not imported, referenced by scripts, or found in known config files"
    }
  ],
  "phantoms": [
    {
      "name": "left-pad",
      "file": "src/index.js",
      "line": 2,
      "import_count": 1
    }
  ],
  "removed": [],
  "ignored": [],
  "elapsed_ms": 8
}
```

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — install and lockfile reconciliation
* [`lpm uninstall`](/docs/packages/uninstall) — explicit dependency removal
* [`lpm audit`](/docs/packages/audit) — vulnerability and behavior findings
* [`lpm.toml`](/docs/reference/lpm-toml) — project-level tidy ignore config


# lpm trust (/docs/packages/trust)



```bash
lpm trust diff [--assert-none] [--json]
lpm trust prune [--dry-run] [--yes] [--json]
lpm trust lifecycle-scope add <SCOPE> [--json]
lpm trust lifecycle-scope remove <SCOPE> [--json]
lpm trust lifecycle-scope list [--json]
lpm trust release-age-exclude add <SELECTOR> [--json]
lpm trust release-age-exclude remove <SELECTOR> [--json]
lpm trust release-age-exclude list [--json]
```

`lpm trust` manages security decisions that belong to the current project. These decisions stay in `package.json` so that the project can share them.

For lifecycle scripts, `lpm trust` manages project scope trust. It also inspects and cleans `trustedDependencies`.

Use [`lpm approve-scripts`](/docs/packages/approve-scripts) for strict package approvals. These approvals include the version, integrity, script hash, and requested capabilities.

For the release-age gate, `lpm trust` manages `minimumReleaseAgeExclude`. These selectors exempt selected packages from the project cooldown.

**Project trust only.** Global lifecycle trust uses `~/.lpm/global/trusted-dependencies.json`. Manage it with [`lpm approve-scripts --global`](/docs/packages/approve-scripts#global-vs-project).

User release-age exclusions use `~/.lpm/config.toml`. Manage them with [`lpm config release-age-exclude`](#user-release-age-exclusions).

## Storage contract [#storage-contract]

LPM stores each exception or approval in one policy layer. A command does not copy a policy entry into another file.

| Decision                        | Command                                 | Persistent location                                |
| ------------------------------- | --------------------------------------- | -------------------------------------------------- |
| Project release-age exclusion   | `lpm trust release-age-exclude`         | `package.json > lpm > minimumReleaseAgeExclude`    |
| Project lifecycle scope         | `lpm trust lifecycle-scope`             | `package.json > lpm > scripts > trustedScopes`     |
| Project package approval        | `lpm approve-scripts`                   | `package.json > lpm > trustedDependencies`         |
| User release-age exclusion      | `lpm config release-age-exclude`        | `~/.lpm/config.toml > minimum-release-age-exclude` |
| Global-install package approval | `lpm approve-scripts --global`          | `~/.lpm/global/trusted-dependencies.json`          |
| One-run release-age exclusion   | `lpm install --min-release-age-exclude` | None                                               |

`lpm.lock` never stores release-age exclusions, lifecycle scopes, or lifecycle approvals. `lpm trust prune` reads the lockfile only to find stale project approvals.

Each command writes one declared policy layer. It does not cascade through `package.json`, user configuration, global trust, or the lockfile.

Project commands use the `package.json` in the current directory. In a workspace member, the member manifest receives the policy entry.

The workspace root and sibling manifests stay unchanged. User and global commands continue to use their fixed locations under `~/.lpm`.

List commands read only their declared layer. They do not show the effective merged release-age list.

During an install, release-age exclusions merge in this order: CLI entries, project entries, then user entries. Duplicate selectors collapse.

An exact-version selector matches only that version. A scope wildcard matches every package in that scope.

A lifecycle scope addition also requires local signed authorization under `~/.lpm/security`. This authorization approves the write and is not another trust list.

Mutating policy commands use atomic file replacement. A duplicate addition does not rewrite a normalized destination file.

## Subcommands [#subcommands]

| Subcommand                                              | What it does                                                                        |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| [`diff`](#lpm-trust-diff)                               | Show how the current `trustedDependencies` differs from the last install's snapshot |
| [`prune`](#lpm-trust-prune)                             | Remove entries whose package is no longer in the resolved tree                      |
| [`lifecycle-scope`](#lpm-trust-lifecycle-scope)         | Add, remove, or list project lifecycle scope trust                                  |
| [`release-age-exclude`](#lpm-trust-release-age-exclude) | Add, remove, or list project release-age exclusions                                 |

## Flag Reference [#flag-reference]

| Command                         | Flag            | Effect                                                                                     |
| ------------------------------- | --------------- | ------------------------------------------------------------------------------------------ |
| `lpm trust diff`                | `--json`        | Emit the diff envelope instead of human output. Same as global `--json` for this command.  |
| `lpm trust diff`                | `--assert-none` | Exit non-zero if any added, removed, or changed entries are present.                       |
| `lpm trust prune`               | `-y`, `--yes`   | Skip the confirmation prompt. Required on non-TTY / CI when pruning would write.           |
| `lpm trust prune`               | `--dry-run`     | Preview stale entries without writing `package.json`.                                      |
| `lpm trust prune`               | `--json`        | Emit the prune envelope instead of human output. Same as global `--json` for this command. |
| `lpm trust lifecycle-scope`     | `--json`        | Emit the lifecycle scope envelope instead of human output.                                 |
| `lpm trust release-age-exclude` | `--json`        | Emit the release-age exclusion envelope instead of human output.                           |

## `lpm trust lifecycle-scope` [#lpm-trust-lifecycle-scope]

```bash
lpm trust lifecycle-scope add '@company/*'
lpm trust lifecycle-scope remove '@company/*'
lpm trust lifecycle-scope list
```

A lifecycle scope trusts every package in one npm scope. The trust applies to all package versions and script contents.

Use this command only when your organization controls the complete scope. Use [`lpm approve-scripts`](/docs/packages/approve-scripts) for one package.

The command accepts lowercase npm scope wildcards such as `@company/*`. It rejects package names, package versions, uppercase scopes, and nested patterns.

`add` writes `package.json > lpm > scripts > trustedScopes`. It preserves all other fields in `package.json`.

An addition widens the project security policy. In an interactive terminal, LPM CLI requests native user approval before it writes the scope.

For an agent or JSON command, create a signed project unlock before you add the scope:

```bash
lpm security unlock trust-scope-widen --project . --ttl 10m
lpm trust lifecycle-scope add '@company/*' --json
```

The unlock is local to the project and user. The project scope list remains in `package.json` for the team.

The signed authorization remains under `~/.lpm/security`. LPM CLI does not write lifecycle scope trust to `lpm.lock`.

`remove` narrows the security policy and does not need an unlock. It removes the signed authorization for that scope.

It also revokes `trust-scope-widen` from each active temporary unlock for the project. Other scopes in the same unlock remain active.

As a result, a later `add` requires new authorization. Removal matches the complete scope selector.

`list` shows the configured project scopes. It does not show strict package approvals or global lifecycle trust.

The JSON envelope contains `success`, `schema_version`, `command`, `scope`, `action`, `changed`, `normalized`, `count`, and `scopes`. The `add` and `remove` actions also contain `selector`.

`changed` reports whether the requested scope changed. `normalized` reports whether the command repaired existing stored entries. `count` is the number of scopes.

## `lpm trust release-age-exclude` [#lpm-trust-release-age-exclude]

```bash
lpm trust release-age-exclude add react
lpm trust release-age-exclude add '@company/*'
lpm trust release-age-exclude add react@19.1.0
lpm trust release-age-exclude remove react@19.1.0
lpm trust release-age-exclude list
```

The command writes `package.json > lpm > minimumReleaseAgeExclude`. It preserves the other fields in `package.json`.

The command accepts these selector types:

| Selector       | Effect                                        |
| -------------- | --------------------------------------------- |
| `react`        | Exclude all versions of `react`.              |
| `@company/*`   | Exclude all packages in the `@company` scope. |
| `react@19.1.0` | Exclude only version `19.1.0` of `react`.     |

Version selectors must contain one exact semantic version. Ranges such as `react@^19` are invalid.

`add` is idempotent. If the selector exists in a normalized list, the command does not rewrite `package.json`.

If stored entries need normalization, the command repairs the list. It returns `changed: false` and `normalized: true` when the selector stays unchanged.

`remove` matches the complete selector. For example, removal of `react@19.1.0` does not remove the `react` selector.

`list` shows only the project list. It does not include the user list or one-run CLI exclusions.

The JSON envelope contains `success`, `schema_version`, `command`, `scope`, `action`, `changed`, `normalized`, `count`, and `exclusions`. The `add` and `remove` actions also contain `selector`.

`changed` reports whether the requested selector changed. `normalized` reports whether the command repaired existing stored entries. `count` is the number of exclusions.

### User release-age exclusions [#user-release-age-exclusions]

Use `lpm config` to apply an exclusion to all installs for the current user:

```bash
lpm config release-age-exclude add '@company/*'
lpm config release-age-exclude remove '@company/*'
lpm config release-age-exclude list
```

The user command writes `~/.lpm/config.toml > minimum-release-age-exclude`. Project and user lists merge during an install.

## `lpm trust diff` [#lpm-trust-diff]

```bash
lpm trust diff
lpm trust diff --assert-none
lpm trust diff --json
lpm trust diff --assert-none --json
```

Read-only. Compares the current `package.json > lpm > trustedDependencies` against the snapshot LPM CLI took on the last `lpm install`. Surfaces three classes of change:

* **Additions** — entries that weren't there last install. Potential silent PR poisoning vector ("someone added `evil-package` to the trust list — was that reviewed?")
* **Removals** — entries that were there but are now gone. Usually intentional cleanup, but worth a glance.
* **Same-key binding changes** — same `name@version` key, but a different `integrity` or `scriptHash`. Indicates the package contents changed without a version bump (re-publish, tampering).

The snapshot lives at `<project>/.lpm/trust-snapshot.json`, written by every successful `lpm install`. If the project has never been installed with LPM CLI, `diff` reports `"no prior snapshot"` and exits cleanly — there's no baseline to compare against yet.

`trust diff` is informational by default — it prints whatever it found. For a built-in CI gate, use `--assert-none`:

```bash
lpm trust diff --assert-none
```

That exits non-zero when any `added`, `removed`, or `changed` entry is present.

If you still want machine-readable output for bots or logs, `--json` works with the same gate:

```bash
lpm trust diff --json --assert-none
```

The JSON envelope: `success`, `schema_version`, `command`, `assertion_failed`, `diff_count`, `snapshot_captured_at`, `current_binding_count`, `added[]`, `removed[]`, `changed[]`. Each entry carries `key` (e.g., `esbuild@0.25.1`), `previous`, and `current` (the integrity / scriptHash before and after). When `--assert-none` fails in JSON mode, LPM CLI emits this same single diff envelope with `success: false`, `error_code: "trust_diff_assert_none"`, and exits non-zero.

| Flag            | Effect                                         |
| --------------- | ---------------------------------------------- |
| `--json`        | Machine-readable output for CI gating via `jq` |
| `--assert-none` | Exit non-zero if any diff entries are present  |

## `lpm trust prune` [#lpm-trust-prune]

```bash
lpm trust prune                   # interactive — prompts before writing
lpm trust prune --dry-run         # preview, don't write
lpm trust prune -y                # skip the prompt (required on non-TTY / CI)
lpm trust prune --json
```

Removes `trustedDependencies` entries whose package isn't in the resolved tree anymore. "No longer in the tree" means: `lpm.lock` has zero entries with this name (regardless of version). Per-version drift on a name that's still installed is **not** considered stale — that's drift, handled by the install-time gate, not by prune.

Entries become stale when:

* A dep was removed from `package.json` but its trust entry stayed
* A dep was renamed (the old name's pin is now orphaned)
* A dep was moved (e.g., from `dependencies` to `optionalDependencies` and never reinstalled)

`lpm trust prune` reads `lpm.lock` to determine what's installed. **If the lockfile is missing**, the command exits with `no lpm.lock found — run \`lpm install\` before pruning trust entries\` — refusing to prune from an empty installed-set (which would mark everything stale).

The interactive prompt confirms the removal list before writing. In CI / non-TTY contexts, `-y` is required — without it, prune errors with `lpm trust prune needs a TTY for confirmation. Pass \`--yes\` to ...\`.

`--dry-run` runs the analysis normally but doesn't touch `package.json`.

The JSON envelope: `success`, `schema_version`, `command`, `dry_run`, `mutated` (the actual final state — `false` on `--dry-run` or no-op), `stale_count`, `stale[]` (the keys removed).

| Flag          | Effect                                             |
| ------------- | -------------------------------------------------- |
| `-y`, `--yes` | Skip the confirmation prompt (required on non-TTY) |
| `--dry-run`   | Preview without writing                            |
| `--json`      | Machine-readable output                            |

## CI usage [#ci-usage]

```yaml
- run: lpm install --offline
- run: lpm trust diff --assert-none
- run: lpm trust prune --dry-run --json # informational — would prune anything?
```

`trust diff --assert-none` is the built-in drift gate: fail the build if `trustedDependencies` changed since the last reviewed install. Add `--json` if you also want structured output in CI logs.

`prune --dry-run` is for hygiene reports only — it always exits zero. Gate it the same way if you want to fail on stale entries:

```bash
lpm trust prune --dry-run --json | jq -e '.stale_count == 0'
```

## See also [#see-also]

* [`lpm approve-scripts`](/docs/packages/approve-scripts) — add entries to the trust list
* [`lpm rebuild`](/docs/packages/rebuild) — runs dependency scripts for trusted packages
* [`package.json` "lpm.scripts"](/docs/reference/package-json-lpm#the-lpmscripts-block) — project lifecycle settings
* [`package.json` "lpm.trustedDependencies"](/docs/reference/package-json-lpm#trusteddependencies) — the file format
* [`package.json` "lpm.minimumReleaseAgeExclude"](/docs/reference/package-json-lpm#minimumreleaseageexclude) — project release-age exclusions
* [`config.toml`](/docs/reference/config-toml) — user release-age exclusions


# lpm uninstall (/docs/packages/uninstall)



```bash
lpm uninstall <packages...>   # aliases: lpm un, lpm unlink
```

Drops one or more packages from `package.json`, removes them from `node_modules`, removes any local `node_modules/.bin` shims owned by those packages, and rewrites `lpm.lock` plus `lpm.lockb` when the post-uninstall graph fits the binary format. When a removed package makes transitive packages unreachable, LPM CLI prunes those orphaned entries from both the lockfile and `node_modules`, removes their owned bin shims as well, then cleans up empty scoped-package directories such as `node_modules/@scope/`.

`lpm uninstall` only touches `dependencies` and `devDependencies&#x60;. **`peerDependencies`, `optionalDependencies`, and `lpm > trustedDependencies` are intentionally left alone** — a peer-dep declaration is a contract with consumers and shouldn't disappear when the underlying package is removed; trust entries persist so that re-adding the same `name@version` doesn't silently re-open the script-trust review. Clean stale trust entries with [`lpm trust prune`](/docs/packages/trust#lpm-trust-prune).

To remove a globally-installed CLI, pass `-g`. To remove source files added with [`lpm add`](/docs/packages/add), use [`lpm remove`](/docs/packages/remove).

Human output reports the direct removals and any orphaned transitives:

```text
- @scope/root@1.0.0
- @scope/leaf@1.0.0 (orphaned)
✓ Cleaned empty directories
✓ Freed 42 B on disk
✓ Done · removed 1 package in 42ms
```

## Examples [#examples]

```bash
lpm uninstall lodash
lpm un react react-dom            # multiple at once
lpm uninstall -g typescript       # remove a global install
lpm uninstall lodash --filter web # workspace member only
lpm uninstall -w eslint           # remove from workspace root
```

## Workspaces [#workspaces]

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

```bash
lpm uninstall lodash --filter web   # remove from packages/web/
lpm uninstall lodash --filter-prod ...web # prod dependency closure only
lpm uninstall -w eslint             # remove from the root package.json
```

`--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 uninstall would mutate more than one member's `package.json`, LPM CLI prompts for confirmation; pass `-y` to skip the prompt.

See [Workspaces](/docs/packages/workspaces) for filter grammar.

## Global [#global]

```bash
lpm uninstall -g eslint
# Equivalent to: lpm global remove eslint
```

`--global` is mutually exclusive with filters / `-w` / `--fail-if-no-match` (those are project-scoped). Both invocations route through the same global-uninstall implementation.

## Flags [#flags]

| Flag                                    | Effect                                                                                            |
| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `-g`, `--global`                        | Remove a globally-installed package from `~/.lpm/global/`                                         |
| `--filter <EXPR>`                       | Workspace filter (mutually exclusive with `-w` and `-g`)                                          |
| `--filter-prod <EXPR>`                  | Workspace filter with production-only dependency closures (mutually exclusive with `-w` and `-g`) |
| `--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 workspace root `package.json`                                                          |
| `--fail-if-no-match`                    | Exit non-zero if filters match no members                                                         |
| `-y`, `--yes`                           | Skip the multi-member confirmation prompt                                                         |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — the forward direction
* [`lpm remove`](/docs/packages/remove) — undo an `lpm add` (different command)
* [`lpm global`](/docs/packages/global) — manage globally-installed CLIs
* [Workspaces](/docs/packages/workspaces)


# lpm upgrade (/docs/packages/upgrade)



```bash
lpm upgrade [package...]
```

Walks `package.json` `dependencies` and `devDependencies`, looks up the newest installable version for each eligible package, and updates the lockfile + `node_modules` accordingly. By default it stays within your declared semver — pass `--major` to bump across breaking-change boundaries.

**Default scope: `@lpm.dev/*` plus npm packages already recorded as public npm or LPM.dev Registry-proxy installs in `lpm.lock`.** That means a normal `react`, `lodash`, or `@scope/pkg` install that currently resolves through `registry.npmjs.org` or the configured LPM.dev Registry proxy is eligible for `lpm upgrade` too. Packages without recorded public npm or LPM.dev Registry-proxy source attribution are skipped by unscoped `lpm upgrade` instead of being queried against `registry.npmjs.org` — this avoids leaking private/custom-registry package names. If you added a public npm package before LPM CLI recorded source metadata, run [`lpm install`](/docs/packages/install) once to refresh `lpm.lock`, then rerun `lpm upgrade`.

Pass package names to target only those direct manifest dependencies:

```bash
lpm upgrade zod
lpm upgrade zod react --dry-run --json
```

Targeted upgrade filters candidates before metadata fetch. In a TTY, the multiselect only shows the requested packages; in CI/non-TTY, only those packages are upgraded. A requested package that is not in `dependencies` or `devDependencies` is a hard error. A requested npm package with no recorded public npm or LPM.dev Registry-proxy source is also a hard error instead of a silent skip.

For npm alias declarations such as `"strip-ansi-cjs": "npm:strip-ansi@^6"`, target the local manifest key:

```bash
lpm upgrade strip-ansi-cjs
```

LPM CLI looks up metadata and lockfile source attribution through the canonical target (`strip-ansi`) while preserving the local key and `npm:strip-ansi@...` shape when it rewrites `package.json`.

In a TTY, `lpm upgrade` shows an interactive multiselect so you can pick which packages to bump. In CI or a non-TTY, it runs non-interactively. Override with `-i` (force interactive) or `-y` (force non-interactive).

The interactive multiselect annotates each candidate with the signals you'd want before saying yes: whether the upgrade target ships install scripts, whether any peer-dependency declaration shifts under your current lockfile, and whether an existing `lpm patch` for this package won't cleanly apply to the new version.

`lpm upgrade` honors the same [minimum release age](/docs/packages/install#recently-published-packages) policy for the direct dependencies it proposes. If the registry's latest version is still inside the cooldown window, upgrade selects the newest mature candidate at or below the current `dist-tags.latest` target instead of rewriting `package.json` to a version the install step would reject. This preserves a maintainer rollback even when an older release has a greater SemVer major.

Before it changes the project, `lpm upgrade` revalidates registry metadata and uses that metadata for the install. This shared snapshot keeps the selected version and integrity stable between planning and installation.

An implicit upgrade only selects a version that is newer than the installed version. If a registry rollback removes a newer release, `lpm upgrade` does not downgrade your project.

If `lpm.lock` exists but is malformed or unsupported, the command stops and reports the lockfile error. It does not plan from incomplete state.

## Examples [#examples]

```bash
lpm upgrade                # interactive at TTY, all-at-once in CI
lpm upgrade zod            # upgrade only zod
lpm upgrade zod react      # upgrade only these direct dependencies
lpm upgrade --dry-run      # show what would change
lpm upgrade --major        # also offer breaking upgrades
lpm upgrade -y --dry-run   # CI-friendly JSON/text preview of upgrade candidates
lpm upgrade -y             # non-interactive (skip prompts)
lpm upgrade -i             # interactive (even in non-TTY)
```

## Major upgrades [#major-upgrades]

```bash
lpm upgrade --major
```

In **non-interactive** mode, `--major` actually performs the major bump on every dep that has one. In **interactive** mode, major upgrades appear as separate rows in the multiselect — you toggle the ones you want.

After a `--major` run, review the changelog of every bumped package before shipping. LPM CLI never reads release notes for you.

## What changes [#what-changes]

* `package.json` — declared ranges are rewritten to match the new resolved versions, respecting your [save policy](/docs/packages/save-policy).
* `lpm.lock` — fully refreshed, with `lpm.lockb` refreshed when the graph fits the binary format.
* `node_modules/` — re-linked with the new versions.

If your `package.json` says `"react": "^18.0.0"` and the lockfile records `react` as coming from public npm or the configured LPM.dev Registry proxy, `lpm upgrade` will land `^18.3.1` when 18.3.1 is the newest mature matching 18.x release. With `--major` and a mature react 19 release available, it would also offer `^19.0.0`.

## Arguments [#arguments]

| Argument       | Effect                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| `<package...>` | Optional direct dependency names to upgrade. Names must already exist in `dependencies` or `devDependencies`. |

## Flags [#flags]

| Flag                  | Effect                                                                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--major`             | Allow upgrading to a higher major version. Mutually exclusive with interactive mode — in interactive mode, major-bumps appear as togglable rows instead |
| `--dry-run`           | Show what would be upgraded without making changes                                                                                                      |
| `-i`, `--interactive` | Force interactive mode even outside a TTY. Mutually exclusive with `-y` and `--json`                                                                    |
| `-y`, `--yes`         | Force non-interactive mode (useful at a TTY). Mutually exclusive with `-i`                                                                              |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm outdated`](/docs/packages/outdated) — see which deps have updates available
* [`lpm install`](/docs/packages/install) — reinstall after a manual `package.json` edit
* [Save policy](/docs/packages/save-policy) — how new ranges get saved


# lpm version (/docs/packages/version)



```bash
lpm version patch
```

Bumps the current package's `package.json` version. By default, LPM CLI writes the manifest, commits it, and creates a git tag just like `npm version`, but with stricter release safety: the git tree must be clean before commit/tag, and exact versions must move forward.

The built-in command wins over a package script named `version`. To run that script, use [`lpm run version`](/docs/dev/run).

## Examples [#examples]

```bash
lpm version patch                         # 1.2.3 -> 1.2.4, commit, tag v1.2.4
lpm version minor --message "release %s"  # commit message: release 1.3.0
lpm version prepatch                      # 1.2.3 -> 1.2.4-0
lpm version prerelease                    # 1.2.4-0 -> 1.2.4-1
lpm version 2.0.0                         # exact forward version
lpm version patch --no-git-tag-version    # edit package.json only
lpm version patch --dry-run --json        # print the plan without writing
```

## Bump values [#bump-values]

| Value        | Effect                                                                                     |
| ------------ | ------------------------------------------------------------------------------------------ |
| `patch`      | Stable patch bump. From a prerelease, releases the same version (`1.2.3-rc.1` -> `1.2.3`). |
| `minor`      | Next minor, patch reset to `0`.                                                            |
| `major`      | Next major, minor and patch reset to `0`.                                                  |
| `prepatch`   | Next patch prerelease (`1.2.3` -> `1.2.4-0`).                                              |
| `preminor`   | Next minor prerelease (`1.2.3` -> `1.3.0-0`).                                              |
| `premajor`   | Next major prerelease (`1.2.3` -> `2.0.0-0`).                                              |
| `prerelease` | Increment the prerelease number, or start the next patch prerelease from a stable version. |
| `<exact>`    | Set an exact semver version, if it is greater than the current version.                    |

## Git behavior [#git-behavior]

With the default git mode, `lpm version`:

1. verifies the current directory is inside a git worktree
2. verifies `git status --porcelain` is clean
3. verifies the target tag name is valid and does not already exist
4. writes `package.json`
5. runs `git add package.json`
6. commits with the message template
7. creates a tag

Use `--no-git-tag-version` when you want only the manifest edit. `--dry-run` prints the same version plan without writing, committing, or tagging.

The manifest write, Git commit, and tag form one recoverable operation. If a commit or tag fails, LPM CLI restores its manifest, index, commit, and tag changes. Changes that a Git hook creates remain available and are not discarded.

If the process stops, run the same command again. An identical retry recognizes a completed commit or tag and does not bump the package twice. An incomplete operation rolls back before the command makes a new plan.

Workspace members use the workspace transaction lock. The command refuses a changed workspace boundary or unrelated staged workspace files instead of committing them.

## Flags [#flags]

| Flag                    | Description                                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--dry-run`             | Print the plan without writing files, committing, or tagging.                                                       |
| `--json`                | Emit a structured plan with `success`, `dry_run`, `git_tag_version`, `commit`, `tag`, and `plan`.                   |
| `--no-git-tag-version`  | Update `package.json` only.                                                                                         |
| `--tag-prefix <PREFIX>` | Prefix for the git tag. Defaults to `v`; the resulting tag must be accepted by `git tag` and cannot start with `-`. |
| `-m, --message <TEXT>`  | Commit message. `%s` is replaced with the new version. Defaults to `v%s`.                                           |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm release`](/docs/packages/release) — workspace release planning and publish orchestration
* [`lpm publish`](/docs/packages/publish) — publish the package after a version bump
* [`lpm run`](/docs/dev/run) — run a package script named `version`


# Workspaces (/docs/packages/workspaces)



LPM CLI treats workspaces as a first-class concept. Declare them in `package.json`, then every command that touches the dep graph (`install`, `uninstall`, `run`, `test`, `bench`, `lint`, `fmt`, `check`, `filter`, `deploy`) accepts `--filter` / `--filter-prod`, `--all`, and `--affected` to scope work. Git-diff selectors can ignore noisy paths with `--changed-files-ignore-pattern` and classify test-only changes with `--test-pattern`. The resolver tracks cross-member edges via the `workspace:*` protocol, and `lpm deploy` produces a self-contained production tree for a single member.

This page covers the conceptual model. For a step-by-step setup walkthrough, see the [Monorepo setup guide](/docs/guides/monorepo-setup).

## Declaring workspaces [#declaring-workspaces]

Two equivalent shapes — both accepted in `package.json`:

```json title="package.json (array form)"
{ "workspaces": ["packages/*", "apps/*"] }
```

```json title="package.json (object form)"
{ "workspaces": { "packages": ["packages/*", "apps/*"] } }
```

Both globs match recursively under the project root. Each match is a workspace **member** — a directory with its own `package.json`, with its own dependencies, scripts, and version.

`pnpm-workspace.yaml` is read as a **fallback** when `package.json > workspaces` is absent. If both are present, `package.json` wins. Migrating from pnpm doesn't require deleting `pnpm-workspace.yaml` — though copying the globs into `package.json > workspaces` is the cleaner long-term shape. (See [Migrating from pnpm](/docs/guides/migrating-from-pnpm).)

Root and member `package.json` files and `pnpm-workspace.yaml` each have a 16 MiB limit enforced before parsing. A missing `pnpm-workspace.yaml` keeps the fallback behavior above; an oversized file fails before workspace discovery proceeds or the resolver contacts a registry. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

## Installing the workspace [#installing-the-workspace]

Run no-argument install from the workspace root:

```bash
lpm install
```

LPM CLI installs members in dependency order and installs the root last. On a cold recursive install, eligible members enter one shared union resolution, with shared expansion passes when an importer requires an additional ambient peer package; LPM CLI then projects an isolated dependency graph for each importer, preserving conflicting direct versions, aliases, optional reachability, peer bindings, overrides, and policy decisions. An importer that cannot be projected safely falls back to its own resolver pass without disabling the union for other members.

The workspace writes one authoritative `lpm.lock` at the root. Its content-addressed `workspace-packages` table stores each distinct locked package row once, while `importers` records the package IDs and root state visible to each member. Member commands read only their importer projection, so `lpm graph`, `lpm licenses`, `lpm sbom`, audit, upgrade, patch, uninstall, tidy, doctor, frozen installs, and ordinary member-local installs keep member semantics without per-member lockfiles. Recursive install commits the root lockfile only after every selected target succeeds.

Existing workspaces migrate automatically: a successful recursive install absorbs legacy member `lpm.lock` files into the root union and removes the obsolete member files and `lpm.lockb` companions. Union workspace lockfiles are TOML-only because the binary companion format cannot represent importer projections.

Bare install from inside a member stays member-local. Widen it explicitly when needed:

```bash
lpm install --recursive
lpm -r install
```

Use `--no-recursive` at the workspace root for a root-only refresh. `--recursive` only refreshes existing manifests; to add a dependency, keep using `lpm install <pkg> --filter <member>` or `-w` for the root.

Filters scope recursive refreshes and automatically include the selected members' required workspace dependency closure:

```bash
lpm install --filter web
lpm install --filter-prod web
```

The production form omits `devDependencies` edges. Filtered refreshes do not install the workspace root or unrelated members.

Filtered mutations update the selected importer projections inside the same root lockfile. Manifest, patch, and lockfile changes share one transaction boundary: if resolution, materialization, or the root lockfile commit fails, LPM CLI restores those files and invalidates affected install hashes. `node_modules` is not copied for rollback; the missing hash makes the next install re-link and converge it.

## Filter grammar [#filter-grammar]

The filter expression — used by `--filter` and `--filter-prod` on every workspace-aware command — has its own small DSL. Filters compose: `--filter web --filter api` unions the two sets.

| Filter                 | Matches                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `web`                  | The member literally named `web`                                                                 |
| `@scope/*`             | Every member with a name under a scope                                                           |
| `@scope/*{./apps/web}` | Members matching both the package-name atom and exact path                                       |
| `./apps/*`             | Every member under a path glob                                                                   |
| `{./apps/web}`         | Exact path match (path with literal characters)                                                  |
| `[origin/main]`        | Members changed since a git ref                                                                  |
| `web...`               | `web` and **every member `web` depends on** (transitive deps — downstream closure)               |
| `web^...`              | Same, excluding `web` itself (deps-only)                                                         |
| `...web`               | `web` and &#x2A;*every member that depends on `web`** (transitive dependents — upstream closure) |
| `...^web`              | Same, excluding `web` itself (dependents-only)                                                   |
| `!web`                 | Exclusion — remove `web` from the resulting set                                                  |

`--filter-prod <expr>` uses the same grammar, but closure operators walk only production graph edges: `dependencies`, `optionalDependencies`, and `peerDependencies`. `devDependencies` edges are skipped. This is useful for production deploy/build selections that should not pull in test-only workspace dependents.

> **Substring matching is not supported.** `--filter core` does NOT match `@babel/core` — write `--filter '*/core'` for that. This is the most common surprise for users coming from `pnpm`.

Preview what an expression selects without running anything:

```bash
lpm filter web...                     # terse name list
lpm filter --filter-prod ...shared    # prod dependents of shared
lpm filter '[main]' --changed-files-ignore-pattern '**/README.md'
lpm filter '...[main]' --test-pattern '**/*.test.js'
lpm filter web... --explain           # full per-package trace (which filter matched, how)
lpm filter web --fail-if-no-match     # exit non-zero if empty (useful in CI)
```

`lpm filter` drives the same `FilterEngine` as `lpm run --filter`, so the selection is identical. In an interactive terminal, the terse preview uses status bullets; when piped or redirected it stays a bare one-name-per-line list. Pass `--json` for structured output suitable for scripts or agents.

## `--affected` [#--affected]

```bash
lpm run test --affected              # default base = main
lpm run test --affected --base develop
lpm run test --affected --changed-files-ignore-pattern '**/README.md'
lpm run test --affected --test-pattern '**/*.test.js'
```

Selects only members affected by changes vs the base branch. Saves CI minutes — a one-line README change in `apps/marketing` doesn't trigger a full-monorepo test run.

Affected-ness is computed by walking the git diff against the base ref, then expanding through dep edges (a change in `packages/shared` triggers everything that imports it).

`--changed-files-ignore-pattern <glob>` drops matching paths from the git diff before LPM CLI maps files to workspace members. Pass it more than once for multiple globs.

`--test-pattern <glob>` marks matching changed files as test-only. The directly changed package still runs, but reverse closures and `--affected` do not add dependents from packages whose changes are only tests. This keeps `packages/shared/src/foo.test.ts` from rebuilding every app that imports `shared`.

Both git-diff tuning flags can be set as project defaults:

```toml title="lpm.toml"
[workspace]
changed-files-ignore-pattern = ["**/README.md", "docs/**"]
test-pattern = ["**/*.test.js", "**/*.spec.ts"]
```

## `--fail-if-no-match` [#--fail-if-no-match]

A typo'd filter (`--filter we` instead of `--filter web`) produces an empty match set, and most commands silently exit success with no work done. `--fail-if-no-match` makes that exit non-zero — strongly recommended in CI:

```bash
lpm run test --filter-prod ...shared --fail-if-no-match
```

Available on `install`, `uninstall`, `run`, `test`, `bench`, `lint`, `fmt`, `check`, `filter`.

## Multi-member confirmation [#multi-member-confirmation]

When `lpm install <pkg> --filter ...` would mutate more than one member's `package.json`, LPM CLI prompts for confirmation. JSON mode and non-TTY stdin already skip the prompt automatically; `-y` covers the interactive-terminal-but-no-manual-review case (scripts, agents).

## `workspace:*` protocol [#workspace-protocol]

Cross-member deps:

```json title="packages/web/package.json"
{
  "dependencies": {
    "@my-co/shared": "workspace:*"
  }
}
```

Tells `lpm install` "use the local workspace package, not a published version." The target can be a discovered member or the named package at the workspace root, so a docs member can depend on a root package such as `vitepress` via `"vitepress": "workspace:*"`. Workspace deps are extracted before dependency resolution and linked from disk afterward — they never touch the registry. The linker symlinks the local package into `node_modules/` (isolated layout) instead of pulling a published version. Topological order is respected for tasks (`dependsOn: ["^build"]` waits for upstream member's build to finish first).

Named workspace-root packages and members can also satisfy compatible plain semver edges that appear transitively through registry packages. If a registry package depends on `@my-co/shared` and the current workspace has a matching local package, LPM CLI resolves that edge locally instead of leaking the lookup to the registry.

Self-dependencies are invalid: a workspace package that declares itself via `workspace:*` in `dependencies`, `devDependencies`, `peerDependencies`, or `optionalDependencies` fails before LPM CLI writes lockfiles or `node_modules`.

Variants:

| Spec               | Meaning                                              |
| ------------------ | ---------------------------------------------------- |
| `"workspace:*"`    | Any local version                                    |
| `"workspace:^"`    | Caret range against the local version                |
| `"workspace:~"`    | Tilde range against the local version                |
| `"workspace:^1.2"` | Explicit range applied as if local were that version |

All variants are preserved verbatim by [save policy](/docs/packages/save-policy) — they never get rewritten.

## Catalogs [#catalogs]

Centralize version pins across members:

```json title="package.json (root)"
{
  "catalogs": {
    "default": { "react": "^18.2.0", "react-dom": "^18.2.0" },
    "testing": { "jest": "^29.0.0", "vitest": "^1.0.0" }
  }
}
```

Members reference catalog versions:

```json title="packages/web/package.json"
{
  "dependencies": {
    "react": "catalog:",            // → ^18.2.0 (default catalog)
    "react-dom": "catalog:",
    "vitest": "catalog:testing"     // → ^1.0.0 (named catalog)
  }
}
```

One pin, every member uses it. Bump the catalog entry in one place to update every member. No more "the API package is on react 18.0 but the web package is on 18.2 because someone forgot."

Set `package.json > lpm.cleanupUnusedCatalogs = true` or `pnpm-workspace.yaml > cleanupUnusedCatalogs: true` when you want `lpm install` to remove catalog entries that no root or member manifest references. By default, LPM CLI preserves unused catalog entries.

Inspect catalog state with:

```bash
lpm catalog list --unused
lpm catalog show --resolved --json
```

`catalog list` reads the root catalog declarations and marks which entries are referenced by root/member manifests. `catalog show --resolved` reads the resolved catalog snapshot in `lpm.lock`; if manifests reference a catalog entry that is missing from the lockfile snapshot, LPM CLI exits non-zero and asks you to run `lpm install` so the snapshot cannot look falsely clean.

For the full command reference, see [`lpm catalog`](/docs/packages/catalog).

## `lpm deploy` [#lpm-deploy]

For Docker / serverless — materialize one member's deploy closure into a self-contained directory:

```bash
lpm deploy /prod/api --filter api               # build the deploy output
lpm deploy /prod/api --filter api --dry-run     # preview, no filesystem changes
lpm deploy /prod/api --filter api --force       # overwrite a non-empty output dir
lpm deploy /prod/api --filter api --no-optional # omit optionalDependencies
lpm deploy /prod/api --filter api --dev         # deploy devDependencies instead
```

Output dir contains:

* The member's publishable source files, copied via hardlink when possible (zero disk cost on the same filesystem; falls back to byte-copy for cross-device)
* Local workspace dependencies copied under `.lpm/deploy-workspace/` and referenced with relative `file:` specs
* A populated `node_modules/` (install pipeline runs at the output dir)
* A deploy-local `.lpm/store/` and `lpm.lock` for the deploy tree

The source copy honors package publishing rules: `files` when present, otherwise `.npmignore`, otherwise `.gitignore`. It also applies a deny list at **every level** (not just the root). Excluded basenames:

* **LPM CLI internal state** that the install pipeline recreates: `node_modules`, `.lpm`, `lpm.lock`, `lpm.lockb`
* **Secrets** — every `.env*` variant: `.env`, `.env.local`, `.env.development[.local]`, `.env.production[.local]`, `.env.test[.local]`. Critical: a developer-only `.env.local` riding into a Docker image is a credential leak
* **Version control / packaging control files**: `.git`, `.gitignore`, `.npmignore`, `.gitattributes`, `.svn`, `.hg`
* **OS / editor cruft**: `.DS_Store`, `Thumbs.db`

In a Dockerfile:

```dockerfile
FROM workspace as pruned
RUN lpm deploy /prod/api --filter api

FROM node:22-alpine
COPY --from=pruned /prod/api /app
WORKDIR /app
CMD ["node", "server.js"]
```

Constraints:

* `--filter` or `--filter-prod` is required and must match exactly one member. `--changed-files-ignore-pattern` and `--test-pattern` apply when the filter contains a `[git-ref]` atom.
* The output directory must be outside the workspace tree.
* `--prod` is the default dependency mode. `--dev` deploys dev dependencies instead, and `--no-optional` omits optional dependencies from the manifest and resolver graph.

## See also [#see-also]

* [Monorepo setup guide](/docs/guides/monorepo-setup) — step-by-step walkthrough
* [`lpm catalog`](/docs/packages/catalog) — inspect catalog usage and resolved snapshots
* [`lpm install --filter`](/docs/packages/install#workspaces) — install scoped to members
* [`lpm uninstall --filter`](/docs/packages/uninstall#workspaces) — remove scoped to members
* [`lpm run --filter --affected`](/docs/dev/run#workspaces) — task running with workspace selection
* [`lpm filter`](#filter-grammar) — preview a filter expression with `--explain` / `--json`
* [Save policy](/docs/packages/save-policy) — `workspace:*` is preserved verbatim
