Built-in tools
How LPM ships Oxlint and Biome, plus how `lpm check`, `lpm pack`, and `lpm bundle` pick their backends.
LPM ships a small set of built-in tools — Oxlint for lpm lint, Biome for lpm fmt, a type-check wrapper for lpm check, a package-build wrapper for lpm pack, and a bundler wrapper for lpm bundle. Oxlint and Biome are plugin-backed: lazy-downloaded on first use, cached, and pinned per-project. lpm check --engine tsgo and lpm bundle use a separate managed engine cache under ~/.lpm/engines.
This page covers the design — how the plugin system works, where binaries land, how versions are resolved, and the security model.
Why lazy-download
Bundling 50+ MB of toolchains into the LPM binary would bloat every install and waste disk for users who don't lint. Bundling nothing and using npx adds 200–250 ms of resolution overhead per invocation. The middle path: download tools on first use, cache them globally, hash-verify, share across every project.
Measured cost: lpm lint is 78 ms (vs npx oxlint 250 ms — 3.2× faster). lpm fmt is 13 ms (vs npx biome 264 ms — 20× faster). The win is not the tool itself — it's the resolution overhead npx pays per invocation.
What ships as a plugin today
| Plugin | Backs | Source | Format |
|---|---|---|---|
oxlint | lpm lint | oxc-project/oxc GitHub Releases | .tar.gz archive |
biome | lpm fmt | biomejs/biome GitHub Releases | Direct binary |
Managed engines sit beside the plugin system:
tscshells out through the same PATH-injection mechanismlpm runuses. PATH-injection prependsnode_modules/.binand falls back to the systemPATH, so a globally-installedtsccan run too — but the project-local install is the right answer.lpm doctorflags the system-only case astypescript_missing_for_tsconfig(warn).tsdownstays project-local.lpm packwalksnode_modules/.binfrom the current package up to the workspace root and requires a reachabletsdownthere. LPM owns the workspace selection,--jsonenvelope, and stable top-level flags; the backend version still comes from the repo's ownpackage.json.tsgois a managed engine. LPM downloads the pinned platform tarball on first use, verifies its SRI integrity, preserves the full extracted layout, and reuses it from~/.lpm/engines/tsgo/<version>/<platform>/on later runs.rolldownis also a managed engine.lpm bundleinstalls the rootrolldownpackage,@rolldown/pluginutils,@oxc-project/types, and the current platform binding into one verified cached layout under~/.lpm/engines/rolldown/<version>/<platform>/.
That split is intentional: tsc and tsdown preserve repo-local version ownership, while tsgo and rolldown need managed layouts that do not fit the single-binary plugin model.
Managed engine layout
~/.lpm/engines/
├── tsgo/
│ └── 7.0.0-dev.20260525.1/
│ └── darwin-arm64/
│ ├── lib/tsgo
│ ├── lib/lib.d.ts
│ └── .lpm-engine.json
└── rolldown/
└── 1.0.2/
└── darwin-arm64/
├── bin/cli.mjs
├── node_modules/@rolldown/pluginutils/
├── node_modules/@oxc-project/types/
├── node_modules/@rolldown/binding-darwin-arm64/
└── .lpm-engine.jsonThe engine sidecar records the engine identity, platform, entry path, every package's install subdir and tarball metadata, per-package SHA-256s, and a hash of the extracted layout. Reuse re-hashes the installed layout before the engine runs, so a stray binary without a sidecar, a tampered lib/*.d.ts file, or a stale Rolldown package set is treated as a cache miss instead of trusted state.
On-disk layout
~/.lpm/plugins/
├── oxlint/
│ └── 1.58.0/
│ └── darwin-arm64/
│ ├── oxlint ← downloaded binary
│ └── .lpm-plugin.json ← sidecar (verification metadata)
├── biome/
│ └── 2.4.10/
│ └── linux-x64/
│ ├── biome
│ └── .lpm-plugin.json
└── .version-cache.json ← cached "latest version" mapEach plugin gets a directory keyed by {version}/{platform}. The platform segment is required: a $LPM_HOME shared across architectures (cross-arch CI runners, NFS, Docker bind mounts) would otherwise let one host install a binary the next host can't exec.
Multiple versions can coexist; multiple platforms within a version can coexist too. The .version-cache.json is the install-selection cache: each entry is the highest version that has been successfully verified-and-installed via lpm plugin update. Sticky by design — entries never expire by time, and the file is only written after a successful verified install (so a transient missing upstream .sha256, parse failure, or download failure cannot poison it and route every future invocation down a permanently-failing install path).
The .lpm-plugin.json sidecar records the plugin name, version, platform, asset URL, asset and on-disk binary checksums, and how the install was verified (bundled, upstream, or unverified-override). It is the source of truth for whether a cached binary may be reused — a bare binary with no sidecar, or a sidecar that fails any check, is treated as a cache miss and re-verified.
Version resolution
The lookup order, when lpm lint or lpm fmt starts:
- Pinned version in
lpm.json > tools.<name>— exact match. max(hardcoded, approved-cache)— whichever is newer, the version baked into the LPM binary or one a previous successfullpm plugin updateapproved.- Auto-download if the resolved version isn't installed for the current platform yet.
ensure_plugin never touches the network for version discovery — it only reads the install-selection cache. To check upstream for a newer release, run lpm plugin update; that command runs the discovery probe, prints the download and SHA-256 verification phases for human runs, and only then approves the verified new version into the cache.
Pin a version per-project:
{
"tools": {
"oxlint": "1.57.0",
"biome": "2.4.8"
}
}Update to the latest (sticky — the new "latest" is cached for future invocations):
lpm plugin update # all plugins
lpm plugin update oxlint # one pluginForce a fresh download of an existing version (useful for corrupted installs):
LPM_FORCE_TOOL_INSTALL=1 lpm lintChecksum verification
Every install — whether the registry's pinned latest_version, a lpm.json-pinned version, or a freshly-fetched lpm plugin update result — must pass a SHA-256 checksum gate. The gate tries three sources in order:
- Bundled checksum — SHA-256 baked into the LPM binary at build time. Used when the requested version equals the registry's hardcoded
latest_version. The fast path; no network round-trip. - Upstream sidecar — SHA-256 fetched from
<asset_url>.sha256(oxlint via cargo-dist, biome publishes per-asset sidecars). Used for pinned versions andlpm plugin updateresults. - Unverified override — only if
LPM_ALLOW_UNVERIFIED_PLUGINS=1is set. Recordsverification-source: unverified-overridein the sidecar; reuse requires the same env var on every subsequent invocation.
A mismatch at any stage is a hard error and the partial download is removed.
If neither bundled nor upstream verification is available — typically when the upstream sidecar is unreachable for an old or yanked release — and LPM_ALLOW_UNVERIFIED_PLUGINS is not set, the install fails with an actionable message:
refusing to install oxlint 1.42.0 for darwin-arm64: no bundled checksum and
upstream sidecar https://.../oxlint-aarch64-apple-darwin.tar.gz.sha256 is
unavailable. Update LPM (newer releases ship pinned checksums for newer plugin
versions), pin a different version, or set LPM_ALLOW_UNVERIFIED_PLUGINS=1
to install without verification.The bundled-checksum table is declared inline in the registry:
PluginDef {
name: "oxlint",
latest_version: "1.58.0",
// ...
checksums: &[
("darwin-arm64", "422756416c840b77212c673ae4aa88c8ef27e0e09b8ae51aeed21a2cef6b7191"),
("darwin-x64", "f4d49bb4a636c8a0810e4c5a56adb02be9cf448570292a102d8a8835f7ba1980"),
// ...
],
}Reuse from cache
The sidecar — not bare file existence — gates reuse. Every cache hit re-validates:
schema_versionmatches the current LPM binary,plugin_nameandversionmatch the request,platformmatches the current host,verification_sourceis compatible with the current trust posture (unverified-overrideis only honored whenLPM_ALLOW_UNVERIFIED_PLUGINS=1is set in the consuming process),- the binary's on-disk SHA-256 matches
binary_sha256recorded at install time (tamper detection).
Anything that fails gets treated as a cache miss and re-verified through the full download pipeline.
Supported platforms
Each plugin definition declares which platforms get a binary:
| Plugin | Platforms |
|---|---|
oxlint | darwin-arm64, darwin-x64, linux-x64, linux-arm64, win-x64 |
biome | darwin-arm64, darwin-x64, linux-x64, linux-arm64, win-x64 |
LPM detects the current platform and downloads the matching asset. Unsupported platforms get an immediate error rather than a download attempt.
Manage installed plugins
lpm plugin list # installed plugins, current vs latest
lpm plugin update # pull latest for every plugin
lpm plugin update oxlint # one plugin
lpm plugin remove biome # delete cached binariesSee lpm plugin for the full subcommand surface.
Why the registry is hardcoded
The plugin registry ships inside the LPM binary, not as a runtime-fetched config. New plugins land in an LPM release — there's no add-your-own-plugin path. This is deliberately conservative: tool distribution bugs ship with the LPM binary and pass through the team's QA, instead of a registry config that could change between two lpm lint invocations on the same machine.
If you want to use a tool that isn't in the plugin set today, install it as a normal dep and run it through your package.json scripts:
lpm install -D eslint
lpm run lint # runs the package.json `lint` scriptCI behavior
In CI, plugins behave the same way as locally — first invocation downloads, subsequent invocations reuse. Cache ~/.lpm/plugins/ in your CI to skip the download step:
- name: Cache LPM plugins
uses: actions/cache@v4
with:
path: ~/.lpm/plugins
key: lpm-plugins-${{ hashFiles('lpm.json') }}The cache key is the lpm.json hash — when tools versions change, the cache invalidates and re-downloads.
See also
lpm lint— backed by oxlintlpm fmt— backed by biomelpm check— project-localtscby default, managedtsgowhen selectedlpm pack— project-local tsdown with LPM workspace orchestrationlpm bundle— managed Rolldown enginelpm plugin— manage installed pluginslpm.jsontools — version pinning per project