Exit codes
What each LPM CLI exit code means and how to handle them in CI.
LPM CLI 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 <file>, 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 CLI 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:
{
"schema_version": 1,
"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).
schema_version identifies the JSON document shape. On top-level error envelopes, version 1 is the shared error-envelope schema. Command success envelopes may also expose schema_version; those versions are command-specific.
When LPM CLI can suggest a concrete remediation command, the envelope includes next_steps:
{
"schema_version": 1,
"success": false,
"error": "authentication required",
"error_code": "auth_required",
"next_steps": [
{
"description": "Authenticate with the LPM.dev Registry",
"command": "lpm login"
}
]
}next_steps is optional and only appears when LPM CLI has a safe command to hand to a human, CI job, or agent.
Most errors put a human-facing string in error; rich diagnostics may use an object with a message field plus machine-readable context. Resolver failures use error_code: "resolution_failed" and include package, requested, required_by, kind, and candidate-summary fields; see Resolver failures.
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 |
resolution_failed | Dependency resolution could not find a compatible package version, platform, policy-allowed candidate, or complete solution graph |
typosquat_suspected | A new direct package name looks like a popular package and is not allow-listed in lpm.toml |
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 | A workspace-root engines.lpm / engines.node or required dependency 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 | Managed plugin tool (Oxlint, Biome, Rolldown) failed to download, 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 CLI 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 CLI 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 CLI task metadata envelope even for one script.
Workspace and multi-task aggregation DOES emit an LPM CLI 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 install --json finds an unresolvable dep | 1 (and error_code: "resolution_failed") |
lpm install axois --json hits the typosquat guard | 1 (and error_code: "typosquat_suspected") |
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 audit cannot complete its OSV or LPM.dev Registry vulnerability lookup | 1 (never reported as a clean audit) |
lpm audit --level severe uses an unknown severity | 2 (usage error) |
lpm tidy finds unused dependencies or phantom imports | 1 |
lpm tidy --fix removes all fixable unused dependencies and no findings remain | 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