lpm env
Manage project environment variables and secrets — local-file storage, cloud sync, platform integrations, and OIDC policies.
lpm env set KEY=VALUE # set one or many vars
lpm env get KEY # read one (masked unless --reveal)
lpm env list # list all (masked unless --reveal)
lpm env delete KEY # remove
lpm env push # encrypt + push to lpm.dev
lpm env pull # pull + decrypt
lpm env push --to vercel # push to a connected platform
lpm env push --to coolify # push to a connected Coolify app
lpm env push --to fly # push write-only secrets to a Fly.io app
lpm env push --to railway # push to a connected Railway target
lpm env push --to github-actions # push variables + sealed secrets to GitHublpm env manages encrypted environment variables for the project. Local values use OS-keychain-backed storage, cloud sync persists only ciphertext to lpm.dev, and the Vercel, Coolify, Fly.io, Railway, and GitHub Actions integrations send application env values directly from the CLI to the selected platform.
For the underlying storage and encryption model, see env encryption architecture.
Local-file management
lpm env list # list all vars (values masked)
lpm env list --reveal # list with values shown
lpm env get DATABASE_URL # get one var
lpm env get DATABASE_URL --reveal # get with value shown
lpm env set DATABASE_URL=postgres://... # set one
lpm env set FOO=bar BAZ=qux # set many at once
lpm env delete FOO BAZ # delete one or more
lpm env import .env.production # import from a file
lpm env import .env --overwrite # overwrite existing values
lpm env export .env.backup # export to a fileDotenv inputs are limited to 16 MiB per file before parsing. Missing optional .env* files retain their existing fallback behavior; an oversized import or run-time env file fails with its path and byte limit, before any configured script or child process is spawned. See local configuration size limits.
Per-environment scoping
Pass --env=<name> to scope reads and writes to a named environment (e.g., staging, production). Environments are stored independently — staging and production keep separate value sets for the same key.
lpm env set --env=staging API_URL=https://staging.example.com
lpm env set --env=production API_URL=https://api.example.com
lpm env list --env=stagingOther local commands
lpm env init # interactive environment setup
lpm env ls # environment overview table with sync/update status
lpm env copy <src> <dst> # copy all secrets between envs
lpm env print # print resolved env (for `eval`)
lpm env check # validate against the project schema
lpm env example # generate `.env.example` from the schemaResolved env surfaces such as lpm env print, lpm run, lpm <file> / lpm exec, and platform pushes ignore runtime-hook names such as NODE_OPTIONS, LD_PRELOAD, LD_AUDIT, BASH_ENV, and DYLD_INSERT_LIBRARIES when they come from env files, env secrets, or env-schema defaults. LPM CLI also strips inherited values for these names before spawning scripts.
Cloud sync
lpm env pull and lpm env push round-trip the local env project to lpm.dev's encrypted storage. The server stores ciphertext and wrapped key material; it never sees plaintext values.
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 validate # check remote against the schema
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 confirmation prompt (NOT recommended)
lpm env unpair # revoke this device's pairing
lpm env log # last 50 audit log entries
lpm env rotate-key # rotate the env project's encryption key
lpm env rotate-sharing-key # rotate your X25519 sharing key (interactive, step-up reauth)Cloud sync is plan-gated. Free accounts get local-only storage. Pro plans unlock personal sync, and active Organization plans unlock organization sync and sharing. Every push uses fresh randomized encryption and increments the server version, even when the plaintext did not change.
Rotating your sharing key
lpm env rotate-sharing-key generates a fresh X25519 keypair, uploads the new public key after step-up authentication, and invalidates every organization env-project wrapped-key entry the server held for you. The CLI keeps the new private key in a pending slot until the server acknowledges the rotation, so a crash mid-flight is safe; re-running the command resumes the pending slot if the server already moved on. The flow:
- Prints the blast radius — every organization env project you can access needs an owner or admin to run
lpm env share --org <slug>before pulls resume. - Asks you to type
ROTATE(uppercase) to confirm. - Prompts for your password (and authenticator code, if MFA is enrolled) to mint the step-up proof.
- Uploads the new public key; the server invalidates the old wrapped-key rows and sends an out-of-band security email to your account plus an impact email to every affected org's owners and admins.
- Promotes the pending slot to the live keychain entry. The dashboard's env-project Member Access view shows the affected rows as Needs share.
rotate-sharing-key refuses to run without a TTY — there's no --yes / non-interactive path, by design. On every machine where you want to keep pulling, you re-register the rotated key the next time the CLI needs it: any org-touching verb (env share, env pull --org, etc.) detects a missing server-side key and walks you through a vault:public-key:set step-up to register it.
The pre-rotation key never re-establishes itself. If a teammate's machine still has the old private key it cannot decrypt anything pushed under the new AES key, and the dashboard's audit log records the rotation with both fingerprints so cross-team forensics are trivial.
Deployment platform integrations
Vercel, Coolify, Fly.io, Railway, and GitHub Actions are the supported env platforms. Application env values travel directly between the Rust CLI and the selected platform; they never pass through lpm.dev. lpm.dev stores the platform credential encrypted at rest, non-secret connection configuration, authorization scope, and bounded 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 mutationThe connect command prompts for the platform token and verifies it directly with the platform before saving the encrypted credential to lpm.dev.
For Vercel, optional connection flags are --team, --label, --linked-env, and comma-separated --target values (production, preview, development). Omitting --target selects all three. Targetless values, duplicate relevant keys, and variables shared with an unselected target fail closed before mutation.
For Coolify, create the token as a team administrator. Use either a root
token or a token with read, write, and read:sensitive. Coolify hides
application values from non-administrators and from tokens without
read:sensitive; the CLI fails closed instead of importing a hidden value as
an empty string.
--url must be the HTTPS origin of the Coolify instance, without credentials,
a path, query parameters, or a fragment. --application selects the
application UUID. Add --preview to target preview values instead of
production values. --label and --linked-env have the same meaning as on
Vercel. The CLI disables redirects for every Coolify request so the API token
and env values cannot be forwarded to another origin.
The CLI syncs Coolify's raw stored value, not its deployment-rendered value, and preserves the existing literal, multiline, and shown-once flags during an update. Shared-variable references and shown-once values cannot be read authoritatively and are rejected before mutation. Convert shared references to application values, or delete and recreate shown-once values as readable values, before syncing.
For a new production key, the CLI first reserves the preview key with a unique
non-secret ownership value. It creates production with that same non-secret
value, then updates only the owned production row with the real value. A
409 Conflict is accepted only after Coolify confirms exactly one existing
preview value; that pre-existing value is never changed or deleted. If a
create response is lost, malformed, or too large to read safely, the CLI
relists the application and recovers only an exact key, target, and unique
operation-value match. Cleanup deletes only rows proven to belong to that
operation by UUID or unique value; if recovery or cleanup cannot establish the
final state, synchronization fails closed.
For Fly.io, --app selects one canonical app. Connect discovers and stores the immutable app ID, organization ID, and organization slug. Every later operation verifies the canonical name, app ID, and organization ID before mutation, so an app rename, transfer, deletion, or wrong target fails closed.
Fly.io app secrets are write-only. Every selected local key is sent as a Fly secret, regardless of envSchema. A push refreshes all expected values in one setSecrets release; --clean can create a second unsetSecrets release for extra names. If that removal response fails or disconnects, the CLI rereads the authoritative names and counts only keys confirmed absent; if the reread fails, no exact failed-operation audit is sent. Status is names_only when expected names exist, without claiming value equality. Pull cannot recover secret values and reports no_readable_values plus skippedSecrets. Fly-managed runtime names such as FLY_APP_NAME, FLY_REGION, and FLY_MACHINE_ID are excluded.
For Railway, --project and --environment are required. --service selects service variables; omit it to select shared environment variables. Account, workspace, and OAuth tokens use Bearer authentication. Add --project-token when the credential is a Railway project token so the CLI uses Project-Access-Token. Reads request unrendered values so ${{Service.VARIABLE}} references round-trip intact. The adapter disables redirects, treats GraphQL errors as failures even with HTTP 200, performs one bulk upsert, then rereads the target and reports success only after exact verification.
For GitHub Actions, --repository=<owner/name> is required. Add --environment=<name> to target environment-level values; omit it for repository-level values. The CLI stores the canonical repository name plus immutable numeric repository ID and verifies both before every operation. A rename, transfer, wrong repository, or unconfigured environment fails closed.
GitHub Actions variables are readable, but Actions secrets are write-only. Only a key explicitly declared as client: true and not secret: true in lpm.json > envSchema becomes an Actions variable. Undeclared keys, secret-marked keys, and client: true, secret: true keys become Actions secrets. Without an env schema, every key is a secret.
{
"envSchema": {
"vars": {
"PUBLIC_ORIGIN": { "client": true },
"API_TOKEN": { "secret": true },
"AMBIGUOUS_TOKEN": { "client": true, "secret": true }
}
}
}Each secret is sealed locally with GitHub's Actions public key; only ciphertext reaches GitHub. Status returns names_only when readable variables match and every expected secret name exists—it never claims that secret values are equal. Pull imports readable variables, preserves local-only values, reports skipped secret names, and never overwrites local secrets with unavailable values. Push refreshes every expected secret. --clean removes extra variable and secret names. After an ambiguous mutation response, the CLI rereads comparable variable or secret-name state before deciding whether the operation committed.
Fine-grained GitHub tokens need Metadata read access. Repository-scoped connections additionally need Variables read/write and Secrets read/write for full sync. Environment-scoped connections need Environments read/write. Read-only permission is sufficient only for status and readable pulls.
Normal pushes preserve values that exist only on the platform. --clean removes platform-only values within the configured target. Platform-managed variables are always excluded. Fly runtime names and Railway-provided runtime and Git variables are managed; user-configurable Railway names such as RAILWAY_DOCKERFILE_PATH remain syncable. lpm env status performs an exact name comparison for Fly.io and an exact value comparison where platforms return plaintext, so preserved platform-only values are reported as drift until they are removed or added locally.
Direct platform pushes record exact failed-operation counts only when acknowledged responses or authoritative final-state evidence prove them. Readable values can be reconciled by value, while Fly.io removals and GitHub secret creation or removal can be reconciled by name. An ambiguous existing-secret update remains unprovable because Fly.io and GitHub never return the value; in that case, or when reconciliation itself fails, the CLI returns the provider error without sending a falsely exact push_failed audit.
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 GitHubThe two systems cannot participate in an atomic transaction. A failure is reported for the operation that failed and does not claim to roll back a successful operation against the other system. After lpm env pull recovers the encrypted state on another paired machine, lpm env status verifies that state directly against the platform.
OIDC policies (CI)
Issue short-lived tokens to CI runs that prove repo identity, then exchange them for env decrypt access — no long-lived tokens stored in CI secrets.
lpm env oidc allow --provider=github --repo=owner/repo --branch=main --env=production
lpm env oidc list # list configured policies
lpm env pull --oidc --env=production --output=.env # CI-side decryptlpm setup ci github-actions and lpm setup ci gitlab generate ready-to-paste snippets that wire OIDC and call lpm env pull --oidc automatically. The two providers expose OIDC differently:
- GitHub Actions — the GitHub runtime exposes
ACTIONS_ID_TOKEN_REQUEST_URL+ACTIONS_ID_TOKEN_REQUEST_TOKENto any job that declarespermissions: id-token: write, and the CLI fetches the JWT at run time. The emitted snippet relies on this — it does not setLPM_OIDC_TOKEN. - GitLab CI — the runtime has no equivalent of GitHub's runtime fetch, so the snippet mints
LPM_OIDC_TOKENvia theid_tokensblock withaud: https://lpm.dev. LPM CLI then routes it through the registry-exchange surface as a pre-supplied token.
You can also set LPM_OIDC_TOKEN by hand on either provider — for self-hosted GitHub runners that lack the runtime endpoint, or for any non-standard CI where you've fetched a https://lpm.dev-audience JWT some other way.
CI output
lpm env print --ci --env=staging
lpm env export --ci --env=staging ci.envlpm env print --ci emits resolved environment variables in a CI-native format. GitHub Actions runners get GitHub's masked-output format, Vercel gets dotenv, and generic CI gets shell-export lines.
lpm env export --ci <file> writes a dotenv-formatted copy to the given path. --env=<mode> selects which environment file is loaded. Secret-marked vars from lpm.json > envSchema are masked in CI-native output where the platform supports it.
lpm ci is still the frozen-lockfile install command. See lpm install.
Flags
| Flag | Effect |
|---|---|
--env=<name> | Scope to a named environment (default: default) |
--reveal | Show values in get / list (default is masked) |
--overwrite | import overwrites existing keys instead of skipping |
--to <vercel|coolify|fly|railway|github-actions> | Push directly from the CLI to the connected platform |
--from <vercel|coolify|fly|railway|github-actions> | Pull readable values directly from the platform into the selected local env |
--clean | Replace platform values instead of merging |
--yes | Skip confirmation prompts |
--ci | Emit CI-native output for print or dotenv output for export |
--oidc | Use OIDC token exchange (CI) instead of stored creds |
--output <file> | Write pull results to a file instead of local env storage |
--org <slug> | Org-scoped sync target |
Plus the global flags.
See also
- Env encryption architecture — storage and encryption design, sharing-key lifecycle, CLI step-up reauth
lpm setup ci— generate CI YAML with OIDC wiringlpm.json—envSchemaand environment definitions