lpm approve-scripts
Review and approve packages whose lifecycle scripts were blocked by the default-deny policy.
lpm approve-scripts [package]Pairs with lpm install's default-deny script policy. After install, 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 lifecycle name so you can review the surface area; the execution layer only runs preinstall, install, and postinstall. A 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.
Approvals are bound to {name, version, integrity, scriptHash} — any change to the package tarball or its scripts re-opens the package for review on the next install. The strict binding is what makes the deny-by-default model usable: you grant trust to a specific build, not to a name.
Examples
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 packageWhat 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.
Project scope (default)
Approvals land in package.json > lpm > trustedDependencies as the rich-map form (not the legacy array form):
{
"lpm": {
"trustedDependencies": {
"esbuild@0.25.1": {
"integrity": "sha512-...",
"scriptHash": "sha256-..."
}
}
}
}Global scope (--global)
Approvals land in ~/.lpm/global/trusted-dependencies.json — a separate per-machine trust file. The bound fields are the same plus a provenanceAtApproval snapshot used by the install-time drift gate:
{
"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
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 refuses deterministically with security_approval_required instead of opening a native approval prompt.
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 to inspect drift events for project trust.)
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 (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 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
By default, approve-scripts operates on the current project's blocked set, and writes approvals into the project's package.json.
lpm approve-scripts --globalOperates 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.
lpm approve-scripts --global --groupWhen 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. --dry-run omits next_step 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
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, 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. 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 a next_step: {kind: "reinstall_globals", origins: [...]} payload telling the caller which lpm uninstall -g … && lpm install -g … pairs to run. --dry-run omits next_step since no mutation happened.
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 — --json is especially useful here.
See also
lpm install— what produces the blocked setlpm rebuild— runs scripts for already-trusted packageslpm trust— diff and prune the trust allowlistpackage.json"lpm.trustedDependencies" — the underlying file