LPM-cli

lpm security

Temporary approvals, effective machine floor, managed policy, and local security-state repair.

lpm security <unlock|lock|status|repair>

LPM treats some security-lowering requests as guarded operations. Examples:

  • lpm install --no-sandbox
  • lpm install --allow-new
  • lpm install --yolo
  • package.json > lpm.scriptPolicy = "allow"
  • package.json > lpm.minimumReleaseAge = 0
  • lpm.toml > [sandbox] mode = "none"

lpm security is the command surface for two related jobs:

  • unlock creates a short-lived approval for one guarded weakening. Manual unlock defaults to the machine-global target unless you pass --project.
  • lock revokes one active temporary unlock. Manual lock also defaults to the machine-global target unless you pass --project.
  • status shows the effective security floor, active runtime overrides, and whichever unlock scope you asked to inspect. status still defaults to the current project.
  • repair quarantines signed local security-state files that can no longer be verified.

Examples

lpm security unlock default --ttl 10m
lpm security unlock sandbox-none --project . --ttl 10m
lpm security unlock provenance-ignore-drift --project . --package esbuild --ttl 10m
lpm security unlock provenance-unverified --global --ttl 10m
lpm security unlock all --global --ttl 1h

lpm security lock default
lpm security lock provenance-ignore-drift --project . --package esbuild
lpm security lock all --global

lpm security status
lpm security status --project /path/to/repo
lpm security status --global
lpm security status --json

lpm security repair
lpm security repair --json

Subcommands

SubcommandWhat it does
lpm security unlock <scope>Create a temporary approval for one guarded weakening, defaulting to the global trust surface
lpm security lock <scope>Revoke one active temporary approval, defaulting to the global trust surface
lpm security statusPrint the effective floor, runtime overrides, source attribution, and active unlocks for the selected scope
lpm security repairQuarantine signed local security state that fails verification

unlock

lpm security unlock <scope|all|default> [--project PATH | --global] [--ttl 10m] [--package PKG]

Creates a temporary approval under ~/.lpm/security/unlocks/. If you pass --project, the unlock is tied to that project root. If you pass --global, or omit both target flags, the unlock applies to the machine-global trust surface. The command does not run the risky action itself. It only creates the unlock so you can rerun the original install, rebuild, or trust command.

Default TTL is 10m. Maximum TTL is 365d.

Accepted TTL inputs use the shared duration parser, so m, h, d, and raw seconds are recognized. For lpm security unlock, the parsed value must still resolve to at most 365d, so examples such as 5m, 1h, 30d, 365d, and 600 all work.

Manual unlock defaults global, but suggested commands coming from guarded install/config errors stay explicit (--project . or --global) so you can see the intended scope before approving it.

Use --package <name> only with a concrete scope when the weakening is package-specific. Repeat --package to cover more than one package. Package filter values must not be empty. A package-scoped unlock only covers requests for those package names; it does not cover blanket all-package flags such as --ignore-provenance-drift-all or --unverified-provenance-all. Bundle selectors all and default reject --package.

Bundle selectors

  • all expands to every concrete unlock scope.
  • default expands to the common install/runtime weakeners: cooldown-bypass, cooldown-window, provenance-ignore-drift, provenance-unverified, scripts-triage, scripts-allow, sandbox-default, sandbox-none, and sandbox-allow-degraded.
  • default intentionally excludes trust-bulk-approve, trust-scope-widen, capability-widen, and floor-edit.

Common scopes

Copy the exact scope from the error's suggested_command whenever possible. The most common ones today are:

ScopeTypical trigger
sandbox-nonelpm install --no-sandbox or lpm.toml > [sandbox] mode = "none"
sandbox-allow-degradedlpm.toml or ~/.lpm/config.toml enables [sandbox].allow-degraded = true beyond the current floor
cooldown-bypasslpm install --allow-new, --min-release-age=0, or a lower minimumReleaseAge
scripts-allowlpm install --yolo, --policy=allow, or scriptPolicy = "allow"
scripts-triagelpm install --triage or scriptPolicy = "triage" when that weakens the current floor
trust-bulk-approvelpm approve-scripts --yes, lpm approve-scripts --global --yes, or direct trustedDependencies widening
trust-scope-widendirect trustedScopes widening in package.json
capability-widendirect widening of passEnv, readProject, or sandboxLimits in package.json
provenance-ignore-drift--ignore-provenance-drift <pkg> or --ignore-provenance-drift-all
provenance-unverifiedLPM_PROVENANCE_ENFORCE=warn, LPM_PROVENANCE_ENFORCE=off, raw or persisted [sigstore].verify = "warn" or "off", or --unverified-provenance*

lock

lpm security lock <scope|all|default> [--project PATH | --global] [--package PKG]

Revokes active temporary unlocks from ~/.lpm/security/unlocks/. If you pass --project, lock inspects that project's unlocks. If you pass --global, or omit both target flags, it inspects machine-global unlocks.

lock is non-interactive. If a matched unlock contains multiple scopes, lock removes only the requested scopes and leaves the rest active. lock only touches temporary unlock grants; it does not rewrite the approved machine posture, project policy approvals, or managed policy.

Use --package <name> only with a concrete scope, and pass a non-empty package value. For lock, package filters match unlocks created with that exact package filter set.

Interactive vs automation

lpm security unlock is interactive today. In a TTY it asks for native system approval, then writes the signed unlock grant on success.

In any of these modes, it refuses instead of prompting:

  • --json
  • non-TTY stdin/stdout
  • CI (CI=1, true, yes, etc.)

The refusal uses error_code: "security_approval_required" and includes a suggested_command.

Guarded install/config attempts plus interactive unlock decisions are appended to ~/.lpm/security/audit.jsonl as signed hash-chained entries. The fast refusal path for lpm security unlock in --json, CI, or other non-interactive mode returns the structured error immediately and does not append a separate unlock-declined entry. Production builds also store the audit head in the OS keyring so local truncation or rewrite attempts can be detected on the next append. This is local tamper evidence, not a remote immutable log.

status

lpm security status [--project PATH | --global]

Shows the effective security floor for the selected project root, or for the global trust surface when you pass --global.

Human output includes:

  • the selected target (project or global)
  • the selected root for that target: project in project mode, global-root in global mode
  • the effective values for script policy, release-age floor, sandbox mode, sandbox degraded mode, and Sigstore verification
  • whether each value came from the built-in default, the signed approved-posture store, or a managed policy
  • the path to the signed approved-posture store
  • the active managed policy file, if one is present
  • any active runtime overrides such as LPM_PROVENANCE_ENFORCE or raw ~/.lpm/config.toml > [sigstore].verify
  • the active unlocks for that scope, including package scoping and min-release-age limits when present

With --json, the envelope looks like:

{
  "success": true,
  "status": {
    "target": "project",
    "project_root": "/abs/path/to/repo",
    "effective_floor": {
      "script_policy": "deny",
      "minimum_release_age_secs": 86400,
      "sandbox_mode": "default",
      "sandbox_allow_degraded": false,
      "sigstore_verify": "deny"
    },
    "floor_sources": {
      "script_policy": "approved-store",
      "minimum_release_age_secs": "managed-policy",
      "sandbox_mode": "builtin-default",
      "sandbox_allow_degraded": "builtin-default",
      "sigstore_verify": "approved-store"
    },
    "approved_posture_path": "/Users/alice/.lpm/security/approved-posture.json",
    "approved_posture_source": "approved-store",
    "managed_policy": {
      "path": "/etc/lpm/security-policy.toml",
      "name": "corp-default",
      "source": "mdm",
      "enforced_controls": ["minimum-release-age-secs"]
    },
    "active_runtime_overrides": [
      {
        "control": "sigstore.verify",
        "value": "warn",
        "source": "LPM_PROVENANCE_ENFORCE"
      }
    ],
    "active_unlocks": []
  }
}

The JSON field name stays project_root for envelope stability even under --global; in that mode it points at the canonical global trust root.

repair

lpm security repair

Quarantines signed local security-state files under ~/.lpm/security/ when LPM cannot verify them with the current signing secret. This is the recovery path for errors such as:

security approval store refused: signed security state file ~/.lpm/security/approved-posture.json failed signature verification

Typical causes are OS keyring reset, machine restore without the matching keyring secret, copying ~/.lpm/security/ from another machine, or manual edits to signed files.

repair is explicit by design: ordinary commands fail closed instead of silently trusting or deleting unverified state. The command moves unverified state aside with a .unverified-... suffix and does not create a replacement signing secret. After repair, lpm security status falls back to the built-in floor plus any managed policy until you intentionally approve new local state.

The repair pass checks signed posture, project/global approval state, unlock grants, and the signed audit log. If the signing secret exists but the OS keyring cannot be read, repair stops rather than quarantining files it could not actually verify.

With --json, the envelope looks like:

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

How guarded approvals work

1. CLI weakeners in an interactive shell

For direct CLI weakeners such as:

lpm install --no-sandbox
lpm install --allow-new
lpm install --yolo
LPM_PROVENANCE_ENFORCE=off lpm install

LPM may ask inline for confirmation in an interactive terminal. If you approve, it mints the same temporary unlock internally and continues the command. Global guarded flows, such as lpm install -g and lpm approve-scripts --global, use global unlocks and suggest lpm security unlock <scope> --global.

2. CLI weakeners in CI, --json, or non-TTY mode

The same command does not prompt. It fails with security_approval_required and includes the exact scope and suggested_command.

3. Repo-file weakeners

Security-sensitive repo config is treated as a proposal, not authority.

Examples:

  • package.json > lpm.scriptPolicy = "allow"
  • package.json > lpm.minimumReleaseAge = 0
  • lpm.toml > [sandbox] mode = "none"
  • direct trustedDependencies, trustedScopes, passEnv, readProject, or sandboxLimits widening

LPM does not prompt inline for those repo-file changes. Instead, install or rebuild fails and points you at lpm security unlock ... for a temporary project exception, or at a persistent machine-level config change if that is what you want.

4. Persistent machine-wide config changes

For security-sensitive lpm config mutations such as:

lpm config scripts --set allow
lpm config release-age --set 0
lpm config sandbox --set none
lpm config sigstore --set off

LPM asks for interactive confirmation in a TTY before persisting the weaker machine posture.

In CI, --json, or any non-interactive shell, those same writes fail with security_approval_required instead of silently lowering the machine floor.

5. Raw file edits are not enough

Editing package.json, lpm.toml, or ~/.lpm/config.toml by hand is not enough to authorize a weaker security posture on its own.

If the file asks for something weaker than the currently approved floor, LPM treats that value as an unauthorized proposal and either:

  • fails with security_approval_required, or
  • fails with security_floor when a higher-authority managed policy owns that control

Managed machine policy

Managed machines and headless CI can provide a higher-authority floor at:

Unix/macOS: /etc/lpm/security-policy.toml
Windows:    C:\ProgramData\lpm\security-policy.toml

Example:

/etc/lpm/security-policy.toml
[policy]
name = "corp-default"
source = "mdm"

script-policy = "deny"
minimum-release-age-secs = 86400

[sandbox]
mode = "strict"
allow-degraded = false

[sigstore]
verify = "deny"

When this file owns a control, weaker repo or user changes do not win. They fail with error_code: "security_floor" instead. In production, LPM only loads managed policy from the managed path itself. On Unix/macOS, the file and parent directories must be root-owned and not group/world writable. On Windows, the file and parent directory must resolve to the ProgramData path, must not be reparse points, must be owned by SYSTEM or Administrators, and must not grant write access to non-administrator principals.

Use lpm security status to see exactly which controls are currently coming from managed policy.

See also