# lpm approve-scripts (/docs/packages/approve-scripts)



```bash
lpm approve-scripts [package]
```

Pairs with [`lpm install`](/docs/packages/install)'s default-deny dependency script policy. After install, dependency 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 dependency lifecycle name so you can review the surface area; the execution layer only runs dependency `preinstall`, `install`, and `postinstall`. A dependency 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](/docs/packages/rebuild#what-runs).

Approvals are bound to &#x2A;*`{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 [#examples]

```bash
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 [#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) [#project-scope-default]

Approvals land in `package.json > lpm > trustedDependencies` as the **rich-map form** (not the legacy array form):

```json title="package.json"
{
  "lpm": {
    "trustedDependencies": {
      "esbuild@0.25.1": {
        "integrity": "sha512-...",
        "scriptHash": "sha256-..."
      }
    }
  }
}
```

### Global scope (`--global`) [#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:

```json title="~/.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 [#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 CLI refuses deterministically with `security_approval_required` instead of opening a native approval prompt.

### Drift semantics (both scopes) [#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`](/docs/packages/trust) to inspect drift events for project trust.)

## Modes [#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](/docs/packages/security-audit#layer-5-triage) (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 CLI 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 [#global-vs-project]

By default, `approve-scripts` operates on the current project's blocked set, and writes approvals into the project's `package.json`.

```bash
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`.

```bash
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` and also includes `next_steps[]` command objects for agents. `--dry-run` omits both follow-up fields 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 [#json-output]

```bash
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`, `success`, `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`. Successful project approvals that actually write trust include `next_steps: [{description, command}]` with `command: "lpm rebuild"`. 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 both `next_step: {kind: "reinstall_globals", origins: [...]}` and `next_steps[]` command objects telling the caller which `lpm uninstall -g … && lpm install -g …` pairs to run. `--dry-run` omits follow-up commands since no mutation happened.

## Flags [#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](/docs/commands#global-flags) — `--json` is especially useful here.

## See also [#see-also]

* [`lpm install`](/docs/packages/install#lifecycle-scripts) — what produces the blocked set
* [`lpm rebuild`](/docs/packages/rebuild) — runs scripts for already-trusted packages
* [`lpm trust`](/docs/packages/trust) — diff and prune the trust allowlist
* [`package.json` "lpm.trustedDependencies"](/docs/reference/package-json-lpm#trusteddependencies) — the underlying file
