lpm.json
Canonical schema reference for the lpm.json project file.
lpm.json sits next to package.json and provides LPM CLI-specific configuration that doesn't fit npm's manifest shape — runtime pinning, task graph, dev services, local-domain proxy config, tunnel domain, env-file mapping, env-var schema, and multi-registry publish settings.
The file is optional. Every field has a default. When lpm.json is missing, LPM CLI falls back to package.json semantics (scripts, engines, etc.).
LPM CLI reads lpm.json with a 16 MiB limit enforced before JSON parsing. Missing retains the optional fallback above; an oversized file is a path-specific error and aborts before a configured script, service, or child process is spawned. See local configuration size limits.
This page is the schema-by-field reference. For a usage-oriented overview of what lpm dev reads from lpm.json, see lpm dev.
Editor autocomplete
LPM CLI publishes a JSON Schema for lpm.json at https://cli.lpm.dev/schemas/lpm.json — auto-derived from the typed Rust struct that the CLI consumes. Add a $schema line to get inline validation and field completion in any editor that supports JSON Schema (VS Code, Cursor, JetBrains):
{
"$schema": "https://cli.lpm.dev/schemas/lpm.json",
"runtime": { "node": ">=22.0.0", "bun": "1.3.14" }
}Or emit the schema yourself from the locally-installed CLI:
lpm schema lpm.json # to stdout
lpm schema lpm.json -o lpm.schema.json # to a fileBoth lpm schema lpm.json and the published copy are regenerated from the same Rust struct that parses your manifest at install time, so they describe the surface the CLI actually accepts. The published copy is updated alongside CLI releases — between releases, lpm schema lpm.json is the source of truth for whichever version is on your machine.
The older https://lpm.dev/schemas/lpm.json URL remains a compatibility alias for existing repositories.
When stdout is an interactive terminal, schema output may use syntax color. Redirected or piped output remains plain pretty-printed JSON, including when color is forced globally.
Top-level shape
{
"$schema": "https://cli.lpm.dev/schemas/lpm.json",
"vault": "<uuid-v4>",
"vaultSync": { /* LPM-managed sync metadata */ },
"runtime": { "<runtime>": "<semver>" },
"tools": { "<tool>": "<version>" },
"https": true,
"cert": { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false },
"tunnel": { "domain": "<full-domain>" },
"proxy": { "host": "<local-hostname>", "port": 443, "httpRedirect": true },
"env": { "<script>": "<.env-file>" },
"envSchema": { "vars": { "<NAME>": { "required": true, "format": "url" } } },
"environments": { "<name>": { "extends": "<base>", "file": "<.env-file>" } },
"tasks": { "<name>": { /* TaskConfig */ } },
"services": { "<name>": { /* ServiceConfig */ } },
"publish": { /* PublishConfig */ }
}Every top-level field is optional and defaults to empty / null.
runtime
{ "runtime": { "node": ">=22.0.0", "bun": "1.3.14" } }| Field | Type | Default | Notes |
|---|---|---|---|
node | string | — | Managed Node version spec. Exact versions, prefixes, semver ranges, latest, and lts are accepted. |
bun | string | — | Managed Bun version spec. Exact versions, vX.Y.Z, bun-vX.Y.Z, latest, prefixes, and semver ranges are accepted. lts is rejected because Bun has no LTS channel. |
runtime.node takes precedence over .nvmrc and .node-version. package.json > engines.node validates the selected or PATH Node but never selects or installs one. runtime.bun is the only Bun runtime detection source; package.json > engines.bun is not enforced. Both runtime fields are auto-installed via lpm use.
When both runtimes are selected, script PATH is prepended as node_modules/.bin, managed Node bin, managed Bun bin, inherited PATH. runtime.bun exposes bun to scripts; it does not make lpm run invoke bun run.
Workspace members and dev services resolve selectors from their directories. A local selector replaces the same root runtime.
Each unselected runtime inherits from the root.
tools
{ "tools": { "oxlint": "1.57.0", "biome": "2.4.8", "rolldown": "1.1.3" } }| Field | Type | Notes |
|---|---|---|
<tool> | string | Pin a built-in managed plugin to a specific version. Honored by lpm lint, lpm fmt, and lpm bundle. |
Only oxlint, biome, and rolldown are managed plugin keys today. Any other key (tools.typescript, tools.tsgo, tools.eslint, …) is ignored by the resolver — lpm warns once per command invocation that the pin is ignored. To pin the lpm check engines, declare typescript or @typescript/native-preview in package.json > devDependencies like any other project dep.
When an Oxlint or Biome pin isn't already in ~/.lpm/plugins/, the next invocation downloads and verifies it against the upstream <asset_url>.sha256 sidecar. A Rolldown pin must already be the bundled floor or an approved version from lpm plugin update rolldown; lpm bundle does not check upstream during normal execution. See Built-in tools for the trust model.
https
{ "https": true }| Type | Default | Notes |
|---|---|---|
bool | null (unset) | When true, lpm dev serves over HTTPS by default. CLI --no-https overrides. |
cert
{ "cert": { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false } }| Field | Type | Default | Notes |
|---|---|---|---|
extraPermittedDns | string[] | [] | Extra validated DNS subtrees to permit on the constrained project intermediate. These are policy constraints, not extra browser SANs and not proxy routes. |
allowPublicDns | bool | false | Allows public DNS names in extraPermittedDns and local-domain host fields. Leave off for purely local names. |
extraPermittedDns entries must be bare multi-label names such as myapp.local; LPM CLI derives both the exact subtree and subdomain subtree for the project intermediate. Use --host or proxy.host / services.<name>.host for actual browser hostnames.
tunnel
{ "tunnel": { "domain": "acme-api.lpm.llc" } }| Field | Type | Notes |
|---|---|---|
domain | string | Stable tunnel domain (must already be claimed via lpm tunnel claim). Pro/Org only — free users get an ephemeral random domain. |
CLI flags: lpm dev --tunnel, lpm dev --domain <NAME>, lpm tunnel <port> <domain>.
proxy
{ "proxy": { "host": "app.localhost", "port": 443, "httpRedirect": true } }| Field | Type | Default | Notes |
|---|---|---|---|
host | string | — | Friendly top-level local hostname registered by lpm dev when the proxy daemon has an HTTPS listener. lpm dev also prepares constrained project cert-chain coverage and updates the hosts file when this host is not under .localhost. |
port | number | 443 | HTTPS proxy listen port used by lpm proxy start when no explicit listener flags are passed. 0 lets the OS pick a free port. |
httpRedirect | bool | true | Whether lpm proxy start also binds an HTTP-to-HTTPS redirect listener on port 80 when no explicit listener flags are passed. |
proxy.host and services.<name>.host are normalized to lowercase, must use a local TLD (.localhost, .test, .local, .internal, .home.arpa) unless cert.allowPublicDns is true, and cannot duplicate another local-domain host in the same file.
When local-domain config is present and no daemon is running, lpm dev starts lpm proxy start --detach automatically. With no explicit listener flags, that start path uses proxy.port and proxy.httpRedirect. With a running HTTPS listener, lpm dev prepares the project certificate chain for configured hosts through the normal trust consent flow, writes managed hosts-file entries for hosts that are not localhost / *.localhost, registers routes against final assigned service ports, and releases both route and hosts-file entries when the dev session exits. On Unix, LPM CLI uses sudo for the system hosts file when the current process lacks permission; on Windows, it asks for Administrator elevation through UAC. Use lpm hosts clean to remove orphaned managed blocks after interrupted sessions. The TLS daemon also prepares or refreshes project certificate chains during route registration, but it does not install the root CA into the trust store. The startup banner marks proxy endpoints as resolving until assignment; exact route lines print after registration, and dashboard service rows update to the final ports. LPM CLI does not inject certificate paths or framework HTTPS variables into the app; https / --https enables a separate LPM CLI-owned TLS frontend over the verified HTTP child endpoint.
vault
{ "vault": "7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b" }| Type | Notes |
|---|---|
| string | This stable identifier links the project to a vault row on LPM.dev Registry. New values use UUID v4. Safe legacy slug IDs remain supported. The CLI creates the value during the first lpm env command if the field is missing. Commit this value to git. The value is a pointer, not a secret. |
The CLI rejects empty values, path separators, .., leading ~, control characters, colons, and absolute drive prefixes. This validation prevents path traversal in the file fallback at ~/.lpm/vaults/<id>.enc.
If you remove the field, the next lpm env command creates a new UUID. The previous server data remains available under the old identifier.
LPM_VAULT_ID is read only by lpm env pull --oidc. A non-empty, trimmed value takes precedence over this local vault field and can bootstrap a checkout without one. If the variable is unset, empty, or whitespace-only, LPM CLI falls back to this field; the pull fails if neither source exists. Other lpm env operations ignore it. See Secrets vault — Per-project identity.
vaultSync
{
"vaultSync": {
"personalVersion": 7,
"personalSyncedAt": "2026-08-13T09:00:00Z",
"orgVersions": { "acme": 12 },
"orgSyncedAt": { "acme": "2026-08-13T09:01:00Z" },
"personalPlatformBindings": {
"https://registry.lpm.dev": {
"registryUrl": "https://registry.lpm.dev",
"principalId": "user_123"
}
},
"authorityCheckpoints": {
"personal": {
"https://registry.lpm.dev": {
"user_123": { "version": 7, "syncedAt": "2026-08-13T09:00:00Z" }
}
},
"organizations": {
"acme": {
"https://registry.lpm.dev": {
"org_123": { "version": 12, "syncedAt": "2026-08-13T09:01:00Z" }
}
}
}
}
}
}| Field | Type | Notes |
|---|---|---|
personalVersion | int | Non-authoritative summary of the last known personal cloud-vault version. |
personalSyncedAt | string | Non-authoritative summary of the last successful personal sync time in RFC 3339 format. |
orgVersions.<slug> | int | Non-authoritative summary of the last known org-shared cloud-vault version per organization slug. |
orgSyncedAt.<slug> | string | Non-authoritative summary of the last successful sync time per organization in RFC 3339 format. |
personalPlatformBindings.<registry> | object | Pins platform credentials to the canonical Registry URL and authenticated user ID that created them. |
authorityCheckpoints.personal.<registry>.<principal> | object | Authoritative personal-vault revision floor and optional last-sync time for one Registry and immutable user ID. |
authorityCheckpoints.organizations.<slug>.<registry>.<principal> | object | Authoritative organization-vault revision floor and optional last-sync time for one Registry, organization slug, and immutable organization ID. |
The CLI owns every vaultSync field. authorityCheckpoints provides the durable account binding and rollback floor used by personal and organization cloud operations. personalPlatformBindings prevents platform credentials created by one account from being reused by another account at the same Registry. The version and timestamp fields are non-authoritative summaries for status display and compatibility.
Do not edit or remove vaultSync. Removing its authority checkpoints resets local rollback and account-binding protection. A later operation can then establish a new principal and revision checkpoint for that checkout.
If a version does not match, the server returns 409 VaultConflictError. Then pull the remote values, resolve the conflict, and push again.
Commands use a project-local lock when they update lpm.json. Each command preserves unrelated fields and atomically replaces the file.
The CLI rejects a symbolic-link or junction lpm.json during an update. This rule prevents a cloned project from redirecting the write.
The parser accepts unknown fields for forward compatibility. The lpm doctor command reports unknown nested fields with their complete paths.
For example, lpm doctor reports the ignored typo proxy.httpRediect. An older CLI can still open configuration from a newer project.
env
{
"env": {
"dev": ".env.development",
"staging": ".env.staging",
"prod": ".env.production"
}
}Map of script name to environment file. lpm run dev loads .env.development. lpm run staging loads .env.staging.
CLI --env=<MODE> overrides tasks.<name>.env and this mapping. The flag also applies to lpm dev.
Existing environment files must resolve inside the project directory. LPM CLI rejects configured paths and symlinks that resolve outside the project.
envSchema
{
"envSchema": {
"vars": {
"DATABASE_URL": { "required": true, "format": "url" },
"API_KEY": { "required": true, "secret": true },
"LOG_LEVEL": { "default": "info", "pattern": "^(trace|debug|info|warn|error)$" }
}
}
}Schema for env-var validation. Drives the env check that runs before lpm run, lpm dev, lpm <file>, and lpm exec. Per-var fields:
| Field | Type | Notes |
|---|---|---|
required | bool | Fail if not set |
format | string | Built-in validator. One of: url, email, port, boolean, integer, hostname, ip |
pattern | string | A Rust regex that the value must match. ^ and $ require a complete-value match |
enum | string[] | The allowed values |
default | string | The default value for an unset variable. It must satisfy format, pattern, and enum |
secret | bool | Marks the value as sensitive. LPM CLI fully redacts the value in validation errors and logs |
client | bool | Allows client-side exposure for integrations that support public variables |
description | string | Adds variable details to errors and generated .env.example files |
Variable names must contain 1 to 256 ASCII letters, numbers, or underscores. The first character must be a letter or underscore.
An invalid name, regex, or default stops the command before it starts a child process. An empty default cannot satisfy a required variable. Validation errors escape terminal control characters from schema fields and non-secret values.
Skip the check per-invocation with --no-env-check.
environments
{
"environments": {
"base": { "file": ".env" },
"staging": { "extends": "base", "file": ".env.staging" },
"preview": { "extends": "staging", "file": ".env.preview" }
}
}Named environment definitions with inheritance. Useful when you have many .env.* variants and want to avoid duplicating shared keys. extends chains resolve transitively.
tasks.<name>
{
"tasks": {
"build": {
"command": "tsup",
"dependsOn": ["^build"],
"cache": true,
"outputs": ["dist/**"],
"inputs": ["src/**", "package.json"],
"env": "production"
}
}
}| Field | Type | Default | Notes |
|---|---|---|---|
command | string | falls back to package.json > scripts.<name> | Override the script body |
dependsOn | string[] | [] | Other tasks that must finish first. "build" = same package; "^build" = the same task in upstream workspace deps. |
cache | bool | false | Enable task caching. Requires outputs. |
outputs | string[] | [] | Globs of files this task produces (e.g. ["dist/**"]). Required when cache: true. |
inputs | string[] | sensible defaults* | Globs that invalidate the cache. |
env | string | — | Env mode for this task ("production" loads .env.production). |
cacheEnv | string[] | all inherited variables | Exact inherited variable names that affect cached output. An empty list selects none. Project-loaded values always remain inputs. |
Use cacheEnv to share cache entries across machines or CI jobs with different incidental environment values.
Include every inherited variable that affects the task. This field does not change the child environment or permit secret-bearing remote uploads.
*Default inputs: src/**, lib/**, app/**, pages/**, components/**,
scripts/**, bin/**, root *.{js,cjs,mjs,ts,tsx,jsx} files, package.json,
lpm.json, tsconfig.json, tsconfig.*.json, and *.config.{js,ts,mjs}.
A cached task also includes the cache identities of its transitive same-project dependencies. LPM hashes direct dependency outputs as task inputs. It validates each declared dependency output before a cache hit or cache publication.
For workspace task runs, each task includes the identity of every reachable
^task dependency. An upstream input or execution-contract change invalidates
the cache of each reachable downstream task.
Each cache key for a workspace member also includes the root package.json,
lpm.json, and pnpm-workspace.yaml files. Root lockfiles use the same rules
as member lockfiles.
The cache key includes the complete canonical package.json, including helper
scripts and module settings. It also includes lpm.lock and supported npm,
Yarn, pnpm, Bun, and Deno lockfiles.
Each inputs and outputs item must be a valid, project-relative glob. LPM
rejects absolute paths, parent traversal, and invalid glob syntax before it
starts a task.
On a cache hit, LPM restores the exact set of declared output files. It removes declared files that are absent from the cached result.
remoteCache
{
"remoteCache": {
"enabled": true,
"team": "acme",
"url": "https://lpm.dev/v8",
"signature": true,
"readOnly": false,
"env": {
"include": ["CI", "NODE_ENV"],
"exclude": ["*_TOKEN", "*_SECRET", "DATABASE_URL"]
}
}
}Hosted cache settings for tasks that already opt in with tasks.<name>.cache: true and outputs. LPM CLI checks the local task cache first, then the hosted cache, then runs the task and uploads the result when the run succeeds.
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Enable hosted cache reads and writes |
team | string | personal namespace | Organization slug/team namespace |
url | string | configured registry + /v8 | Remote cache endpoint. Hosts outside the configured registry origin require LPM_REMOTE_CACHE_TOKEN and LPM_REMOTE_CACHE_SIGNATURE_KEY. |
signature | bool | false | Require LPM_REMOTE_CACHE_SIGNATURE_KEY; downloads without a valid HMAC tag are treated as misses. Third-party cache hosts always require a signing key. |
readOnly | bool | false | Read hosted artifacts but skip uploads |
env.include | string[] | [] | Variable-name patterns explicitly allowed for remote uploads |
env.exclude | string[] | [] | Variable-name patterns that block remote uploads |
env.allowSecrets | bool | false | Permit secret-looking env var names after project opt-in |
Secret-looking loaded or inherited env vars (TOKEN, SECRET, PASSWORD, DATABASE_URL, *_KEY, and similar names) block remote uploads by default. Local task caching still works.
services.<name>
{
"services": {
"db": { "command": "docker compose up postgres", "readyPort": 5432, "readyTimeout": 60 },
"api": {
"command": "node server.js",
"port": 4000,
"dependsOn": ["db"],
"env": { "DATABASE_URL": "postgres://localhost:5432/myapp" }
},
"web": { "command": "next dev", "port": 3000, "primary": true, "host": "web.app.localhost" }
}
}| Field | Type | Default | Notes |
|---|---|---|---|
command | string | required | Shell command to run |
port | number | — | Port the service listens on. If omitted for a host-only service or the implicit or explicit primary service, lpm dev auto-assigns and persists one |
dependsOn | string[] | [] | Services that must be ready before this one starts |
readyPort | number | falls back to port | TCP port to poll for readiness |
readyUrl | string | — | HTTP URL to poll for readiness — first 2xx wins |
readyTimeout | number | 30 | Seconds before a readiness failure stops the initial startup |
env | object | {} | Extra env vars injected into this service only |
restart | bool | false | Auto-restart on crash with exponential backoff |
primary | bool | false | Its verified endpoint receives HTTPS, tunnel, network, and browser-open handling |
host | string | — | Friendly local hostname registered by lpm dev against the final assigned service port when the proxy daemon has an HTTPS listener. lpm dev also prepares constrained project cert-chain coverage and updates the hosts file when the host is not under .localhost. With port, localhost:<port> keeps working; without port, lpm dev injects the assigned port as PORT. |
cwd | string | project root | Working directory, relative to project root |
publish
{
"publish": {
"registries": ["lpm", "npm"],
"lpm": { "name": "@lpm.dev/owner.pkg" },
"npm": { "name": "@scope/pkg", "access": "public", "tag": "latest", "otpRequired": true },
"github": { "name": "@owner/pkg", "access": "public" },
"gitlab": { "name": "pkg", "access": "public", "projectId": "12345", "registry": "https://gitlab.com" }
}
}Top-level
| Field | Type | Default | Notes |
|---|---|---|---|
registries | string[] | ["lpm"] | Targets a single lpm publish invocation hits. Values are lpm, npm, github, gitlab, or an https:// custom registry URL. Custom URL entries require an exact token from lpm login --login-registry <URL> --token <T>. CLI flags (--lpm, --npm, --github, --gitlab, --publish-registry) override. |
publish.lpm
| Field | Type | Notes |
|---|---|---|
name | string | Override the lpm.dev package name. Must match @lpm.dev/owner.pkg. |
publish.npm
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | package.json > name | Override the npm package name (e.g., "@scope/pkg"). Required if package.json > name starts with @lpm.dev/. |
access | "public" | "restricted" | "public" | Visibility. npm provenance requires effective access to be "public". |
tag | string | "latest" | dist-tag for the published version |
registry | string | https://registry.npmjs.org | Custom npm-compatible registry URL. Non-default registries require an exact token from lpm login --login-registry <URL> --token <T>; NPM_TOKEN and lpm login --npm auth are only used for the default npm registry. |
otpRequired | bool | false | Prompt for OTP before the first publish attempt (saves a round-trip) |
publish.github
| Field | Type | Notes |
|---|---|---|
name | string | Override the GitHub Packages name. Must be scoped (@owner/pkg). |
access | "public" | "restricted" | Visibility |
publish.gitlab
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Override the GitLab Packages name |
access | "public" | "restricted" | — | Visibility |
projectId | string | required | GitLab project ID — needed to construct the registry URL |
registry | string | https://gitlab.com | GitLab instance URL |
Precedence with other config
For fields with multiple sources, the precedence chain is:
- CLI flag (highest)
lpm.jsonpackage.json(where applicable —engines,scripts)~/.lpm/config.toml(user-level, where applicable)- Built-in default (lowest)
See also
lpm dev— the most common consumer, with usage-oriented exampleslpm run— readstasksandenvlpm publish— readspublishpackage.json"lpm" key — sibling config block- JSON Schemas — published schema URLs and
lpm schema lpm.toml— project-level CLI defaults~/.lpm/config.toml— user-level CLI defaults