LPM CLI

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.

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 <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 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:

{
  "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_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
resolution_failedDependency resolution could not find a compatible package version, platform, policy-allowed candidate, or complete solution graph
typosquat_suspectedA new direct package name looks like a popular package and is not allow-listed in lpm.toml
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_mismatchA workspace-root engines.lpm / engines.node or required dependency 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)
pluginManaged plugin tool (Oxlint, Biome, Rolldown) failed to download, 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 CLI 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 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 / 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 install --json finds an unresolvable dep1 (and error_code: "resolution_failed")
lpm install axois --json hits the typosquat guard1 (and error_code: "typosquat_suspected")
lpm audit finds a vulnerability under the default --fail-on=all1
lpm audit finds nothing (or a finding excluded by --fail-on)0
lpm audit cannot complete its OSV or LPM.dev Registry vulnerability lookup1 (never reported as a clean audit)
lpm audit --level severe uses an unknown severity2 (usage error)
lpm tidy finds unused dependencies or phantom imports1
lpm tidy --fix removes all fixable unused dependencies and no findings remain0
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