LPM-cli

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.

CodeMeaning
0Success. The command completed without error.
1General 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.
2Usage error. lpm was invoked with no subcommand, or with arguments clap couldn't parse. The CLI's help text is printed before exit.
passthroughForwarded 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 with

This 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_codeEmitted when
auth_requiredOperation needs an authenticated session and none is present
session_expiredA stored session token has expired and self-heal didn't recover
forbiddenThe server accepted the token but rejected the operation (plan gate, ACL, etc.)
not_foundPackage, version, vault, or other addressable resource doesn't exist
rate_limitedServer (or a downstream like GitHub) returned a rate-limit response
networkUnderlying network failure (DNS, TCP, TLS) before the server replied
httpServer replied but the response was malformed or an unhandled HTTP status
registryRegistry-side error not covered by a more specific code
invalid_package_nameArgument doesn't match the package-name grammar (e.g., bad scope, invalid chars)
invalid_integrityAn SRI string is structurally malformed
integrity_mismatchA downloaded tarball's hash differs from the lockfile / manifest expectation
invalid_versionArgument isn't a valid semver
invalid_version_rangeArgument isn't a valid semver range
engine_mismatchengines.lpm or engines.node constraint isn't satisfied
scriptA lifecycle script (or lpm run script body) failed (script field also covers ScriptWithOutput)
taskTask-runner failure (graph cycle, missing dep, cache corruption)
workspaceWorkspace-config error (bad glob, member resolution, schema)
env_validationenvSchema validation in lpm.json failed (missing required var, format mismatch)
pluginPlugin-backed tool (Oxlint, Biome) failed to download, hash-verify, or launch
certmkcert / cert install error from lpm cert
tunnelTunnel relay / domain claim / WebSocket failure
storeContent-addressable store error (corruption, missing object, write failure)
ioFilesystem I/O failed below a higher-level operation
jsonJSON parse / serialize failure on a file or response body
security_floorA higher-authority floor or managed policy refused a weaker security posture
security_approval_storeLocal signed security state could not be verified; run lpm security repair to quarantine stale state
security_approval_requiredA guarded weakening needs explicit approval before LPM will proceed
self_update_pausedSelf-update was administratively paused for the resolved release channel
self_update_rate_limitedSelf-update hit a per-IP probe rate limit
exit_codeSubprocess 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 / bench workspace 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 as exit_code: null paired with an error string, distinguishing "ran and exited non-zero" from "couldn't even launch." For test / bench, a member with no installed runner (no vitest/jest/mocha for test, no vitest or scripts.bench for bench) shows up as a per-member detection failure rather than aborting the entire run.
  • lpm run emits 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-match is not set, JSON mode emits a successful zero-package envelope. To debug a failing member, re-run that member without --json.

Common scenarios

ScenarioExit code
lpm install succeeds0
lpm install finds an unresolvable dep1
lpm audit finds a vulnerability under the default --fail-on=all1
lpm audit finds nothing (or a finding excluded by --fail-on)0
lpm doctor finds an issue1
lpm doctor finds nothing0
lpm test runs vitest, vitest exits 00
lpm test runs vitest, vitest exits 11 (forwarded)
lpm install --no-sandbox --json without approval1 (and error_code: "security_approval_required")
A managed policy blocks lpm config sandbox --set none1 (and error_code: "security_floor")
lpm (no args)2
lpm install --frobnicate (unknown flag)2
lpm install blocked by missing auth1 (and error_code: "auth_required" in --json mode)

CI gating idioms

.github/workflows/ci.yml (excerpt)
- run: lpm install --offline --strict-integrity
- run: lpm audit --fail-on vuln
- run: lpm test
- run: lpm lint
- run: lpm fmt --check
- run: lpm check

Each step passes / fails on its own exit code. No special handling required.

See also