LPM CLI

~/.lpm/config.toml

User-level CLI defaults — applies to every project on this machine.

~/.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 and 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 for the complete scope and exclusions.

You can edit the file directly, or use lpm config to manage it from the CLI:

lpm config set save-prefix '~'
lpm config get save-prefix
lpm config list
lpm config delete save-prefix

Known keys

~/.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 = 86400
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

# 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

# Run `lpm audit` after every successful install and print a one-line advisory
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"
static_only_suspicious = "warn"

# Local install policy extensions
[policy.extensions.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"]

# 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"
KeyTypeDefaultNotes
save-prefix"^" | "~" | """^"Prefix used when lpm install <pkg> writes to package.json. * is rejected.
save-exactboolfalseForce 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. Only active when script-policy = "triage". Approvals are ephemeral — never written to disk.
minimum-release-age-secsu6486400 (24h)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.
minimum-release-age-excludestring array[]Exact canonical package names exempted from the cooldown. Merged with CLI and package.json excludes. Globs, version selectors, and protocol specifiers are rejected.
sandbox.mode"default" | "strict" | "none""default"Default lifecycle-script sandbox posture for this machine. Set with lpm config sandbox.
sandbox.allow-degradedboolfalseAllow 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.
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.
signaturesboolfalseVerify npm registry package signatures during install and fail closed when a registry package cannot be verified. Set with lpm config signatures. 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.
typosquat-guard"default" | "on" | "off"unset / "default" (enabled)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 is a guarded machine-wide weakening. Prefer project policy.typosquat.allow 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; CI can use LPM_TOKEN or registry-audience OIDC. 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. 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.static_only_suspicious"allow" | "warn" | "block""warn"User action for static-only suspicious signals that LPM Firewall has not confirmed with AI or trusted public intel. The wizard offers warn or allow for this warn-default group; hand-edited "block" values are preserved when you review the profile.
policy.extensions.<name>.commandstring arrayrequiredDirect-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 enforceWhat to do when the extension cannot be spawned, times out, exits non-zero, emits oversized output, or returns invalid JSON.
policy.extensions.<name>.timeout-mspositive integer5000Per-extension timeout for the full exchange: stdin write, process exit, stdout/stderr drain, and response validation. Maximum accepted value is 120000.
policy.extensions.<name>.eventsstring 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. Per-process override: LPM_V2_OBJECT_INTEGRITY.
engine-strictbooltrueEnforce workspace-root engines.lpm / engines.node and dependency engines.node constraints. Set to false for warning-only behavior; incompatible dependencies remain installed.
strict-peer-dependenciesboolfalseTurn 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-peersbooltrueAuto-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-skillsbooltrueReconcile package-published skills from installed @lpm.dev/* packages. Set with lpm config lpm-skills. 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.
workspace-concurrencypositive integeravailable parallelismUser 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-installboolfalseRun lpm audit silently after every successful lpm install and emit a one-line advisory (! Audited N packages, V vulnerabilities, S suspicious in Tms — run \lpm audit`). Informational only — findings never fail the install. Per-invocation overrides: --audit-after-install/--no-audit-after-install. Per-process env: LPM_AUDIT_AFTER_INSTALL`. See Audit after install.
linker"isolated" | "hoisted""hoisted" (workspaces and default peer-conflict installs use "isolated")node_modules layout for installs run by this user. 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-allowarray of strings[]Explicitly authorize lifecycle-script reads of project-relative paths that match the secret-file deny list. 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 — duplicates are deduplicated. Allowlisted secrets remain readable; if every matching secret is allowlisted, Linux skips the namespace overlay.
max-sandbox-write-rootsarray of absolute paths[]User-authorized roots that may cover absolute package.json > lpm > scripts > sandboxWriteDirs 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-urlstring"wss://relay.lpm.fyi/connect"WebSocket URL for the tunnel relay used by LPM CLI. Per-process override: LPM_TUNNEL_RELAY. Each host gets its own TOFU pin under ~/.lpm/relay-pins/<host>.

Unknown keys are accepted silently — forward-compatible by design.

String coercion

lpm config set writes ordinary generic values as TOML strings. Known typed keys can preserve native TOML types; today lpm config set signatures true and lpm config set auto-install-lpm-skills false write booleans. Readers accept both native types and the string form for typed keys:

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
typosquat-guard = "on"                # canonical string
integrity = "tree"                    # canonical string

[firewall]
mode = "monitor"                      # canonical string

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

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.

[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"
static_only_suspicious = "warn"

Every policy group accepts allow, warn, or block in hand-edited config. The interactive wizard shows the profile on one review screen when you choose enforce: critical_vulnerability offers block, warn, or allow; static_only_suspicious normally offers warn or allow, while preserving a hand-edited block value if one already exists. See the Firewall for npm guide for group meanings and examples.

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:

[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"]

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. Unknown fields fail config loading, so typos such as mdoe = "enforce" cannot fall back to report mode.

Use lpm policy to inspect and test active extensions:

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

Request:

{
  "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:

{
  "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

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

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. For persistent changes, use lpm config rather than relying on a hand edit alone. Raw [sigstore].verify = "warn" / "off", raw typosquat-guard = "off", and raw [firewall].mode = "off" or "monitor" below the approved floor are reported by lpm security status as runtime overrides until they are removed, approved, or blocked by managed policy.

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.

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 enabled, 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; CI can use LPM_TOKEN or registry-audience OIDC instead of an interactive login. 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

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

PlatformPath
macOS / Linux~/.lpm/config.toml
Windows%USERPROFILE%\.lpm\config.toml

Override the entire ~/.lpm/ root with the LPM_HOME 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