# Security & audit (/docs/packages/security-audit)



LPM CLI's security model is **layered**. No single check is the answer; instead, several independent gates each catch a different class of threat. This page is the conceptual overview — what each layer does, how they compose, and where to dig deeper.

The CLI surfaces are [`lpm audit`](/docs/packages/audit) (broad report), [`lpm query`](/docs/packages/query) (precision selectors), [`lpm approve-scripts`](/docs/packages/approve-scripts) (script trust), and [`lpm trust`](/docs/packages/trust) (drift inspection). All of them read from the same underlying analysis pipeline.

## The five layers [#the-five-layers]

| Layer                          | What it catches                                                                                                | Where it runs                                          |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| **Lifecycle script gate**      | Postinstall script supply-chain attacks                                                                        | At install time, blocks until approval                 |
| **Behavioral analysis**        | Risky API usage, obfuscation, telemetry, license issues                                                        | At extract time (cached forever per `(name, version)`) |
| **Vulnerability data**         | Known CVEs from OSV.dev + registry advisories                                                                  | At `lpm audit` time                                    |
| **Publisher trust + cooldown** | Recently-published direct versions, registry-signature failures, trust downgrades, or publisher-identity drift | At install time and via `lpm audit signatures`         |
| **Triage**                     | Tiered automation for the script gate                                                                          | At install time, per-package                           |

Each layer is independent — a package can pass one and fail another. Most attacks in the wild trip more than one.

## Layer 1: Lifecycle script gate (default-deny) [#layer-1-lifecycle-script-gate-default-deny]

Bare `lpm install` runs the root project's install lifecycle, including `prepare`. Dependency package lifecycle scripts (`preinstall`, `install`, `postinstall`) do **not** run by default. To execute dependency scripts, you either:

* **Approve them explicitly** via [`lpm approve-scripts`](/docs/packages/approve-scripts) — the package gets recorded in `package.json > lpm > trustedDependencies` (project) or `~/.lpm/global/trusted-dependencies.json` (`--global`) with its integrity + script hash.
* **Opt out per-invocation** with `lpm install --policy=allow` (or `--yolo`) — runs everything, no gate. Works on `lpm install -g` too.
* **Opt out persistently** with `package.json > lpm > scriptPolicy: "allow"` or `~/.lpm/config.toml > script-policy = "allow"`. The user-config tier applies to both project and global installs.

Approvals are **strict**: bound to `{name, version, integrity, scriptHash}`. A republished version with different bytes invalidates the approval and re-opens the package for review. This is what makes the deny-by-default model usable across long-running projects — trust is for a specific build, not a name.

The dependency lifecycle phases that actually run during `lpm install`'s build pipeline are `preinstall`, `install`, `postinstall`. The broader BLOCKED set (which detection scans for) also includes `preuninstall`, `uninstall`, `postuninstall`, `prepare`, and `prepublishOnly` — dependency packages that declare any of these get flagged even if the current dependency pipeline doesn't execute them.

## Layer 2: Behavioral analysis [#layer-2-behavioral-analysis]

The `lpm-security` crate's `behavioral` module performs static analysis on every extracted package. Results are cached in `.lpm-security.json` next to the package files — computed once per `(name, version)`, forever.

Three groups of tags — source behavior (what the code does), supply-chain signals (what the artifact looks like), and manifest declarations (what `package.json` declares):

**Source tags** — what the source code does:

| Tag                | Detects                                            |
| ------------------ | -------------------------------------------------- |
| `:eval`            | Use `eval`, `Function()`, or `vm.runInThisContext` |
| `:network`         | Make outbound HTTP / WS connections                |
| `:fs`              | Touch the filesystem outside their own directory   |
| `:shell`           | Spawn shells (`spawn`, `exec`, `execSync`)         |
| `:child-process`   | Use `child_process` (any form)                     |
| `:native`          | Ship native modules (`.node`, `.wasm`)             |
| `:crypto`          | Use cryptographic primitives                       |
| `:dynamic-require` | Use dynamic `require()` (variable arg)             |
| `:env`             | Read `process.env`                                 |
| `:ws`              | Use WebSockets                                     |

**Supply-chain tags** — what the artifact looks like:

| Tag             | Detects                                                           |
| --------------- | ----------------------------------------------------------------- |
| `:obfuscated`   | Show signs of code obfuscation                                    |
| `:high-entropy` | Contain high-entropy string blobs                                 |
| `:minified`     | Ship minified-only source                                         |
| `:telemetry`    | Make telemetry / analytics calls                                  |
| `:url-strings`  | Contain URL string literals                                       |
| `:trivial`      | Tiny — measured by AST node count                                 |
| `:protestware`  | Match a curated list of known protest-license / sabotage packages |

**Manifest tags** — what the `package.json` declares:

| Tag             | Detects                            |
| --------------- | ---------------------------------- |
| `:git-dep`      | Installed from a git URL           |
| `:http-dep`     | Installed from an HTTP tarball URL |
| `:wildcard-dep` | Declared with `*` or `latest`      |
| `:copyleft`     | Copyleft license (GPL family)      |
| `:no-license`   | No `license` field                 |

Plus state tags driven by other layers (`:scripts`, `:built`, `:vulnerable`, `:deprecated`, `:lpm`, `:npm`, `:critical`/`:high`/`:medium`/`:info`).

### Performance design [#performance-design]

* `RegexSet` + `OnceLock` for compile-once, single-pass matching across all behavioral patterns.
* All regex uses Rust's `regex` crate (Thompson NFA, **linear-time guarantees**). The `fancy-regex` crate is **banned** for security analysis — backtracking against untrusted input is a ReDoS vector.
* File extension filter before I/O (skip non-source files).
* Per-file size cap: 2MB. Per-package total: 50MB scanned. Per-package file count: 5,000.
* Shannon entropy pre-filter skips \~95% of files before the expensive extraction pass.

Target: \< 100ms per typical 100-file package.

### Querying the tags [#querying-the-tags]

```bash
lpm audit                         # broad report — all tags grouped
lpm query :eval                   # one tag
lpm query :scripts:not(:built)    # combinator: scripted but never built
lpm query :critical --assert-none # CI gate: fail if any critical-tagged package
```

[`lpm query`](/docs/packages/query) is the precision tool — exposes the same data the audit pipeline produces, with combinators for ad-hoc gates.

## Layer 3: Vulnerability data (OSV) [#layer-3-vulnerability-data-osv]

`lpm audit` cross-references every installed package against [OSV.dev](https://osv.dev) (for npm packages) and any LPM.dev Registry-side advisories (for `@lpm.dev` packages). Findings are surfaced inline with the behavioral report. Severity ladder: `critical` > `high` > `moderate` (alias `medium`) > `info` (alias `low`).

```bash
lpm audit                       # all findings
lpm audit --level high          # only high-severity vulnerabilities
lpm audit --fail-on vuln        # CI: fail on confirmed vulns only (not behavior)
```

The vulnerability check is **online** — it hits OSV/registry. Cache hits keep the cost low on repeat runs.

## Layer 4: Provenance + cooldown [#layer-4-provenance--cooldown]

Install-time and audit-time gates that catch attacks against the publisher identity and registry metadata itself. They apply to both `lpm install` and `lpm install -g` unless noted.

**Minimum release age** — newly-published dependency versions are avoided for 24 hours by default. On project installs, the default policy checks direct/root dependencies only. For ranges on checked packages, the resolver skips too-new matching versions and picks the newest older candidate that still satisfies the range. Exact pins to a too-new version fail. Transitive dependencies of an allowed direct package are not separately halted by the default install cooldown. Opt into strict scope with `package.json > lpm > minimumReleaseAgePolicy: "strict"` or `~/.lpm/config.toml > release-age-policy = "strict"` to check direct and transitive dependencies and revalidate lockfile replays from persisted publish timestamps. Commands that select one package version directly, such as `lpm install -g`, `lpm dlx`, `lpm upgrade`, and `lpm outdated`, apply the same policy to the package version being selected or reported. Configurable per-invocation (`--min-release-age=<DUR>`), per-project (`package.json > lpm > minimumReleaseAge`), or per-user (`~/.lpm/config.toml > minimum-release-age-secs`). Bypass for one install with `--allow-new`, or exempt exact canonical package names with `--min-release-age-exclude <pkg>`, `package.json > lpm > minimumReleaseAgeExclude`, or `~/.lpm/config.toml > minimum-release-age-exclude`; exclude lists merge instead of replacing each other. On `-g` the project-level tier is N/A (the synthesized package.json doesn't carry `lpm.minimumReleaseAge`, `lpm.minimumReleaseAgePolicy`, or `lpm.minimumReleaseAgeExclude`); the cooldown window chain collapses to CLI flag > `~/.lpm/config.toml` > 24h default.

**Registry signatures** — npm packages can carry detached registry signatures in `dist.signatures`. Run [`lpm audit signatures`](/docs/packages/audit#registry-signatures) to verify the installed tree on demand. Install-time verification is disabled by default; enable it with:

```bash
lpm config signatures --set true
```

or for one process:

```bash
LPM_VERIFY_REGISTRY_SIGNATURES=1 lpm install
```

When enabled on install, LPM CLI verifies registry signatures after resolution and before the install commits. A missing signature, missing integrity hash, missing signing key, expired key, or invalid signature fails the install for npm registry packages. `@lpm.dev/*` packages and non-registry sources are skipped by this specific check and still pass through the other layers.

**Trust policy** — optional no-downgrade policy for npm trust evidence:

```bash
lpm config trust-policy --set no-downgrade
```

When enabled, LPM CLI enforces two history floors. During resolution, it refuses a release whose npm trusted-publisher or staged-publish evidence is weaker than an earlier published release; ranges can select an older acceptable version, while an exact downgraded pin fails. After selection, verified lockfile history adds an artifact-level floor: once any locked version has artifact-bound Sigstore evidence, a later selected version must also verify. An absent attestation, unavailable attestation, invalid bundle, frozen lockfile without matching evidence, or disabled/skipped verifier fails instead of allowing provenance to disappear. This artifact-level rule takes precedence over `verify=warn`, `verify=off`, per-package verification skips, and best-effort availability; disable `trust-policy` itself if you intentionally want to permit a downgrade. Registry attestation pointers alone count toward neither floor. The default remains `off`.

**Provenance drift** — when a previously-approved package is re-resolved, LPM CLI compares the candidate version's Sigstore provenance against the snapshot captured at approval time:

```text
(now,      approved)
(Some(n),  Some(a))   if n == a   → pass
(Some(_),  Some(_))               → drift! identity changed
(None,     Some(_))               → drift! provenance dropped
(Some(_),  None)                  → OK — present now, wasn't at approval
(None,     None)                  → never had it; layers 1/2/4 decide
```

A drift event blocks the install. Re-approve via [`lpm approve-scripts`](/docs/packages/approve-scripts) (or `lpm approve-scripts --global`) to capture the new identity, or opt out per-package with `--ignore-provenance-drift <pkg>` / blanket-waive with `--ignore-provenance-drift-all`. The reference snapshot lives in `package.json > lpm > trustedDependencies > <pkg>@<ver> > provenanceAtApproval` for project trust and in `~/.lpm/global/trusted-dependencies.json` for global trust.

The verifier binds the signed in-toto subject to the exact npm package URL (`pkg:npm/<name>@<version>`) and the package tarball's SHA-512 integrity. It also verifies the Sigstore certificate chain, transparency-log inclusion, publisher/workflow identity, and bundle digest. Registry metadata that merely points at an attestation is not proof.

Attestation URLs must be HTTPS and match the package registry origin. Redirects are checked at every hop and cannot change origin or downgrade HTTPS. The provenance cache stores the original bundle bytes, not a writable “verified” result; every cache hit reruns cryptographic verification and artifact binding. A malformed or locally modified cache entry is discarded and fetched again.

Verified evidence is written sparsely to `lpm.lock`, including the publisher/workflow snapshot, subject purl and SHA-512, transparency-log identity/index/time, and bundle SHA-256. Frozen and offline installs validate that evidence against the locked package name, version, source, and integrity before replay. The lockfile contains derived evidence rather than the original Sigstore bundle, so frozen replay cannot independently re-prove the publisher and transparency-log claims after a malicious lockfile edit. Treat `lpm.lock` as trusted, reviewable project input; online cache hits are different because the cache retains the original bundle and reruns verification.

To broaden or tighten verification explicitly:

```bash
lpm config sigstore --set scope=all
lpm config sigstore --set availability=strict
```

`scope=all` checks every resolved package; the default `scope=approved` limits checks to packages with an approved provenance identity. `availability=strict` requires an attestation; the default `best-effort` preserves missing/temporarily unavailable attestations as absence or unknown without blocking. A present bundle that fails verification is still rejected by the default `verify=deny` posture.

`--allow-new`, registry signatures, trust-policy, and `--ignore-provenance-drift[-all]` are independent. `--allow-new` only skips the cooldown; signature verification and drift still apply unless their own controls are disabled or waived.

The cooldown threshold also gates the Layer 5 [identity widening](#layer-5-triage) — a recent-publish package whose script body matches an identity-Green shape still stays Amber until the publish age clears the cooldown. The two axes are independently bypassable: `--allow-new` skips the cooldown halt, `--policy=allow` skips the script-tier review.

## Layer 5: Triage [#layer-5-triage]

The tiered gate that lets some scripted packages auto-approve. Active under `script-policy: "triage"` (or `lpm install --triage` / `--policy=triage`).

| Tier      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Green** | A curated catalog of shape patterns (`node-gyp rebuild`, `tsc`, `prisma generate`, `husky install`, etc.) plus identity-matched delegating installers — a `node install.js` / `node postinstall.js` / `node scripts/install.js` body (+ `.cjs`/`.mjs` variants) Greens when the package's `repository` URL or a `bin` entry plainly names the package. Auto-approves and runs in the [filesystem sandbox](/docs/reference/package-json-lpm#the-lpmscripts-block). |
| **Amber** | Doesn't fit a green pattern, doesn't match a red. Defers to layers 2/3/4 (trust manifest, provenance + cooldown, optional LLM advisor). Network binary downloaders (`puppeteer install`, `playwright install`, `cypress install`) live here by design.                                                                                                                                                                                                            |
| **Red**   | Hand-curated blocklist (pipe-to-shell, base64-decode-to-execute, nested package-manager installs, etc.). **Blocks unconditionally**. Never reaches the advisor.                                                                                                                                                                                                                                                                                                   |

Worst-wins reducer across phases — a package whose `preinstall` is green but `postinstall` is red is classified as red.

Identity widening is shape-keyed, not name-keyed: there is no per-package allowlist. The classifier looks at the script body's structure (exactly `node <reserved>.{js,cjs,mjs}`), reads the manifest's `repository` URL and `bin` entries, and matches by the package's base name (case-insensitive, with `.git` suffixes stripped). Generic / too-short base names (anything ≤ 2 chars, plus `js`, `lib`, `core`, `node`, `src`, `util`, `utils`) carry no identity payload and never widen.

**Cooldown defense-in-depth.** Identity widening refuses to fire when the configured [minimum release age](#layer-4-provenance--cooldown) is above zero AND the package's publish age is below the threshold (or unknown). This classifier gate is separate from the direct/root install halt — `lpm install --allow-new <pkg>` bypasses the cooldown halt but a recent-publish package whose identity matches still stays Amber and surfaces in the script-tier review. To opt out of both, set `minimum-release-age` to `0` (universally disabling cooldown also disables the defense-in-depth check) or use `--policy=allow`.

The sandbox itself is Seatbelt on macOS, landlock on Linux, AppContainer on Windows. Scripts can read the project, write to their own package directory, and write to anything declared in `package.json > lpm > scripts.sandboxWriteDirs`. Anything else is denied. Capability-widening (extra env vars, full project read, looser rlimits) requires explicit approval.

## Putting it together — `script-policy` × `sandbox-mode` [#putting-it-together--script-policy--sandbox-mode]

The two axes are **independent**. `script-policy` decides whether a script runs; `sandbox-mode` decides what it can do once running. Every combination is valid and configured through separate precedence chains.

| script-policy                                         | sandbox-mode | What happens to a scripted package                                                                                                                                                            |
| ----------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`deny`](#layer-1-lifecycle-script-gate-default-deny) | `default`    | Script blocked at the script-policy gate. After [`lpm approve-scripts`](/docs/packages/approve-scripts): runs with filesystem + env containment, outbound network allowed.                    |
| `deny`                                                | `strict`     | Same gate. After approval: runs with containment **+ outbound network denied**.                                                                                                               |
| `deny`                                                | `none`       | Same gate. After approval: runs unsandboxed (full host access including credential env).                                                                                                      |
| [`triage`](#layer-5-triage)                           | `default`    | L1-L3 tier decides per package. Greens auto-run with containment + network allowed. Ambers go to the [`lpm approve-scripts`](/docs/packages/approve-scripts) queue. Reds blocked permanently. |
| `triage`                                              | `strict`     | Same tiering. Whatever runs runs with containment **+ outbound network denied**.                                                                                                              |
| `triage`                                              | `none`       | Same tiering. Whatever runs runs unsandboxed.                                                                                                                                                 |
| `allow`                                               | `default`    | Every script runs with containment + network allowed.                                                                                                                                         |
| `allow`                                               | `strict`     | Every script runs with containment **+ outbound network denied**. Trust-the-lockfile + deny-install-time-network shape.                                                                       |
| `allow`                                               | `none`       | Every script runs unsandboxed — equivalent to `npm install`.                                                                                                                                  |

Each axis is configured independently:

* **`script-policy`** — `package.json > lpm > scriptPolicy` (project), `~/.lpm/config.toml > script-policy` (user), CLI flag (`--policy=<v>` / `--yolo` / `--triage`), or the [`lpm config scripts`](/docs/infra/config#setup-wizards) wizard.
* **`sandbox-mode`** — `./lpm.toml > [sandbox] mode` (project) or `~/.lpm/config.toml > [sandbox] mode` (user), CLI flag (`--strict-sandbox` / `--paranoid` / `--no-sandbox`), `LPM_STRICT_SANDBOX=1` env, or the [`lpm config sandbox`](/docs/infra/config#setup-wizards) wizard.

[`lpm approve-scripts`](/docs/packages/approve-scripts) grants permission to **run** — it never escalates the sandbox. An approved script under `sandbox-mode = strict` still runs without network access; an approved script under `sandbox-mode = none` runs with full host access.

### Optional LLM advisor [#optional-llm-advisor]

Layer 5 includes an opt-in advisor that uplifts Amber packages to AutoRun for the current install only. Configure via `triage-advisor`:

| Value              | Provider                                                                         |
| ------------------ | -------------------------------------------------------------------------------- |
| `"none"` (default) | No advisor — portable layers 1-4 only.                                           |
| `"claude-cli"`     | [Claude CLI](https://github.com/anthropics/claude-code) — local subprocess call. |
| `"codex"`          | OpenAI [Codex CLI](https://github.com/openai/codex) — local subprocess call.     |
| `"ollama"`         | Ollama-served local model — local HTTP call.                                     |

```toml title="~/.lpm/config.toml"
script-policy   = "triage"
triage-advisor  = "claude-cli"
```

```json title="package.json"
{ "lpm": { "scriptPolicy": "triage", "triageAdvisor": "claude-cli" } }
```

The two keys are **independent**. `script-policy: "triage"` with `triage-advisor: "none"` is the portable L1-4 baseline. `script-policy: "deny"` with any advisor setting still blocks unconditionally — the advisor never runs outside `triage`.

Precedence for `triage-advisor` (highest first): `package.json > lpm > triageAdvisor` > `~/.lpm/config.toml > triage-advisor` > default `"none"`.

**Properties:**

* **Ephemeral.** Approvals never write to disk as trust state. A second `lpm install` invokes the advisor again — the verdict is non-deterministic and LPM CLI does not pretend otherwise. Strict bindings via [`lpm approve-scripts`](/docs/packages/approve-scripts) exist for the "approve once, remember forever" use case.
* **Source-aware identity.** Approvals key on `(name, version, integrity)` so a registry copy of a package can never inherit approval from a workspace copy of the same coordinates.
* **Degrade-and-warn.** Missing binary, timeout, unparseable response → install continues with one stderr warning. Never fails on the advisor itself.
* **Prompt-injection defense.** Untrusted script body is fenced with a per-call random nonce; the verdict parser strict-matches `Approve` and loose-matches only the safe-default verdicts.
* **Worst-of-phases per package.** Approve only if every amber phase classified Approve; one Manual or Abstain blocks the whole package.

### Prompt context [#prompt-context]

The advisor sees a fixed template — role framing, verdict-format reminder, and a structured context block:

| Field              | Source                                                                             | Notes                                                                                                                                                                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Package`          | `name@version`                                                                     | Always present.                                                                                                                                                                                                                                                              |
| `Repository:`      | `package.json > repository` (URL or shorthand)                                     | Emitted only when the manifest carries a non-empty value. Treated as untrusted hint — pairs with the body's actual behaviour, never approves on URL alone.                                                                                                                   |
| `Lifecycle phase`  | `preinstall` / `install` / `postinstall`                                           | Always present.                                                                                                                                                                                                                                                              |
| `Script body`      | The phase's exact `package.json` value                                             | Fenced with `<<UNTRUSTED-SCRIPT-BEGIN-{nonce}>>` markers using a per-call random nonce.                                                                                                                                                                                      |
| `Referenced files` | Files the body delegates to (e.g. `install.js` when the body is `node install.js`) | Each file fenced with its own per-file nonce. Caps: depth 1 (no recursive `require` following), ≤ 32 KB per file (truncated with explicit marker), safe-relative paths only (no `..`, abs paths, env-var expansion), text-only (binary files fall back to no-context Amber). |

### Verdict cache [#verdict-cache]

Advisor verdicts are persisted at `$LPM_HOME/cache/l4-verdicts.json`. The next install of the same `(name, version)` with byte-identical script bodies skips the advisor round-trip and replays the cached verdict.

The cache key folds in the script identity, the prompt template hash, the provider slug, and the model version. Any of those changing produces a different key and the next invocation re-classifies — there is no manual invalidation step. Cached verdicts expire after 30 days by default. Hits return the cached `Approve` / `Manual` / `Abstain` value verbatim; the cache does not promote anything.

Tunables (env vars):

* `LPM_L4_CACHE=0` — disable the cache entirely. Useful for measurement runs where the comparison must include the round-trip cost.
* `LPM_L4_CACHE_PATH=<path>` — override the cache file location.
* `LPM_L4_CACHE_TTL_SECS=<n>` — override the 30-day default TTL.

Set up interactively:

```bash
lpm config scripts                 # pick deny / triage / allow
lpm config triage                  # pick none / claude-cli / codex / ollama
```

Both wizards accept `--set <value>` for non-interactive use.

## Typosquatting [#typosquatting]

Independent of the other layers, `lpm install` and `lpm add` run typosquatting detection against a curated list of popular npm packages. The detector covers edit-distance typos, adjacent transpositions (`axois` → `axios`), delimiter variants (`crossenv` → `cross-env`), and flag-shaped package names after `--`.

The check fires only when a suspicious direct package name is newly entering the project. Existing lockfile replays, including `CI=true` frozen installs, do not fail because the name was already reviewed into `lpm.lock`. Interactive terminals can switch to the suggested package or write a committed `lpm.toml` allow-list entry; `--json`, CI, non-TTY, and `--yes` runs fail closed with `error_code: "typosquat_suspected"`.

Operates entirely offline. See [`lpm.toml` typosquat policy](/docs/reference/lpm-toml#typosquat-policy) for intentional exceptions.

## npm firewall verdicts [#npm-firewall-verdicts]

LPM Firewall is an opt-in verdict check at `firewall.lpm.dev` for selected public npm package versions. It does not proxy npm metadata or tarballs: LPM CLI still fetches from the normal npm registry path, then batches selected `(name, version)` checks with LPM Firewall before tarballs are allowed through.

LPM Firewall is an LPM.dev Registry Pro/Org feature. Active `monitor` and `enforce` modes send LPM.dev Registry auth, so run `lpm login` first. Entitlement failures are warnings in `monitor` mode and blocking failures in `enforce` mode.

Configure it globally:

```bash
lpm config firewall --set monitor  # show what would block and continue
lpm config firewall --set enforce  # block malicious verdicts
```

`off` is the default. `monitor` is useful while evaluating coverage; `enforce` is the blocking mode. The legacy string `report` is still accepted as an alias for `monitor`. Once monitor or enforce mode is part of the approved machine posture, disabling or downshifting it is a guarded weakening through [`lpm security`](/docs/infra/security).

LPM Firewall also carries policy metadata for configurable enforcement. Recommended enforcement blocks trusted public malicious advisories and LPM Firewall AI-confirmed malware; it warns for LPM Firewall AI-agent control-surface policy, critical vulnerabilities, and static-only suspicious signals. See the [Firewall for npm guide](/docs/guides/firewall) for the full policy profile.

## Skill content (LPM.dev Registry only) [#skill-content-lpmdev-registry-only]

Packages on LPM.dev Registry that ship [agent skills](/docs/reference/ai-agent-skills) get a separate LLM security scan against the skill content at publish time. Consumers receive attested skill markdown — drift in the skill body invalidates the attestation.

This layer doesn't exist for non-LPM.dev Registry packages today (no per-tarball skills declaration yet).

## CI integration [#ci-integration]

The recommended CI gate stack:

```yaml
- run: lpm install --offline --strict-integrity
- run: lpm audit signatures
- run: lpm trust diff --assert-none
- run: lpm audit --fail-on vuln # confirmed vulns
- run: lpm audit --secrets --fail-on secrets # hardcoded credentials
- run: lpm licenses --fail-on copyleft --deny GPL-3.0 # license policy
- run: lpm query ":vulnerable:not(:built)" --assert-none
- run: lpm query ":eval:scripts" --assert-none # eval + scripts is a yellow flag
```

`lpm audit --fail-on` covers the broad-strokes gate; `lpm audit signatures` checks npm registry signatures; [`lpm licenses`](/docs/packages/licenses) gates license compliance; `lpm query --assert-none` covers your project's specific concerns; `lpm trust diff --assert-none` gates reviewed script-trust drift directly. Together they're cheap and they catch most real classes of supply-chain trouble.

## See also [#see-also]

* [`lpm audit`](/docs/packages/audit) — broad report CLI
* [`lpm query`](/docs/packages/query) — precision selectors
* [`lpm licenses`](/docs/packages/licenses) — license inventory and policy gates
* [`lpm approve-scripts`](/docs/packages/approve-scripts) — script-trust workflow
* [`lpm trust`](/docs/packages/trust) — drift inspection
* [`lpm install --policy`](/docs/packages/install#lifecycle-scripts) — script policy override
* [`package.json` "lpm.scripts"](/docs/reference/package-json-lpm#the-lpmscripts-block) — capability + sandbox config
