# 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 newly published dependency version is installable. Default `86400` (24h). By default, project installs apply this to packages declared by the project or passed to `lpm install <pkg>`; transitive dependencies of an allowed direct package are not separately halted. Set [`minimumReleaseAgePolicy: "strict"`](#minimumreleaseagepolicy) to apply the same cooldown to transitive dependencies and lockfile replays. For ranges on checked packages, the resolver skips too-new candidates and can pick an older satisfying version; `latest` fallbacks never exceed the registry's current `dist-tags.latest` target in SemVer order, and exact pins to too-new versions fail. Bypass per-invocation with `lpm install --allow-new`, override with `lpm install --min-release-age=<DUR>`, or exempt exact package names with [`minimumReleaseAgeExclude`](#minimumreleaseageexclude).

Precedence: CLI > `package.json > lpm.minimumReleaseAge` > `~/.lpm/config.toml > minimum-release-age-secs` > 24h default.

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", "@scope/pkg"] } }
```

Exact package names exempted from the minimum-release-age resolver and install halt. Alias dependencies match by their canonical target name, not the local alias key. CLI, package, and global exclude lists merge in that order, with duplicates removed. On project installs, the exclude is evaluated inside the direct/root dependency scope.

Unsupported in v1: globs such as `@scope/*`, version selectors such as `react@19.0.0`, and protocol specifiers such as `npm:react`.

### `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, 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`                 |

On conflict with the top-level `overrides` / `resolutions` fields, `lpm.overrides` wins. Multi-segment paths (`a>b>c`) are rejected at parse time.

### `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\[]              | Scope globs (e.g. `"@myorg/*"`) whose packages are auto-trusted for script execution                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `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.

## 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. `lpm.json > runtime.node` takes precedence over the root `engines.node` as a **pin source** for runtime auto-install; every Node **enforcement** check still runs against the resolved Node version.

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. `lpm.overrides` (above) takes precedence when there's a conflict.

### `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
