LPM CLI

lpm env

Store local secrets, load project environments, and sync values with LPM.dev Registry or deployment platforms.

lpm env manages project secrets and environment variables. Use it for local development, deployment settings, and CI credentials.

lpm env [<EXTRA>...]

# Store a value, inspect its name, then load the project environment
lpm env set API_TOKEN=example-token
lpm env list
lpm env print --format=json

Bare lpm env lists the default local secrets with masked values.

Run lpm env from anywhere in the project. It acts on the nearest directory at or above the current one that has a package.json or an lpm.json, stopping before your home directory, and prints that directory when it isn't the current one. File paths that you type, such as lpm env export local.env, --output=.env, or --out-dir, stay relative to the directory where you ran the command. Without a project around it, lpm env uses the current directory. It refuses a directory whose .lpm would be the LPM home, such as your home directory.

CommandsPurpose
set, get, list, deleteManage stored local secrets.
import, export, init, ls, copyMove local values and inspect environments.
print, check, validate, exampleLoad values, validate them, or generate an example file.
generateGenerate typed server and client modules from the schema.
push, pull, diff, list-remote, logSync or inspect encrypted cloud environments.
share, pair, unpair, rotate-key, rotate-sharing-keyManage access and encryption keys.
connect, statusConnect deployment platforms and inspect drift.
oidcAuthorize CI jobs to retrieve an environment.

Local storage does not require an account. Cloud operations use LPM.dev Registry and require an eligible plan and a logged-in session.

Project configuration

lpm.json can map scripts to environments, define file inheritance, and validate variables.

lpm.json
{
  "env": {
    "dev": ".env.development",
    "deploy": ".env.production"
  },
  "environments": {
    "base": ".env",
    "development": { "extends": "base", "file": ".env.development" },
    "production": { "extends": "base", "file": ".env.production" }
  },
  "envSchema": {
    "vars": {
      "API_TOKEN": { "required": true, "secret": true },
      "PORT": { "default": "3000", "format": "port" },
      "LOG_LEVEL": { "default": "info", "enum": ["debug", "info", "warn", "error"] }
    }
  }
}
lpm env set --env=development API_TOKEN=development-token
lpm env set --env=production API_TOKEN=production-token
lpm env print --env=dev --format=json
lpm env check --json
lpm run dev

--env=dev resolves the configured alias to development. The CLI also accepts canonical names such as production.

The env map selects an environment for the matching script in lpm run. An explicit run-time --env overrides that selection.

environments defines file inheritance. Parent values load first, then child values override them. Local stored secrets override file values.

envSchema supports scalar rules, browser visibility, independent CI storage, conditions, variable groups, and context-specific scopes. Use min and max for exact integer bounds, minLength and maxLength for Unicode lengths, and protocols for URL schemes. requiredWhen accepts equality or presence against another declared variable. Root groups enforce allOrNone, exactlyOne, or atLeastOne relationships. LPM CLI injects every default before it checks these relationships. See the schema reference for limits and examples. pattern uses regular expressions. Add ^ and $ for a complete-value match.

Supported formats are url, port, email, boolean, integer, hostname, and ip. Defaults apply to missing variables and must satisfy the same rules as explicit values. The CLI checks every default before it reads values.

Set empty to "missing" (default), "allow", or "reject". "missing" triggers defaults for empty values. "allow" validates empty values against the other rules. "reject" rejects empty values even when a default exists. Required values must always be non-empty.

Unknown fields and duplicate definitions are errors. A schema supports at most 4096 variables. Schema text permits tabs and line breaks. It rejects other C0 controls, DEL, and C1 controls.

Cloud project metadata must also fit within 256 KiB of UTF-8 JSON. Compact rules fit at the count limit; long descriptions, patterns, or literal values can reach the byte limit earlier. Secret rules cannot contain literal default, defaultsIn, or enum values. Use lpm env set or an environment file. The url format rejects malformed URL syntax. The email format accepts ASCII dot-atom mailboxes with dotted DNS hostnames.

secret: true redacts validation diagnostics and enables masking in GitHub Actions output. It does not hide values from print or plaintext exports. client: true marks a value as safe for client exposure. GitHub Actions storage uses ci, independently of browser visibility. Use envSchema.clientPrefixes to declare additional public prefixes.

When upgrading, add client: true to every declared browser-visible name. Built-in prefixes are NEXT_PUBLIC_, VITE_, PUBLIC_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, and NUXT_PUBLIC_. A public rule cannot declare secret: true. Declaration errors always stop project commands, including commands with --no-env-check.

Project commands reject malformed or invalid configuration. Account-level operations, such as sharing-key rotation, do not require project configuration.

When a project defines services, --service must select one of those names. Without configured services, --service supplies an abstract context for scoped rules.

Local file management

lpm env set [--env=<name>] KEY=VALUE [KEY2=VALUE2...]
lpm env get [--env=<name>] <key> [--reveal]
lpm env list [--env=<name>] [--reveal]
lpm env delete [--env=<name>] <key> [<key>...]

lpm env set API_URL=https://api.example.com 'API_TOKEN=example with spaces'
lpm env get API_TOKEN
lpm env get API_TOKEN --reveal
lpm env list --env=production --json
lpm env delete OLD_TOKEN

Names use letters, digits, and underscores. A name cannot start with a digit. Empty values, such as OPTIONAL_TOKEN=, are valid.

The CLI validates all assignments before it stores any value. Unknown flags, missing flag values, duplicate environment selectors, and extra operands produce errors.

get and list read stored secrets. They do not merge dotenv files or schema defaults. Values remain masked unless you add --reveal.

Each named environment has its own values. Explicit reads of a named environment do not merge the default environment.

Local secrets use protected OS storage or encrypted files in the LPM home (~/.lpm, or $LPM_HOME when set), depending on the platform and available backend. Each LPM home has its own local secrets. See local secrets for storage, project identity, and recovery.

Import, export, and copy

lpm env import [--env=<name>] <file> [--overwrite]
lpm env export [--env=<name>] <file> [--ci]
lpm env copy <source-env> <target-env> [--overwrite]

lpm env import --env=production .env.production
lpm env import .env --overwrite
lpm env export --env=production .env.backup
lpm env copy development staging
lpm env copy development staging --overwrite

Import and copy preserve existing destination keys by default. --overwrite replaces matching keys and preserves destination-only keys. cp is an alias for copy.

Ordinary export writes stored secrets. export --ci writes the resolved project environment, including files and schema defaults. If ordinary export has no stored values, it leaves the destination unchanged.

Exports contain plaintext. Before LPM CLI writes an export inside the project, it makes sure that Git ignores the file:

  • A destination that Git tracks stops the export. Untrack it with the git rm --cached command that the error names, then run the command again.
  • If Git doesn't already ignore the file, LPM CLI appends an anchored rule for that path, such as /.env.backup, to .gitignore. export --ci adds the rule to .git/info/exclude instead, which leaves the work tree clean.
  • If the rule can't be written, or a later rule keeps the file from being ignored, the export stops and writes nothing. Outside a Git repository, a rule that can't be written is a warning.

The file is written atomically with owner-only permissions (0600 on Unix, a protected access list on Windows). LPM CLI replaces a symbolic link at the destination without changing its target. In the project, it refuses a parent directory that is a link or doesn't exist. A destination outside the project is followed through links: in a Git work tree, it is kept out of Git the same way, with the rule in the .gitignore at that work tree's root; in no work tree, it is written without changes to an ignore file.

import keeps its source file out of Git the same way, after the import, where the file is once links are followed: it adds a .gitignore rule when Git doesn't already ignore the file. A failure, or a source file that Git already tracks, is a warning. Linked or non-regular ignore files are refused. Human output prints these warnings; JSON output lists them in warnings. See secret files in a project.

Dotenv files support quoted values, escaped double quotes, and multiline double-quoted values. $NAME references remain literal. Each input file has a 16 MiB size limit.

Initialize and inspect environments

lpm env init [--force]
lpm env ls

lpm env init --json
lpm env ls --json

init runs without a prompt. It imports files for known environments and creates empty named environments when their configured files are absent. Configured file paths are relative to the project directory. init keeps each file it finds out of Git, as import does. Known environments come from the configuration and local storage. The command does not import every arbitrary .env.* file in the directory. Invalid configured aliases or environment names are skipped and reported. Valid environments continue to import. init --json includes a skipped array with each rejected identity and its validation error.

If an environment already contains stored values, init preserves it. --force imports matching file values again and overwrites matching stored keys. ls shows environment names, aliases, variable counts, and available update or sync metadata.

With an env schema, ls --json adds these fields to each environment row:

FieldMeaning
schemaTotalNumber of declared variables. Groups do not add to this count.
schemaValidDeclared variables without a value or group error in the resolved environment.
schemaErrorAn error that prevents evaluation of this row, such as an invalid identity or an unreadable file.

Optional absent variables count as valid. A group error invalidates each affected variable once. Defaults and inherited values participate in evaluation. If evaluation fails, schemaValid is zero and schemaError explains the failure. Other environment rows remain available. These counts use the same variable rules as env check.

lpm env print [--env=<name>] [--stage=<stage>] [--service=<name>] [--format=<format>] [--schema-only] [--client-only] [--ci]

lpm env print --env=production --format=json
lpm env print --env=production --schema-only
lpm env print --format=shell > project-env.sh
. ./project-env.sh

Without file inheritance, loading uses .env, .env.local, .env.<name>, then .env.<name>.local. Later files override earlier files. Stored values then override file values. Schema defaults fill missing values.

If the selected local environment is missing or empty, resolved loading uses the default stored environment. A nonempty named environment does not merge missing keys from the default environment. This fallback applies to resolved loading, including scripts. It does not apply to explicit local get or list.

Inherited process values take precedence over dotenv values. Stored secrets can override inherited process values during script execution. print returns project values. It does not dump the complete inherited process environment.

Runtime-hook names, such as NODE_OPTIONS, LD_PRELOAD, and BASH_ENV, are excluded from resolved project values. LPM CLI also removes inherited runtime hooks before it starts scripts.

FormatResult
dotenv (default), envDotenv assignments with quoted and escaped values.
shell, shShell-quoted export commands.
jsonA JSON object sorted by key.
dockerUnquoted assignments for Docker --env-file. Multiline values are omitted with a comment.
github-actions, github, gh, ghaA shell script that masks secret-marked values and writes to $GITHUB_ENV.

--schema-only emits only keys declared in a nonempty envSchema. It fails if that schema is absent or empty. --client-only emits only declared browser-visible keys and also requires a nonempty schema. --json selects JSON output for print. It conflicts with --ci and non-JSON formats.

print validates the selected environment before it emits values. To correct a validation error, update the value or its schema rule. For a single script run, lpm run --no-env-check skips value validation and schema defaults. Declaration errors in lpm.json always stop the command.

Validate and generate examples

lpm env check
lpm env schema
lpm env validate [--strict]
lpm env example [--env=<name>]

lpm env check --json
lpm env schema --json
lpm env validate --strict --json
lpm env example --env=production
lpm env example --json

check validates every known resolved environment against envSchema. With --env, it validates only the selected environment. It exits nonzero if any selected environment is invalid. Use --stage and --service on check or print to evaluate scoped requirements and defaults. The default stage is runtime; service context is absent unless selected. Static service env values apply before validation. See scope selectors for matching rules and execution stages. JSON output includes per-environment results and a success field.

schema checks only declarations and imports. It does not load environment values. Its JSON output reports diagnostic codes, source pointers, origins, and a content fingerprint. See reusable fragments and presets for composition rules and limits.

validate compares names in .env.example with the default stored secrets. It ignores example values, named environments, files, and schema rules. Missing keys fail validation. --strict also rejects extra stored keys. JSON fields success and valid agree with the exit status.

example writes schema descriptions and defaults to .env.example. --env=production changes the output to .env.production.example. The file holds no values and is meant to be committed. LPM CLI creates it with your usual file permissions and keeps the mode of an existing file. The file contains global defaults and scope comments. Scoped defaults do not become assignments. Secret literal defaults are invalid. example --json returns a preview with the filename and content. It does not write a file.

Generate typed runtime modules

lpm env generate
lpm env generate --adapter=nextjs --env=production --stage=build
lpm env generate --adapter=vite --out-dir=src/env.generated
lpm env generate --check --json

generate resolves envSchema, including fragments and presets, and writes seven files to env.generated at the project root, or to --out-dir:

  • server.js and server.d.ts
  • client.js and client.d.ts
  • package.json, .gitattributes, and .lpm-env-generated.json

--out-dir is relative to the directory where you run the command and must stay inside the project. A path that leaves the project, a .. after a directory name, and linked directories are rejected. Its parent directories must already exist. The generated .gitattributes disables Git line-ending conversion to preserve each file hash. LPM replaces only a complete generated directory whose files still match its ownership manifest. If you edit a generated file or add an extra file, generation fails and preserves the directory. For a directory with a valid ownership manifest, the error names extra or modified files. Remove extra files, including .DS_Store, only from that generated directory. Restore modified generated files from version control, or move the generated directory aside. If the directory has no valid ownership manifest, choose a nonexistent output path or move the directory aside. This rule also applies to an existing empty directory. Generation never claims ownership of an existing ordinary directory. Exclude the output directory from formatters, linters that rewrite files, and code transformation tools. For Prettier, add env.generated/ to .prettierignore, or use your configured --out-dir path.

--out-dir uses portable names on every operating system. Each component must avoid trailing dots, trailing spaces, control characters, and : < > " | ? * or backslashes. Windows reserved names are invalid in any case and with extensions, including con.json, Nul.txt, COM¹–COM³, LPT¹–LPT³, CONIN$, and CONOUT$. Use a relative path inside the project. The parent directory must exist. Use a nonexistent output path, or move the generated directory aside before regeneration.

The server module validates every declared variable. The client module contains only rules with client: true. Client modules omit private declarations, patterns, and relationships that reference private variables. Import the module for the application's execution context directly.

import { getEnv } from "./env.generated/server.js";

// Call this function at application startup.
const env = getEnv();
const port: number = env.PORT;

createEnv(input) accepts explicit raw strings in every adapter:

import { createEnv, EnvError } from "./env.generated/server.js";

try {
  const env = createEnv({ API_TOKEN: "runtime-token", PORT: "3000" });
  console.log(env.PORT);
} catch (error) {
  if (error instanceof EnvError) {
    console.error(error.issues);
  }
}

Validated output is immutable. Integer values use bigint, ports use number, and boolean values use boolean. Other formats use strings. Optional values can be undefined. Declarations require TypeScript with an ES2020 target or later. Errors contain variable names, stable codes, and constraint names. They omit input values and original exception causes.

AdapterAutomatic getEnv() source
defaultServer reads process variables. Client uses explicit input or declared defaults.
nodeReads process variables in each module.
nextjsEmits static process.env.NEXT_PUBLIC_* reads for public keys and process reads for private server keys.
viteEmits static import.meta.env.VITE_* reads for public keys and process reads for private server keys.

Next.js and Vite require their framework prefixes on every client declaration. Framework builds can embed public values in the application bundle. Regenerate modules and rebuild the application after public schema changes.

--env, --stage, and --service select the fixed context in the generated modules. When services are configured, --service must name a configured service. Without configured services, it can select an abstract scope context. Environment aliases resolve to canonical names. The default stage is runtime. Use identical selections for generation and --check. Generation reads schema declarations; it does not read stored secrets or environment files. Supply runtime values through getEnv() or createEnv(input).

--check compares every generated byte without rewriting files or creating project locks. If generated output is stale, rerun the same command without --check to preserve its adapter, context, and output path. It fails for missing, edited, or stale output. Generation errors explain the recovery action. Declaration errors must be corrected before regeneration. Schema formatting, descriptions, and CI classification do not change generated identity. Schema rules, selected context, adapter, and generator version do change identity.

Generation limits the complete output to 8 MiB and each pattern source to 32 KiB. Compiled patterns permit 65,536 aggregate states and 262,144 aggregate edges per module. Runtime validation permits 1 MiB per declared value, 4 MiB total input, and 10 million work units. Limit failures report env.resource_limit at runtime. They do not report a pattern mismatch.

Cloud sync

push uploads encrypted local environments to LPM.dev Registry. pull replaces local environments with the remote payload. push and share preserve named empty environments. Projects with no keys can also sync. Ordinary sync stores ciphertext and wrapped keys. OIDC setup explicitly enables server decryption for CI.

The project's env ID stays in lpm.json > vault, which you commit. Each checkout records its own sync state, such as the versions it last saw and the accounts it syncs with, in .lpm/env-sync.json. Git never sees that file. Earlier releases kept this state in lpm.json > vaultSync. The next push or pull moves it, or drops it when Git has it committed, in the project's repository or one around it, since a committed one isn't this checkout's. lpm doctor warns until then. Commit lpm.json after the move.

lpm env push                              # encrypt + push the local env project
lpm env pull                              # pull + decrypt remote → local
lpm env diff                              # diff local vs remote (default env)
lpm env diff staging                      # diff local staging vs remote staging
lpm env diff staging production           # diff two local environments
lpm env share --org <org-slug>            # share the env project with an org
lpm env pair <CODE>                       # pair this device via dashboard code (interactive confirmation)
lpm env pair <CODE> --yes                 # skip the comparison prompt for a trusted dashboard command
lpm env unpair                            # revoke all browser pairings for this account
lpm env log                               # last 50 audit log entries
lpm env rotate-key                        # rotate the complete personal cloud payload
lpm env rotate-key --org <org-slug>       # rotate an organization content key
lpm env rotate-sharing-key                # rotate your X25519 sharing key (interactive, step-up reauth)

Free accounts have local storage. Pro plans include personal sync. Active Organization and Enterprise plans include organization sync and sharing. Each push uses a fresh content key and advances the remote revision, even with unchanged values. Personal projects use independent project keys, protected by a device-local root scoped to the Registry origin and account. Ordinary sync keeps that root local. Existing remote projects require the correct root and authenticated decryption before a write.

The next push or browser save migrates supported older personal payloads with fresh project and content keys. Migration disables prior CI access and revokes issued credentials. Enable it again with the complete policy and --allow-server-decryption. Older clients cannot write the retired format. See environment encryption and access.

Sync and discover projects

lpm env push --yes
lpm env pull --yes
lpm env pull --org=acme --json
lpm env list-remote
lpm env list-remote --org=acme

list-remote lists accessible cloud projects. ls-remote is an alias. Personal pushes and pulls prompt in human output mode unless you pass --yes or -y. JSON mode skips these prompts. An organization pull replaces local environments immediately, without a confirmation prompt. Export any local values that you need to keep first.

Personal push --force can overwrite the remote revision despite a version conflict. It still authenticates and decrypts the current remote payload. Use it only after you review the local and remote values. Organization share --force has a narrower purpose: it recreates a deleted organization project already bound to this checkout. It does not replace an existing remote project or establish a new binding.

Recover from a metadata warning

Invalid or oversized project metadata does not block an authorized cloud write of encrypted values. The server drops invalid environment entries and entries that depend on them. It validates the schema and its configuration together. If that unit is invalid, it retains the last valid schema and configuration.

Current clients show a warning after the successful write. CLI JSON output includes a warnings entry with code env_metadata_dropped, a message, and a hint. The dashboard shows a banner with the reason and an upgrade link. The audit records the metadata omission with the successful value write. Older clients still sync encrypted values. Use the dashboard to see their metadata warnings.

Upgrade LPM CLI or the desktop app, then correct the declarations in lpm.json:

lpm env schema --json
lpm env check
lpm env push --yes

For an organization project, repeat the approved share --org=<slug> operation with the corrected metadata. A valid schema write clears the stored warning. A write that omits the schema leaves the warning in place. Authentication failures, invalid encrypted payloads, and revision conflicts still stop the write.

Review organization recipient keys

A first share, or a changed recipient set, requires an explicit review of recipient identities and public-key fingerprints. The command prints the current set and its digest. After you check those bindings, retry with that exact digest:

lpm env share --org=acme --accept-recipient-keys=<64-character-digest>
lpm env rotate-key --org=acme --accept-recipient-keys=<64-character-digest>

An incorrect or stale digest cannot authorize the upload. An unchanged, previously trusted set does not need another acceptance flag.

Pair a dashboard browser

Create a pairing code on the dashboard Secrets page. Then run this command on the CLI machine that holds your env key:

lpm env pair <CODE>

The browser and CLI exchange temporary P-256 public keys. Both clients derive an eight-digit comparison number from the shared ECDH secret.

The number has two groups of four digits. Make sure that the number in the terminal equals the dashboard number.

If the numbers are equal, type y. The Registry relays the public keys, but it cannot calculate the shared secret.

The pairing code expires after five minutes. Use lpm env unpair to revoke all paired browsers.

Personal pairing transfers this device's root for the Registry and account, plus an existing legacy key for older projects. It does not transfer keys to another CLI machine. Independent device roots can open different projects. Pairing another device replaces the browser's personal pairing. Re-pair the original device to reopen its projects. Use a current CLI and a fresh code. Retired personal pairing sessions cannot authorize current-format writes.

Start pairing from an organization project for organization access. Personal and organization pairings use separate keys and browser storage.

Organization pairing requires the CLI machine with your registered account sharing key. It does not register or rotate a sharing key.

The browser can decrypt projects shared with your account. Owners, administrators, and maintainers can save changes. Other members have read access.

The Registry checks membership, subscription access, and key versions before a save. If rotation is required, an owner or administrator must run lpm env rotate-key --org=<slug> first.

After a sharing-key change, pair the browser again and ask an owner or administrator to refresh your environment access.

CAUTION: The --yes flag skips the comparison. Use this flag only for a command that you copied from your trusted dashboard.

Rotate encryption keys

lpm env rotate-key
lpm env rotate-key --org=acme
lpm env rotate-sharing-key

Personal rotate-key retrieves the current cloud payload and encrypts it with a fresh content key. For migrated projects, it preserves the project key and CI access. Use lpm env oidc disable to rotate the project key and revoke CI access. It preserves all environments and project metadata, including empty environments and values absent from the local copy. A version conflict leaves the previous remote payload unchanged. After a timeout, run the command again to retrieve the current remote version. After a confirmed personal update, local checkpoint failures appear as warnings. JSON retains success: true and includes a warnings array. Restore local storage or the original project's lpm.json and write access. Then pull the latest revision.

Organization rotate-key requires an owner or administrator with current project access. It encrypts the current cloud payload again and grants the new content key to current members with registered sharing keys. A membership or key change during the operation causes a conflict. Review the current recipients before you retry.

After you remove a member, rotate the organization content key. Rotation prevents the old key from decrypting future ciphertext. It cannot erase keys or plaintext that a former member copied. The dashboard shows Needs share when a member needs renewed access.

rotate-sharing-key replaces your account sharing key. Every organization project that you can access needs renewed sharing from an owner or administrator. The command requires a terminal. It has no --yes or non-interactive mode.

  1. Review the affected organization projects.
  2. Type ROTATE to confirm.
  3. Enter your password and, if enrolled, your authenticator code.
  4. After rotation, ask an owner or administrator to share each affected project again.

The command retains a pending key until the server acknowledges rotation. After an interruption, rerun it to resume safely. The Registry sends security notifications to your account and affected organization owners and administrators.

A new account sharing key requires step-up authentication before registration. A machine with a different local key cannot silently replace the registered key. Deliberate rotation from that machine changes the account key again and requires renewed organization sharing. After any sharing-key change, pair your dashboard browser again.

Deployment platform integrations

Supported platforms are Vercel, Coolify, Fly.io, Railway, and GitHub Actions. Application values travel directly between LPM CLI and the platform. LPM.dev Registry stores the encrypted platform credential, connection configuration, authorization scope, and operation counts.

# Vercel
lpm env connect vercel --project=<id> --linked-env=production --target=production
lpm env push --to vercel                           # merge current env into Vercel
lpm env push --to vercel --env=production --clean  # remove Vercel-only values too
lpm env pull --from vercel --env=production        # import readable values into the local env

# Coolify
lpm env connect coolify --url=https://coolify.example.com \
  --application=<application-uuid> --linked-env=production
lpm env push --to coolify                          # merge into production values
lpm env push --to coolify --clean                  # remove Coolify-only production values
lpm env pull --from coolify --env=production

# Fly.io app secrets
lpm env connect fly --app=<fly-app-name> --linked-env=production
lpm env push --to fly                              # refresh every expected secret
lpm env push --to fly --clean                      # remove Fly-only secret names
lpm env pull --from fly --env=production           # reports names skipped; values are write-only

# Railway service values
lpm env connect railway --project=<project-id> \
  --environment=<environment-id> --service=<service-id> \
  --linked-env=production
lpm env push --to railway                          # merge into the service
lpm env push --to railway --clean                  # exact service user-values
lpm env pull --from railway --env=production

# Railway shared environment values
lpm env connect railway --project=<project-id> \
  --environment=<environment-id>

# GitHub repository or environment values
lpm env connect github-actions --repository=<owner/repository> \
  --environment=production --linked-env=production
lpm env push --to github-actions
lpm env push --to github-actions --clean
lpm env pull --from github-actions

lpm env status                                     # compare without mutation

connect validates the platform token directly with the provider before LPM.dev Registry stores the encrypted credential. For unattended setup, pass the token explicitly:

lpm env connect vercel --project=<id> --linked-env=production --target=production --token="$VERCEL_TOKEN"
lpm env push --to=vercel --env=staging --yes
lpm env connect vercel --org=acme --project=<id> --linked-env=production --target=production --token="$VERCEL_TOKEN"
lpm env push --to=vercel --org=acme --yes
lpm env status --org=acme

Avoid literal tokens in shell history. Organization platform operations require owner or administrator access and an active Organization or Enterprise plan.

Platform push and pull select explicit --env, then the connection's --linked-env, then the default environment. An explicit override does not change the saved connection. Push uses resolved project values, including dotenv files, stored secrets, and schema defaults.

Platform configuration and behavior

PlatformConnection flagsPush, pull, and status
Vercel--project, optional --team, comma-separated --targetTargets are production, preview, and development. The default includes all three.
Coolify--url, --application, optional --previewSync raw application values. Preview values require --preview.
Fly.io--appPush secrets. Pull cannot retrieve values. Status compares names.
Railway--project, --environment, optional --service, --project-tokenSelect service values or, without --service, shared environment values.
GitHub Actions--repository, optional --environmentSelect repository or environment values. Pull retrieves variables and skips secrets.

All connections support --label and --linked-env. Normal pushes preserve platform-only values. --clean deletes extra values within the configured target. Platform-managed runtime variables remain excluded. User-configurable Railway names such as RAILWAY_DOCKERFILE_PATH remain eligible for sync.

Vercel pushes reject ambiguous targets, duplicate keys, and values shared with an unselected target before mutation.

Coolify requires a team administrator token with root or all three permissions: read, write, and read:sensitive. The instance URL must be an HTTPS origin without credentials, a path, query parameters, or a fragment. Hidden values, shared-variable references, and shown-once values cannot be read reliably. The CLI rejects them before sync. Existing literal and multiline attributes remain unchanged. New production values leave existing preview values unchanged. After an uncertain create response, cleanup affects only entries that the operation created.

Fly.io secrets are write-only. Push refreshes every expected secret, regardless of the schema. --clean can create a second release to delete extra secrets. Pull reports no_readable_values and skippedSecrets when secret names exist. An empty target reports no_values. Status reports names_only when secret names match exactly. An empty matching target reports synced. An app rename, transfer, deletion, or identity mismatch stops the operation.

Railway reads preserve unrendered references such as ${{Service.VARIABLE}}. Add --project-token for a Railway project token. Account, workspace, and OAuth tokens use the default authentication mode. The CLI checks the final values after a push before reporting success.

GitHub Actions uses the schema to distinguish readable variables from write-only secrets:

lpm.json
{
  "envSchema": {
    "vars": {
      "PUBLIC_ORIGIN": { "client": true, "ci": "variable" },
      "API_TOKEN": { "secret": true }
    }
  }
}

Only ci: "variable" creates an Actions variable. Browser visibility does not select CI storage. A secret rule cannot use ci: "variable". Use ci: "secret" to select secret storage explicitly. All other keys become Actions secrets, including undeclared keys and keys without a schema. Existing client: true declarations need ci: "variable" to retain readable CI storage. Without that field, a push moves the key to Actions secrets and removes its old variable, even without --clean. The CLI encrypts secret values locally with the GitHub public key before upload.

GitHub pull imports readable variables and preserves local-only values. It reports skipped secrets without replacing their local values. Status reports names_only when variables and secret names match exactly and the target contains secrets. A matching target with no secrets reports synced. Status cannot compare secret values. A repository rename, transfer, identity mismatch, or unconfigured environment stops the operation.

Fine-grained GitHub tokens require Metadata read access. Repository connections also require Variables and Secrets read/write permissions for full sync. Environment connections require Environments read/write. Read permissions suffice for status and readable pulls.

status reports platform-only values as drift. Add those values locally or delete them on the platform to resolve that drift. If an operation has an uncertain result, the CLI checks readable values or secret names again. Write-only secret updates cannot always be confirmed. In that case, the command returns an error without claiming exact changes.

Encrypted cloud storage and platform deployment remain two explicit operations:

lpm env push                  # ciphertext + wrapped key to lpm.dev
lpm env push --to fly         # plaintext directly from the CLI into write-only Fly secrets
lpm env push --to railway     # plaintext directly from the CLI to Railway
lpm env push --to github-actions # values and sealed secrets directly to GitHub

Cloud sync and platform sync are separate operations. A failure in one does not undo a successful change in the other. After a cloud pull on another machine, use status to compare the recovered values with the platform.

OIDC policies (CI)

OIDC pulls support personal Pro projects and organization projects on an active Organization or Enterprise plan. GitHub Actions and GitLab.com CI are supported. Self-managed GitLab issuers are not supported. CI does not store a long-lived Registry token.

lpm env push
lpm env oidc allow \
  --allow-server-decryption \
  --provider=github \
  --repo=owner/repo \
  --workflow=.github/workflows/deploy.yml \
  --branch=main \
  --env=production
lpm env oidc list

Run these commands from a logged-in development machine. First, use push to create or migrate the personal cloud project. Both personal and organization activation require --allow-server-decryption.

Personal activation transfers only this project's key. The device root and legacy global key stay local. The Registry can decrypt the complete project. Environment allowlists control authorized CI responses, not separate encryption boundaries. The workflow path is required. It must name a .yml or .yaml file directly under .github/workflows/. Defaults are provider github, branch main, and event push. Use comma-separated values for --branch, --env, --workflow, and --events.

Policy listMaximum entriesMaximum characters per entry
Branch patterns50200
Environments50100
GitHub workflow paths201,024

Policy requests also have a 256 KiB body limit. GitLab project IDs contain at most 20 decimal digits.

To allow pull_request_target, include it in --events and add --allow-forks. Ordinary pull_request events do not use this flag. The remaining identity, branch, workflow, and environment restrictions still apply. The --env policy list selects env-project environments. It does not configure a GitHub deployment environment.

For an organization project, an owner or admin must first share or pull its current encrypted revision. Then explicitly enable server decryption:

lpm env pull --org=acme
lpm env oidc allow \
  --org=acme \
  --allow-server-decryption \
  --provider=github \
  --repo=owner/repo \
  --workflow=.github/workflows/deploy.yml \
  --branch=main \
  --env=production
lpm env oidc list --org=acme

This organization opt-in gives the Registry only the project's current content key. Your sharing private key and personal device root stay local.

The organization owns these policies and CI credentials. They do not depend on the original policy creator remaining an admin. The Registry checks current billing, policy restrictions, token validity, and content-key state at each pull.

Content-key rotation or membership key invalidation disables CI decryption and revokes issued credentials. After the new key is shared, an owner or admin must run the complete oidc allow command again.

Owners and admins can add or replace policies, delete policies, and disable CI decryption from the project's Access page. To disable it from the CLI:

lpm env oidc disable --org=acme

Disabling decryption also revokes existing CI credentials. Enabling it again does not restore those credentials. Disabling remains available when organization billing lapses. The CLI requires a checkout with an existing authenticated organization binding. If you lack that checkout, use the dashboard Access page. A fresh CLI checkout cannot establish the binding through a pull while billing is lapsed.

The CI pull command is the same for personal and organization projects. Its policy ID selects the ownership scope, so the CI job does not need --org.

After a successful oidc allow, LPM CLI prints the server-issued policy ID. Copy this UUID into the CI configuration as LPM_OIDC_POLICY_ID.

The policy ID is a selector, not a bearer credential. It cannot authorize a pull without a valid CI identity token. Protect its integrity because a changed value can select the wrong policy.

Each lpm env pull --oidc command requires this selector. You can pass --policy-id=<uuid> instead. The command flag takes precedence over LPM_OIDC_POLICY_ID.

By default, LPM CLI gets the immutable numeric repository ID from the GitHub API. Public repository requests do not need GitHub authentication.

If the repository is private, set GITHUB_TOKEN or GH_TOKEN before you run the command. Alternatively, add --repository-id=<numeric-id>.

The policy binds the repository name and its numeric ID. Both values must match during the OIDC exchange and the CI pull.

A repository rename or transfer stops CI access. Run oidc allow again with the current repository name.

A different repository cannot use the policy if it reuses the old name. The new repository has a different numeric ID.

Existing GitHub policies without a repository ID fail closed. If lpm env oidc list reports a missing ID, run oidc allow again.

GitLab.com uses the stable numeric project_id claim and has no GitHub workflow/event/fork fields:

lpm env oidc allow \
  --allow-server-decryption \
  --provider=gitlab \
  --project-id=12345 \
  --branch=main \
  --env=production

--repo, --repository-id, --workflow, --events, and --allow-forks are rejected for GitLab.com policies. The canonical subject is project:12345. Exchanges require ref_type: "branch". A tag whose name matches an allowed branch is still rejected.

lpm env oidc allow replaces the policy's complete allowlists rather than merging an individual branch or environment. Before changing an existing policy, inspect it with lpm env oidc list and review the full replacement surface with lpm env oidc allow --help.

Personal activation authenticates the current cloud revision and validates its project key. The Registry commits encrypted escrow and the complete policy together. A stale revision, invalid key, or failed write preserves the previous policy and escrow state.

To disable personal CI access:

lpm env oidc disable
lpm env oidc list

Personal disable rotates the project and content keys while preserving all remote values. It clears escrow, disables policies, and revokes issued credentials. A concurrent cloud change causes a conflict. Retry from a machine with current project access. The policy list reports active and disabled policies. Re-enabling access never restores revoked credentials.

For lpm env pull --oidc, a non-empty LPM_VAULT_ID takes precedence over lpm.json > vault and can bootstrap a checkout that has no local vault field. If the variable is unset, empty, or whitespace-only, LPM CLI falls back to the local field. Commit the generated local value or pass the same vault ID through the workflow environment.

.github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @lpm-registry/cli
      - name: Load production environment
        run: lpm env pull --oidc --env=production --output=.env
        env:
          LPM_VAULT_ID: 7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b
          LPM_OIDC_POLICY_ID: ${{ vars.LPM_OIDC_POLICY_ID }}
      - run: ./deploy.sh

GitHub exposes its OIDC runtime endpoint when the job has id-token: write. LPM CLI requests the https://lpm.dev audience automatically. The Registry checks the repository name, numeric ID, workflow, event, branch, and environment before it returns values.

--output writes the file atomically with owner-only permissions. If Git doesn't already ignore the file, LPM CLI adds a rule for it to the repository's .git/info/exclude, which leaves the work tree clean. A destination that Git tracks stops the command before the file is written.

Create LPM_OIDC_POLICY_ID as a GitHub repository variable for this job. If you use an environment variable, add environment: <name> to the job. GitHub does not expose an environment variable to a job that does not target that environment.

GitLab.com CI supplies LPM_OIDC_TOKEN with audience https://lpm.dev. LPM.dev Registry checks its exact numeric project ID. Self-managed GitLab issuers are not supported. Organization projects use the same CI token exchange after explicit decryption setup.

.gitlab-ci.yml
deploy:
  id_tokens:
    LPM_OIDC_TOKEN:
      aud: https://lpm.dev
  variables:
    LPM_VAULT_ID: 7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b
  script:
    - npm install -g @lpm-registry/cli
    - lpm env pull --oidc --env=production --output=.env
    - ./deploy.sh

Create LPM_OIDC_POLICY_ID as a GitLab CI/CD variable. Mark it protected only when every branch or tag allowed by the OIDC policy is also protected. GitLab does not expose protected variables to unprotected refs.

CI output

lpm env print --ci --env=staging
lpm env export --ci --env=staging ci.env --json

print --ci selects GitHub Actions output when GITHUB_ACTIONS is set, dotenv when VERCEL is set, or shell exports otherwise. It cannot be combined with --format, --schema-only, --client-only, --stage, --service, or --json.

The output is a script or a file format. Printing it does not change the environment of the parent shell. In a GitHub Actions step, apply the generated script:

- name: Load the local project environment
  shell: bash
  run: |
    lpm env print --format=github-actions > "$RUNNER_TEMP/project-env.sh"
    bash "$RUNNER_TEMP/project-env.sh"
    rm "$RUNNER_TEMP/project-env.sh"

Secret-marked values are masked before assignments reach $GITHUB_ENV. Percent signs and newlines are escaped in mask commands. GitHub applies $GITHUB_ENV values to subsequent steps. Multiline values that contain the reserved __LPM_GHA_EOF__ delimiter are omitted with a comment.

export --ci always writes dotenv. It keeps the file out of Git through .git/info/exclude, as described in Import, export, and copy. Its optional JSON result reports the file, environment, variable count, and warnings without printing values.

Common recovery steps

ProblemAction
A value is missing from list.Use print --format=json to inspect resolved file and schema values, or select the correct stored environment.
check fails.Correct missing or invalid values in each reported environment.
An import preserves an old value.Repeat the import with --overwrite if replacement is intended.
A running application uses old values.Restart the application after you change a stored secret.
A cloud push reports a conflict.Inspect the remote state and pull the current revision before retrying.
A platform pull skips secrets.Restore values from local storage or another trusted copy. Write-only platforms cannot return plaintext secrets.

Flags

Flags belong to the indicated commands. They are not interchangeable across every env command.

FlagCommandsEffect
--env=<name>Local values, print, check, example, platform sync, OIDCSelect an environment. Local and platform commands also accept a separate value.
--revealget, listPrint stored values instead of masks.
--overwriteimport, copyReplace matching destination values.
--forceinitImport file values into existing local environments.
--forcePersonal pushReplace the remote payload despite a version conflict.
--forceOrganization shareRecreate a deleted remote project already bound to this checkout.
--accept-recipient-keys=<digest>share, organization rotate-keyAccept the reviewed recipient set.
--token=<token>Platform connectSupply the platform credential without a prompt.
--format <format>printSelect the output format.
--stage=<stage>print, checkEvaluate development, build, runtime, ci, or test.
--service=<name>print, checkApply service scopes and configured static service values.
--schema-onlyprintInclude only schema keys. A nonempty schema is required.
--client-onlyprintInclude only declared browser-visible keys. A nonempty schema is required.
--strictvalidateReject extra stored keys.
--ciprint, exportSelect CI output or export resolved values as dotenv.
--to <platform>pushSend values to a connected deployment platform.
--from <platform>pullImport readable platform values.
--cleanPlatform pushDelete platform-only values within the selected target.
--yes, -ySupported cloud operationsAccept the operation's confirmation. Pairing uses --yes.
--org=<slug>Organization operationsSelect organization access or a connection. OIDC commands require the equals form.
--oidcpullAuthenticate a CI pull through its workload identity.
--allow-server-decryptionoidc allowRequired opt-in to send this project's decryption key for CI access.
--policy-id=<uuid>OIDC pullOverride LPM_OIDC_POLICY_ID.
--output=<file>OIDC pullWrite retrieved values to an owner-only file that Git ignores through .git/info/exclude.
--jsonCommands with structured resultsEmit JSON. For print, select JSON values.

OIDC options that take a value require --flag=value. Connection and policy flags appear in their examples. See also the global flags.

See also