# lpm env (/docs/dev/env)



```bash
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 GitHub
```

`lpm 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](/docs/infra/secrets-vault).

## Local-file management [#local-file-management]

```bash
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 file
```

Dotenv 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](/docs/project-setup#local-configuration-size-limits).

### Per-environment scoping [#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.

```bash
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=staging
```

### Other local commands [#other-local-commands]

```bash
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 schema
```

Resolved env surfaces such as `lpm env print`, [`lpm run`](/docs/dev/run), [`lpm <file>` / `lpm exec`](/docs/dev/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 [#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.

```bash
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 [#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:

1. Prints the blast radius — every organization env project you can access needs an owner or admin to run [`lpm env share --org <slug>`](#cloud-sync) before pulls resume.
2. Asks you to type `ROTATE` (uppercase) to confirm.
3. Prompts for your password (and authenticator code, if MFA is enrolled) to mint the step-up proof.
4. 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.
5. 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 [#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.

```bash
# 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
```

The 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.

```json title="lpm.json"
{
  "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:

```bash
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
```

The 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) [#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.

```bash
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 decrypt
```

`lpm 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_TOKEN` to any job that declares `permissions: id-token: write`, and the CLI fetches the JWT at run time. The emitted snippet relies on this — it does *not* set `LPM_OIDC_TOKEN`.
* **GitLab CI** — the runtime has no equivalent of GitHub's runtime fetch, so the snippet mints `LPM_OIDC_TOKEN` via the `id_tokens` block with `aud: 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 [#ci-output]

```bash
lpm env print --ci --env=staging
lpm env export --ci --env=staging ci.env
```

`lpm 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`](/docs/packages/install#frozen-lockfile-and-ci).

## Flags [#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](/docs/commands#global-flags).

## See also [#see-also]

* [Env encryption architecture](/docs/infra/secrets-vault) — storage and encryption design, sharing-key lifecycle, CLI step-up reauth
* [`lpm setup ci`](/docs/infra/setup) — generate CI YAML with OIDC wiring
* [`lpm.json`](/docs/reference/lpm-json) — `envSchema` and environment definitions
