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:
preinstallinstallpostinstall
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 timeoutHuman 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.04sWhen 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), ORpackage.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.
| 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
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) | 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) |
lpm rebuild --strict-sandbox # filesystem + env + network containment for this run
lpm rebuild --paranoid # alias — same behaviorStrict 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 buildCache 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'sinstall.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, andTEMPvalues - 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 root —
id_rsa,id_ecdsa,id_ed25519,id_dsa(and.pubvariants) - 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:
{
"lpm": {
"scripts": {
"sandboxReadAllow": [".env", "services/api/.env"]
}
}
}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-sandboxA 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-logmacOS-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 esbuildBy 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-allRefuses 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
| 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) |
Plus the global flags.
See also
lpm install— installs packages and can auto-run this build pipeline when policy permitslpm approve-scripts— manage the trust allowlistlpm trust— inspect drift in the allowlistpackage.json"lpm.scripts" — capability and sandbox config