# lpm rebuild (/docs/packages/rebuild)



```bash
lpm rebuild [packages...]
```

Bare default-policy `lpm install` downloads and links dependencies while blocking dependency lifecycle scripts. `lpm rebuild` is the manual and retry surface for those scripts: it selectively runs `preinstall`, `install`, and `postinstall` based on the active [script policy](/docs/packages/install#lifecycle-scripts) and the project's `trustedDependencies`. Installs that opt into allow, auto-build, or trusted-package builds use the same pipeline automatically.

Matches `npm rebuild` / `pnpm rebuild` semantics.

## What runs [#what-runs]

`lpm rebuild` executes exactly three phases, in order:

1. `preinstall`
2. `install`
3. `postinstall`

Other lifecycle names — `prepare`, `prepublishOnly`, `preuninstall`, `uninstall`, `postuninstall` — are **recognized** by LPM CLI for detection (a package declaring them shows up in [`lpm audit`](/docs/packages/audit) and the [`lpm approve-scripts`](/docs/packages/approve-scripts) review queue) but **never executed** by the install pipeline. Approving a package whose only declared script is `prepare` is harmless: nothing runs.

## Examples [#examples]

```bash
lpm rebuild                                  # rebuild every trusted package
lpm rebuild esbuild                          # rebuild a specific package
lpm rebuild esbuild sharp                    # multiple
lpm rebuild --dry-run                        # preview, run nothing
lpm rebuild --force                          # re-run scripts even for already-built packages
lpm rebuild --all                            # bypass trust policy — DANGEROUS
lpm rebuild --policy=allow                   # allow every scripted package this run
lpm rebuild --triage                         # tiered gate (greens auto, ambers/reds blocked)
lpm rebuild --timeout 600                    # 10-minute per-script timeout
```

Human output reports each completed lifecycle phase as one compact row and keeps the script command body out of the transcript:

```bash
› Rebuilding lifecycle scripts for trusted packages
  ✓ esbuild@0.25.1  postinstall
  ✓ sharp@0.34.2    install

✓ Completed 2 scripts
  skipped: 1 blocked package
  hint: run lpm approve-scripts

✓ Done · rebuild finished in 1.04s
```

When you name packages explicitly, every requested package must be installed and must have an executable lifecycle script. A typo or an installed package with no `preinstall` / `install` / `postinstall` exits non-zero instead of silently succeeding as an empty rebuild.

## Default selection [#default-selection]

With no arguments and no `--all`, `lpm rebuild` runs scripts for packages whose name appears in:

* `package.json > lpm > trustedDependencies` (legacy array form), OR
* `package.json > lpm > trustedDependencies` (rich-map form, with matching integrity + script hash), OR
* A scope glob in `package.json > lpm > scripts.trustedScopes` (e.g. `@myorg/*`)

Manage that allowlist with [`lpm trust`](/docs/packages/trust) and [`lpm approve-scripts`](/docs/packages/approve-scripts).

## Policy override [#policy-override]

The CLI policy flags share semantics with [`lpm install`](/docs/packages/install#lifecycle-scripts) — they govern which scripted packages are eligible to run.

| Flag                           | Behavior                                                                         |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `--policy=deny` (default)      | Filter to `trustedDependencies` only                                             |
| `--policy=allow` / `--yolo`    | Include every scripted package regardless of trust                               |
| `--policy=triage` / `--triage` | Filter to trusted-only, but tiered greens are auto-promoted into the rebuild set |

`--all` overrides the filter under every policy.

`--policy`, `--yolo`, and `--triage` are mutually exclusive.

## Sandbox [#sandbox]

Lifecycle scripts run inside a **filesystem sandbox** by default — Seatbelt on macOS, landlock on Linux, AppContainer + Job Object on Windows. Three modes:

| Mode         | Filesystem                                                                                                     | Env                                                                       | Outbound network | Project secret files                                                                     | How to engage                                                                                                                                                |
| ------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Default**  | Write-containment to the package's own directory + anything in `package.json > lpm > scripts.sandboxWriteDirs` | Credential vars stripped (`LPM_TOKEN`, `NPM_TOKEN`, `GITHUB_TOKEN`, etc.) | Allowed          | **Denied** — `.env*`, `.npmrc`, `.aws/`, `*.pem`, … (see [below](#project-secret-files)) | Nothing — this is the default                                                                                                                                |
| **Strict**   | Same as default                                                                                                | Same as default                                                           | **Denied**       | **Denied**                                                                               | `--strict-sandbox` (or alias `--paranoid`), persistent via `[sandbox] mode = "strict"` in `~/.lpm/config.toml` / `./lpm.toml`, or env `LPM_STRICT_SANDBOX=1` |
| **Disabled** | No containment                                                                                                 | **Not scrubbed** — `LPM_TOKEN`, `NPM_TOKEN`, etc. all pass through        | Allowed          | **Readable** — every file in `project_dir` reachable                                     | `--no-sandbox` (per-command, loud banner), or `lpm config sandbox --set none` (persistent)                                                                   |

```bash
lpm rebuild --strict-sandbox     # filesystem + env + network containment for this run
lpm rebuild --paranoid           # alias — same behavior
```

Strict network denial coverage is platform-asymmetric: macOS Seatbelt denies every socket family; Linux landlock denies TCP `connect(2)`/`bind(2)` and seccomp-bpf denies direct UDP / raw / AF\_PACKET / AF\_NETLINK sockets; Windows uses AppContainer + Windows Filtering Platform. On platforms where strict isn't fully implementable, the sandbox refuses to start unless the `allow_degraded` config is set.

## Native build cache [#native-build-cache]

On macOS and Linux, strict rebuilds automatically reuse successful native
dependency builds. The first eligible build publishes the completed package
tree under `~/.lpm/store/v2/builds/<build-key>/`; an identical reinstall can
restore that tree instead of rerunning `node-gyp`, `prebuild-install`,
esbuild's installer, or sharp's installer. There is no cache flag to enable.

```bash
lpm install
lpm rebuild --strict-sandbox --all   # miss: build and publish

rm -rf node_modules
lpm install                          # pristine rematerialization
lpm rebuild --strict-sandbox --all   # hit: restore the cached build
```

Cache lookup happens **after** normal trust and policy selection. A cached
artifact never makes an untrusted package eligible to execute or restore.

### Eligibility and isolation [#eligibility-and-isolation]

An artifact is cacheable only when all of these are true:

* The package uses the v2 content-addressable store.
* The lifecycle command belongs to a recognized native-build family:
  `node-gyp`, `node-gyp-build`, `node-gyp-build-optional-packages`,
  `prebuild-install`, `electron-rebuild`, `cmake-js`, esbuild's `install.js`,
  or sharp's installer. Recognition is based on shell command position and
  package identity; mentions in comments or unrelated arguments do not
  qualify.
* The effective sandbox posture is fully strict and outbound network access is
  denied.
* The project did not widen sandbox read/write paths or lifecycle
  capabilities.
* The host is macOS or Linux. Windows rebuilds still run normally, but native
  artifact reuse stays disabled until the AppContainer backend has the same
  cacheable input boundary.

During an eligible miss, persistent writes are narrowed to the package tree;
temporary writes use a stable private directory inside the locked graph entry,
so its path is deterministic for equivalent builds. The dependency graph,
system toolchain, and local Node header cache remain readable. User npm caches,
project-wide write roots, and the broader default-sandbox write set are not
part of the cacheable build.

If any eligibility check fails, LPM CLI runs the lifecycle command through the
normal rebuild path and reports the package as bypassed in JSON metrics.

### Cache key and invalidation [#cache-key-and-invalidation]

The build key includes:

* package tarball integrity and v2 graph identity
* canonical dependency edges, aliases, peer targets, source identities, and
  patch fingerprints across the installed graph
* ordered lifecycle phases and exact command bodies
* OS, architecture, libc, and CPU-feature compatibility
* Node version, modules ABI, N-API, V8 version, and runtime executable identity
* strict sandbox posture and allowed-input policy
* the complete environment exposed to the lifecycle script, including custom
  variables plus LPM CLI's effective `PATH`, `INIT_CWD`, `TMPDIR`, `TMP`, and
  `TEMP` values
* compiler, linker, SDK, CMake, Ninja, invoked build-executable, operating
  system, dynamic-linker cache, and Node-header fingerprints when discoverable
* compiler sysroot/search configuration, pkg-config databases, and
  discoverable OS package-manager, Homebrew, macOS receipt, and Xcode state

The complete host-toolchain fingerprint is persisted under
`~/.lpm/cache/metadata/native-toolchains/v1/`. On later rebuilds, LPM CLI first
validates compiler identities, package-database generations, pkg-config
directories, SDK selection, Homebrew/macOS receipts, Rust toolchain selection,
and the selected Node header cache. An unchanged snapshot avoids rerunning the
compiler and package-manager probes. Changed, expired, oversized, or malformed
state fails closed to the complete fingerprint pass; snapshots have a one-hour
maximum lifetime and `lpm cache clean metadata` removes them. Project-visible
executables are still fingerprinted for every key and are never executed by
the host probe path.

Because absolute lifecycle paths are observable inputs, reuse across separate
checkouts requires those paths to match. This is normally true for repeated CI
runs that restore the LPM CLI store into the same workspace path; a differently
located checkout receives a different key rather than reusing an artifact that
could contain the previous path. The complete environment is also exact: CI
values such as `GITHUB_RUN_ID`, `GITHUB_RUN_NUMBER`, and
`GITHUB_RUN_ATTEMPT` intentionally invalidate reuse unless the workflow
normalizes them before invoking LPM CLI.

The host fingerprint is conservative, not a claim that native builds are
fully hermetic. It detects normal compiler, SDK, package-manager, header-cache,
and pkg-config changes without hashing every readable byte under `/usr` or a
macOS SDK on every lookup. An administrator can still replace an unmanaged
system header or library in place without updating discoverable package state.
After such a manual host mutation, use
`lpm rebuild --strict-sandbox --all --force` to rebuild from pristine package
contents; reference-aware cache pruning can remove the older artifact
afterward.

Changing any keyed input invalidates both the artifact and the package's local
`.lpm-built` state. LPM CLI rematerializes the pristine package object before the
new command runs, so stale outputs cannot leak into the replacement artifact.
Corrupt or incomplete artifacts are treated as misses.

`--force` always bypasses lookup, rebuilds from pristine source, and replaces
the artifact for the resulting key.

### Measured performance [#measured-performance]

On macOS arm64 with Node 22.22.1 and the sharp `NativeToolchain` fixture (10
release samples), a local hit completed in **59.07 ms median** versus a
**173.10 ms** strict artifact miss: **2.93× faster** and **65.88% lower wall
time**. Recreating a CI checkout at the same stable path while retaining the
warm LPM CLI store completed in **58.49 ms median** (**2.96× faster**, **66.21%
lower**). Warm host-key derivation took 15–24 ms. The first complete host
fingerprint took 126 ms before its validated snapshot existed, so these
steady-state numbers do not hide the cold security cost. Every scenario used
the same strict sandbox, and the median retained artifact used 503,469 bytes.
The benchmark and raw methodology live in the rust-client repository under
`bench/scripts/native-build-cache-benchmark.mjs` and
`bench/perf-results/native-build-cache-20260712-macos-arm64-native-toolchain.*`.

### Project secret files [#project-secret-files]

**On macOS and Linux, the sandbox blocks lifecycle script reads of project secrets by default; opt in via `script-read-allow` if your build needs a specific secret file.**

Lifecycle scripts have read access to most of `project_dir` so they can locate sources, configs, generated files, etc. But certain conventional secret-file locations are denied even though they sit inside the project tree — a malicious or compromised script has no legitimate reason to read your `.env`, `.aws/credentials`, or a `*.pem` private key.

The deny list covers:

* **dotenv conventions** — `.env`, `.env.local`, `.env.production(.local)`, `.env.development(.local)`, `.env.staging(.local)`, `.env.test(.local)`, `.envrc`
* **package-manager auth files** — `.npmrc`, `.yarnrc`, `.yarnrc.yml`, `.pypirc`
* **shell / HTTP auth files** — `.netrc`, `_netrc`, `.git-credentials`, `.htpasswd`
* **git credential URLs** — `.git/config`, `.git/credentials`
* **SSH keys at project root** — `id_rsa`, `id_ecdsa`, `id_ed25519`, `id_dsa` (and `.pub` variants)
* **cloud / credential dirs** — `.ssh/`, `.aws/`, `.kube/`, `.gcp/`, `.config/gcloud/`, `.terraform/`, `secrets/`, `secret/`
* **filetype patterns in the bounded project scan** — `*.pem`, `*.key`, `*.pfx`, `*.p12`, `*.tfstate`, `*.tfvars`, `*.tfvars.json`

On macOS the denial fires as a Seatbelt `(deny file-read* ...)` rule layered after the project-dir allow (SBPL last-match-wins). On Linux the sandbox enters a fresh user and mount namespace, makes mount propagation private, and bind-mounts `/dev/null` over each enumerated secret file before the command executes. Reads then return zero bytes.

Linux treats that overlay as required whenever at least one protected file needs masking. Namespace creation, ID mapping, propagation setup, and every bind mount must all succeed. If a hardened host blocks any step — for example through `user.max_user_namespaces=0`, AppArmor or SELinux policy, or container restrictions — LPM CLI refuses to execute the lifecycle script. It does not fall back to the broad Landlock project-read access. Projects with no matching secret files, or whose matches are all explicitly allowlisted, skip the namespace setup and continue normally.

The protection is a bounded pre-spawn snapshot, not a dynamic filesystem monitor. Files created after enumeration are not added to the overlay. The extension scan stops below four nested project levels and prunes generated or dependency-heavy directories such as `node_modules`, `.git`, `target`, `dist`, and `build`; explicit literal paths such as `.git/config` are still checked separately. Linux does not follow symlink entries while enumerating protected files. Windows AppContainer has no equivalent project-secret overlay today.

#### Opting specific files back in [#opting-specific-files-back-in]

Some builds genuinely need access — `prisma generate` reads `DATABASE_URL` from `.env`, a custom build script may read a project-rooted CA bundle, etc. Two surfaces let you exempt named files:

```json title="package.json (per-project)"
{
  "lpm": {
    "scripts": {
      "sandboxReadAllow": [".env", "services/api/.env"]
    }
  }
}
```

```toml title="~/.lpm/config.toml (per-user, applied to every project)"
script-read-allow = [".env", ".npmrc"]
```

Per-project (`package.json > lpm > scripts > sandboxReadAllow`) and per-user (`~/.lpm/config.toml > script-read-allow`) lists are **unioned** at install time. Every entry must resolve to a path **inside** `project_dir` — traversal escapes (`..`) and absolute paths pointing outside the project are rejected with an actionable error naming the config source and key. Duplicate entries across the two lists are deduplicated. These entries are the explicit read opt-in: an allowlisted secret remains readable, and a project whose matching secrets are all allowlisted does not need the Linux namespace overlay.

### Disabling the sandbox [#disabling-the-sandbox]

```bash
lpm rebuild --no-sandbox
```

A **single flag** drops both containment AND env scrubbing — scripts run with full host access including credential-bearing env (`LPM_TOKEN`, `NPM_TOKEN`, `GITHUB_TOKEN`). Reserve for debugging a sandbox false-positive that `sandboxWriteDirs` can't express.

`--no-sandbox` is mutually exclusive with `--strict-sandbox`, `--paranoid`, and `--sandbox-log`. Persistent off-mode goes through `lpm config sandbox --set none` instead — the CLI flag is the per-command escape.

### Diagnostic mode [#diagnostic-mode]

```bash
lpm rebuild --sandbox-log
```

macOS-only. Rule triggers are **logged** via `sandboxd` but **not enforced**. View accesses via `log show --last 5m --predicate 'senderImagePath CONTAINS "Sandbox"'` and grep for the script's pid. **Not a safety signal** — a clean run under `--sandbox-log` does not mean the script would pass under the full sandbox; it only means the logged accesses were visible for review.

On Linux and Windows, `--sandbox-log` errors at sandbox init (no native observe-only primitive) and points at `--no-sandbox` for the same debug case.

## `--force` flag [#--force-flag]

```bash
lpm rebuild --force esbuild
```

By default, packages whose scripts have already run (and whose `script_hash` hasn't changed) are skipped. `--force` ignores that record and re-runs them. Useful after deleting `node_modules` if you want fresh `postinstall` work without a full reinstall.

## `--deny-all` [#--deny-all]

```bash
lpm rebuild --deny-all
```

Refuses to run **any** scripts, even trusted ones. Useful as a CI smoke test: "would my install want to execute scripts?" Or paired with `package.json > lpm > scripts.denyAll: true` for a project-wide kill switch.

## JSON output [#json-output]

`lpm rebuild --json --dry-run` emits a dry-run envelope with `dry_run: true` and `packages[]`, including when the set is empty. A live `lpm rebuild --json` emits `success`, `built`, `failed`, and `build_cache`; an empty rebuild reports `success: true`, `built: 0`, `failed: 0` with zeroed cache counters.

`build_cache` contains `eligible`, `hits`, `misses`, `bypassed`,
`local_state_hits`, `scripts_avoided`, `restored_bytes`, and
`lifecycle_ms_avoided`. Its `timings_ms` object breaks out fingerprint
preparation, keying, lookup, restore, pristine rematerialization, and
publication time. A `hit` means an artifact was restored; a
`local_state_hit` means the already-materialized package's keyed build marker
still matched and no restore was needed.

Errors such as a missing `lpm.lock`, a failing lifecycle script, or an explicitly requested package that is not rebuildable emit `success: false` in the top-level JSON error envelope and exit non-zero.

## Flags [#flags]

| Flag                             | Effect                                                                                                                                    |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `--all`                          | Rebuild every scripted package (bypasses trust)                                                                                           |
| `--dry-run`                      | Preview without executing scripts                                                                                                         |
| `--force`                        | Re-run scripts even for already-built packages                                                                                            |
| `--timeout <SECS>`               | Per-script timeout (default `300` = 5 min)                                                                                                |
| `--deny-all`                     | Refuse to run any scripts                                                                                                                 |
| `--policy <deny\|allow\|triage>` | Override the policy for this invocation                                                                                                   |
| `--yolo`                         | Alias for `--policy=allow`                                                                                                                |
| `--triage`                       | Alias for `--policy=triage`                                                                                                               |
| `--strict-sandbox`               | Engage strict sandbox (filesystem + env + outbound network denial)                                                                        |
| `--paranoid`                     | Alias for `--strict-sandbox`                                                                                                              |
| `--no-sandbox`                   | Drop all containment for this run — also drops env scrubbing. Mutually exclusive with `--strict-sandbox` / `--paranoid` / `--sandbox-log` |
| `--sandbox-log`                  | macOS only: observe-only sandbox                                                                                                          |
| `--no-engine-strict`             | Use warning-only workspace-root engine checks (see [`lpm install`](/docs/packages/install#engines-enforcement))                           |

Plus the [global flags](/docs/commands#global-flags).

## See also [#see-also]

* [`lpm install`](/docs/packages/install#lifecycle-scripts) — installs packages and can auto-run this build pipeline when policy permits
* [`lpm approve-scripts`](/docs/packages/approve-scripts) — manage the trust allowlist
* [`lpm trust`](/docs/packages/trust) — inspect drift in the allowlist
* [`package.json` "lpm.scripts"](/docs/reference/package-json-lpm#the-lpmscripts-block) — capability and sandbox config
