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-sandboxlpm install --allow-newlpm install --yolopackage.json > lpm.scriptPolicy = "allow"package.json > lpm.minimumReleaseAge = 0lpm.toml > [sandbox] mode = "none"
lpm security is the command surface for two related jobs:
unlockcreates a short-lived approval for one guarded weakening. Manualunlockdefaults to the machine-global target unless you pass--project.lockrevokes one active temporary unlock. Manuallockalso defaults to the machine-global target unless you pass--project.statusshows the effective security floor, active runtime overrides, and whichever unlock scope you asked to inspect.statusstill defaults to the current project.repairquarantines 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 --jsonSubcommands
| Subcommand | What 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 status | Print the effective floor, runtime overrides, source attribution, and active unlocks for the selected scope |
lpm security repair | Quarantine 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
allexpands to every concrete unlock scope.defaultexpands to the common install/runtime weakeners:cooldown-bypass,cooldown-window,provenance-ignore-drift,provenance-unverified,scripts-triage,scripts-allow,sandbox-default,sandbox-none, andsandbox-allow-degraded.defaultintentionally excludestrust-bulk-approve,trust-scope-widen,capability-widen, andfloor-edit.
Common scopes
Copy the exact scope from the error's suggested_command whenever possible. The most common ones today are:
| Scope | Typical trigger |
|---|---|
sandbox-none | lpm install --no-sandbox or lpm.toml > [sandbox] mode = "none" |
sandbox-allow-degraded | lpm.toml or ~/.lpm/config.toml enables [sandbox].allow-degraded = true beyond the current floor |
cooldown-bypass | lpm install --allow-new, --min-release-age=0, or a lower minimumReleaseAge |
scripts-allow | lpm install --yolo, --policy=allow, or scriptPolicy = "allow" |
scripts-triage | lpm install --triage or scriptPolicy = "triage" when that weakens the current floor |
trust-bulk-approve | lpm approve-scripts --yes, lpm approve-scripts --global --yes, or direct trustedDependencies widening |
trust-scope-widen | direct trustedScopes widening in package.json |
capability-widen | direct widening of passEnv, readProject, or sandboxLimits in package.json |
provenance-ignore-drift | --ignore-provenance-drift <pkg> or --ignore-provenance-drift-all |
provenance-unverified | LPM_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 (
projectorglobal) - the selected root for that target:
projectin project mode,global-rootin 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_ENFORCEor 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 repairQuarantines 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 verificationTypical 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 installLPM 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 = 0lpm.toml > [sandbox] mode = "none"- direct
trustedDependencies,trustedScopes,passEnv,readProject, orsandboxLimitswidening
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 offLPM 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_floorwhen 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.tomlExample:
[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.