Exit codes
What each LPM exit code means and how to handle them in CI.
LPM follows the conventional Unix exit-code convention: 0 for success, non-zero for failure. The CLI is intentionally narrow about which non-zero codes it produces — most failures exit 1. A few specific scenarios warrant a different code.
| Code | Meaning |
|---|---|
0 | Success. The command completed without error. |
1 | General error. Network failure, registry error, parse failure, validation failure, missing dependency, audit found vulnerabilities, etc. The error message on stderr (or in --json output) describes the specific cause. |
2 | Usage error. lpm was invoked with no subcommand, or with arguments clap couldn't parse. The CLI's help text is printed before exit. |
| passthrough | Forwarded subprocess exit code. Commands that wrap external processes (lpm run, lpm exec, lpm dlx / lpx, lpm test, lpm bench, lpm lint, lpm fmt, lpm check) exit with the wrapped command's exit code. |
Subprocess passthrough
When a wrapped tool exits non-zero — your test suite fails, your script crashes, oxlint reports an error — LPM forwards the exit code so CI gates see the right signal:
lpm test
echo "tests exit code: $?" # whatever vitest / jest exited withThis is implemented via LpmError::ExitCode(code) flowing up to the top of main, which calls std::process::exit(code). No code mangling — lpm test exiting 1 from vitest looks identical to running vitest directly.
In --json mode
With --json, every error path also emits a structured JSON object on stdout before exiting:
{
"success": false,
"error": "registry: not found",
"error_code": "not_found"
}The error_code field is the canonical short name for the error variant. Use it for programmatic dispatch in CI / agents (the error message text is human-facing and subject to change; error_code is the stable contract).
Full error_code catalog
error_code | Emitted when |
|---|---|
auth_required | Operation needs an authenticated session and none is present |
session_expired | A stored session token has expired and self-heal didn't recover |
forbidden | The server accepted the token but rejected the operation (plan gate, ACL, etc.) |
not_found | Package, version, vault, or other addressable resource doesn't exist |
rate_limited | Server (or a downstream like GitHub) returned a rate-limit response |
network | Underlying network failure (DNS, TCP, TLS) before the server replied |
http | Server replied but the response was malformed or an unhandled HTTP status |
registry | Registry-side error not covered by a more specific code |
invalid_package_name | Argument doesn't match the package-name grammar (e.g., bad scope, invalid chars) |
invalid_integrity | An SRI string is structurally malformed |
integrity_mismatch | A downloaded tarball's hash differs from the lockfile / manifest expectation |
invalid_version | Argument isn't a valid semver |
invalid_version_range | Argument isn't a valid semver range |
engine_mismatch | engines.lpm or engines.node constraint isn't satisfied |
script | A lifecycle script (or lpm run script body) failed (script field also covers ScriptWithOutput) |
task | Task-runner failure (graph cycle, missing dep, cache corruption) |
workspace | Workspace-config error (bad glob, member resolution, schema) |
env_validation | envSchema validation in lpm.json failed (missing required var, format mismatch) |
plugin | Plugin-backed tool (Oxlint, Biome) failed to download, hash-verify, or launch |
cert | mkcert / cert install error from lpm cert |
tunnel | Tunnel relay / domain claim / WebSocket failure |
store | Content-addressable store error (corruption, missing object, write failure) |
io | Filesystem I/O failed below a higher-level operation |
json | JSON parse / serialize failure on a file or response body |
security_floor | A higher-authority floor or managed policy refused a weaker security posture |
security_approval_store | Local signed security state could not be verified; run lpm security repair to quarantine stale state |
security_approval_required | A guarded weakening needs explicit approval before LPM will proceed |
self_update_paused | Self-update was administratively paused for the resolved release channel |
self_update_rate_limited | Self-update hit a per-IP probe rate limit |
exit_code | Subprocess passthrough — the exit code came from a wrapped tool (vitest, oxlint, etc.); see Subprocess passthrough above |
For single-package subprocess passthrough (lpm test, lpm bench, single-package lpm lint / fmt / check), --json preserves the wrapped command's own stdout — LPM does not wrap it. This keeps the "single JSON result" contract for tools that already emit JSON. lpm run --json is different: it emits an LPM task metadata envelope even for one script.
Workspace and multi-task aggregation DOES emit an LPM envelope on stdout, but the envelope shape varies by command:
lpm lint/fmt/check/test/benchworkspace mode (--all/--filter/--affected) capture each member's subprocess stdout/stderr and surface them inside the envelope only on failure (truncated at 10 MB). Spawn / config / plugin / detection failures appear asexit_code: nullpaired with anerrorstring, distinguishing "ran and exited non-zero" from "couldn't even launch." Fortest/bench, a member with no installed runner (no vitest/jest/mocha fortest, no vitest orscripts.benchforbench) shows up as a per-member detection failure rather than aborting the entire run.lpm runemits a metadata-only envelope for single-script, multi-script, and workspace runs — each task/member's status, exit code, duration, cache hit, skip reason — but never the captured subprocess stdout/stderr. If a workspace filter matches no packages and--fail-if-no-matchis not set, JSON mode emits a successful zero-package envelope. To debug a failing member, re-run that member without--json.
Common scenarios
| Scenario | Exit code |
|---|---|
lpm install succeeds | 0 |
lpm install finds an unresolvable dep | 1 |
lpm audit finds a vulnerability under the default --fail-on=all | 1 |
lpm audit finds nothing (or a finding excluded by --fail-on) | 0 |
lpm doctor finds an issue | 1 |
lpm doctor finds nothing | 0 |
lpm test runs vitest, vitest exits 0 | 0 |
lpm test runs vitest, vitest exits 1 | 1 (forwarded) |
lpm install --no-sandbox --json without approval | 1 (and error_code: "security_approval_required") |
A managed policy blocks lpm config sandbox --set none | 1 (and error_code: "security_floor") |
lpm (no args) | 2 |
lpm install --frobnicate (unknown flag) | 2 |
lpm install blocked by missing auth | 1 (and error_code: "auth_required" in --json mode) |
CI gating idioms
- run: lpm install --offline --strict-integrity
- run: lpm audit --fail-on vuln
- run: lpm test
- run: lpm lint
- run: lpm fmt --check
- run: lpm checkEach step passes / fails on its own exit code. No special handling required.
See also
lpm audit --fail-on— gate exit code by finding typelpm fmt --check— exit non-zero on unformatted files- Global flags —
--jsonfor structured failure output