LPM CLI

lpm rebuild

Run or rerun lifecycle scripts for installed packages.

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

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 and the lpm 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

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:

 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

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 and lpm approve-scripts.

Policy override

The CLI policy flags share semantics with lpm install — they govern which scripted packages are eligible to run.

FlagBehavior
--policy=deny (default)Filter to trustedDependencies only
--policy=allow / --yoloInclude every scripted package regardless of trust
--policy=triage / --triageFilter 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

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

ModeFilesystemEnvOutbound networkProject secret filesHow to engage
DefaultWrite-containment to the package's own directory + anything in package.json > lpm > scripts.sandboxWriteDirsCredential vars stripped (LPM_TOKEN, NPM_TOKEN, GITHUB_TOKEN, etc.)AllowedDenied.env*, .npmrc, .aws/, *.pem, … (see below)Nothing — this is the default
StrictSame as defaultSame as defaultDeniedDenied--strict-sandbox (or alias --paranoid), persistent via [sandbox] mode = "strict" in ~/.lpm/config.toml / ./lpm.toml, or env LPM_STRICT_SANDBOX=1
DisabledNo containmentNot scrubbedLPM_TOKEN, NPM_TOKEN, etc. all pass throughAllowedReadable — every file in project_dir reachable--no-sandbox (per-command, loud banner), or lpm config sandbox --set none (persistent)
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

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.

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

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

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

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

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 rootid_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

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:

package.json (per-project)
{
  "lpm": {
    "scripts": {
      "sandboxReadAllow": [".env", "services/api/.env"]
    }
  }
}
~/.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

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

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

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

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

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

FlagEffect
--allRebuild every scripted package (bypasses trust)
--dry-runPreview without executing scripts
--forceRe-run scripts even for already-built packages
--timeout <SECS>Per-script timeout (default 300 = 5 min)
--deny-allRefuse to run any scripts
--policy <deny|allow|triage>Override the policy for this invocation
--yoloAlias for --policy=allow
--triageAlias for --policy=triage
--strict-sandboxEngage strict sandbox (filesystem + env + outbound network denial)
--paranoidAlias for --strict-sandbox
--no-sandboxDrop all containment for this run — also drops env scrubbing. Mutually exclusive with --strict-sandbox / --paranoid / --sandbox-log
--sandbox-logmacOS only: observe-only sandbox
--no-engine-strictUse warning-only workspace-root engine checks (see lpm install)

Plus the global flags.

See also