LPM-cli

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 package

What 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):

package.json
{
  "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:

~/.lpm/global/trusted-dependencies.json
{
  "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

ModeBehavior
(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.
--listRead-only — print the blocked set, no prompts, no mutations. Mutually exclusive with --yes.
--yesBulk 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-runRun 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 --global

Operates 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 --group

When 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

FlagEffect
<package>Approve a specific package directly (name or name@version). Auto-confirms in non-TTY / JSON mode.
--yesBulk-approve every blocked package (loud). Refuses to bulk-approve amber / red tier packages.
--listRead-only listing
--dry-runPreview without writing
--globalOperate 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