# Envpilot — Complete Documentation # Generated from source at 2026-09-07T00:38:05.226Z # https://docs.envpilot.dev/llms-full.txt Envpilot is a secure environment variable management platform for teams. Secrets are encrypted at rest using AES-256 via WorkOS Vault. Access is controlled through role-based permissions. Three client surfaces: CLI, VS Code extension, and web dashboard. Website: https://www.envpilot.dev npm: https://www.npmjs.com/package/@envpilot/cli VS Code: https://marketplace.visualstudio.com/items?itemName=envpilot.envpilot GitHub: https://github.com/rafay99-epic/envpilot.dev ======================================================================== QUICKSTART ======================================================================== Source: https://docs.envpilot.dev/start/quickstart # Quickstart Three surfaces, one dataset. A variable created in the dashboard shows up on your next `envpilot pull` and in the VS Code sidebar. Pick whichever surface you are already sitting in. If you want the model before the mechanics, read [Core concepts](/start/concepts) first — it is one page. ## Create an organization and a project ## Your first secret ### From the dashboard Click **Add Variable** — the button reads **Request Variable** instead if your role cannot write directly, see [Roles & permissions](/platform/rbac). Fill in: - **Key** — e.g. `DATABASE_URL` - **Value** - **Description** — optional - **Environments** — one or more; each is colour-coded (green for development, amber for staging, red for production) - **Mark as sensitive** — masks the value in the UI by default Submit. The variable appears in the project's table immediately, or lands in the Requests inbox if your role needs approval. The same drawer has a **Bulk Paste** tab for pasting a whole `.env` block at once. ### From the CLI ```bash npm install -g @envpilot/cli envpilot sync ``` `envpilot sync` chains login, project selection and a first pull into one flow: it authenticates in the browser, walks you through organization → project → default environment, writes a local `.envpilot` config, pulls your variables, and adds `.env` to `.gitignore`. To set a single secret without a pull/push round trip: ```bash envpilot secrets set STRIPE_SECRET_KEY -e production ``` The value is typed into a masked prompt, so it never lands in your shell history. To inject secrets into a process without writing any file to disk: ```bash envpilot run -- bun dev ``` Full details in the [CLI overview](/cli/overview). ### From VS Code More in the [VS Code overview](/extension/overview). ## Invite a teammate From the organization's **Members** page, open the invite panel: 1. Type an email — Envpilot autocompletes existing users and flags anyone who is already a member or already invited. 2. Pick a **role**. Roles are described inline as you pick; the full model is in [Roles & permissions](/platform/rbac). 3. For roles that are not organization-wide, choose which projects the person is assigned to. Envpilot emails an **Accept Invitation** link that expires after 7 days. The invite only becomes a membership when the invitee signs in with the **same email address** it was sent to, so a forwarded link is not redeemable by anyone else. Pending invitations can be resent (fresh token and expiry) or cancelled at any time. ## What this does not cover - Secrets that are not text — keystores, SSH keys, `.p12` certificates and service-account JSON are [secret files](/platform/secret-files), a separate object with its own upload flow. - CI and agents — machine access uses [API keys](/api/authentication), not your login. - Free-plan ceilings — 3 projects, 50 variables per project, 3 members. Full table in [Plans & limits](/limits/plans). ## Where to go next - [Core concepts](/start/concepts) — the object model in one page - [Architecture](/start/architecture) — how the five surfaces share one enforcement core - [CLI](/cli/overview) · [VS Code](/extension/overview) · [Dashboard](/dashboard/overview) - [API quickstart](/api/quickstart) — read variables from CI or an agent ======================================================================== CORE CONCEPTS ======================================================================== Source: https://docs.envpilot.dev/start/concepts # Core concepts Six objects, and one rule about how they combine. Everything else in these docs is a surface over this model. ## Organization The top-level container for your team: a name, a URL slug, members, billing, and API keys. If you are the first person on your team to sign up, you create one before anything else exists. A user can belong to several organizations; the free plan allows one organization per owner. ## Project A project lives inside an organization and holds variables and secret files — typically one project per app or service (`api`, `web`, `worker-jobs`). Projects have their own name, slug, optional description, and project-level role assignments. ## Environment Every project has exactly **three** environments: `development`, `staging`, `production`. There is no fourth, and custom environments cannot be added — the three-environment model is fixed across the dashboard, CLI, extension, GitHub Action, REST API and MCP server. Every variable and every secret file is scoped to one or more of them. ## Variable A key, a value, a set of environments, an optional description, tags, and a "sensitive" flag. That invariant — every (key, environment) pair resolves to at most one active variable — is what makes `envpilot pull`, extension sync, and the public API deterministic. It is enforced on every write path, including request approvals and restores from trash. See [Variables](/platform/variables). ## Sensitive is a display flag, not an encryption flag Marking a variable **sensitive** masks it (`••••••••`) in the dashboard until someone reveals it. That is all it does. Every value, sensitive or not, is encrypted at rest in WorkOS Vault; Convex stores only a vault reference id, never plaintext. "Sensitive" controls who casually reads a value over your shoulder, not how it is stored. See [Security](/platform/security). ## Secret file Some secrets are not text you can paste into a `.env`: an Android signing keystore, an SSH private key, a `.p12` certificate, a service-account JSON. Those are **secret files** — binary blobs with a recorded destination path and POSIX mode, so a fresh clone can materialise everything a build needs. They are stored differently from variables (envelope encryption: ciphertext in Convex storage, key in Vault) and carry their own limits. See [Secret files](/platform/secret-files). ## Role Every member holds one organization role, plus optional per-project roles and per-variable or per-file grants. Roles decide two things: what you can read, and whether your write lands immediately or becomes a request for someone else to approve. See [Roles & permissions](/platform/rbac). ## Request A request is an ask, not a write: "I need `STRIPE_SECRET_KEY` in production, here is why." Developers file them, reviewers approve them and supply the value. Coding agents can file them too over the MCP server — which is the only mutation any machine credential is allowed to perform. See [Requests & approvals](/platform/requests). ## How the pieces map to surfaces | You want to… | Use | | --------------------------------- | ---------------------------------------- | | Manage everything, invite people | [Dashboard](/dashboard/overview) | | Work in a terminal, script CI | [CLI](/cli/overview) | | Stay in the editor | [VS Code extension](/extension/overview) | | Pull secrets in a workflow run | [GitHub Action](/action/overview) | | Read from your own program | [REST API](/api/overview) | | Give a coding agent scoped access | [MCP server](/mcp/overview) | ## Next - [Architecture](/start/architecture) — one enforcement core behind all six surfaces - [Plans & limits](/limits/plans) — what each tier allows ## Limits worth knowing early - **Three environments**, fixed. No custom environments on any plan. - **One role per member**, organization-wide; project assignments narrow where it applies. - **Free tier**: 3 projects, 50 variables per project, 3 members, 3 secret files, 1 organization. - Deleted variables, accounts and files are recoverable for **7 days**, then purged permanently. ======================================================================== ARCHITECTURE: THE MACHINE SURFACES ======================================================================== Source: https://docs.envpilot.dev/start/architecture # Architecture: the machine surfaces Envpilot exposes your projects and variables through five client surfaces. Two are driven by a human at a keyboard, two are driven by programs, and one is a hard-locked CI reader. They differ in how they authenticate and in what they are allowed to do — but they all funnel through a **single enforcement core**, so there is exactly one place where scope, plan, and audit are decided. ## The five surfaces | Surface | Who drives it | Auth | What it can do | | ----------------- | ------------- | ---------------------------------- | ---------------------------- | | CLI | A human | WorkOS device flow (browser login) | Read and write (role-gated) | | VS Code extension | A human | WorkOS device flow (browser login) | Read and write (role-gated) | | REST API | A program | API key (`envpk_…`) | Read | | MCP server | An agent | API key (`envpk_…`) | Read, and **file** a request | | GitHub Action | CI | API key (`envpk_…`) | Read only — never requests | The CLI and the VS Code extension are **human** surfaces: a person completes a WorkOS device-flow login in a browser, and their organization/project role decides what they can read and write. The REST API and the MCP server are **machine** surfaces: a program or an AI agent presents an API key. The GitHub Action is a machine surface too, but a deliberately narrow one — a CI reader locked to a single project's variables. ## One key, one core Every machine surface authenticates with the same credential: an **API key** minted in **Organization Settings → API Keys**. There is no separate "service token" concept anymore — Action credentials are API keys with the GitHub Action surface selected. A key carries: - **A SHA-256 hash only.** The plaintext `envpk_…` is shown exactly once at creation; Envpilot stores only its hash. Lose it, and you revoke and re-issue. - **Scope** — projects (all or a list), environments (all or a list), and resources (`variables`, `accounts`, `projects`, optionally `files`, and optionally `requests`). The `files` resource, which unlocks [secret files](/platform/secret-files), is never granted by default. - **Surfaces** — which faces the key may use: `rest_api`, `mcp_server`, `github_action`. Because all three machine surfaces route through one authorizer, there is no second implementation of "is this key allowed" to drift out of sync. Denials are **returned, not thrown**, so the audit write always survives; an unknown, revoked, or expired key gets one uniform answer; and the plan gate (`public_api` / `mcp_server` per surface) is re-checked on **every** request. ```mermaid flowchart TD A[CLI] --> W[WorkOS identity
human auth] B[VS Code extension] --> W D[REST API] --> C E[MCP server] --> C F[GitHub Action] --> C C[_authorizeRequest
one enforcement core for API keys] --> G[(Convex
metadata + refs)] W --> G C --> H[(WorkOS Vault
encrypted values)] ``` ## The trust model: agents request, humans approve The core rule for machine credentials is simple: **a machine credential never writes a secret.** No API key — for the REST API, the MCP server, or the GitHub Action — can create, edit, or delete a variable's value. There is exactly **one** mutation a machine credential may perform, and only over the MCP server with a scope that includes the `requests` resource: it can **file a variable request**. A request is not a write. It is an ask, with a required justification, that lands in a human reviewer's queue in the dashboard. The reviewer decides, and if they approve, **the reviewer supplies the value.** The agent never proposes a value — it states what it needs and why, and a human fills in the secret. This keeps the blast radius of a leaked machine key bounded to _reads within its scope_ plus _the ability to ask a human for more_ — never the ability to silently change what a deploy pulls. GitHub Action keys are the strict case: they read a single project's variables — plus its [secret files](/action/secret-files) if the key was explicitly given the `files` resource — and they can **never** file a request. CI reads; it does not negotiate. ## The agent request loop When an agent needs a value it can't see, the round trip looks like this: ```mermaid sequenceDiagram participant Agent participant MCP as MCP server participant Reviewer as Human reviewer Agent->>MCP: envpilot_request_variable(key, justification) MCP-->>Agent: request filed (status: pending) Note over Reviewer: Sees the request in the dashboard Reviewer->>Reviewer: Approve and supply the value loop Poll Agent->>MCP: envpilot_get_request_status MCP-->>Agent: pending / approved / rejected end Agent->>MCP: envpilot_get_variable(key) MCP-->>Agent: value (now readable) ``` If the reviewer rejects instead, `envpilot_get_request_status` returns `rejected` along with the reviewer's reason, so the agent can report back why it was turned down rather than silently retrying. To keep a retry-looping agent from becoming reviewer-alert spam, request creation is tightly rate-limited and capped — see [Rate limits](/limits/rate-limits). The auth and audit contract shared across every surface is covered in [API Security](/api/authentication). ## Limits of this model - **No machine credential can write a secret.** There is no endpoint, tool, or flag that changes this. - **Scope is immutable** on an API key — widening access means minting a replacement. - **Human surfaces need a browser** for the device flow; a headless machine cannot sign in as a person. - **Machine surfaces are Pro-gated** (`public_api`, `mcp_server`) and re-checked per request. - The GitHub Action can never file a request, regardless of scope. ======================================================================== DATA MODEL ======================================================================== Source: https://docs.envpilot.dev/platform/data-model # Data model Two different secrets, two different storage patterns. Understanding which is which explains most of the behaviour you will meet in the CLI, the API, and the dashboard. ## The shape of things ```mermaid flowchart TD O[Organization] --> P[Project] O --> K[API keys] O --> M[Members + roles] P --> V[Variables] P --> F[Secret files] P --> A[Shared accounts] V --> S[Share links] V --> H[Version history] ``` An organization owns projects, members, and API keys. A project owns variables, secret files, and shared accounts. Everything below the project is scoped to one or more of the three fixed environments. ## Pattern 1 — variables: reference, never plaintext A variable row in Convex holds its key, environments, description, tags, flags, timestamps… and a **vault reference id**. The value itself lives in WorkOS Vault, encrypted at rest. ``` Convex row → vaultSecretId → WorkOS Vault → plaintext value ``` Convex never sees plaintext. A database dump is a list of key names and pointers. Reading a value requires a live, authorized call that resolves the reference through Vault. ## Pattern 2 — secret files: envelope encryption A keystore is not a string you can hand a key-value vault, so [secret files](/platform/secret-files) split the secret across **two** stores: ``` plaintext --AES-256-GCM--> ciphertext → Convex file storage key + iv → WorkOS Vault ``` Neither store alone is enough: Convex holds bytes it cannot read, Vault holds a key to something it does not have. That is strictly stronger than the variable model, where the Vault object _is_ the secret. Every upload mints a **fresh** key and nonce. There is no "re-encrypt in place" path, so the one catastrophic failure mode of AES-GCM — reusing a (key, nonce) pair — is unreachable by construction rather than prevented by a check somebody could later delete. ## What each store knows | Store | Holds | Useless without | | ----------------- | ------------------------------------------------------------------------ | ---------------------------------- | | Convex | Metadata, roles, grants, audit log, vault reference ids, file ciphertext | Vault (for values and file keys) | | WorkOS Vault | Variable values, shared-account credentials, secret-file key material | Convex (for which secret is which) | | Your machine / CI | Whatever a pull just wrote to disk | — | The third row is the honest one: once you pull, the plaintext is on your disk under your control. Everything Envpilot does after that — [commit guards](/extension/protection), `.gitignore` writes, value cloaking — is about keeping that copy from escaping. ## Invariants worth knowing - **(key, environment) is unique per project.** Enforced on every variable write path — create, update, request approval, restore. See [Variables](/platform/variables). - **(path, environment) is unique per project** for secret files, with the same logic. - **Deletes are soft** for 7 days, then purged with their vault objects. Restores re-run the uniqueness check rather than merging silently. - **Reads are bounded and never partial.** A read that cannot be completed in full fails loudly instead of returning a truncated set — a half-populated `.env` is worse than an error, because the process starts and then misbehaves. - **Every value-returning machine read is audited** against the API key that made it. ## Identifiers you will see | Prefix / shape | What it is | | ---------------- | --------------------------------------------------------------- | | `envpk_…` | An API key. Shown once at creation; only its SHA-256 is stored. | | Project slug | URL-safe project identifier, used by the CLI and REST API. | | Destination path | Where a secret file is written, relative to the project root. | ## Limits - Convex holds no plaintext values, so nothing in the database alone can be decrypted — but a compromised Vault credential _and_ a compromised database is a different story. - 1000 active secret files per project; every reader is bounded by that same number. - Version history is Pro-only; on Free, changes apply without a comparable record. - Once a client pulls, the plaintext on that disk is outside this model. ## See also - [Security](/platform/security) — encryption, revocation, audit - [Secret files](/platform/secret-files) — the file object in full - [Architecture](/start/architecture) — the enforcement core every surface shares ======================================================================== VARIABLES ======================================================================== Source: https://docs.envpilot.dev/platform/variables # Variables A variable in Envpilot isn't a row that gets overwritten in place: every change is versioned and every deletion is recoverable for a week. This page covers the lifecycle. Scheduled rotation and expiry reminders live on their own page — [Rotation & expiry](/platform/rotation). ## Per-environment key uniqueness Before anything else, one rule underlies every write path: **the same key may exist on multiple active variables in a project only if their environment sets are disjoint.** `DATABASE_URL` scoped to `[development]` and `DATABASE_URL` scoped to `[production]` are two independent variables; `DATABASE_URL` scoped to `[development, staging]` conflicts with one already scoped to `[staging]` because they overlap on `staging`. Attempting to create, update, or restore a variable into an overlapping environment fails with an error naming the clashing environment(s). This keeps every (key, environment) pair resolving to exactly one active variable — which is what makes `envpilot pull`, extension sync, and the public API deterministic. The check runs on every write path: create, update (when environments change), variable-request approval, and restore from trash. ## Version history Every value or metadata change to a variable inserts a new row into its version history: who changed it, when, which environments and description applied, and (for updates) an optional reason you can type in — up to 200 characters. The very first version is recorded automatically as "Initial creation." The history view (opened from a variable's detail panel) lets you filter to updates vs. rollbacks and compare versions side by side. It's driven entirely by these version records, so nothing is inferred after the fact. Version history is a Pro-tier feature (`variable_version_history`) — on Free tier the history list is empty. ## Rollback Rolling back restores a variable to an earlier version's value, description, and environments, and — like every other write — records a new version (`"Rolled back to version N"`) rather than deleting anything, so the history stays a complete, append-only log. Rollback is restricted to organization **Admins**; Team Leads, Developers, and Members cannot roll back a variable even if they can otherwise edit it. If the target version's secret value was written before per-change vault objects existed, only its metadata (description, environments) is restored, not the value — the mutation reports whether the value itself was actually restored. ## Trash and restore Deleting a variable is a soft delete: it's hidden from the active list, every active grant on it is revoked, and any live [share links](/platform/sharing) pointing at it are revoked too — a deleted variable can't leave a working external link behind. It then shows up in the project's **Trash** page for **7 days**, alongside any soft-deleted [shared accounts](/platform/shared-accounts), each labeled with how long ago it was deleted and how many days remain. Restoring re-runs the per-environment uniqueness check described above — if a new variable with the same key and an overlapping environment was created while the old one sat in the trash, the restore is rejected rather than silently creating a conflict. Restoring does **not** re-grant permissions or shares that were revoked on delete; those have to be re-created explicitly. Past the 7-day window, a background sweep purges the variable (and its vault object) permanently, and it's no longer restorable. Anyone with delete access to a project can also **Empty trash** to purge everything early, which destroys the underlying vault values immediately. ## Limits - **Version history** is Pro-only (`variable_version_history`). On Free the history list is empty — changes still apply, they just aren't recorded for comparison. - **Rollback** is Admin-only, regardless of tier or of who can edit the variable. - **Trash** holds deleted variables for exactly **7 days**, then a sweep purges them and the underlying vault object permanently. - **Variables per project**: 50 on Free, unlimited on Pro (`max_variables_per_project`). - Restores are rejected — never silently merged — when they would break the uniqueness rule. ## See also - [Rotation & expiry](/platform/rotation) — scheduled rotation reminders. - [Roles & permissions](/platform/rbac) — who can restore, rollback, and manage trash. - [Secret sharing links](/platform/sharing) — how shares interact with deletion. - [Shared accounts](/platform/shared-accounts) — the parallel trash/restore behaviour for accounts. ======================================================================== SECRET FILES ======================================================================== Source: https://docs.envpilot.dev/platform/secret-files # Secret files Some secrets do not fit in a `.env`. An Android signing keystore, an Apple `.p8`, a `.p12` certificate chain, an SSH private key, a Google service-account JSON — these are files, and teams have been passing them around in Slack for years because there was nowhere else to put them. A **secret file** is a binary blob plus the two things that make it reproducible: **where it goes** and **what mode it gets**. A fresh clone plus one pull materialises everything a build needs. ## The object | Field | Meaning | | ---------------- | ---------------------------------------------------------------------------------------------------- | | Name | Display name. Defaults to the filename. Max 120 characters. | | Destination path | Where clients write it, relative to the project root — `android/app/upload.jks`. Max 400 characters. | | Mode | POSIX mode applied on write: `0600` (default) or `0400`. Nothing else. | | Environments | One or more of `development`, `staging`, `production`. | | Size, SHA-256 | Recorded server-side, so clients can diff without decrypting. | | Description | Optional. | ## How it is stored Envelope encryption. The bytes are sealed with a fresh AES-256-GCM key, the ciphertext goes to Convex file storage, and the key plus nonce go to WorkOS Vault. Neither store alone can read the file. Details in [Data model](/platform/data-model). ## The path is a trust boundary Every client writes this string to disk — the CLI into a repo, the GitHub Action into a runner. A path that escapes the project root turns "pull my secrets" into arbitrary file write, so validation is deliberately strict and rejects anything ambiguous rather than trying to sanitise it: - must be **relative** — no leading `/`, no `~`, no `C:` drive letter - forward slashes only; a backslash is rejected outright - no `..` segment, ever — it is refused, never resolved away - no NUL characters - no segment ending in a space or a period (Windows silently strips those, which changes where the file lands) - `./` and `//` are collapsed; the canonical form is what gets stored Reserved destinations are refused because writing them would let a secret file rewrite the tooling doing the pull, or the repository's history: - exact: `.git`, `.gitignore`, `.envpilot` - prefixes: `.git/`, `.envpilot/` Path uniqueness works like variable keys: the same path may exist in several environments as long as those environment sets do not overlap. ## Who can do what | Capability | What it allows | | ---------------------- | ------------------------------------ | | `project.files.create` | Upload a new secret file | | `project.files.update` | Blanket write on every in-scope file | | `project.files.delete` | Soft-delete and restore | Reading follows project access plus per-file grants, the same model variables use — a developer can be granted one keystore without being granted the rest. Uploads are additionally confined to the uploader's environment scope: you cannot upload a production file if you are scoped to development. ## Reading is audited, listing is not `list` and `status` operations are **metadata-only**: path, size, mode, checksum, environments. Nothing is decrypted and nothing is recorded as a download, so exploring what a project holds is cheap and quiet. Fetching **contents** is a different act. Every one is decrypted server-side, returned base64-encoded, and written to the audit log against the identity or API key that asked. ## Limits | Limit | Free | Pro | | ------------------------------ | ------ | --------- | | `secret_files` — feature | On | On | | `secret_files_limit` — per org | 3 | Unlimited | | `secret_files_max_bytes` | 256 KB | 8 MB | Plus one structural ceiling that no plan lifts: **1000 active files per project**. It is enforced at insert, not inferred later, because every reader is bounded by the same number — the listing, the path-collision scan, and the download rate-limit burst are all sized to it. A project that could exceed it would be unlistable and unpullable. Rate limits: **20 uploads per minute** per user, and file content reads refill at **60/minute** with a burst equal to the project ceiling, so a cold pull of any legal project fits in one burst. See [Rate limits](/limits/rate-limits). ## What cannot do secret files - **No client uploads except the CLI and the dashboard.** The VS Code extension materialises files during sync but has no upload command; machine credentials cannot write at all. - **Any machine credential, for writing.** API keys can read files (with the `files` resource) and can never upload, edit, or delete them. - **API keys by default.** The `files` resource is never granted automatically; you select it explicitly when minting a key. - **More than one environment per linked directory.** Because a file has one path, the extension materialises the first linked environment only — a dev and a prod `google-services.json` cannot both land in one directory. ## Working with them | Surface | How | | ------------- | ---------------------------------------------------------------------------------------------- | | Dashboard | Project → **Files**: upload, edit, per-file permissions, trash | | CLI | [`envpilot files`](/cli/files) — `list`, `status`, `pull`, `add`, `get`, `rm` | | VS Code | Materialised automatically on [sync](/extension/sync), with the same guards as a synced `.env` | | GitHub Action | [`files: true`](/action/secret-files) with `project` and optional `files-dir` | | REST API | [`GET /v1/files`](/api/files) | | MCP | [`envpilot_list_files` / `envpilot_get_file`](/mcp/tools) | ## See also - [Android keystore in CI](/guides/android-keystore-ci) — the end-to-end walkthrough - [Data model](/platform/data-model) — envelope encryption in detail - [Roles & permissions](/platform/rbac) ======================================================================== SHARED ACCOUNTS ======================================================================== Source: https://docs.envpilot.dev/platform/shared-accounts # Shared Accounts Not every credential your team shares is an environment variable. A login for a third-party vendor dashboard, a shared admin account for a SaaS tool, a support portal your whole team needs into — these are username/password pairs, not `KEY=value` secrets. Shared Accounts are Envpilot's object for exactly that: a project-scoped credential vault distinct from your variables. ## How it's stored An account record (`projectAccounts`) holds non-secret metadata directly in Convex — name, an optional website URL, an optional description, and which environments it applies to. The credentials themselves (username and password) are serialized as JSON (`{"username", "password"}`) and encrypted into WorkOS Vault; Convex stores only the opaque vault reference, never the plaintext values. This mirrors exactly how environment variable values are stored — same vault, same never-touches-the-database guarantee. ## Environment scoping Every account belongs to one or more of a project's environments (development, staging, production), just like variables. A scoped Developer whose project assignment restricts them to specific environments can only see and manage accounts that fall inside that scope — an account tagged production-only stays invisible to someone scoped to development. ## Who can create and access accounts Access follows the same role model as variables: - **Owners** (Admins) have write access to every account in the org. - **Project Managers / Team Leads** assigned to a project have write access to its accounts. - **Developers** assigned to a project see account metadata for everything, but only get the actual credentials (the vault reference) for accounts they hold an explicit grant on. Creating an account automatically grants its creator write access if their role doesn't already have blanket write. - **Unassigned org members** see nothing unless an account has been explicitly shared with them. Grants are per-account, read or write, and can carry an optional expiry — set from the account's **Share → Team member** tab, alongside revoking existing grants. This is a separate mechanism from [external share links](/platform/sharing), which hand a one-time or time-limited view to someone outside the organization entirely (the same drawer's **External link** tab). ## Dashboard flow From a project's **Accounts** page: - **Add Account** opens a form for name, website URL, description, username, password, and which environments it applies to. - Each account row supports **Edit** (update metadata or rotate the credentials — see below), **Delete** (soft delete), **Share** (external link), and, for anyone who can manage permissions, granting/revoking team-member access. - A **Reveal** action fetches and displays the current credentials for anyone with read access; every reveal is logged (see Audit below). - An environment filter narrows the list to one environment at a time. ## Rotating credentials There's no scheduled rotation cron for accounts the way there is for [variable expiry](/platform/variables) — rotation here means manually updating the stored username/password. Editing an account with new credentials rewrites the vault object in place (same reference, new version) after snapshotting the previous value; if the metadata update that follows fails for any reason, the previous credentials are restored so Vault and the account record never drift apart. The audit trail records that credentials changed without ever recording the values themselves. ## Deletion, trash, and restore Deleting an account soft-deletes it: it disappears from the active list, all of its active permission grants are revoked, and any live external share links pointing at it are revoked too — a deleted account can't leave a working share link behind. It stays recoverable from the project's **Trash** page for 7 days, after which it (and its vault object) is purged permanently. Restoring an account clears the deletion but does **not** re-grant previously revoked permissions or shares — those have to be re-created deliberately. ## Tier limits Shared Accounts are available on both tiers, capped by count: | Gate | Free | Pro | | -------------------------------------- | ------- | --------- | | `shared_accounts` (feature on/off) | Enabled | Enabled | | `shared_accounts_limit` (max accounts) | 5 | Unlimited | See [Plans & Feature Availability](/limits/plans) for the full tier matrix. ## Audit trail Every account lifecycle event is audited as a sensitive-data action: creation, updates (with which fields changed, and whether credentials were part of the change), deletion, restore, and every credential reveal (who, when, and — where available — IP/user agent). Audit entries never contain the credential values themselves. ## Limits - Available on both tiers, capped at **5 accounts on Free**, unlimited on Pro (`shared_accounts_limit`). - Credentials are a username/password pair plus metadata — not arbitrary files. For those, see [secret files](/platform/secret-files). - Deletion is a soft delete with the same **7-day** trash window as variables. - Machine credentials can read accounts with the `accounts` resource; nothing machine-driven can create or rotate one. ## See also - [Roles & Permissions](/platform/rbac) — the underlying role and grant model shared with variables. - [Secret Sharing Links](/platform/sharing) — sending an account's credentials to someone outside the org. - [Security](/platform/security) — Envpilot's encryption and access-control architecture. ======================================================================== SECRET SHARING LINKS ======================================================================== Source: https://docs.envpilot.dev/platform/sharing # Secret Sharing Links Every team eventually needs to hand a credential to someone who isn't in Envpilot — a contractor, a vendor's support engineer, a client setting up their own deployment. The usual answer is pasting it into Slack or email, where it sits in plaintext forever. Sharing links are Envpilot's alternative: a link that reveals one secret, to one verified recipient, then either destroys itself or expires. A share link can point at either of the two secret types Envpilot stores: - A **variable** — a single environment variable's value. - A **shared account** — a full [username/password credential pair](/platform/shared-accounts). ## Security model The share pipeline is designed so the server never has the plaintext: 1. **Client-side encryption.** The browser encrypts the secret with AES-256-GCM using a randomly generated key **before** anything leaves the browser. 2. **The key never touches the server.** It travels only in the URL fragment (the part after `#`), which browsers never send in HTTP requests. The link looks like `https://www.envpilot.dev/s/#` — the server only ever sees ``. 3. **Ciphertext at rest.** The encrypted payload is stored in WorkOS Vault; Convex holds only a vault reference, never the ciphertext or the key. 4. **Optional passphrase.** You can add a second factor — a passphrase the recipient must also know — which is mixed into the decryption on top of the URL-fragment key. 5. **Email + OTP-gated reveal.** A recipient must enter an email address matching the list you specified, then a 6-digit one-time code sent to that address, before the ciphertext is released to their browser for decryption. The code expires after 5 minutes; 5 failed attempts locks that email out of the share entirely. 6. **Self-share is blocked.** You cannot add your own account email as a recipient — you already have access to the resource. Because decryption happens entirely in the recipient's browser, the encrypted payload is never persisted anywhere outside memory on either end. ## Creating a share From a project's Variables or Accounts page, open the item you want to share and choose **Share** (accounts have a separate **External link** tab alongside sharing with a team member — see [Shared Accounts](/platform/shared-accounts)). The share form asks for: - **Recipient emails** — up to 10, added one at a time. - **Share mode** — one of: - **One-time view** (recommended, and the default) — the link is destroyed the instant the recipient successfully decrypts it. A second visit, even by the same recipient, fails. - **Time-limited** — the link stays valid for a fixed window and can be viewed multiple times until it expires. - **Expiry** — 1 hour, 6 hours, 24 hours, or 7 days. - **Passphrase** (optional) — an extra secret the recipient must enter separately from the OTP. Generating the link requires an active org membership and at least read access to the resource being shared — you can't mint a link for something you can't see yourself. On generation, Envpilot emails each recipient and shows you the link once. **The link cannot be regenerated** — if you lose it, revoke the share and create a new one. ## The recipient experience The link opens a standalone page (`/s/[token]`) with no Envpilot account required: 1. **Verify email** — the recipient enters their email address. Envpilot always responds with success here regardless of whether it matches, to avoid leaking which emails are on the recipient list. 2. **Enter the code** — a 6-digit OTP emailed to that address, with a 5-minute countdown and a "Request new code" option once it expires. 3. **Passphrase**, if one was set. 4. **Reveal** — the decrypted secret renders in the browser. A variable share shows `KEY=value`; an account share shows the account name, URL, username, and a masked password (each field individually copyable, the password with a show/hide toggle). A banner explains whether the credential was just destroyed (one-time) or remains live until expiry (time-limited). If the underlying variable or account is deleted before the link is used, the recipient sees a "revoked" state rather than stale data — deleting a resource revokes every active share pointing at it. ## Revocation and limits Anyone with an Admin or Team Lead org role can revoke a share, in addition to the person who created it — useful if a link was sent to the wrong person. Revoking immediately invalidates the token; the vault ciphertext is deleted in the background. Creating shares is rate-limited to 10 per hour per organization. Two feature-registry gates control availability: - `secret_sharing` — whether the org can create share links at all (Pro tier; disabled on Free). - `max_active_shares` — how many can be active at once (unlimited on Pro). See [Plans & Feature Availability](/limits/plans) for the current Free/Pro breakdown. ## Safety guidance - Prefer **one-time view** unless the recipient genuinely needs repeat access — it minimizes the window a leaked link stays useful. - Add a **passphrase** and send it through a different channel (a phone call, a separate message) than the link itself, so a compromised inbox alone isn't enough to read the secret. - Treat a copied share link as equivalent to the secret itself until it's used or expires — the decryption key is embedded in the URL. - Revoke a link the moment you suspect it went to the wrong person; don't wait for it to expire on its own. ## Limits - **Pro only** (`secret_sharing`). On Free the share button is unavailable, and `max_active_shares` is 0. - **10 new shares per hour** per organization. - A share reveals **one** variable or account, never a set. - The decryption key rides in the URL — anyone holding the link holds the secret until it expires or is revoked. - Revocation is immediate; expiry is not extendable — issue a new link instead. ## See also - [Shared Accounts](/platform/shared-accounts) — the other resource type that can be shared this way. - [Security](/platform/security) — Envpilot's broader encryption and access-control model. - [Plans & Feature Availability](/limits/plans) — tier gating for sharing and shared accounts. ======================================================================== DIAGRAMS IN DOCUMENTATION ======================================================================== Source: https://docs.envpilot.dev/platform/doc-diagrams # Diagrams in documentation A page describing a deploy pipeline, an auth handshake or an approval flow is mostly a picture with sentences around it. An uploaded image goes stale the moment the flow changes, and nobody can diff a PNG. A documentation page body can instead carry [mermaid](https://mermaid.js.org) source in a fenced code block. The diagram lives in the page as text, so it diffs like prose, is found by search like prose, and can be written by a coding agent over MCP like prose. ## Writing one Fence a block with the language `mermaid`, anywhere in the body: ````markdown ```mermaid flowchart LR A[Draft] --> B[Published] ``` ```` In the editor, **Insert → Blocks → Diagram** drops in a starter flowchart so you do not have to remember the fence. The same fence renders on every surface that shows a page body: | Surface | What renders | | ------------------------------------- | ----------------------------------------- | | Dashboard reader | The diagram | | Split-view editor | The diagram, live, as you type | | [Shared pages](/platform/doc-sharing) | The diagram, exactly as its author saw it | The renderer is ~500 KB, so it is loaded only once a page actually contains a `mermaid` fence. A page without one costs nothing. ## Two examples Both of these are plain fences in this page's markdown source — open the raw markdown from the page actions if you want to copy one. A flowchart of the documentation review gate: ```mermaid flowchart TD A[Agent drafts a page over MCP] --> B{A human reviews it} B -- Not yet --> C[Stays a draft] B -- Approved --> D[Published] D --> E[Readable by the team] D --> F[Shareable] ``` A sequence diagram of a CLI pull: ```mermaid sequenceDiagram participant Dev as Developer participant CLI as envpilot CLI participant API as Envpilot API participant Vault as WorkOS Vault Dev->>CLI: envpilot pull CLI->>API: token, project, environment API->>Vault: fetch values Vault-->>API: plaintext API-->>CLI: variables CLI-->>Dev: .env written ``` ## What a diagram may not do Mermaid runs at `securityLevel: "strict"`. HTML inside a node label is escaped rather than rendered, and `click` callbacks are ignored. ## Bounds and the fallback Two parser bounds apply, because a body of `graph TD` edges is perfectly legal markdown and would otherwise hang the reader's tab: | Bound | Value | | ------------- | ----------------- | | `maxTextSize` | 50,000 characters | | `maxEdges` | 500 | A diagram that exceeds a bound, or that does not parse, **falls back to its own source rendered as a plain code block**. The rest of the page is unaffected — one broken diagram never costs you the page around it. The editor preview depends on this: a diagram fails on nearly every keystroke while you are typing it, and recovers the moment it parses. ## No tier gate Rendering happens in the browser and costs the platform nothing, so there is no feature key for it and no Free/Pro difference. The [page caps](/limits/plans) on documentation still apply — a diagram is part of a page, not a resource of its own. ## See also - [Sharing documentation](/platform/doc-sharing) — handing a published page to a teammate or minting a public link - [MCP tools](/mcp/tools) — how an agent drafts a page, and what a human must do to publish it - [Plans & limits](/limits/plans) — documentation page caps per project and per organization ======================================================================== SHARING DOCUMENTATION ======================================================================== Source: https://docs.envpilot.dev/platform/doc-sharing # Sharing documentation Anyone assigned to a project already reads every published page in it, so sharing only means something in two cases: reaching an organization member who is **not** on the project, and reaching someone who is not on Envpilot at all. Both are grants to a **person or a URL**, never to a machine. Both are scoped to exactly what you chose to share — one page, or one module — and never to the project around it: not its other modules, and nothing about its variables, files or accounts. ## Sharing with teammates From a published page, open **Share** and pick organization members. Set how long the access lasts (24 hours, 7 days, or 30 days) and, optionally, a one-line note explaining why you sent it. Each recipient gets an email, and the page appears under **Shared with me** in their dashboard sidebar. That entry only appears once something has actually been shared with them, so it is not a permanently empty inbox for the rest of the team. If a recipient turns out to have normal project access anyway, they are sent to the real page instead of the stripped-down reader. Re-sharing a page to someone who already holds it **extends their access** rather than creating a second grant — no duplicate row in your shared-with list and no second email for access they already have. | Property | Value | | ---------- | ----------------------------------------------------------------- | | Capability | `project.docs.share` | | Roles | Owner, Project Manager, Team Lead, Developer, Editor — not Viewer | | Tier key | `doc_sharing` — available on Free and Pro | | Expiry | 24 hours, 7 days, or 30 days | | Note | Up to 280 characters | | Recipients | Up to 20 per action | ## Sharing a whole module Handing over twenty pages one at a time means twenty shares and twenty emails to the same person. The **Whole module** option in the share drawer replaces all of that with one share and one message, on either audience. A module share is a **subscription, not a snapshot**. It names the module; the pages inside it are resolved every time the recipient opens it. A page published into the module next week is covered without anyone re-sharing, and a page returned to draft disappears from the recipient's index on its own. Recipients get an index of the module and read pages from there. A module with no published pages cannot be shared at all — the drawer refuses rather than handing over an empty index. | Property | Value | | ---------- | ------------------------------------------------------------------------------------------------------ | | Capability | Same as the audience: `project.docs.share` internally, `project.docs.share.external` for a public link | | Emails | One per recipient, naming the module and its page count | | Pages | Up to 200 per module share | | Link cap | A module link counts as **one** against `max_active_doc_links` | Both scopes appear together on the project's **Shared** tab, alongside shared variables, with the same status filters. ## Public preview links The **Public link** tab mints a URL at `/d/`, where the token is `dshr_` followed by 64 hex characters of randomness. Anyone holding the URL can read that one page until it expires or is revoked; no account is required. Expiry is **mandatory** and capped at **30 days**. There is no permanent link. You may add a **passphrase** (8–200 characters). It is hashed with scrypt server-side, and every read verifies it inside Convex — there is no "already unlocked" flag a caller could assert on a direct call. A verified reader gets a short-lived cookie carrying the hash, which Convex re-checks anyway. A shared page carries a `noindex` robots meta tag, and the API responses behind it carry `X-Robots-Tag: noindex, nofollow, noarchive`. The link is already unguessable; the point of both is that one crawler reaching one link would otherwise publish the page permanently. | Property | Value | | ---------- | ----------------------------------------------------------------------------------- | | Capability | `project.docs.share.external` | | Roles | Owner and Project Manager only | | Tier keys | `doc_public_links` (Pro only), `max_active_doc_links` (0 on Free, unlimited on Pro) | | Expiry | 24 hours, 7 days or 30 days in the dashboard; 1 hour to 30 days over the API | | Passphrase | Optional, 8–200 characters, scrypt-hashed | ## What kills a share Every one of these is re-checked on **every read**, not just when the share was created — nothing here trusts create-time state: - the expiry passing - a manual revoke - the page being unpublished - the page being moved to trash - the recipient leaving, or being suspended from, the organization - documentation being switched off for the organization - for a public link, the organization dropping to a tier without the feature Unpublishing or trashing a page additionally revokes every live share pointing at it on the spot, rather than leaving dead links for readers to discover. An hourly job marks past-expiry rows `expired`, but that is bookkeeping for the shared-with list and the active-link count — a row it has not reached yet is already dead to readers. The sender may always revoke their own share; revoking someone else's needs `project.docs.delete`. ## Shares are never granted to a machine The MCP tools (`envpilot_search_docs`, `envpilot_get_doc`) and the REST API do not consult the share table at all. An agent reads documentation because its API key is scoped to the project, never because a human shared a page with someone. Widening a share to a machine identity is not something you can configure — the code path does not exist. ## Auditing and notifications | Audit action | When | | ------------------- | ----------------------------------- | | `doc.shared` | A share is created, either audience | | `doc.share_revoked` | A share is revoked | | `doc.share_viewed` | A shared page is read | Every share also carries a view count and a last-viewed timestamp, visible in the page's shared-with list. View rows are attributed to the share's **creator** — that row answers "who used the link I created", and an anonymous external reader has no user row to attribute to; their IP and user agent travel on the audit row instead. `doc.shared` fires the `docs` event group, so Slack and Discord webhooks pick it up alongside `doc.published`. See [Notifications](/integrations/notifications). ## Limits - **30 share actions per hour** per organization, burst 10. Every share sends mail, so the bound is on outbound volume — handing one page to a five-person team in one action is a legitimate burst. - **10 passphrase attempts per hour** per IP. Only a wrong guess costs a token; asking for the salt, or a first visit with no attempt, costs nothing. - **`max_active_doc_links`** caps how many public links may be live at once — 0 on Free, unlimited on Pro. Revoking or expiring one frees its slot. ## See also - [Diagrams in documentation](/platform/doc-diagrams) — they render on shared pages too - [Roles & permissions](/platform/rbac) — the capability model both gates use - [Plans & limits](/limits/plans) — the documentation tier matrix - [Secret sharing links](/platform/sharing) — the equivalent for a single secret value ======================================================================== ROLES & PERMISSIONS ======================================================================== Source: https://docs.envpilot.dev/platform/rbac # Roles & permissions Envpilot has a **unified role model**: each member holds exactly one role in the organization, and that role is a named set of granular capabilities. Which projects the role applies to is a separate question, answered by project assignments. ``` who you are → one organization role (capabilities) where it applies → project assignments (+ optional environment scope) exceptions → per-variable / per-file grants ``` ## The roles Four system roles ship with every organization, plus two seeded non-system roles. | Role | Level | In one line | | ------------------- | ----- | ------------------------------------------------------------------------------- | | **Owner** | 100 | Every capability, always. Billing, deletion, ownership transfer. | | **Project Manager** | 80 | Full control of assigned projects; manages team leads and developers. | | **Team Lead** | 60 | Manages variables, files and access inside assigned projects. | | **Editor** | 50 | Creates and edits secrets in assigned projects. No approvals, members, sharing. | | **Developer** | 40 | Works in assigned projects; values need explicit grants. Can file requests. | | **Viewer** | 20 | Sees everything in assigned projects; changes nothing. | Level is a hierarchy, not decoration: several actions (removing a member, applying a Security Hold, changing someone's role) require the target to be strictly below you. ## What each role can do | Capability area | Owner | Project Manager | Team Lead | Editor | Developer | Viewer | | ---------------------------------------- | ----- | --------------- | --------- | ------ | --------- | ------ | | Read assigned projects | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Create variables / accounts / files | ✓ | ✓ | ✓ | ✓ | ✓ | — | | Update / delete variables & files | ✓ | ✓ | ✓ | ✓ | — | — | | Reveal secret values locally | ✓ | ✓ | ✓ | ✓ | — | — | | Draft documentation pages | ✓ | ✓ | ✓ | ✓ | ✓ | — | | Edit anyone's documentation page | ✓ | ✓ | ✓ | ✓ | — | — | | Publish a documentation page | ✓ | ✓ | ✓ | ✓ | — | — | | Delete anyone's documentation page | ✓ | ✓ | ✓ | — | — | — | | Share a page with a teammate | ✓ | ✓ | ✓ | ✓ | ✓ | — | | Mint a public documentation link | ✓ | ✓ | — | — | — | — | | Review requests | ✓ | ✓ | ✓ | — | — | — | | File a request | — | — | — | — | ✓ | — | | Manage per-secret permissions | ✓ | ✓ | ✓ | — | — | — | | Manage project members | ✓ | ✓ | ✓ | — | — | — | | Create share links | ✓ | ✓ | ✓ | — | ✓ | — | | Invite organization members | ✓ | ✓ | ✓ | — | — | — | | Create projects | ✓ | ✓ | — | — | — | — | | Roll back a variable version | ✓ | — | — | — | — | — | | Manage API keys | ✓ | — | — | — | — | — | | Billing, organization settings, deletion | ✓ | — | — | — | — | — | Owners do not file requests because they never need to — they create directly. Owners also hold every capability by construction, including any capability shipped after their organization was created. Two documentation rows have an authorship exception: you can always revise your own page, and you can always trash your own **draft**. Once a page is published it belongs to the team, so deleting it needs the delete capability no matter who wrote it. ## Project assignments and environment scope A role says what you may do; a project assignment says where. A member with no assignment for a project cannot see it, whatever their role — Owners excepted, who see everything. Assignments can additionally carry an **environment scope**, used for developers: a variable is only accessible if _all_ of its environments fall inside the scope. A developer scoped to `[development]` never sees a `[staging, production]` variable, and cannot upload a production secret file. ## Per-secret grants Beyond roles, access can be granted on an individual variable or secret file. That is how a developer gets exactly the one credential they need. A grant carries: - **Target** — the specific variable or file - **Level** — `read` or `write` - **Expiry** — optional; access revokes itself when the date passes - **Granted by** — recorded for audit Developers receive an automatic write grant on secrets they create themselves, which is why they can create without being able to edit everything. ## How push behaves by role ```bash # Owner / Project Manager / Team Lead / Editor envpilot push # ✓ Pushed 5 variables to production # Created: 3 Updated: 2 # Developer — writes what they hold a grant for, refuses the rest envpilot push # ✓ Pushed 2 variables to development # ⚠ 3 variable(s) were NOT written (access denied): # ✗ STRIPE_SECRET_KEY ``` ## Requests When a developer needs a secret they cannot create or see, they file a request and a reviewer (Owner, Project Manager, or Team Lead) approves it and supplies the value. Agents can file requests too, over MCP. The full loop, including the caps that stop an agent from flooding reviewers, is in [Requests & approvals](/platform/requests). ## Security Hold Security Hold freezes a member's access across the entire organization **without removing them** — membership, role, assignments and grants all stay exactly as they are. It exists for the moment access must stop now, before anyone has decided whether the person is leaving. **What it does.** Every authorization check denies that member immediately, reads and writes alike. Active CLI tokens and VS Code sessions are revoked in the same action, and the resulting revocation events tell the extension to delete its locally synced `.env` files. A held member can sign in and see that they are suspended, with the date — not who did it or why. Lifting the hold restores the exact prior shape; nothing has to be rebuilt. **Who can apply it.** The same capability as removing a member, plus the hierarchy rule: only someone strictly below you, never yourself, and never an Owner (transfer ownership first if an Owner's access is the incident). **Credential sweep.** Applying a hold surfaces any organization API keys the held member created that are still active so they can be reviewed and revoked — opt-in, not automatic, since a shared key may still be powering everyone else's CI. Security Hold is Pro-gated (`security_hold`). ## Custom roles The role registry is capability-backed, so new roles are data rather than code. Today they are managed by the Envpilot team from the internal admin panel — creating a role, editing its capabilities, activating or deactivating it. **There is no self-serve custom-role UI for organizations yet.** If you need a shape the six roles above do not cover, contact support. ## Limits - One organization role per member. There is no "two roles at once". - Rollback is Owner-only. - API key management is Owner-only (org-wide creation). - Environment scope applies to assignments, not to roles. - Tier enforcement being disabled never relaxes RBAC — feature gates and roles are independent systems. ## See also - [Requests & approvals](/platform/requests) - [Security](/platform/security) — revocation, audit, sessions - [Secret files](/platform/secret-files) — file-specific capabilities ======================================================================== REQUESTS & APPROVALS ======================================================================== Source: https://docs.envpilot.dev/platform/requests # Requests & approvals A request is an ask, not a change: _"I need `STRIPE_SECRET_KEY` in production, here is why."_ It lands in a reviewer's queue, and if the reviewer approves, **the reviewer supplies the value**. The requester never proposes secret material. That single rule is what lets Envpilot hand a coding agent real access without handing it write access. ## Who can file, who can review | Actor | Files requests | Reviews requests | | ----------------------------------------------- | -------------------- | ---------------- | | Owner, Project Manager, Team Lead | — (creates directly) | ✓ | | Developer | ✓ | — | | Editor, Viewer | — | — | | API key with the `requests` resource (MCP only) | ✓ | — | Reviewing needs the `project.requests.review` capability. Developers see only their own requests; reviewers see every request in their projects. ## The human loop Rejection carries the reviewer's reason back to the requester, so nobody has to guess whether they were denied or ignored. Cancelling is available to the requester while a request is still pending. ## The agent loop An API key with the `requests` resource — used over the [MCP server](/mcp/overview) — can file exactly the same kind of request. It cannot approve one, and it cannot supply a value. ```mermaid sequenceDiagram participant Agent participant MCP as MCP server participant Reviewer as Human reviewer Agent->>MCP: envpilot_request_variable(key, justification) MCP-->>Agent: filed (status: pending) Reviewer->>Reviewer: Approve and supply the value loop Poll Agent->>MCP: envpilot_get_request_status MCP-->>Agent: pending / approved / rejected end Agent->>MCP: envpilot_get_variable(key) MCP-->>Agent: value ``` A justification is **required** on machine-filed requests. A reviewer approving a request should be able to tell what the agent was doing when it asked. ## Limits Every approval fans out an email, so an agent stuck in a retry loop is a reviewer-spam engine unless it is capped twice: | Cap | Value | | ------------------------------ | ------------------------------- | | Machine request rate | 5 per hour per API key, burst 2 | | Machine requests standing open | 5 pending per API key | Hitting the standing cap returns a plain refusal: wait for a human to review what you already filed. Both caps are per **key**, so revoking a runaway agent's key stops it instantly. See [Rate limits](/limits/rate-limits). Other boundaries worth knowing: - Requests create variables. They are not a channel for editing an existing value — that is a direct write by someone who holds the capability. - The GitHub Action can never file a request. CI reads; it does not negotiate. - Approval is the only path where a machine-originated request produces a value, and a human always types it. ## See also - [CLI: requests](/cli/requests) — `request`, `requests list/approve/reject/cancel` - [MCP: agent requests](/mcp/agent-requests) - [Roles & permissions](/platform/rbac) ======================================================================== PROTECTED ENVIRONMENTS ======================================================================== Source: https://docs.envpilot.dev/platform/protected-environments # Protected environments Mark an environment as protected and nobody writes to it directly. Not the owner, not a lead, not an agent. A write becomes a change request: the new secret is encrypted at once, the request waits in the inbox, and a second person applies it. Production stays exactly as it was until then. Protection is enforced in one place, the backend mutations that write variables, accounts, and files. Every client calls those same mutations, so a new client cannot route around it. ## Turning it on Project Settings, Protection. Pick the environments to protect. Needs the `project.protection.manage` capability (owner, project manager, team lead by default) and the Pro plan. Turning it off never needs the plan. An organization that lost the feature can always remove protection on purpose. Doing so is audited as critical and sent to your security notification channel. ## Who does what | Role | Sees | Writes dev and staging | Writes production | Proposes a production change | Approves | Configures | Override | | --------------- | --------------- | ---------------------- | ----------------- | ---------------------------- | -------------- | ---------- | ------------------------ | | Owner | all | yes | via approval | yes | yes, never own | yes | yes, audited as critical | | Project manager | all | yes | via approval | yes | yes, never own | yes | no | | Team lead | all | yes | via approval | yes | yes, never own | yes | no | | Editor | dev and staging | yes | never, cannot see | no | no | no | no | | Developer | dev | yes | never, cannot see | no | no | no | no | | Viewer | all | no | no | no | no | no | no | Three rules hold this together. 1. Nobody writes directly into a protected environment. The only live-write path is override, and every use is audited as critical. 2. The person who proposed a change never approves it. Two people, always. 3. Proposing needs no new permission. If you could edit the resource before protection, you can propose the edit after. ## Role environment defaults Each role carries a default environment scope. Developers get development. Editors get development and staging. Leads, owners, and viewers see everything. Admins change these per role in the admin panel, and a project member's scope can narrow the role default but never widen it. Out-of-scope resources are hidden, not refused. A developer never sees a production variable, so there is nothing to click. The refusal only appears when someone reaches for it blind, for example `envpilot push --env production`. ## The flow 1. A lead edits `DATABASE_URL` in production and saves. 2. The backend checks the normal write capability and environment scope, then sees production is protected. It encrypts the new value into the vault, stores a pending change request, and returns "sent for approval". Production is untouched. 3. Approvers get an email and, if configured, a Slack or Discord message. After 48 hours idle, one reminder goes out. 4. A second lead opens the request, sees the current and proposed metadata (never plaintext), and approves with a reason. The backend re-checks that the approver is not the requester, that the resource did not change since the request was filed, and that no key conflict appeared, then applies the write in the same transaction. 5. On reject, cancel, or expiry (30 days idle), the staged secret is deleted from the vault. ## What counts as a protected write Any write that touches a protected environment, before or after the change: create, update, delete, restore from trash, rollback to an older version, and adding or removing a protected environment from a resource's list. Reads, exports, tags, share links, and rotation reminders are unaffected. ## Every client | Surface | Protected write | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | Dashboard | Save becomes "Propose change". The request appears in the Requests page. | | CLI `push` | Refuses and lists the protected environments. `envpilot push --request` files one change request per changed key. | | CLI `secrets set`, `files add` | Offers to file a change request. | | MCP, VS Code, JetBrains | Already request-only. Unchanged. | | GitHub Action, REST API | Read-only. Unchanged. | ## Activity log Every step writes an audit row: `protection.enabled`, `protection.disabled`, `change.requested`, `change.applied`, `change.rejected`, `change.canceled`, `change.expired`, `change.overridden`, `change.reminder_sent`. Each carries the environments it touched, so searching the audit log for "production" finds them. ======================================================================== ROTATION & EXPIRY ======================================================================== Source: https://docs.envpilot.dev/platform/rotation # Rotation & expiry Credentials that never rotate are credentials that leak eventually. A variable can carry a rotation schedule, and Envpilot handles the nagging. ## Setting a schedule Set `rotationFrequencyDays` (1–3650) when creating or updating a variable, from the dashboard's variable drawer. Enabling it stamps an `expiresAt` timestamp and a `rotationStatus` of `active`. ## What the scheduler does An hourly job walks every rotation-enabled variable and: - moves it to **`expiring_soon`** once it is within **7 days** of expiry, and emails a reminder, - sends a second reminder at the **1-day** mark, - moves it to **`expired`** once the timestamp passes, with a final email. No variable gets more than one reminder per 24 hours, so a long-expired secret does not turn into an hourly mail loop. ## Seeing what's due The dashboard surfaces an "expiring soon" list scoped to what you can actually see: an environment-scoped developer only sees variables inside their scope, never the whole project. ## Limits | Gate | Free | Pro | | -------------------------------------------------------- | ------------------------- | --------- | | `secret_rotation` — feature on/off | Disabled | Enabled | | `secret_rotation_limit` — max rotation-enabled variables | 7 (unreachable while off) | Unlimited | Rotation is one of the features that carries **two** gates, because each enabled variable costs recurring compute: the hourly scan reads it and may send mail. On Free the boolean gate is off, so no schedule can be created regardless of the numeric cap. Rotation does **not** rotate anything for you. It reminds a human to rotate the credential at the provider and update the value here — Envpilot never contacts your upstream vendor. ## See also - [Variables](/platform/variables) — version history, rollback, trash - [Plans & limits](/limits/plans) — the full tier matrix - [Notifications](/integrations/notifications) — routing these events to Slack or Discord ======================================================================== SECURITY ======================================================================== Source: https://docs.envpilot.dev/platform/security # Security Secrets are encrypted at rest with per-organization key isolation, access is enforced per secret, and every action is attributed in an audit log. ## Encryption at rest Variable values are encrypted with **AES-256** in [WorkOS Vault](https://workos.com/vault). Envpilot's database stores only vault reference ids — never plaintext. Each secret carries its own encryption context derived from organization, project, and (optionally) environment, so secrets are cryptographically isolated across organizations. [Secret files](/platform/secret-files) go further, using **envelope encryption**: the file is sealed with a fresh AES-256-GCM key, the ciphertext lands in Convex file storage, and the key material lands in Vault. Neither store alone can read the file. See [Data model](/platform/data-model). ## What the application layer holds 1. You create or update a variable; the value goes to Vault under your organization's key context. 2. Vault encrypts and returns a reference id. 3. Envpilot stores the reference id. 4. An authorized read sends the reference id back to Vault. 5. Vault decrypts and returns the value. At no point does the application layer keep an unencrypted secret in persistent storage. A database dump is a list of key names and pointers. ## Access control Three layers, in order: 1. **Organization role** — one capability set per member ([Roles & permissions](/platform/rbac)) 2. **Project assignment** — where the role applies, with optional environment scope 3. **Per-secret grants** — read or write on an individual variable or file, with optional expiry A developer who needs something they cannot see files a [request](/platform/requests) rather than escalating their own role. ## Instant revocation Revoking access takes effect everywhere, not on the next login: - **CLI** — the next call returns 401 and local tokens are cleared. - **VS Code** — the revocation arrives over the live connection within seconds and the extension deletes the synced `.env` files it wrote (`envpilot.preventCopyOnRevoke`). - **Dashboard** — the variable disappears from view. - **API keys** — revoked keys fail their very next request; there is no cached-authorization window. [Security Hold](/platform/rbac#security-hold) does all of the above at once, org-wide, without deleting the membership. ## Audit trail Over 40 action types are recorded — variable reads, writes and deletions, secret-file downloads, permission grants and revocations, authentication events, invitations and role changes, CLI and extension sessions, and every API-key use that returned secret material. Each entry carries the actor, IP address, user agent, and timestamp. Denials are logged too: the authorizer returns denials rather than throwing, precisely so the audit write survives the refusal. Logs can be filtered in the dashboard and exported as JSON. Retention is tier-dependent: **7 days** on Free, **365 days** on Pro (`audit_log_retention_days`). ## Authentication | Surface | Method | | ------------------- | ----------------------------------------------------------------------------------- | | Web dashboard | WorkOS AuthKit SSO — Google, GitHub, email | | CLI | Device-code flow: one-time code, browser confirmation, polling; tokens auto-refresh | | VS Code extension | OAuth in the browser; the session is stored in VS Code's secure credential storage | | REST / MCP / Action | API key (`envpk_…`) — SHA-256 hash stored, plaintext shown once | Sessions use short-lived access tokens with automatic refresh, and individual sessions can be revoked from Settings. ## What stays on your side Once a pull completes, plaintext is on your disk under your control. Envpilot's local defences are real but they are defences of last resort: - The CLI writes secrets only where you asked, and adds pulled paths to `.gitignore` before writing. - The VS Code extension ships a dual-layer [commit guard](/extension/protection) — a staging-time block plus a git pre-commit hook — along with clipboard protection and value cloaking. - `envpilot run` avoids the problem entirely by injecting secrets into a child process without writing a file. - All API traffic is HTTPS with TLS 1.2+. ## Limits - **Audit retention** is 7 days on Free, 365 on Pro. - **Metadata-only reads are not audited individually** — they carry no secret exposure, and logging them would drown the signal. - Envpilot cannot revoke a secret that has already left a machine: rotation is the remedy. - SSO is registered as a feature but is **not enabled on any tier** today. - Self-hosting is not offered; the encryption model assumes WorkOS Vault. ## See also - [Data model](/platform/data-model) · [Secret files](/platform/secret-files) - [API authentication](/api/authentication) — the machine-credential contract - [Rate limits](/limits/rate-limits) ======================================================================== PLANS & LIMITS ======================================================================== Source: https://docs.envpilot.dev/limits/plans # Plans & Limits Envpilot has two tiers, Free and Pro. Every gatable feature — resource caps, tool access, security controls — is defined once in a dynamic feature registry and resolved per-organization on every request. This page lists every feature and its value on each tier. > Limits are enforced server-side and can change independently of this page. Check `envpilot usage` (add `--json` for scripting) or the dashboard **Usage** page for your organization's live numbers against its actual limits. No pricing is listed here — see the website for current pricing. ## Resources | Feature | What it does | Free | Pro | | ------------------------- | ------------------------------------------------------ | ---- | --------- | | Max Projects | Number of active projects an organization can create | 3 | Unlimited | | Max Variables per Project | Number of active variables allowed in a single project | 50 | Unlimited | | Max Organizations | Number of organizations a user can create | 1 | Unlimited | ## Team | Feature | What it does | Free | Pro | | ----------------------- | ------------------------------------------------------------- | ---- | --------- | | Max Team Members | Active members plus pending invitations, counted together | 3 | Unlimited | | Max Pending Invitations | Outstanding invitations an organization can have open at once | 5 | Unlimited | ## Variables | Feature | What it does | Free | Pro | | ------------------------ | ---------------------------------------------------- | ---- | --- | | Variable Version History | Keep and roll back to prior values of a variable | No | Yes | | Bulk Import | Import a `.env` file as many variables in one action | No | Yes | | Bulk Delete | Delete multiple variables in one action | Yes | Yes | | Bulk Export | Export multiple variables in one action | No | Yes | | Variable Tags | Tag variables for organization and filtering | Yes | Yes | ## Shared variables | Feature | What it does | Free | Pro | | ------------------------------ | ------------------------------------------------------------------- | ---- | --------- | | Shared Variables | One row read by several projects, merged from existing duplicates | No | Yes | | Max Shared Groups | Groups of shared variables an organization can hold | 0 | Unlimited | | Max Projects per Shared Group | Projects one group can reach; bounds the per-write conflict lookups | 0 | 50 | | Max Shared Groups per Project | Groups one project can read; bounds the per-pull reads | 0 | 5 | | Max Variables per Shared Group | Rows one group can hold | 0 | Unlimited | ## Secret files | Feature | What it does | Free | Pro | | ---------------- | ---------------------------------------------------------------- | ------ | --------- | | Secret Files | Store keystores, SSH keys, certificates and service-account JSON | Yes | Yes | | Max Secret Files | Active secret files an organization can store | 3 | Unlimited | | Max File Size | Largest single secret file, before encryption | 256 KB | 8 MB | See [Secret files](/platform/secret-files) for the object model and the structural ceilings that no tier lifts. ## Documentation | Feature | What it does | Free | Pro | | -------------------------- | ------------------------------------------------------------ | ---- | --------- | | Project Documentation | Write project pages agents draft over MCP and humans publish | Yes | Yes | | Max Pages per Project | Active documentation pages allowed in a single project | 10 | Unlimited | | Max Pages per Organization | Active documentation pages across every project in the org | 25 | Unlimited | | Documentation Sharing | Hand a published page to a named organization member | Yes | Yes | | Public Documentation Links | Mint an expiring `/d/` preview link for a page | No | Yes | | Max Active Public Links | Public documentation links that may be live at once | 0 | Unlimited | Both page caps count active pages only — a page in the trash frees its slot immediately, and restoring one re-checks the caps. Creation over MCP is bounded by the same numbers as the dashboard. See [MCP tools](/mcp/tools) for how an agent drafts a page and what a human must do to publish it, and [Sharing documentation](/platform/doc-sharing) for what the two sharing gates allow. ## Tools | Feature | What it does | Free | Pro | | ---------------------------- | ------------------------------------------------------ | ---- | --- | | API Access | Use the REST API surface at all | Yes | Yes | | VS Code Extension | Use the VS Code extension to sync variables | Yes | Yes | | JetBrains IDE Plugin | Use the JetBrains plugin to sync variables | Yes | Yes | | CLI Access | Use the `envpilot` CLI | Yes | Yes | | VS Code Unsync Customization | Customize which files the extension excludes from sync | No | Yes | ## Security | Feature | What it does | Free | Pro | | ------------------------------------- | --------------------------------------------------------------- | ---- | --------- | | Granular Permissions | Per-variable read/write grants beyond role defaults | Yes | Yes | | Audit Log Retention (days) | How many days of audit log history stay queryable | 7 | 365 | | SSO | Single sign-on | No | No | | Secret Rotation & Expiry | Scheduled rotation reminders and expiry on variables | No | Yes | | Max Rotation-Enabled Variables | Number of variables that can have rotation enabled | 7 | Unlimited | | Secret Sharing Links | Create expiring links to share a secret externally | No | Yes | | Security Hold (Suspend Member Access) | Freeze a member's org-wide access without removing them | No | Yes | | Max Active Shares | Number of active secret-sharing links allowed at once | 0 | Unlimited | | Shared Accounts | Store credentials for a shared third-party account on a project | Yes | Yes | | Max Shared Accounts | Number of shared accounts an organization can store | 5 | Unlimited | > SSO shows `No` on both tiers today — the feature is registered but not yet enabled for any tier. ## Customization | Feature | What it does | Free | Pro | | ------------------------- | --------------------------------------------- | ---- | --- | | Custom Keyboard Shortcuts | Rebind CLI TUI / dashboard keyboard shortcuts | Yes | Yes | | Custom Branding | Replace default branding in the dashboard | No | Yes | ## Analytics | Feature | What it does | Free | Pro | | -------------------------- | ----------------------------------------- | ---- | --- | | Analytics Retention (days) | How many days of usage analytics are kept | 7 | 30 | ## Support | Feature | What it does | Free | Pro | | ---------------- | ----------------------------------- | ---- | --- | | Priority Support | Priority queue for support requests | No | Yes | ## Integrations | Feature | What it does | Free | Pro | | ------------------------- | ---------------------------------------------------------- | ---- | --- | | Public REST API | Programmatic access via API keys to `/api/v1/*` | No | Yes | | MCP Server | Access via the Model Context Protocol server for AI agents | No | Yes | | Team Notifications | Slack and Discord delivery of project events | No | Yes | | Max Notification Channels | Webhook destinations an organization can register at once | 0 | 10 | The GitHub Action authenticates with an API key over the same core as the REST API, so it inherits the `public_api` gate. ## Ceilings no plan lifts Some limits are structural, not commercial: | Ceiling | Value | | ------------------------------- | --------------------------------------------------------- | | Environments per project | 3 — `development`, `staging`, `production` | | Active secret files per project | 1000 | | Secret file path length | 400 characters | | Secret file modes | `0600` or `0400` | | Rotation frequency | 1–3650 days | | Trash retention | 7 days, then permanent purge | | API read size | Bounded; a read that cannot complete in full fails loudly | ## How enforcement works Every gatable feature lives in one dynamic feature registry, resolved fresh on every request — never a hardcoded per-tier check scattered through the app. **Resolution chain.** A boolean feature resolves: organization → owner → the owner's tier → an active billing grace period (if downgrading) → the tier's feature override → the registry default. Numeric features resolve the same way, returning either a number or `null` for unlimited. **Where it's checked.** Backend mutations and queries check the resolved value at the exact point of the write or read they're gating — e.g. creating a project checks the project limit against the organization's current active project count before inserting. The dashboard mirrors the same check so the UI reflects what the backend will actually allow, and CLI/extension calls route through the same registry over the API. **Dual gates.** Features with recurring background cost (rotation reminders, scheduled scans) pair a boolean gate with a numeric limit — e.g. secret rotation being available at all, plus how many variables can have it enabled. Both are checked before the feature runs. **At a limit.** A denied check returns a readable, user-facing error — "Limit reached (3/3). Upgrade your tier for more." — never a silent no-op or a generic server error, and nothing is written before the check passes. **Pre-alpha bypass.** All enforcement is gated behind a platform-wide admin toggle. When it's off, every boolean check resolves to allowed and every numeric limit resolves to unlimited — RBAC role checks are unaffected either way. ======================================================================== RATE LIMITS ======================================================================== Source: https://docs.envpilot.dev/limits/rate-limits # Rate limits Envpilot is an open-source platform, and its rate limits are documented so you can build integrations that respect them instead of discovering them by getting throttled. Every machine surface — the REST API, the MCP server, the GitHub Action, the Docker image, and machine-filed variable requests — is metered per credential with a token-bucket limiter. ## The buckets Each limit is scoped **per key**. Two keys never share a bucket. | Bucket | Limit | Applies to | | -------------------- | --------------------------- | ------------------------------------------------------------------------------ | | Value pulls | 30 / min | CI/CD secret pulls (GitHub Action), and any REST/MCP call that decrypts values | | Docker pulls | refill 30 / min, burst 120 | Container secret pulls — every call the Docker image makes that returns values | | Metadata reads | 120 / min | REST + MCP metadata calls — org, project lists, `metadata_only=true` reads | | Secret file reads | refill 60 / min, burst 1000 | Any call returning secret-file **contents** — CLI pull, Action, REST, MCP | | Secret file uploads | 20 / min | Uploading a secret file (per user, dashboard or CLI) | | Variable writes | 60 / min, burst 120 | Creating one variable at a time in the dashboard | | Bulk variable writes | refill 300 / min, burst 500 | Imports, `envpilot push`, and project templates — charged once per batch | | Variable requests | 5 / hour, burst 2 | Machine-filed variable requests (`envpilot_request_variable`) | | Documentation pages | 30 / hour, burst 10 | Machine-authored documentation drafts (`envpilot_create_doc`) | The **value-pull** bucket has full capacity available as a burst (deploys fan out matrix builds, so a spike is normal) while the sustained rate stays at 30/min. The **metadata** bucket is higher because those reads never touch the vault — no decrypt cost. The **Docker** bucket is separate from value pulls on purpose: a fleet of containers restarting must not spend the budget your CI pipeline depends on, and a crash-looping container must not throttle your deploys. Its numbers come from the workload rather than a round number — a container start costs at most two value-returning calls (the variables pull, plus one file-content batch when the entrypoint uses `--files`), and the burst is sized for 60 of them starting at once, so one rolling restart of a large replica set fits in a single burst. It then refills that full burst over four minutes, which is far above any healthy restart rate and far below a loop. The image honours `Retry-After` automatically, so a project whose secret files span several batches slows down rather than failing. The **bulk variable write** bucket is what an import, a `envpilot push`, or a project template spends, and it is charged **once for the whole batch** rather than once per variable. That distinction matters: charging per variable meant a 48-variable `.env` import stopped at the 31st key and left the project half-populated. A batch is now one reservation and one transaction, so it either lands completely or not at all. The burst equals the 500-variable ceiling a single batch may contain, so any batch the write path accepts can always be paid for in one go. The **secret file read** bucket is the odd one, and deliberately: burst and refill are different numbers. Clients fetch one file per call, so a cold pull of a large project is a burst of hundreds — a bucket that refilled as fast as it drained would license a sustained 10 files/second forever, which is exactly an exfiltration profile. Splitting them gives a first pull its one-off burst and then settles to one file per second. The burst equals the hard ceiling of 1000 files per project, so any project that can exist can always be pulled in one go. A second pull moments later is served from files already in sync and decrypts nothing at all. Secret-file **uploads** are metered per user rather than per key, because every upload encrypts, writes a blob, and creates a vault object. ## Variable requests are capped twice Machine-filed requests get an extra layer beyond the per-hour rate limit, because every created request emails a human reviewer — a retry-looping agent must be stopped before it becomes reviewer alert fatigue: - **Rate limit** — 5 per hour per key, with a burst of 2. This throttles how fast requests can be filed. - **Standing open-pendings cap** — a key may have at most **5 open pending requests** at once. Even within the rate limit, the 6th outstanding request is refused until a human reviews one of the existing five. - **Rejection cooldown** — after a request for a given key is **rejected**, the same key cannot re-file a request for that variable for **24 hours**. A rejected ask is a decision, not an invitation to immediately retry. The GitHub Action never files requests at all, so none of these apply to it — it only draws from the value-pull bucket. The Docker image never files requests either, and draws only from the Docker bucket. ## How many keys you may hold Rate limits bound how _often_ a credential is used. A separate, plan-level limit bounds how _many_ live credentials exist per surface, because each one is a standing key that returns plaintext every time it runs. | Surface | Free | Pro | | ------------- | ---- | --- | | Docker | 0 | 10 | | GitHub Action | 0 | 10 | Both surfaces are Pro features, so the free tier holds none. Revoked and expired keys free their slot immediately, which is what makes rotating a credential possible while at the limit. All surfaces together are additionally bounded by 25 keys per organization. These are plan defaults and can be adjusted per organization. ## What happens when you exceed a limit - **You are temporarily blocked, not queued.** The request is rejected immediately with a `429`. Envpilot does not hold requests and replay them later — an over-limit call fails and it is your integration's job to back off. - **The error tells you how long to wait.** A `429` carries a `Retry-After` header (and the error message names the retry-after window) with the number of seconds until the bucket refills enough to try again. - **Bursts are blocked instantly.** Once a bucket is empty, further calls fail on arrival — there is no grace window and no partial service. This is what keeps an abusive loop from amplifying load. - **Unknown keys are throttled separately.** Requests presenting a key hash that matches nothing on file are rate-limited per hash, to slow brute-force key guessing without affecting real keys. ## Keeping this page honest This table mirrors the limiter configuration in `convex/lib/rateLimits.ts` — where the Docker bucket's capacity and refill are computed from named constants rather than written as literals — and the request caps in the variable-request mutations. That source file carries a matching comment pointing back here, so the two are kept in sync when limits change. If you are integrating against Envpilot and something here looks off, the code is the source of truth — and it's public. See [Architecture](/start/architecture) for how the surfaces and the request loop fit together, and [API Security](/api/authentication) for the auth and audit model behind every request. ======================================================================== CLI OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/cli/overview # CLI overview [`@envpilot/cli`](https://www.npmjs.com/package/@envpilot/cli) v1.23.1 — pull, push, run, request, and manage secret files without leaving the terminal. Requires **Node.js 22+**. ## Install ```bash npm install -g @envpilot/cli ``` ```bash bun install -g @envpilot/cli ``` Or run one command without installing anything: ```bash npx @envpilot/cli login ``` ## First run ```bash envpilot sync ``` `sync` chains the three steps you would otherwise run yourself: authenticate in the browser, pick an organization → project → default environment, then pull. It writes a local `.envpilot` link file, adds `.env` to `.gitignore`, and installs the pre-commit guard (skip with `--no-guard`). Step by step, if you prefer: ```bash envpilot login # browser device-code flow envpilot init # link this directory to a project envpilot pull # write .env ``` ## The command set | Group | Commands | Page | | -------------- | --------------------------------------------- | -------------------------------- | | Account | `login` `logout` `whoami` `accounts` `config` | [Authentication](/cli/auth) | | Project links | `init` `switch` `unlink` `list linked` | [Linking projects](/cli/link) | | Sync | `sync` `pull` `push` | [Pull & push](/cli/pull-push) | | Run | `run` | [Running commands](/cli/run) | | Single secrets | `secrets set` `secrets rm` | [Single secrets](/cli/secrets) | | Approvals | `request` `requests` | [Requests](/cli/requests) | | Files | `files` | [Secret files](/cli/files) | | Browse | `list` `usage` `man` | [Full reference](/cli/reference) | ## Interactive terminal UI Run `envpilot` with no arguments and you get a terminal dashboard: arrow keys to browse commands, Enter to run, Esc to exit. It returns to the list after each command finishes. `envpilot ui` (alias `dashboard`) opens it explicitly. The TUI only opens when stdout is an interactive terminal, so scripts and CI never end up inside it. ## Where state lives | Path | Holds | | -------------------------------------- | ---------------------------------------------------------------- | | `.envpilot` (in your repo) | Which project(s) this directory is linked to, and the active one | | Global config (`envpilot config path`) | Authenticated accounts, tokens, API URL | | `~/.config/envpilot/run-cache/` | `envpilot run`'s metadata cache, mode `0600` | Secrets are never written to any of those. The only plaintext the CLI puts on disk is what a `pull` or `files pull` was explicitly asked to write. ## Version policy The CLI checks the server's release manifest before each command. If your version is below the server's minimum, the command stops with an upgrade prompt; if it is merely behind, you get a one-line notice and the command runs. Network failures fail **open** — a flaky connection never bricks the CLI. ## Limits - Node.js 22 or newer. There is no browser build. - `push` writes what your role allows and reports the rest as denied — it never files approval requests. Use [`request`](/cli/requests) for that. - Free plan: 3 projects, 50 variables per project, 3 secret files. See [Plans](/limits/plans). - Everything the CLI can do is bounded by your role — see [Roles & permissions](/platform/rbac). ## Next - [Authentication & accounts](/cli/auth) - [Full command reference](/cli/reference) — generated from the CLI's own catalog - [CLI in CI](/cli/ci) — non-interactive usage ======================================================================== AUTHENTICATION & ACCOUNTS ======================================================================== Source: https://docs.envpilot.dev/cli/auth # Authentication & accounts The CLI authenticates as **you**, not as a service. Everything it can read or write is decided by your organization role. ## Sign in ```bash envpilot login envpilot login --no-browser # print the URL instead of opening it envpilot login --api-url # non-production instances only ``` A device-code flow: the CLI mints a one-time code, your browser confirms it, and the CLI polls until the session is live. Tokens refresh automatically afterwards. ## Several accounts, no logout dance Each `envpilot login` **adds** an account rather than replacing the current one — a work identity and a personal one can coexist. ```bash envpilot accounts # list authenticated accounts envpilot accounts switch you@example.com # switch the active account envpilot accounts remove you@example.com # forget one account ``` Identifiers are an account id or the account's email, case-insensitive. Switching logs nobody out. ## Who am I right now ```bash envpilot whoami ``` Prints the authenticated user, the API target, and the active organization/project/environment context — the first thing to run when a command behaves as though it belongs to someone else. It validates the token against the server, so it also catches a stale session. ## Sign out ```bash envpilot logout # the active account envpilot logout --all # every account added via login ``` Local tokens are cleared even if the remote revoke call fails, so a logout on a plane still logs you out locally. With `--all`, a failing remote revoke stops the run at that account — re-run it once you are online to clear the rest. ## Local configuration ```bash envpilot config # show current config envpilot config list envpilot config get envpilot config set envpilot config path # where the global and project config live envpilot config reset ``` Useful mainly for pointing a development build at a non-production API URL, and for finding the config file when something looks wrong. ## Limits - The device flow needs a browser somewhere — a fully headless machine cannot complete a login. For CI, use an [API key](/api/authentication) with the REST API or the [GitHub Action](/action/overview) instead of a user session. - Sessions are revoked instantly when an admin revokes them, or when [Security Hold](/platform/rbac#security-hold) is applied — the next command returns 401 and clears local tokens. - `--api-url` affects only the current login; it does not migrate existing accounts. ## Next - [Linking projects](/cli/link) - [CLI in CI](/cli/ci) ======================================================================== LINKING PROJECTS ======================================================================== Source: https://docs.envpilot.dev/cli/link # Linking projects A link tells the CLI which project and environment this directory belongs to, so `pull`, `push`, `run` and `files` need no flags in daily use. Links live in a plain `.envpilot` file you can commit. ## Link a directory ```bash envpilot init envpilot init -o -p -e production envpilot init --add # link an ADDITIONAL project to this directory envpilot init --force # overwrite the existing link ``` Interactive by default: pick organization → project → default environment. ## Multiple projects in one directory A monorepo often needs several: `--add` appends a link instead of replacing one. One link is **active** at a time; commands act on the active link unless you pass `--project`. ```bash envpilot list linked # what this directory is linked to, and which is active envpilot switch --active api # make the 'api' link active ``` ## Switch the active target ```bash envpilot switch production # bare argument: a project slug or an environment name envpilot switch --env production envpilot switch --project api envpilot switch --organization envpilot switch --active api ``` `switch` edits local state only — it never rewrites your whole project config, and it never touches the server. ## Unlink ```bash envpilot unlink envpilot unlink api --force ``` Removes the link and updates the active-project state. **Existing `.env` files are left on disk untouched** — unlinking is not a cleanup command. ## Limits - A link is local. Nothing about it is stored server-side, and it grants no access by itself. - The active link is per directory, not per shell — two terminals in the same folder share it. - Environments are fixed at `development`, `staging`, `production`; `--env` accepts nothing else. - `unlink` does not delete secrets from disk. Delete the `.env` yourself, or let the [VS Code extension](/extension/protection) clean up on revocation. ## Next - [Pull & push](/cli/pull-push) - [Secret files](/cli/files) ======================================================================== PULL & PUSH ======================================================================== Source: https://docs.envpilot.dev/cli/pull-push # Pull & push `pull` brings variables down to a file. `push` sends a local file back up. Both act on the active link unless told otherwise. ## Pull ```bash envpilot pull # .env for the active project + environment envpilot pull --env staging --dry-run # show what would be written, write nothing envpilot pull --file .env.production # custom target envpilot pull --prefix NEXT_PUBLIC_ # only keys with this prefix envpilot pull --project api # a specific linked project envpilot pull --all # every linked project envpilot pull --force # overwrite without confirmation ``` Pulled paths are added to `.gitignore` before the file is written, not after. ### Output formats ```bash envpilot pull # .env (default) envpilot pull --format json # JSON envpilot pull --format yaml # YAML envpilot pull --format vercel # Vercel envpilot pull --format netlify # Netlify TOML envpilot pull --format aws # AWS Parameter Store JSON envpilot pull --format docker-compose # Docker Compose ``` ## Push ```bash envpilot push # merge .env into the active environment envpilot push --replace # replace: keys absent locally are deleted remotely envpilot push --dry-run # show the diff, change nothing envpilot push --file .env.local envpilot push --env staging envpilot push --project api envpilot push --force # skip the confirmation prompt ``` `--merge` is the default: local keys are created or updated, remote-only keys are left alone. `--replace` is the destructive one — it deletes remote keys missing from your file. Run it with `--dry-run` first. ## What your role lets through | Role | Push behaviour | | -------------------------------------------- | ------------------------------------------------------------------------------------ | | Owner / Project Manager / Team Lead / Editor | Writes every key in the project | | Developer with write grants | Writes the keys they hold a grant for; the rest are listed as denied and not written | | Developer with no access to the environment | The push is refused outright — nothing is written | | Viewer | Push is unavailable | ```bash envpilot push # ✓ Pushed 2 variables to development # Created: 1 Updated: 1 # ⚠ 3 variable(s) were NOT written (access denied): # ✗ STRIPE_SECRET_KEY ``` Pulling as a scope-limited role is symmetric: you receive the variables you are allowed to see, and nothing else appears in the file. Nothing signals that other keys exist. ## Protected environments If the target environment is marked protected (Settings → Protection), a push touching it is refused outright — nothing is written, remote or vault. Re-run with `--request` to file one change request per changed or new key instead; each prints its key and request id, and a second person with the approve capability has to accept it before anything changes. `--replace` deletions are never proposed this way — narrow the diff or push without `--replace` first. ## Limits - Three environments only. `--env` accepts `development`, `staging`, `production`. - `--replace` deletes remote keys that are absent locally — there is no undo beyond the [7-day trash](/platform/variables). - Invalid keys in the local file are reported and skipped rather than silently mangled. - Bulk export is Pro-gated (`bulk_export`), as is bulk import (`bulk_import`). - Pull writes plaintext to disk. If you would rather it never touched disk, use [`envpilot run`](/cli/run). ## Next - [Running commands with secrets](/cli/run) - [Single-secret edits](/cli/secrets) ======================================================================== RUNNING COMMANDS WITH SECRETS ======================================================================== Source: https://docs.envpilot.dev/cli/run # Running commands with secrets ```bash envpilot run -- bun dev ``` Secrets are fetched, injected into the child process environment, and gone when it exits. No file is written, so there is nothing to `.gitignore`, nothing to forget to delete, and nothing for a backup tool to pick up. Everything after `--` is the command to run. ```bash envpilot run -- npm test envpilot run -- python manage.py runserver envpilot run --env production -- node dist/server.js envpilot run --project api -- pnpm test ``` ## Options | Flag | Default | What it does | | ---------------------------- | -------------- | -------------------------------------------------------------------- | | `-e, --env ` | linked env | Environment to load | | `-p, --project ` | active project | Override the linked project | | `-o, --organization ` | linked org | Override the organization | | `--keep-existing` | off | Let your shell's variables win over fetched secrets | | `--print` | off | Preview what would be injected, run nothing | | `--shell` | off | Run through the user's shell, enabling pipes, `&&`, `$VAR` expansion | | `--no-cache` | off | Always fetch fresh | | `--cache-ttl ` | `0` | Serve cached secrets without even a freshness check for this long | | `-q, --quiet` | off | Suppress informational output | ## Freshness without the round trip Every run does one cheap metadata fingerprint check (roughly 50–100 ms, no decryption). Only a changed fingerprint triggers the expensive vault fetch: | State | What happens | Vault calls | | ------------------------- | --------------------------------------- | ----------- | | Unchanged | Fingerprint matches, cache is served | **0** | | Changed | Fingerprint differs, secrets fetched | 1× | | First run / cache cleared | Full fetch, cache written | 1× | | Offline | Cache served with a loud offline notice | **0** | A variable changed in the dashboard is visible on your very next run. `--cache-ttl` skips even the fingerprint check for a window — fastest, and blind to changes for that long. Cache files live in `~/.config/envpilot/run-cache/` with mode `0600`. A different account, server URL, or token invalidates the cache automatically. ## Overriding shell variables Fetched secrets win by default. `--keep-existing` reverses that, which is how you override one value locally without editing anything remote: ```bash DATABASE_URL=postgres://localhost/dev envpilot run --keep-existing -- bun dev ``` ## Preview ```bash envpilot run --print # Would inject 12 variables from backend/staging: # # DATABASE_URL=post…rd (52 chars) # API_SECRET=sk_t…2x (40 chars) # Dry run — no command executed. ``` `run` also tells you when variables you can access exist only in **other** environments — `Injected 8 of 12 — 4 not in development: FOO, BAR` — instead of quietly dropping them. ## Process behaviour - Signals (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT`) are forwarded to the child. - The child's exit code becomes the CLI's exit code, so `run` composes in scripts and CI. - On Windows the command goes through the shell, so `.cmd` and `.bat` files resolve. ## Limits - Secrets exist in the child's environment. Anything that can read `/proc//environ` or a crash dump can read them — `run` removes the _file_ risk, not every risk. - Without `--shell`, shell syntax (`&&`, pipes, globs) is not interpreted — that is deliberate, so a command cannot be smuggled through an argument. - The cache holds metadata for freshness checks, not plaintext secrets. - `run` reads only variables; [secret files](/cli/files) still have to be materialised with `files pull`. ## In CI ```bash envpilot run --env production --quiet --no-cache -- ./scripts/deploy.sh ``` `--no-cache` guarantees freshness, `--quiet` keeps the log clean. See [CLI in CI](/cli/ci). ======================================================================== SINGLE SECRETS ======================================================================== Source: https://docs.envpilot.dev/cli/secrets # Single secrets ## Set one secret ```bash envpilot secrets set # guided: key → masked value → sensitive? envpilot secrets set STRIPE_SECRET_KEY -e production # key given, value prompted masked envpilot secrets set API_URL=https://api.example.com # inline — CI only ``` Two-step by default: the key is validated first, then the value is typed into a **masked prompt**, so it never reaches your shell history or `ps` output. The inline `KEY=VALUE` form exists for CI and prints a history warning when you use it interactively. `envpilot var …` is an alias for the same command. **Options** — `-e, --env ` · `-p, --project ` · `-d, --description ` · `--sensitive` · `--all-envs` Behaviour worth knowing: - **Role-aware.** If your role cannot write directly, the same flow files a [variable request](/cli/requests) instead of rejecting you. - **Shared values need consent.** A value shared across several environments lives on one variable, so updating it changes all of them — the CLI asks first, and `--all-envs` answers non-interactively. - Plan limits are enforced server-side and reported readably; `envpilot usage` shows where you stand. ## Protected environments If the target environment is marked protected (Settings → Protection), `secrets set` doesn't fail or need a flag — it automatically files a change request instead of writing, and prints the request id. `secrets rm` asks first (`production is protected. File a change request to delete it?`); answer no and nothing changes. Non-interactively, without `--yes`, it fails with the same message and a hint to re-run with `--yes`. Either way a second person with the approve capability has to accept the request before the variable actually changes. ## Delete one secret ```bash envpilot secrets rm OLD_FLAG envpilot secrets rm OLD_FLAG -e staging --yes ``` What happens depends on how widely the key is scoped: | Situation | Result | | ------------------------------------------- | --------------------------------------------------------------------- | | Key exists only in the selected environment | Moved to trash — recoverable from the dashboard for 7 days | | Key shared across environments | Only this environment is detached; the value stays live in the others | ## Limits - `secrets set` writes one key in one environment per invocation. For bulk work use [`push`](/cli/pull-push). - `secrets rm` never bypasses the trash: recovery is via the dashboard, not the CLI. - `--value` inline forms land in shell history. Prefer the masked prompt; in CI prefer stdin, see [CLI in CI](/cli/ci). ## Next - [Requests](/cli/requests) - [Variables](/platform/variables) — trash, versions, rollback ======================================================================== REQUESTS ======================================================================== Source: https://docs.envpilot.dev/cli/requests # Requests Developers ask; reviewers decide. Both halves work from the terminal. ## File a request ```bash envpilot request envpilot request --project api ``` An interactive wizard: key → masked value → description → environments. Your environment choices are limited to your assigned scope. Only roles that carry the request capability (developers, and any custom role given it) file requests — owners, project managers and team leads create variables directly instead. ## See the queue ```bash envpilot requests # the linked project's requests envpilot requests --status pending envpilot requests --project api --json envpilot requests --changes # change requests (protected environments) instead ``` Reviewers see every request in their projects; developers see only their own. The `ID` column is what the review subcommands take. `--json` always prints a plain array — the requests array by default, or the change-requests array with `--changes`. ## Review ```bash envpilot requests approve k5738xq2… envpilot requests reject k5738xq2… --reason "use the shared key" envpilot requests cancel k5738xq2… ``` `approve`, `reject`, and `cancel` also accept a change-request id (from `envpilot requests --changes`) — they look it up in the linked project automatically and route it to the right queue. Pass `--project` if the id belongs to a different linked project. Approving a **machine-filed** request — one with no value, because an agent may never propose secret material — prompts you, masked, for the value: ```bash # CI: read the value from stdin so it never lands in argv or shell history printf %s "$SECRET" | envpilot requests approve k5738xq2… --value-stdin ``` `--value ` exists for CI without a usable stdin and is **rejected in interactive sessions**, because it would put the secret in your shell history. ## Limits - `--status`, `--json` and `--changes` apply to listing only; `--value`, `--value-stdin` and `--reason` apply to review subcommands only. - Cancelling is the requester's action, on their own pending request. - A rejected request for a given key cannot be re-filed by the same API key for **24 hours**. A rejection is a decision, not a retry prompt. - Machine-filed requests are capped at 5 per hour per key with at most 5 open at once. See [Rate limits](/limits/rate-limits). - Requests create variables. Editing an existing value is a direct write by someone who holds the capability. ## Next - [Requests & approvals](/platform/requests) — the model behind this - [MCP agent requests](/mcp/agent-requests) ======================================================================== SECRET FILES ======================================================================== Source: https://docs.envpilot.dev/cli/files # Secret files Keystores, SSH keys, `.p12` certificates, service-account JSON — the secrets that do not fit in a `.env`. Each carries a destination path and a mode, so one command turns a fresh clone into a buildable checkout. Concepts and storage model: [Secret files](/platform/secret-files). ## Inspect without decrypting ```bash envpilot files list # alias: envpilot files ls envpilot files list -e production envpilot files status ``` Both are **metadata-only**: path, size, mode, checksum, environments. Nothing is decrypted, and nothing is recorded as a download — exploring what a project holds is free. `status` compares each recorded file against your working directory and reports `missing`, `in sync`, or `modified`. ## Materialise them ```bash envpilot files pull envpilot files pull -e production envpilot files pull --force ``` Writes every in-scope file to its recorded path with its recorded mode. Two safeguards: - **Paths are gitignored before they are written**, not after. - **A local file that differs from the server is never silently replaced.** The pull refuses, lists the conflicting paths, and exits non-zero until you pass `--force`. Someone's debug keystore is worth more than a tidy diff. One file at a time: ```bash envpilot files get android/app/upload.jks envpilot files get android/app/upload.jks --force ``` ## Upload ```bash envpilot files add ./upload.jks \ --path android/app/upload.jks \ --env production \ --mode 0600 \ --name "Play upload keystore" \ --description "Signing key for release builds" ``` | Flag | Default | Meaning | | ------------------------ | ---------------------- | ----------------------------------------- | | `--path ` | the file's own path | Where clients will write it | | `-n, --name ` | the filename | Display name | | `-e, --env ` | the linked environment | Comma-separated environments | | `--mode ` | `0600` | `0600` or `0400` — nothing else | | `-d, --description ` | — | Optional note | | `--project ` | active link | Use a specific linked project | | `--request` | off | Propose instead of failing when protected | ## Remove ```bash envpilot files rm android/app/upload.jks envpilot files rm android/app/upload.jks -e staging --yes envpilot files rm android/app/upload.jks --all-envs ``` Like variables, removal is environment-aware: by default only the selected environment is detached, and the file stays live in the others. `--all-envs` trashes it everywhere. ## Protected environments If the target environment is marked protected (Settings → Protection), a plain `files add` is refused outright — re-run with `--request` to file a change request per upload instead. `files rm` asks first ("File a change request to delete it?" / "...remove it from ``?"); non-interactively, without `--yes`, it fails with the same message and a hint to re-run with `--yes`. Either way a second person with the approve capability has to accept the request before the file actually changes. ## Limits - **Reading contents is audited.** Every `pull` and `get` is recorded against you. `list` and `status` are not. - **File size**: 256 KB on Free, 8 MB on Pro. **Count**: 3 files on Free, unlimited on Pro — with a hard ceiling of **1000 active files per project** that no plan lifts. - **Rate**: content reads refill at 60/min with a burst of 1000; uploads are 20/min per user. - **Paths are strict** — relative only, forward slashes, no `..`, and `.git`, `.gitignore`, `.envpilot` are reserved. Rejected paths are refused, never sanitised. - **Modes** are `0600` or `0400`. There is no `0755`; these are secrets, not scripts. - **Uploading is CLI or dashboard only.** The VS Code extension writes secret files during [sync](/extension/sync) but cannot upload, edit, or delete them. ## Next - [Android keystore in CI](/guides/android-keystore-ci) - [GitHub Action: secret files](/action/secret-files) ======================================================================== COMMAND REFERENCE ======================================================================== Source: https://docs.envpilot.dev/cli/reference # Command reference Complete surface of `@envpilot/cli` v1.23.1. Run `envpilot man ` for the same information offline. {/* generated:cli-commands start */} | Command | Group | What it does | | ----------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `envpilot` | Get Started | Open the Ink-powered terminal dashboard for discovering and running commands. Aliases: `envpilot ui, envpilot dashboard`. | | `envpilot ui` | Get Started | Open the interactive Ink-powered terminal UI. Aliases: `envpilot dashboard`. | | `envpilot sync` | Sync | Authenticate, select a project, pull variables, and set up local protection in one flow. | | `envpilot man` | Get Started | Show the CLI manual page with commands, workflows, and security guidance. | | `envpilot login` | Get Started | Authenticate the CLI against the Envpilot web app. | | `envpilot init` | Get Started | Link the current directory to a project and choose a default environment. | | `envpilot pull` | Sync | Download project variables into a local file or export format. | | `envpilot push` | Sync | Upload local variables back to Envpilot, writing only the keys you have access to. | | `envpilot request` | Sync | Submit a request to create a new environment variable for review (developers only). | | `envpilot requests` | Browse | List variable requests, or approve/reject/cancel them without leaving the terminal. | | `envpilot secrets` | Sync | Change one secret without pull/edit/push. Two-step by default: key first, value prompted MASKED (never in shell history). Aliases: `envpilot var`. | | `envpilot run` | Sync | Inject project secrets into a child process without writing a .env file (envpilot run -- bun dev). | | `envpilot list` | Browse | List organizations, projects, variables, or linked projects from the terminal. | | `envpilot list organizations` | Browse | List organizations available to the current user. Aliases: `envpilot list orgs`. | | `envpilot list projects` | Browse | Browse projects in the active organization. | | `envpilot list variables` | Browse | Inspect variables for a project with environment and tag filtering. Aliases: `envpilot list vars`. | | `envpilot list linked` | Browse | Show projects linked in the current directory. | | `envpilot switch` | Project | Switch the active organization, project, environment, or linked project. | | `envpilot usage` | Browse | Inspect current plan usage and feature availability for the active organization. | | `envpilot whoami` | Account | Show the authenticated user, API target, and current active CLI context. | | `envpilot accounts` | Account | List authenticated accounts and switch or remove them without logging out. | | `envpilot files` | Project | Manage secret files — keystores, SSH keys, certificates, and service-account JSON — that cannot live in a .env. | | `envpilot config` | Account | Inspect or update local CLI configuration such as the active API URL. | | `envpilot logout` | Account | Revoke the current CLI session and clear local auth state. | | `envpilot unlink` | Project | Remove a linked project from the current directory without deleting local env files. | ## Command details ### `envpilot` Open the Ink-powered terminal dashboard for discovering and running commands. ```bash envpilot envpilot ui envpilot dashboard ``` - This is the default when you run `envpilot` with no subcommand. - Supports search, keyboard navigation, and command launch. ### `envpilot ui` Open the interactive Ink-powered terminal UI. ```bash envpilot ui envpilot dashboard ``` - Use this when you want the UI explicitly instead of relying on the default no-arg launcher. ### `envpilot sync` Authenticate, select a project, pull variables, and set up local protection in one flow. **Arguments** — `[--organization ] [--project ] [--env ]` ```bash envpilot sync envpilot sync --env production ``` - Best first-run workflow for local setup. - Reuses the existing login, init, and pull logic under the hood. ### `envpilot man` Show the CLI manual page with commands, workflows, and security guidance. **Arguments** — `[command]` ```bash envpilot man envpilot man pull ``` - Use this to see the supported command set. - Supports per-command manual sections. ### `envpilot login` Authenticate the CLI against the Envpilot web app. **Arguments** — `[--api-url ] [--no-browser]` ```bash envpilot login envpilot login --no-browser ``` - Opens the web authentication page by default. - Required before organization or project commands will work. ### `envpilot init` Link the current directory to a project and choose a default environment. **Arguments** — `[--organization ] [--project ] [--env ]` ```bash envpilot init envpilot init --add ``` - Creates or updates the local `.envpilot` file. - Supports linking multiple projects with `--add`. ### `envpilot pull` Download project variables into a local file or export format. **Arguments** — `[--env ] [--file ] [--format ]` ```bash envpilot pull envpilot pull --env staging --dry-run ``` - Supports `.env`, JSON, YAML, Vercel, Netlify, AWS, and Docker Compose formats. - Can pull the active linked project or all linked projects. ### `envpilot push` Upload local variables back to Envpilot, writing only the keys you have access to. **Arguments** — `[--env ] [--file ] [--merge|--replace]` ```bash envpilot push envpilot push --replace ``` - Owners, project managers, and team leads write across the project; developers write only the variables they hold a write grant for. - Keys you cannot write are skipped — push does not create approval requests. - Compares local and remote variables before applying changes. ### `envpilot request` Submit a request to create a new environment variable for review (developers only). **Arguments** — `[--project ]` ```bash envpilot request envpilot request --project api ``` - Only assigned developers can submit requests — owners, project managers, and team leads create variables directly. - Environment choices are limited to the developer's assigned environment scope. - An owner, project manager, or team lead must approve the request before the variable is created. ### `envpilot requests` List variable requests, or approve/reject/cancel them without leaving the terminal. **Arguments** — `[list] [--project

] [--status ] [--json] | approve [--value |--value-stdin] [--reason ] | reject [--reason ] | cancel ` ```bash envpilot requests envpilot requests --status pending envpilot requests approve envpilot requests approve --value sk_live_… envpilot requests reject --reason "use the shared key" envpilot requests cancel ``` - Reviewers (owner, assigned project manager/team lead) see every request; developers see only their own. - --status/--json apply to listing only; --value/--value-stdin/--reason apply to review subcommands only. - Approving a machine (valueless) request prompts MASKED for the value; --value-stdin reads it from stdin for CI (keeps it out of argv), --value is a last resort that lands in shell history. - Get the \ from the ID column of `envpilot requests`. ### `envpilot secrets` Change one secret without pull/edit/push. Two-step by default: key first, value prompted MASKED (never in shell history). **Arguments** — `set [|] [-e ] [-p ] [-d ] [--sensitive] [--all-envs] | rm [-e ] [-p ] [--yes]` ```bash envpilot secrets set envpilot secrets set STRIPE_SECRET_KEY --env production envpilot secrets set API_URL=https://api.example.com envpilot secrets rm OLD_FLAG --env staging --yes ``` - Interactive by default: the key is validated first, then the value is prompted masked so it never lands in shell history — KEY=VALUE inline is for CI and prints a history warning. - Role-aware: direct-write roles set immediately; request-only roles are offered the request workflow instead (a reviewer approves with `requests approve`). - Plan limits are enforced server-side and reported readably; check `envpilot usage` for your tier. - set upserts one key in one environment (merge); updating a value shared across environments requires confirmation (--all-envs non-interactively). - rm on a single-environment secret moves it to trash (recoverable from the dashboard); on a shared secret it only removes THIS environment — the value stays live in the others. - `envpilot var …` still works as an alias. ### `envpilot run` Inject project secrets into a child process without writing a .env file (envpilot run -- bun dev). **Arguments** — `[--env ] [--project ] [--keep-existing] [--print] -- [args...]` ```bash envpilot run -- bun dev envpilot run --env production -- node server.js envpilot run --project api -- pnpm test envpilot run --print envpilot run --keep-existing -- bun dev ``` - Use `--` to separate envpilot flags from the command to execute. - Secrets override existing shell vars by default; pass --keep-existing to flip. - On Windows, the command is run through the shell so .cmd / .bat files resolve. - Signals (SIGINT, SIGTERM, SIGHUP, SIGQUIT) are forwarded to the child process. - Use --print to inspect what would be injected without executing anything. ### `envpilot list` List organizations, projects, variables, or linked projects from the terminal. **Arguments** — `[resource]` ```bash envpilot list envpilot list projects envpilot list variables ``` - Default resource is `projects`. - Use `linked` to inspect local `.envpilot` project links. ### `envpilot list organizations` List organizations available to the current user. ```bash envpilot list organizations envpilot list orgs --json ``` - Useful for discovering organization IDs and roles. ### `envpilot list projects` Browse projects in the active organization. **Arguments** — `[--organization ] [--json]` ```bash envpilot list projects envpilot list projects --json ``` - Shows project roles when available. - Useful for selecting project IDs for automation and scripts. ### `envpilot list variables` Inspect variables for a project with environment and tag filtering. **Arguments** — `[--project ] [--env ] [--tag ]` ```bash envpilot list variables envpilot list variables --env production --show-values ``` - Values are masked by default. - Designed to mirror the web app’s searchable variable surface. ### `envpilot list linked` Show projects linked in the current directory. ```bash envpilot list linked ``` - Shows the active linked project and environment mapping. ### `envpilot switch` Switch the active organization, project, environment, or linked project. **Arguments** — `[--organization ] [--project ] [--env ] [--active ]` ```bash envpilot switch --env production envpilot switch --active api ``` - Updates local CLI state without rewriting the whole project config. - Supports both linked projects and remote project lookup. ### `envpilot usage` Inspect current plan usage and feature availability for the active organization. **Arguments** — `[--organization ] [--json]` ```bash envpilot usage envpilot usage --json ``` - Shows project, member, and variable limits. - Useful for CLI feature-gate troubleshooting. ### `envpilot whoami` Show the authenticated user, API target, and current active CLI context. ```bash envpilot whoami ``` - Validates the current access token against the website. - Useful for debugging stale auth or wrong API URL targets. ### `envpilot accounts` List authenticated accounts and switch or remove them without logging out. **Arguments** — `[list|switch |remove ]` ```bash envpilot accounts envpilot accounts switch you@example.com envpilot accounts remove you@example.com ``` - Identifiers can be an account id or the account's email (case-insensitive). - Switching accounts does not log anyone out; use `envpilot logout` to remove the active session. ### `envpilot files` Manage secret files — keystores, SSH keys, certificates, and service-account JSON — that cannot live in a .env. **Arguments** — `[list|status|pull|add |get |rm ]` ```bash envpilot files list envpilot files status envpilot files pull envpilot files add ./upload.jks --path android/app/upload.jks -e production ``` - `list` and `status` are metadata-only: nothing is decrypted and no download is recorded. - `pull` refuses to overwrite a local file that differs from the server unless you pass --force. - Pulled paths are added to .gitignore before the files are written. - Every download of file contents is audited. ### `envpilot config` Inspect or update local CLI configuration such as the active API URL. **Arguments** — `[list|get|set|path|reset]` ```bash envpilot config envpilot config path ``` - Useful for local development against non-production API URLs. - Shows both global and project-level config paths. ### `envpilot logout` Revoke the current CLI session and clear local auth state. ```bash envpilot logout ``` - Best way to reset a stale CLI session cleanly. - Clears local tokens even if the revoke call fails. ### `envpilot unlink` Remove a linked project from the current directory without deleting local env files. **Arguments** — `[project] [--force]` ```bash envpilot unlink envpilot unlink api --force ``` - Updates `.envpilot` and active-project state. - Leaves existing `.env` files on disk. {/* generated:cli-commands end */} ======================================================================== CLI IN CI & TROUBLESHOOTING ======================================================================== Source: https://docs.envpilot.dev/cli/ci # CLI in CI & troubleshooting ## Should you use the CLI in CI at all? Often not. The CLI authenticates as a **person** through a browser device flow, which no CI runner can complete. For pipelines, prefer: | Situation | Use | | ------------------------------- | ----------------------------------------- | | GitHub Actions | [The Envpilot Action](/action/overview) | | Any other CI, or your own tool | [REST API](/api/overview) with an API key | | A local script you run yourself | The CLI, non-interactively as below | ## Non-interactive rules Every command that would prompt takes flags to answer instead: ```bash envpilot pull --env production --force --quiet envpilot push --env staging --merge --force envpilot secrets rm OLD_FLAG -e staging --yes envpilot files pull -e production --force envpilot run --env production --quiet --no-cache -- ./deploy.sh ``` Add `--json` where it is offered (`list`, `usage`, `requests`) and parse that instead of scraping human output, which is free to change. ## Keeping secrets out of argv Anything on a command line is visible in `ps`, in shell history, and often in CI logs. ```bash # Good — the value arrives on stdin printf %s "$SECRET" | envpilot requests approve --value-stdin # Avoid — lands in history and argv envpilot secrets set API_KEY=sk_live_… ``` `--value` is deliberately rejected in interactive sessions for the same reason. ## Exit codes `envpilot run` exits with **the child process's exit code**, so it composes cleanly: ```bash envpilot run -- npm test || exit 1 ``` Other commands exit non-zero on failure — including the deliberate refusals, such as `files pull` declining to overwrite a locally modified file. ## Failures worth recognising | What you see | What it means | | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `Not authenticated` | No session in this environment. Run `envpilot login`, or use an API key for CI. | | 401 on the first call after weeks | The session was revoked — by an admin, by a sign-out, or by [Security Hold](/platform/rbac#security-hold). | | A hard stop asking you to upgrade | Your CLI is below the server's minimum supported version. Upgrade; there is no override. | | `⚠ N variable(s) were NOT written (access denied)` | Your role holds no write grant for those keys. This is not a bug — see [Pull & push](/cli/pull-push). | | `Refusing to overwrite. Re-run with --force` | A local secret file differs from the server. Inspect it before forcing. | | `Limit reached (3/3)` | A plan limit. `envpilot usage --json` shows every limit and your current count. | | A rate-limit message with a retry window | Back off for the stated number of seconds. See [Rate limits](/limits/rate-limits). | ## Diagnostics ```bash envpilot whoami # identity, API target, active context envpilot usage --json # plan limits vs current usage envpilot config path # where local state lives envpilot list linked # what this directory is bound to ``` If the CLI seems to be acting on the wrong project, it is almost always the active link — `envpilot list linked` then `envpilot switch --active `. ## Limits - No headless login. A device flow needs a browser. - The version check fails **open** on network errors, so an outage never blocks your commands; it also means a stale CLI can go unflagged offline. - Machine-readable output exists for some commands, not all; where `--json` is absent, treat output as human-facing and subject to change. ======================================================================== VS CODE OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/extension/overview # VS Code overview Envpilot v1.17.1 for VS Code and Cursor: real-time variable sync, secret files materialised into the workspace, commit and clipboard protection, and editor intelligence over your `.env` files. Requires VS Code 1.85+ or the Cursor equivalent. ## Install Search for **Envpilot** in the Extensions sidebar, or install from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=envpilot.envpilot). The same extension works in Cursor. ## Sign in Several accounts can be signed in at once: **Envpilot: Switch Account** moves between them, **Envpilot: Sign Out** ends the active one, and **Envpilot: Sign Out of All Accounts** clears every one. ## What you get | Area | What happens | | ------------------------------------ | --------------------------------------------------------------------- | | Sidebar | Linked projects, environments and variables in the Activity Bar | | Status bar | Current project, environment, and sync state at a glance | | Dashboard panel | **Envpilot: Open Dashboard Panel** — an in-editor view of the project | | [Sync](/extension/sync) | `.env` files and secret files written and kept current | | [Protection](/extension/protection) | Commit guard, clipboard guard, value cloaking | | [Editor features](/extension/editor) | CodeLens, autocomplete, hover, request-a-variable | ## Trust matters In VS Code **Restricted Mode** the extension never writes secrets: sync, pull and link are disabled until you trust the workspace. Cleanup still runs, so previously synced `.env` files are removed on close and after crashes. Trust is re-checked mid-sync, not just at the start — a window that drops to Restricted must never end up holding a plaintext keystore. ## Limits - **VS Code and Cursor only.** JetBrains IDEs are covered by the [JetBrains plugin](/jetbrains/overview); there is no Neovim or Zed build. - **Uploading secret files is not possible from the editor** — the extension writes them, the CLI and dashboard manage them. - Everything is bounded by your role. Revealing values, for instance, requires a capability an owner grants; see [Protection](/extension/protection). - The extension enforces a minimum supported version against the server and fails open on network errors. ## Next - [Linking & sync](/extension/sync) - [Commands](/extension/commands) · [Settings](/extension/settings) ======================================================================== LINKING & SYNC ======================================================================== Source: https://docs.envpilot.dev/extension/sync # Linking & sync ## Link a project **Envpilot: Unlink Project** removes the link. **Envpilot: Pull Variables** syncs on demand; **Envpilot: Refresh** re-reads state without pulling. ## What sync does - On workspace open (`envpilot.autoSync`), the latest variables are pulled and written. - A live connection keeps values current while the window is open; a background check (`envpilot.syncInterval`, default 300 s) catches permission changes. - Real-time subscriptions pause after the window has been unfocused for `envpilot.idlePauseMinutes` (default 10) and resume the moment focus returns. Set it to `0` to never pause. - When access is revoked, the extension deletes the synced `.env` files it wrote (`envpilot.preventCopyOnRevoke`). ## Secret files in the workspace Secret files sync alongside variables and receive the same treatment as a managed `.env`: recorded in the manifest, protected from clipboard copy, watched for unauthorised edits, and written with their own mode (`0600` or `0400`) rather than a `.env`'s. Two behaviours to know: - **One environment per directory.** A file has exactly one path, so a directory linked to several environments materialises the **first** one. A dev and a prod `google-services.json` cannot both land in the same folder. - **Local edits are not silently overwritten.** A locally modified secret file is reported as a conflict and left alone. The exception is the edit watcher: if you hand-edit a managed secret file, the extension reverts that one file, because reverting an unauthorised edit is the whole point of the watcher. ## Several directories, one workspace Link different directories to different projects or environments; each syncs independently. ``` my-monorepo/ ├── apps/api/.env ← production ├── apps/web/.env.local ← development └── packages/sdk/.env ← staging ``` Manage them with **Envpilot: Add Directory** and **Envpilot: Remove Directory**, and pick environments per link with **Envpilot: Select Environments**. ## When a file already exists `envpilot.defaultConflictResolution` decides what happens when a target file is already there: | Value | Behaviour | | ----------- | ------------------------------------- | | `prompt` | Ask every time (default) | | `overwrite` | Replace the existing file | | `backup` | Back it up, then replace | | `merge` | Merge with the existing file | | `skip` | Leave the conflicting directory alone | ## Cleaning up on close Synced files can be removed when the workspace closes, so a laptop left open in a café is not a `.env` archive. The per-project default and per-member override (`vscodeAutoUnsyncOnClose`) are Pro-gated (`vscode_unsync_customization`); cleanup after a crash runs regardless of trust state. ## Limits - Sync writes what your role can read. Nothing indicates that other variables exist. - Restricted Mode disables writing entirely — trust the workspace first. - `envpilot.syncInterval` is a permission check, not a value poll; values arrive over the live connection. - One target file per linked directory. ## Next - [Protection](/extension/protection) - [Settings reference](/extension/settings) ======================================================================== PROTECTION ======================================================================== Source: https://docs.envpilot.dev/extension/protection # Protection A synced `.env` is plaintext on your disk. These four guards exist because that plaintext has more ways to escape than most people expect. ## Commit guard Dual-layer protection against committing a `.env`: 1. **Staging guard** — VS Code warns when you stage a managed `.env` and offers to unstage it. 2. **Pre-commit hook** — a git hook that blocks the commit at the git level, so it also catches commits made outside the editor. Both are on by default (`envpilot.commitGuard.enabled`, `envpilot.commitGuard.autoInstallHook`). Manage the hook by hand with **Envpilot: Install Commit Guard Hook** and **Envpilot: Remove Commit Guard Hook**. ## Clipboard guard Copy and cut are blocked inside Envpilot-managed files, because "copy the whole `.env` into a chat window" is how most secrets actually travel. | `envpilot.clipboardGuard.scope` | Behaviour | | ------------------------------- | ------------------------------------------------------------- | | `all-managed` (default) | Block copy/cut in every managed `.env`, whatever your role | | `readonly-roles` | Block only in read-only files (viewer and request-only roles) | | `off` | Never block | `Cmd+C` / `Cmd+X` inside a protected file trigger an explanation instead of a copy. Secret files are always treated as strict read-only, because the dashboard and CLI are their write path. ## Value cloaking `envpilot.cloakValues` (on by default) masks values in managed `.env` files with a fixed-length `••••••` decoration. The mask is a constant width, so it does not leak the real value's length, and the file on disk is untouched — sync, diff, and saving all see the real text. - **Envpilot: Toggle Value Cloaking** turns masking off or on. - **Envpilot: Reveal Values for 30 Seconds** unmasks temporarily and re-masks itself. Both the reveal command and the _unmasking_ direction of the toggle are role-gated on the `project.secrets.reveal` capability. Re-masking is always allowed — someone who loses the capability while cloaking is off must still be able to hide values again. ## Unauthorised-edit reversion Managed files are watched. Hand-editing a synced `.env` or a secret file triggers a warning and a revert to the server's version, restoring the file's own mode — a keystore goes back to `0600`/`0400`, not to a `.env`'s more permissive bits. The write path is the dashboard, the CLI, or a [request](/platform/requests). Editing the file on disk is not a write path, and pretending otherwise would mean silent divergence between what you run and what your team ships. ## Revocation cleanup When access is revoked, the extension deletes the synced files it wrote (`envpilot.preventCopyOnRevoke`, on by default). Combined with [Security Hold](/platform/rbac#security-hold), that turns "revoke this person" into "their disk is clean within seconds", not "their disk is clean at their next sign-in". ## Limits - Guards apply to files **Envpilot manages**. A `.env` you created by hand is not tracked, and nothing about it is protected. - The pre-commit hook is a local git hook — a fresh clone has no hook until the extension or `envpilot sync` installs one. - Clipboard guard covers the editor's copy and cut. It cannot stop a screenshot, a terminal `cat`, or a file manager. - None of this survives the file leaving your machine. If a secret is exposed, [rotate it](/platform/rotation). ## Next - [Editor features](/extension/editor) - [Security](/platform/security) ======================================================================== EDITOR FEATURES ======================================================================== Source: https://docs.envpilot.dev/extension/editor # Editor features Beyond syncing files, the extension knows which variables your project actually has — and uses that where you are typing. ## CodeLens on `.env` files `envpilot.enableCodeLens` (on) annotates managed `.env` files with their sync status and quick actions, so you can tell a synced file from a hand-written one without reading its contents. ## Autocomplete for variable names `envpilot.autocomplete.enable` (on) suggests variable names from your linked project when you type an environment-variable reference — `process.env.`, `os.getenv(`, and the equivalents in other languages. Suggestions come from the project's **keys**. Values are never offered as completions. ## Hover `envpilot.hover.enable` (on) shows an Envpilot hover on recognised environment-variable references: which environments define the key, and a **masked** preview. The hover carries a **Reveal value** link gated on the same `project.secrets.reveal` capability as everything else — a role that cannot reveal values cannot reveal them one hover at a time either. ## Request a variable When you need a key you cannot see, run **Envpilot: Request Variable**. It files the same request the CLI and dashboard file, and a reviewer approves it and supplies the value. See [Requests & approvals](/platform/requests). ## Status and dashboard - **Envpilot: Show Status** — a summary of the current session, links and sync state. - **Envpilot: Open Dashboard Panel** — an in-editor panel for the linked project. - **Envpilot: Open Dashboard** — the same project on the web. ## Limits - Autocomplete and hover only know about **linked** projects. An unlinked workspace gets neither. - Hover recognises common reference patterns; an exotic accessor may not be detected. - Reveal is role-gated at the command, not just in the UI — hiding the palette entry alone would be bypassable by keybinding. - These features read project metadata, not secret values; a hover reveal is what triggers a value read. ## Next - [Commands](/extension/commands) · [Settings](/extension/settings) - [Troubleshooting](/extension/troubleshooting) ======================================================================== COMMANDS ======================================================================== Source: https://docs.envpilot.dev/extension/commands # Commands Everything below is available from the command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) by typing **Envpilot**. {/* generated:extension-commands start */} | Command palette entry | Command id | | -------------------------------------- | ----------------------------- | | Envpilot: Sign In | `envpilot.signIn` | | Envpilot: Sign Out | `envpilot.signOut` | | Envpilot: Link Project | `envpilot.linkProject` | | Envpilot: Unlink Project | `envpilot.unlinkProject` | | Envpilot: Pull Variables | `envpilot.pullVariables` | | Envpilot: Refresh | `envpilot.refresh` | | Envpilot: Open Dashboard | `envpilot.openDashboard` | | Envpilot: Show Status | `envpilot.showStatus` | | Envpilot: Add Directory | `envpilot.addDirectory` | | Envpilot: Remove Directory | `envpilot.removeDirectory` | | Envpilot: Select Environments | `envpilot.selectEnvironments` | | Envpilot: Request Variable | `envpilot.requestVariable` | | Envpilot: Install Commit Guard Hook | `envpilot.installCommitGuard` | | Envpilot: Remove Commit Guard Hook | `envpilot.removeCommitGuard` | | Envpilot: Open Dashboard Panel | `envpilot.openDashboardPanel` | | Envpilot: Switch Account | `envpilot.switchAccount` | | Envpilot: Sign Out of All Accounts | `envpilot.signOutAll` | | Envpilot: Toggle Value Cloaking | `envpilot.toggleCloaking` | | Envpilot: Reveal Values for 30 Seconds | `envpilot.revealValues` | {/* generated:extension-commands end */} ## Keybindings `Cmd+C` / `Ctrl+C` and `Cmd+X` / `Ctrl+X` are intercepted inside Envpilot-managed files when the [clipboard guard](/extension/protection) is active. The binding is scoped by a context key, so it never affects any other file you have open. ## Limits - There is no command to upload a [secret file](/platform/secret-files) — the extension writes them, the CLI and dashboard manage them. - Commands that reveal values check your role at invocation, so a keybinding or URI cannot bypass the palette's visibility rules. ======================================================================== SETTINGS ======================================================================== Source: https://docs.envpilot.dev/extension/settings # Settings Configure under **Settings → Extensions → Envpilot** (`Cmd+,`), or in `settings.json`. {/* generated:extension-settings start */} | Setting | Type | Default | What it does | | -------------------------------------- | ------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `envpilot.serverUrl` | string | — | The URL of the Envpilot server (auto-configured at build time) | | `envpilot.autoSync` | boolean | `true` | Automatically sync variables when workspace opens | | `envpilot.syncInterval` | number | `300` | Interval (in seconds) for checking permission changes | | `envpilot.targetFile` | string | `".env.local"` | Default target file for synced variables | | `envpilot.environment` | string | `"development"` | Default environment for synced variables. Options: `development`, `staging`, `production`. | | `envpilot.preventCopyOnRevoke` | boolean | `true` | Delete synced .env files when permissions are revoked | | `envpilot.defaultConflictResolution` | string | `"prompt"` | Default action when existing .env files are found. Options: `prompt`, `overwrite`, `backup`, `merge`, `skip`. | | `envpilot.convexUrl` | string | — | Convex deployment URL for real-time sync (auto-detected from server if empty) | | `envpilot.enableCodeLens` | boolean | `true` | Show CodeLens annotations above .env files with sync status and actions | | `envpilot.commitGuard.enabled` | boolean | `true` | Enable dual-layer .env commit protection (VS Code staging guard + pre-commit hook) | | `envpilot.commitGuard.autoInstallHook` | boolean | `true` | Automatically install a pre-commit hook to block .env file commits | | `envpilot.clipboardGuard.scope` | string | `"all-managed"` | Which Envpilot-managed .env files block clipboard copy/cut. Options: `all-managed`, `readonly-roles`, `off`. | | `envpilot.cloakValues` | boolean | `true` | Visually mask variable values in Envpilot-managed .env files (the file content on disk is unchanged). Masking uses editor decorations, so values may still be visible in surfaces VS Code renders directly: the minimap (with editor.minimap.renderCharacters on), peek views, and diff editors. | | `envpilot.autocomplete.enable` | boolean | `true` | Suggest variable names from your linked Envpilot project when typing environment variable references (process.env., os.getenv, etc.) | | `envpilot.hover.enable` | boolean | `true` | Show a masked Envpilot hover on recognized environment variable references, with a role-checked option to reveal the value | | `envpilot.idlePauseMinutes` | number | `10` | Minutes the window must stay unfocused before real-time sync (Convex WebSocket subscriptions) is paused. Set to 0 to disable idle-pausing and keep real-time sync running regardless of focus. | {/* generated:extension-settings end */} ## Notes on a few of them - **`envpilot.serverUrl` / `envpilot.convexUrl`** are configured at build time and should be left alone unless you are running against a non-production instance. - **`envpilot.cloakValues`** masks with editor decorations. See the caveat in [Protection](/extension/protection). - **`envpilot.idlePauseMinutes`** trades a little freshness for a lot of idle connection cost. `0` disables pausing. - **Unsync-on-close customization** is Pro-gated (`vscode_unsync_customization`); on Free the project default applies. ## Limits - Settings marked `machine` scope cannot be set per workspace — that is deliberate for anything that changes where credentials are sent. - A setting change takes effect on the next sync unless stated otherwise; use **Envpilot: Refresh** to apply immediately. ======================================================================== TROUBLESHOOTING ======================================================================== Source: https://docs.envpilot.dev/extension/troubleshooting # Troubleshooting ## Nothing syncs and no error appears Check workspace trust. In **Restricted Mode** the extension never writes secrets — sync, pull and link are all disabled until you trust the workspace. Cleanup still runs, which is why previously synced files may vanish while nothing new arrives. ## Variables I expect are missing You receive what your role can read, and nothing signals that more exists. Confirm with **Envpilot: Show Status** which project and environment the directory is linked to, then check [Roles & permissions](/platform/rbac). If you genuinely need a key you cannot see, run **Envpilot: Request Variable**. ## My `.env` keeps reverting That is the edit watcher. Managed files are restored from the server when hand-edited — the write path is the dashboard, the CLI, or an approved request. If you meant to change the value, [`envpilot secrets set`](/cli/secrets) does it in one command. ## Copy does nothing in a `.env` The [clipboard guard](/extension/protection) is blocking it. Change `envpilot.clipboardGuard.scope` if your team wants a looser policy — `readonly-roles` limits blocking to read-only files, `off` disables it. ## Values show as `••••••` Cloaking. **Envpilot: Reveal Values for 30 Seconds**, or toggle it off — both need the reveal capability. If you get "your role does not allow unmasking secret values", ask an organization owner; it is a role capability, not a setting. ## Secret files did not appear - Only the **first** linked environment's files are materialised into a directory. - A locally modified copy is treated as a conflict and left alone, so an old hand-placed keystore blocks the new one. Move it aside and sync again. - Uploading is not possible from the editor — use [`envpilot files add`](/cli/files) or the dashboard. ## Suddenly signed out, files deleted Access was revoked, or a [Security Hold](/platform/rbac#security-hold) was applied. Both are immediate and both trigger local cleanup by design. Ask an organization owner. ## An upgrade prompt blocks everything Your extension version is below the server's supported minimum. Update from the Marketplace; there is no override, because the older build calls contracts that no longer exist. ## Still stuck - **Envpilot: Show Status** — session, links, sync state - **Envpilot: Refresh** — re-read state without pulling - **Envpilot: Sign Out of All Accounts**, then sign in again — clears a wedged session - The Output panel's Envpilot channel carries the extension's own log If a problem survives that, the CLI is a useful second opinion: `envpilot whoami` and `envpilot list linked` answer the same questions from outside the editor. ======================================================================== JETBRAINS OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/jetbrains/overview # JetBrains overview Envpilot v0.1.7 for JetBrains IDEs: pull variables and secret files into your project, keep them current over a live connection, and keep them from leaking through the editor. Works on any IntelliJ-platform IDE 2025.1 or newer, with no upper bound: IntelliJ IDEA, Android Studio, PyCharm, GoLand, WebStorm, Rider. ## Install Search for **Envpilot** under **Settings → Plugins → Marketplace**, or press install right here: Machines without Marketplace access: download the signed zip from the [versions page](https://plugins.jetbrains.com/plugin/33946-envpilot/versions) and install it with **Settings → Plugins → ⚙ → Install Plugin from Disk**. ## Sign in Multiple accounts are fine. **Switch Account**, **Sign Out**, and **Sign Out All Accounts** live in the same Tools menu, and each account keeps its own project links and tokens. ## What you get | Area | What happens | | ----------------------------------- | ------------------------------------------------------------------ | | Tool window | Browse organizations, projects and environments, link directories | | Status bar widget | Connection and sync state at a glance | | [Sync](/jetbrains/sync) | Variables written to the target `.env`, secret files materialized | | [Protection](/jetbrains/protection) | Value cloaking, clipboard guard, drift detection, commit guard | | Editor | Autocomplete and gutter markers in `.env` files | | Requests | Ask for a variable through the [approval flow](/platform/requests) | ## Limits - Requires an Envpilot account. [Sign up](https://www.envpilot.dev/sign-up) free. - Requires the **JetBrains IDE Plugin** feature on your organization's plan. See [Plans & limits](/limits/plans). - Writing is a pull path only. The plugin writes what your role can read; uploading secret files happens in the dashboard or CLI. - Reveals and other sensitive actions are capability-gated. See [RBAC](/platform/rbac). ## Next - [Linking & sync](/jetbrains/sync) - [Protection](/jetbrains/protection) ======================================================================== LINKING & SYNC ======================================================================== Source: https://docs.envpilot.dev/jetbrains/sync # Linking & sync ## Link a directory **Tools → Envpilot → Pull Now** syncs on demand. **Show Status** reports connection and sync state. ## What sync does - The loop runs every `syncIntervalSeconds` (default 300, clamped to 60–3600) while `autoSync` is on. - A live connection pushes changes the moment they land server-side, and pulls when you return to the IDE. The interval loop is the fallback, not the source of fresh values. - Timer, realtime pushes, IDE activation, and manual pulls are single-flight. One write at a time, never interleaved files. - `autoUnsyncOnClose` (on by default) removes the synced `.env` files when the project closes, so a laptop left open is not a `.env` archive. ## Secret files Secret files are written with their own recorded permissions. A keystore gets `0600`/`0400`, not a `.env`'s bits. The fetch completes and decrypts fully before any write starts. Incomplete or undecryptable data aborts the pull loudly instead of half-writing a file. ## Settings **Settings → Tools → Envpilot**: | Setting | Default | What it does | | --------------------- | ------------ | --------------------------------------------------- | | `autoSync` | on | Run the sync loop | | `syncIntervalSeconds` | 300 | Permission-check loop, clamped 60–3600 | | `targetFile` | `.env.local` | The file variables are written to | | `autoUnsyncOnClose` | on | Delete synced `.env` files when the project closes | | `conflictResolution` | `merge` | What happens when the target file already exists | | `idlePauseMinutes` | 0 | Pause the loop after N idle minutes; 0 never pauses | | `serverUrl` | baked | Override the Envpilot API base URL | ## Limits - One project and one environment per linked directory. Link different directories to different environments; each syncs independently. - The interval loop is a permission check, not a value poll. Values arrive over the live connection. - Sync writes what your role can read. Nothing indicates that other variables exist. ## Next - [Protection](/jetbrains/protection) - [Secret files](/platform/secret-files) ======================================================================== PROTECTION ======================================================================== Source: https://docs.envpilot.dev/jetbrains/protection # Protection A synced `.env` is plaintext on your disk. The plugin adds guards around it. They live in **Tools → Envpilot**. ## Value cloaking `cloakValues` (on by default) masks managed values in the editor. **Toggle Cloaking** flips it, **Reveal Values for 30 Seconds** unmasks temporarily and re-masks itself, and **Reveal Value at Caret** shows one value after the server checks your permissions. Reveals are capability-gated: the server checks your role before answering. Re-masking is always allowed. ## Clipboard guard Copy and cut are blocked inside Envpilot-managed files. "Copy the whole `.env` into a chat window" is how most secrets actually travel, so the guard explains itself instead of copying. ## Drift detection Managed files are watched. An edit that did not come from a pull is flagged, so what you run and what your team ships do not silently diverge. The write path is the dashboard, the CLI, or a [request](/platform/requests). Editing the file on disk is not a write path. ## Commit guard **Install Commit Guard** blocks commits that would carry an env file out of the repo. **Remove Commit Guard** takes it off again. Both toggles are off by default (`commitGuardEnabled`, `commitGuardAutoInstall`) and also live in **Settings → Tools → Envpilot**. ## Limits - Guards apply to files Envpilot manages. A `.env` you created by hand is not tracked, and nothing about it is protected. - The clipboard guard covers the editor's copy and cut. It cannot stop a screenshot or a file manager. - If a secret is exposed anyway, [rotate it](/platform/rotation). ## Next - [Troubleshooting](/jetbrains/troubleshooting) - [Security](/platform/security) ======================================================================== TROUBLESHOOTING ======================================================================== Source: https://docs.envpilot.dev/jetbrains/troubleshooting # Troubleshooting **The tool window is gone.** **View → Tool Windows → Envpilot** brings it back. **Sign-in does not complete.** The device-flow login opens in your browser and needs your approval there. Pop-up blockers are the usual suspect; copy the code from the IDE notification into the browser page by hand. **Nothing syncs.** Run **Tools → Envpilot → Show Status** first. Then check the basics: signed in, directory still linked, `autoSync` on, and your role can still read the project. A revoked access means no pull, and files removed by `autoUnsyncOnClose` stay gone. **Pull says a value cannot be decrypted.** The pull aborts instead of writing partial data, by design. Re-pull once; if it persists, rewrite the value from the dashboard so it gets a fresh vault reference. **The plugin does not load.** It needs an IntelliJ-platform IDE 2025.1 or newer. **Help → About** shows the IDE version; **Settings → Plugins** shows the installed plugin version. **Still stuck.** [Open an issue](https://github.com/rafay99-epic/envpilot.dev/issues/new) with the IDE version, the plugin version, and what **Show Status** reported. ======================================================================== GITHUB ACTION OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/action/overview # GitHub Action overview `rafay99-epic/envpilot-action@v1` pulls your project's variables into a workflow job, masks every value in the log, and optionally materialises [secret files](/action/secret-files) on the runner. ## Create a key ## Usage ```yaml jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Pull environment variables uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production - name: Deploy run: ./deploy.sh # DATABASE_URL, API_SECRET, … are already in the environment ``` By default (`export-env: "true"`) every pulled variable is appended to `$GITHUB_ENV`, so it becomes a normal environment variable for every later step in the job. No extra wiring. To write a dotenv file instead of, or alongside, exporting: ```yaml - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production export-env: "false" env-file: .env ``` The dotenv file is written with mode `0600`. ## Versioning `@v1` is a floating major tag: it always points at the newest 1.x release, which is how you get non-breaking updates without editing every workflow. Pin `@vX.Y.Z` instead if you need a frozen build. ## Limits - **Read-only.** The key pulls variables and files. It can never create, edit, delete, or [file a request](/platform/requests) — CI reads, it does not negotiate. - **`public_api` tier gate.** Machine access is a Pro feature. - **One environment per step.** Pull twice for two environments. - **`project` is required for files.** The files endpoint is project-scoped even when the token's scope resolves a single project. - **Rate limited per key.** See [Rate limits](/limits/rate-limits). - Runs on `node24` (Node LTS). Needs Actions runner v2.327.1 or newer — GitHub-hosted runners always are; self-hosted runners pinned below that must update. ## Next - [Inputs & outputs](/action/reference) - [Secret files in CI](/action/secret-files) - [Recipes](/action/recipes) · [Security](/action/security) ======================================================================== INPUTS & OUTPUTS ======================================================================== Source: https://docs.envpilot.dev/action/reference # Inputs & outputs ## Inputs {/* generated:action-inputs start */} | Input | Required | Default | What it does | | ------------- | -------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `token` | Yes | — | Envpilot API key (envpk\_…) with the GitHub Action surface, scoped to a project and environment. Create one under Organization → Settings → API Keys. | | `environment` | Yes | — | Environment to pull variables from, e.g. production, staging, development. | | `api-url` | No | `https://www.envpilot.dev` | Envpilot API base URL. Override only for self-hosted or non-production instances. | | `export-env` | No | `true` | When true, exports every pulled variable to $GITHUB_ENV so later steps can read it directly. | | `env-file` | No | — | Optional path to write the pulled variables as a dotenv file. Skipped when unset. | | `files` | No | `false` | When true, also pulls the project's secret files (keystores, SSH keys, certificates) and writes each to its recorded path. The API key must carry the 'files' resource, which is never granted by default. | | `project` | No | — | Project slug. Required when files is true — the files endpoint is project-scoped, unlike variables which the token scope resolves. | | `files-dir` | No | — | Directory that secret file paths are resolved against. Defaults to the workspace root. | {/* generated:action-inputs end */} ## Outputs {/* generated:action-outputs start */} | Output | What it holds | | ------------- | --------------------------------------------------------- | | `count` | Number of variables pulled from Envpilot. | | `files-count` | Number of secret files written. Zero when files is false. | {/* generated:action-outputs end */} Using an output: ```yaml - id: envpilot uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production - run: echo "Pulled ${{ steps.envpilot.outputs.count }} variables" ``` ## Limits - Inputs are strings — GitHub Actions has no boolean type. `export-env: "true"` and `files: "true"` are quoted on purpose. - `api-url` exists for non-production instances; leave it unset otherwise. - `project` is ignored unless `files: true`, where it is required. - `files-dir` is created if it does not exist, then resolved to a real path before anything is written. ======================================================================== SECRET FILES IN CI ======================================================================== Source: https://docs.envpilot.dev/action/secret-files # Secret files in CI A release build usually needs more than variables: a signing keystore, a `.p12`, a service-account JSON. `files: true` writes them onto the runner at their recorded paths, with their recorded modes. ```yaml - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production project: mobile-app # required when files: true files: true ``` After that step, `android/app/upload.jks` (or whatever path the file records) exists, mode `0600`, ready for the build. ## Requirements | Requirement | Why | | --------------------------- | --------------------------------------------------------------- | | `files: true` | Off by default — no workflow pulls file contents unless it asks | | `project: ` | The files endpoint is project-scoped, unlike variables | | `files` resource on the key | Never granted by default; select it when minting the key | Without the resource the request is refused — the key is not "partially allowed". ## Where files land `files-dir` sets the root that destination paths resolve against; it defaults to the workspace. The directory is created if missing, then resolved to a real path. ```yaml with: files: true project: mobile-app files-dir: build/secrets ``` ## How the write is protected The server validates every path, and the Action re-validates on the runner, because it is the process actually creating files. A server bug or a tampered response must not be able to write outside the workspace: - absolute paths are refused - paths that escape the root are refused, including through a **symlinked** intermediate directory - writing **through** a symlink is refused - content is staged into a fresh exclusive temp file at the restrictive mode and renamed over the target, so a pre-existing world-readable file never holds new secret contents at its old mode ## Batching and the 8 MiB rule Files are fetched in batches sized under the server's per-request ceiling of **8 MiB**, so a project with many files still pulls in one step. A **single file** larger than 8 MiB cannot be fetched at all — the Pro per-file limit is 8 MB precisely so this cannot happen through the product. The step logs name, path and size only. Contents never reach the log: masking a multi-megabyte binary is not meaningful, so the rule is that it never gets there. ## Output `files-count` holds the number of files written — zero when `files` is false. ```yaml - id: envpilot uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production project: mobile-app files: true - run: echo "wrote ${{ steps.envpilot.outputs.files-count }} files" ``` ## Limits - Every file fetch is **audited** against the API key. A workflow that pulls on every push produces an audit entry per run — by design. - Content reads are rate limited: refill 60/min, burst 1000. A cold pull of any legal project fits in one burst. - The Action **overwrites** whatever is at the destination path. Unlike `envpilot files pull`, there is no conflict check — a runner is expected to be empty. - No upload path. CI writes to disk, never to Envpilot. ## Next - [Android keystore in CI](/guides/android-keystore-ci) — the full workflow - [Secret files](/platform/secret-files) — the object model ======================================================================== RECIPES ======================================================================== Source: https://docs.envpilot.dev/action/recipes # Recipes ## One workflow, several environments Use a matrix and one key per environment, or one key scoped to all three: ```yaml jobs: deploy: strategy: matrix: environment: [staging, production] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: ${{ matrix.environment }} - run: ./deploy.sh ``` Matrix jobs fan out, and the value-pull bucket is sized for exactly that: full capacity is available as a burst. ## Dotenv file for a container build ```yaml - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production export-env: "false" env-file: .env.production - run: docker build --secret id=env,src=.env.production . ``` ## Monorepo: two projects in one job ```yaml - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_API_TOKEN }} environment: production export-env: "false" env-file: apps/api/.env - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_WEB_TOKEN }} environment: production export-env: "false" env-file: apps/web/.env ``` Two keys, two scopes, two files. Exporting both to `$GITHUB_ENV` would collide on shared key names — write files instead. ## Mobile release build with a keystore ```yaml - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production project: mobile-app files: true - run: ./gradlew bundleRelease ``` Full walkthrough: [Android keystore in CI](/guides/android-keystore-ci). ## Only pull on the branches that need it ```yaml - uses: rafay99-epic/envpilot-action@v1 if: github.ref == 'refs/heads/main' with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production ``` Every pull is audited and rate-limited. A pull-request job that does not need production secrets should not draw them. ## Fork pull requests Repository secrets are not available to workflows triggered by a fork, so the token input is empty and the step fails. That is the correct outcome — gate the step on `github.event.pull_request.head.repo.full_name == github.repository` rather than trying to work around it. ## Limits - One environment per step; one project per step for files. - `$GITHUB_ENV` is per job, not per workflow. Downstream jobs need their own pull. - Values are masked in logs, but a step that writes a value into a file you upload has left masking behind. ======================================================================== ACTION SECURITY ======================================================================== Source: https://docs.envpilot.dev/action/security # Action security ## Masking comes first Every pulled value is registered with GitHub's log masking (`core.setSecret`) **before** anything exports or writes it. Order matters: a value exported first and masked second is a value that can appear in a log line in between. Because masking happens at the runner level, a later step that accidentally echoes a variable prints `***`. Empty values are skipped — masking the empty string would redact every subsequent character of the log. Secret file **contents** are never logged at all. Only name, path and size reach the log, because masking a multi-megabyte binary is not meaningful. ## What the key can do | Can | Cannot | | -------------------------------------------- | ------------------------------------------------ | | Read variables in its scope | Create, edit, or delete anything | | Read secret files, with the `files` resource | File a variable [request](/platform/requests) | | Nothing else | Reach projects or environments outside its scope | The Action is the narrowest surface Envpilot has. CI reads; it does not negotiate. ## Scope it tightly - One project, one environment per key wherever practical. - Add the `files` resource only to keys that pull files. - Give a key an expiry if the pipeline is temporary. - Prefer separate keys per repository, so revoking one does not break the others. ## Revocation Revoke in **Organization → Settings → API Keys**. The next pull is rejected immediately — there is no cached-authorization window and no grace period. Unknown, revoked, and expired keys all get the same uniform answer, so a scanner cannot use the error to tell which it is holding. ## Every pull is audited Each value-returning request is written to the audit log against the key: which project, which environment, which resource. If a key is compromised, the audit log tells you exactly what it read and when. Denials are logged too. ## Runners are ephemeral GitHub-hosted runners are destroyed after the job, so pulled values do not persist — **unless you make them persist**. Uploading an artifact containing a dotenv file, caching a directory that holds a keystore, or writing to a mounted volume all outlive the job. ## Limits - Log masking protects the log, not a file you upload. - Self-hosted runners are not ephemeral. Clean them up yourself, and prefer `env-file` paths inside the workspace so a checkout wipe removes them. - Rate limits apply per key, not per repository — several workflows sharing one key share its bucket. - A key with a tier downgrade behind it stops working: the `public_api` gate is re-checked on every request, not at creation. ## See also - [API authentication](/api/authentication) — the full key model - [Rate limits](/limits/rate-limits) ======================================================================== DOCKER OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/docker/overview # Docker overview `ghcr.io/rafay99-epic/envpilot:1` is a single statically linked binary. Copy it into your image, or mount it during a build, and your project's variables and [secret files](/docker/runtime#secret-files) are there. No Node, no Python, no shell, no libc required. It runs in `scratch`, distroless, `alpine` (musl), `python:3.12-slim`, `golang`, `eclipse-temurin` — whatever base image you are already on. ## Pick your case | You want | Read | | -------------------------------------------- | --------------------------------- | | Secrets available while `docker build` runs | [Build time](/docker/build-time) | | Secrets in the app when the container starts | [Runtime](/docker/runtime) | | A Compose stack | [Docker Compose](/docker/compose) | | Every flag and exit code | [Reference](/docker/reference) | ## Sixty-second setup Your variables are in the environment before `python app.py` starts, and nothing was written to disk. ## How it gets in Two mechanisms, depending on when you need the secrets. **At build time** the binary is _mounted_, so it never becomes a layer in your image and neither do the values: ```dockerfile RUN --mount=type=secret,id=envpilot_token \ --mount=from=ghcr.io/rafay99-epic/envpilot:1,source=/envpilot,target=/envpilot \ ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ /envpilot exec --project checkout-api --env production -- npm ci ``` **At runtime** the binary is _copied_ in, because it has to be present when the container starts: ```dockerfile COPY --from=ghcr.io/rafay99-epic/envpilot:1 /envpilot /usr/local/bin/envpilot ENTRYPOINT ["envpilot", "exec", "--"] ``` Same binary, same flags, both cases. ## The three commands | Command | Does | | ------------------------ | ------------------------------------------------------------- | | `envpilot pull` | Write variables as dotenv text. Stdout unless `--out` is set. | | `envpilot files` | Write secret files to their recorded paths at `0600`/`0400`. | | `envpilot exec -- ` | Inject variables into `` and run it. Nothing hits disk. | There is no `login` and no config file. The binary reads its inputs, makes one or two HTTPS calls, and does one thing. ## Passing the key | Variable | Notes | | --------------------- | ---------------------------------------- | | `ENVPILOT_TOKEN_FILE` | Path to a mounted secret. **Preferred.** | | `ENVPILOT_TOKEN` | The key inline. | `ENVPILOT_TOKEN_FILE` wins when both are set. Prefer the file. An environment variable is readable through `docker inspect` and `/proc//environ` by anyone with access to the daemon, while a Compose or BuildKit secret is a tmpfs mount that never touches the image. There is deliberately no `--token` flag: a credential on a command line shows up in `ps`, in shell history, and in build logs. ## Pinning ``` ghcr.io/rafay99-epic/envpilot:1 # floating major, gets non-breaking updates ghcr.io/rafay99-epic/envpilot:1.0.0 # exact, you control upgrades ``` ## Requirements Docker is its own surface, with its own plan feature (`docker_image`) and its own place on your API keys. It is not a mode of the REST API: your plan can include container delivery without including the public API, and turning one off never touches the other. Create the key under **Organization → Settings → API Keys** and pick the **Docker** preset, or tick **Docker** under Advanced. A key minted only for the REST API or the GitHub Action will be refused here, on purpose. Scope the key to the projects and environments it actually serves. One key per service keeps a leaked container credential from reaching everything else. ======================================================================== BUILD TIME ======================================================================== Source: https://docs.envpilot.dev/docker/build-time # Build time Use this when the build itself needs a credential: installing from a private registry, compiling against a licensed SDK, running a migration, signing an artifact. The rule for all of it: **the values must not survive into the image.** Two BuildKit features do that for you. - `--mount=type=secret` puts the API key in the build without it entering image history. - `--mount=from=` mounts the Envpilot binary for one instruction, so it never becomes a layer either. ## Variables ```dockerfile # syntax=docker/dockerfile:1 FROM node:22-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=secret,id=envpilot_token \ --mount=from=ghcr.io/rafay99-epic/envpilot:1,source=/envpilot,target=/envpilot \ ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ /envpilot exec --project checkout-api --env production -- npm ci COPY . . RUN npm run build ``` Build it with the key passed as a secret: ```bash docker build --secret id=envpilot_token,src=./.envpilot-token . ``` `npm ci` sees `NPM_TOKEN` (or whatever your project stores) in its environment. The next layer does not. The `# syntax=docker/dockerfile:1` line is required — it opts the build into BuildKit's mount syntax. ## Secret files Same shape. The difference is that files land on the filesystem, so delete them in the **same** `RUN` instruction. A file removed in a later instruction is still recoverable from the earlier layer. ```dockerfile RUN --mount=type=secret,id=envpilot_token \ --mount=from=ghcr.io/rafay99-epic/envpilot:1,source=/envpilot,target=/envpilot \ ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ /envpilot files --project mobile --env production --dir /build \ && ./gradlew assembleRelease \ && rm -rf /build/*.jks ``` The key needs the `files` resource, which is never granted by default. ## Both at once `exec --files` writes the secret files, then runs your command with the variables injected: ```dockerfile RUN --mount=type=secret,id=envpilot_token \ --mount=from=ghcr.io/rafay99-epic/envpilot:1,source=/envpilot,target=/envpilot \ ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ /envpilot exec --project mobile --env production --files --dir /build \ -- ./gradlew assembleRelease \ && rm -rf /build/*.jks ``` ## A dotenv file for a later step When a build tool insists on reading a file rather than the environment, write one and remove it in the same instruction: ```dockerfile RUN --mount=type=secret,id=envpilot_token \ --mount=from=ghcr.io/rafay99-epic/envpilot:1,source=/envpilot,target=/envpilot \ ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ /envpilot pull --project web --env production --out .env.production \ && npm run build \ && rm -f .env.production ``` `pull --out` writes at mode `0600`. ## Getting the key to the build In CI you usually already have it in a secret store. Feed it in without ever writing it to the workspace: ```bash # GitHub Actions docker build --secret id=envpilot_token,env=ENVPILOT_TOKEN . ``` ```bash # Locally, from a file you keep out of git echo ".envpilot-token" >> .gitignore docker build --secret id=envpilot_token,src=./.envpilot-token . ``` Compose reads the same secrets in a `build:` block — see [Docker Compose](/docker/compose#build-secrets). ## What not to do ```dockerfile # Wrong — the key is now permanently in the image ARG ENVPILOT_TOKEN RUN /envpilot pull --out .env ``` ```dockerfile # Wrong — .env is baked into this layer forever, even if a later RUN deletes it RUN --mount=type=secret,id=envpilot_token /envpilot pull --out .env RUN npm run build RUN rm .env ``` Keep the fetch, the use, and the cleanup inside one `RUN`. ## Caching BuildKit caches a `RUN` layer on its instruction text, not on what the secret contained. A rotated variable will **not** invalidate the cache on its own. If a build must always see current values, either bust it explicitly or move the fetch to [runtime](/docker/runtime), which is the better answer for anything that is not needed to produce the artifact. ## Troubleshooting | What you see | What it means | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `--mount option is not supported` | BuildKit is off. Add `# syntax=docker/dockerfile:1` and set `DOCKER_BUILDKIT=1`. | | `ENVPILOT_TOKEN_FILE points at … which could not be read` | The `--secret id=` name does not match the path. The default mount is `/run/secrets/`. | | `no such file or directory: /envpilot` | The `--mount=from=` line is missing, or `source=`/`target=` do not match. | | A 403 naming the environment | The key is not scoped to that environment. | | A 403 mentioning Pro | The org is not on a plan that includes the public API. | Every exit code is listed in the [reference](/docker/reference#exit-codes). ======================================================================== RUNTIME ======================================================================== Source: https://docs.envpilot.dev/docker/runtime # Runtime This is the one most people want. The container starts, Envpilot fetches, your app gets its environment, and nothing decrypted is ever written to a filesystem. Copy the binary into your image and make it the entrypoint. ```dockerfile FROM python:3.12-slim COPY --from=ghcr.io/rafay99-epic/envpilot:1 /envpilot /usr/local/bin/envpilot COPY . /app WORKDIR /app ENTRYPOINT ["envpilot", "exec", "--"] CMD ["python", "app.py"] ``` `ENTRYPOINT` plus `CMD` is the useful split: `CMD` stays overridable, so `docker run myapp python manage.py migrate` still works and still gets the variables. ## Any base image The binary is statically linked and carries no runtime and no libc, so it drops into whatever you are already using — including `scratch`, which has no dynamic loader at all. ```dockerfile # Go, distroless FROM gcr.io/distroless/base-debian12 COPY --from=ghcr.io/rafay99-epic/envpilot:1 /envpilot /usr/local/bin/envpilot COPY --from=build /out/server /server ENTRYPOINT ["/usr/local/bin/envpilot", "exec", "--"] CMD ["/server"] ``` ```dockerfile # Java FROM eclipse-temurin:21-jre-alpine COPY --from=ghcr.io/rafay99-epic/envpilot:1 /envpilot /usr/local/bin/envpilot COPY target/app.jar /app.jar ENTRYPOINT ["envpilot", "exec", "--"] CMD ["java", "-jar", "/app.jar"] ``` ## Passing the key Mount it as a file. An environment variable is readable through `docker inspect` and `/proc//environ` by anyone with access to the daemon. ```bash docker run \ -v /etc/envpilot/token:/run/secrets/envpilot_token:ro \ -e ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ -e ENVPILOT_PROJECT=checkout-api \ -e ENVPILOT_ENVIRONMENT=production \ myapp:latest ``` Project and environment are ordinary configuration, not secrets, so environment variables are fine for those. Keeping them out of the image is what lets one image serve staging and production. ## Secret files Certs, keystores and SSH keys land at their recorded paths just before your app starts: ```dockerfile ENTRYPOINT ["envpilot", "exec", "--files", "--"] CMD ["/server"] ``` Add `--dir` to place them somewhere other than the working directory. The key needs the `files` resource, which is never granted by default. For anything long-lived, mount a tmpfs so the values never touch the container's writable layer: ```bash docker run --tmpfs /secrets:rw,mode=0700 \ -e ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ myapp:latest ``` ## Signals and exit codes `exec` is a thin wrapper, not a supervisor. It forwards `SIGINT`, `SIGTERM`, `SIGHUP` and `SIGQUIT` to your process, so `docker stop` reaches your app and your graceful shutdown runs normally. Your app's exit code becomes the container's exit code. A process killed by a signal reports `128 + signal`, the same convention a shell uses. Restart policies, health checks and `docker wait` behave exactly as they would without the wrapper. ## When it cannot fetch The container **does not start**. That is deliberate: an app running on half its configuration fails later, in a harder place to diagnose, and often after it has already accepted traffic. A container that refuses to start is caught by your restart policy and your alerting immediately. The same rule applies mid-pull. If any variable comes back without a value, the whole pull aborts rather than handing your app a blank credential. ## Rotation Variables are read once, at start. A value changed in Envpilot reaches the container on its next restart: ```bash docker restart checkout-api ``` If you need something faster than a restart, pull on a schedule with [`envpilot pull --out`](/docker/reference#pull) and have your app watch the file, or write a small unit that restarts the service after a successful pull. ## Startup cost One or two HTTPS requests, typically well under a second. If your platform runs an aggressive startup probe, give it a couple of seconds of grace. Container restarts share a per-key rate limit. A fleet restarting at once on a **single** key will hit it — the binary honours the server's `Retry-After` and backs off, but the cleaner fix is one key per service, which also shrinks what a leaked credential can reach. ## Troubleshooting | What you see | What it means | | ------------------------------------------------ | ------------------------------------------------------------------------------------- | | `exec: envpilot: not found` | The `COPY --from` line is missing, or the target is not on `PATH`. | | `envpilot: No API key` | Neither `ENVPILOT_TOKEN_FILE` nor `ENVPILOT_TOKEN` reached the container. | | `envpilot: No project` / `No environment` | Pass `--project` / `--env`, or set `ENVPILOT_PROJECT` / `ENVPILOT_ENVIRONMENT`. | | Container exits `2` immediately | Bad invocation, not a network problem. The message names what is missing. | | `rate limited … waiting Ns` | Expected on a mass restart. Split keys per service. | | The app sees a Dockerfile `ENV` value, not yours | It does not — Envpilot values overwrite existing entries. Check the environment name. | ======================================================================== DOCKER COMPOSE ======================================================================== Source: https://docs.envpilot.dev/docker/compose # Docker Compose Compose has real secret support. Use it — the token arrives as a tmpfs mount instead of an environment variable, so it stays out of `docker inspect`. ## The whole thing ```yaml services: api: build: . environment: ENVPILOT_TOKEN_FILE: /run/secrets/envpilot_token ENVPILOT_PROJECT: checkout-api ENVPILOT_ENVIRONMENT: production secrets: [envpilot_token] ports: ["8080:8080"] secrets: envpilot_token: file: ./.envpilot-token ``` With the Dockerfile from [runtime](/docker/runtime): ```dockerfile FROM python:3.12-slim COPY --from=ghcr.io/rafay99-epic/envpilot:1 /envpilot /usr/local/bin/envpilot COPY . /app WORKDIR /app ENTRYPOINT ["envpilot", "exec", "--"] CMD ["python", "app.py"] ``` Then: ```bash echo ".envpilot-token" >> .gitignore docker compose up ``` ## Several services, one project Each service names its own environment, so a single Compose file can run production and a staging worker side by side. ```yaml services: api: build: ./api environment: ENVPILOT_TOKEN_FILE: /run/secrets/envpilot_token ENVPILOT_PROJECT: checkout-api ENVPILOT_ENVIRONMENT: production secrets: [envpilot_token] worker: build: ./worker environment: ENVPILOT_TOKEN_FILE: /run/secrets/envpilot_token ENVPILOT_PROJECT: checkout-api ENVPILOT_ENVIRONMENT: production secrets: [envpilot_token] secrets: envpilot_token: file: ./.envpilot-token ``` A key per service is better than one shared key when you can manage it: revoking a leaked worker credential should not take the API down with it, and it keeps each service off the others' rate-limit bucket. ## Reading the token from the host environment Useful in CI, where the key is already in a secret store and you would rather not write it to a file: ```yaml secrets: envpilot_token: environment: ENVPILOT_TOKEN ``` ```bash ENVPILOT_TOKEN="envpk_…" docker compose up ``` Compose still delivers it to the container as a file at `/run/secrets/envpilot_token`, so nothing in the service definition changes. ## Build secrets Compose passes secrets to a build the same way. See [build time](/docker/build-time) for what to do with them inside the Dockerfile. ```yaml services: web: build: context: . secrets: [envpilot_token] environment: ENVPILOT_TOKEN_FILE: /run/secrets/envpilot_token ENVPILOT_PROJECT: web ENVPILOT_ENVIRONMENT: production secrets: [envpilot_token] secrets: envpilot_token: file: ./.envpilot-token ``` The `secrets:` under `build:` and the one under the service are separate: the first is available during `docker compose build`, the second inside the running container. ## For an image you do not control Sometimes you cannot add a binary to the image — a third-party database, an off-the-shelf service. Pull on the host and hand the result over as an env file: ```bash envpilot_pull() { docker run --rm \ -v "$PWD/.envpilot-token:/run/secrets/envpilot_token:ro" \ -e ENVPILOT_TOKEN_FILE=/run/secrets/envpilot_token \ ghcr.io/rafay99-epic/envpilot:1 \ pull --project "$1" --env "$2" --quiet } envpilot_pull checkout-api production > .env.runtime docker compose --env-file .env.runtime up -d rm -f .env.runtime ``` ## Health checks `exec` execs your process, so a health check written against your app works unchanged: ```yaml healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s start_period: 10s ``` Give `start_period` a couple of seconds more than usual to cover the fetch. ## Picking up rotated values Variables are read at start, so a change in Envpilot reaches the stack on the next restart: ```bash docker compose restart api ``` ======================================================================== DOCKER REFERENCE ======================================================================== Source: https://docs.envpilot.dev/docker/reference # Docker reference ``` ghcr.io/rafay99-epic/envpilot:1 ghcr.io/rafay99-epic/envpilot:1.0.0 ``` Multi-arch: `linux/amd64` and `linux/arm64`. The image contains one file, `/envpilot`, and nothing else — no shell, no package manager, no runtime, no libc. It is a statically linked Go binary, which is what lets the same file run in `scratch` and in `alpine` (musl) and in `debian` (glibc). The binary sits at the image root rather than on a `PATH` on purpose: you choose the name it takes in your image, so it can never shadow the [Envpilot CLI](/cli/overview) on a developer machine. ## Commands ### pull ``` envpilot pull [flags] ``` Writes variables as dotenv text. Stdout by default, or to `--out ` at mode `0600`. Every value is single-quoted with embedded quotes escaped, so values containing spaces, `#`, `$` or newlines survive `set -a; . file` and Compose's `env_file`. ```bash envpilot pull --project checkout-api --env production envpilot pull --project checkout-api --env production --out .env.production ``` ### files ``` envpilot files [flags] ``` Writes secret files to their recorded paths under `--dir` (default: the working directory), at mode `0600` or `0400` as configured on each file. Requires the `files` resource on the key, which is never granted by default. ```bash envpilot files --project mobile --env production --dir /build ``` ### exec ``` envpilot exec [flags] -- [args...] ``` Fetches variables, merges them into the child's environment, and runs the command. Nothing decrypted is written to a filesystem. Envpilot values overwrite entries already present, because a Dockerfile `ENV` is a default and Envpilot is the source of truth. Add `--files` to write secret files first. ```bash envpilot exec --project checkout-api --env production -- ./server envpilot exec --files --dir /secrets -- ./server ``` Everything after `--` belongs to your command and is passed through untouched, including its own flags. ## Flags | Flag | Short | Applies to | Default | Meaning | | ------------------ | ----- | --------------- | -------------------------- | ------------------------------------ | | `--project ` | `-p` | all | `$ENVPILOT_PROJECT` | Project slug | | `--env ` | `-e` | all | `$ENVPILOT_ENVIRONMENT` | Environment name | | `--out ` | `-o` | `pull` | stdout | Write here at `0600` | | `--dir ` | `-d` | `files`, `exec` | working directory | Output directory for secret files | | `--files` | | `exec` | off | Write secret files before running | | `--api-url ` | | all | `https://www.envpilot.dev` | API base URL | | `--quiet` | `-q` | all | off | Suppress the progress line on stderr | | `--help` | `-h` | | | Usage | | `--version` | `-v` | | | Version | Flags win over environment variables, so one image can serve several environments. ## Environment variables | Variable | Required | Meaning | | ---------------------- | -------- | ---------------------------------------------------- | | `ENVPILOT_TOKEN_FILE` | one of | Path to a mounted secret holding the key. Preferred. | | `ENVPILOT_TOKEN` | one of | The key inline. | | `ENVPILOT_PROJECT` | Yes\* | Project slug. \*Unless `--project` is passed. | | `ENVPILOT_ENVIRONMENT` | Yes\* | Environment. \*Unless `--env` is passed. | | `ENVPILOT_API_URL` | No | API base URL. | `ENVPILOT_TOKEN_FILE` wins when both credentials are set. A trailing newline in the file is trimmed, since mounted secrets almost always carry one. There is no `--token` flag by design: a credential on a command line is visible in `ps`, in shell history, and in build logs. ## Exit codes | Code | Meaning | | ------------ | --------------------------------------------------------------------- | | `0` | Success | | `1` | Request or write failure | | `2` | Bad invocation: missing key, project, environment, or an unknown flag | | child's code | `exec` exits with whatever your command exited with | | `128 + N` | `exec`, when the child was killed by signal `N` | ## Key requirements Create keys in **Organization → Settings → API Keys**. | Need | Grant | | ----------------------- | ---------------------------------------------------- | | `pull`, `exec` | `variables` resource | | `files`, `exec --files` | `files` resource (never granted by default) | | Any command | Scope to one project and the environments you deploy | Docker is an independent surface. The key must carry the **Docker** surface, and your plan must include the `docker_image` feature. A key scoped only to the REST API or the GitHub Action is refused, and disabling the public API does not disable Docker. Every request the image makes sends `surface=docker`, so both the key's scope and the plan feature are checked on each call, not just at mint time. ## Behaviour worth knowing **Failures are total.** If any variable comes back without a value, the entire pull aborts. Partial configuration fails later and in a worse place than a container that refuses to start. **Rate limits are honoured.** On a `429` the binary waits exactly as long as the server's `Retry-After` header asks, capped at 60 seconds, for up to five attempts. Large secret-file pulls are split into batches under 6 MiB, so they can legitimately hit the limit mid-pull. Docker has its own bucket — 120 requests of burst refilling at 30/min per key — so a restarting fleet cannot spend your CI pipeline's budget, and vice versa. See [rate limits](/limits/rate-limits). Your plan also caps how many active Docker keys an organization may hold. **Values are never logged.** Progress goes to stderr and names keys, paths and byte counts only. Contents never reach the log in the first place, which is why `pull` can stream dotenv text on stdout safely. **Paths are checked locally.** Secret file paths come from the server and the server validates them, but the binary re-checks anyway: absolute paths, traversal, and writes through a symlink are all refused. Files are staged through an exclusive temp and renamed into place, so an existing file never holds new secret contents at its old permissions. ## Errors | Message | Cause | | --------------------------------------------------- | --------------------------------------------------------------------------------- | | `No API key` | Neither credential variable was set. | | `ENVPILOT_TOKEN_FILE points at … could not be read` | Wrong mount path. The Compose/BuildKit default is `/run/secrets/`. | | `No project` / `No environment` | Missing flag and missing environment variable. | | `Invalid or revoked` (401) | Unknown, revoked or expired key. | | `not scoped to` (403) | Key does not cover that environment or project. | | A message naming Pro (403) | Org plan does not include the public API. | | `Refusing a partial pull` | A variable could not be decrypted. Retry; if it persists, re-save it in Envpilot. | The full REST error list is in the [API errors](/api/errors) reference. ## Source The image is built from `packages/docker-image` and published to [rafay99-epic/envpilot-docker](https://github.com/rafay99-epic/envpilot-docker) on every release. MIT licensed. ======================================================================== API OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/api/overview # API overview ``` https://www.envpilot.dev/api/v1 ``` Every endpoint requires `Authorization: Bearer ` and every endpoint is **read-only**. Requires the Pro plan (`public_api`). ## Endpoints | Endpoint | Returns | Resource | | ----------------------------------------------------- | --------------------------------- | ----------- | | [`GET /v1/organization`](/api/organization) | Organization metadata | any key | | [`GET /v1/projects`](/api/projects) | Projects in scope | `projects` | | [`GET /v1/projects/{slug}`](/api/projects) | One project | `projects` | | [`GET /v1/projects/{slug}/variables`](/api/variables) | Variables, with or without values | `variables` | | [`GET /v1/projects/{slug}/accounts`](/api/accounts) | Shared accounts | `accounts` | | [`GET /v1/files`](/api/files) | Secret files | `files` | ## Conventions - **JSON by default.** `format=env` on the variables endpoint returns dotenv text instead. - **Errors are `{ "error": string, "code": string }`**, plus an `x-request-id` header worth quoting to support. See [Errors](/api/errors). - **Environments** are exactly `development`, `staging`, `production`. - **Timestamps** are epoch milliseconds. ## Three rules that shape everything ### Reads are bounded, and never partial A response that would exceed 1000 items is **refused** with `422` rather than truncated. A silently short list is worse than an error: a deploy that starts with three variables missing fails somewhere far from the cause. ### Failure is loud If a vault decrypt fails for any item in a response, the whole request aborts with `503`. You never receive a partial set, and never a sentinel value standing in for a secret. ### Scope is invisible A project outside your key's scope returns the same `404` as a project that does not exist. Existence cannot be probed by watching which error comes back. ## No pagination There is deliberately no cursor or page parameter. The bounded-read rule means any successful response is complete, and any response that would not be complete is an error. If you are hitting the ceiling, you want more keys with narrower scope, not more pages. ## No CORS The API sends no CORS headers and is not meant to be called from a browser. An API key in client-side code is a published key. ## Limits - Read-only. There is no write endpoint on any surface; the only machine-initiated action is filing a [request](/platform/requests) over MCP. - Pro plan required. The gate is re-checked on **every** request, so a downgrade stops access immediately. - Rate limits are per key: 120/min metadata, 30/min values, plus separate buckets for [files](/api/files). See [Rate limits](/limits/rate-limits). - Every value-returning call is audited against the key. ## Next - [Quickstart](/api/quickstart) — first call end to end - [Authentication](/api/authentication) — the key model ======================================================================== API QUICKSTART ======================================================================== Source: https://docs.envpilot.dev/api/quickstart # API Quickstart Envpilot's public REST API lets you read projects, variables, and shared accounts programmatically — no CLI or extension required. It's read-only in v1 and requires the **Pro** plan. ## Create an API key Go to **Organization Settings → API Keys** — there's a single creation screen; project scope is a choice you make inside it, not a separate settings page. 1. Click **New API Key** 2. Select the surfaces where the key may authenticate: REST API, MCP server, and/or GitHub Action 3. Choose project, environment, and resource scopes (see below) — pick specific projects, or "all projects, including future ones" (owner-only) 4. Copy the key — it's shown **once**, as `envpk_...`. Envpilot only stores a hash of it; if you lose it, revoke it and create a new one — there's no rotate-in-place. Org-wide keys (scope = all projects) can only be created by the organization **Owner**. Project-scoped keys can also be created by a Team Lead. All project, environment, resource, and surface choices are immutable. If the required access changes, create a replacement key with the complete intended scope, switch clients to it, verify it, and then revoke the old key. For an MCP credential, follow [Create an MCP key](/mcp/overview#create-an-mcp-key) and [set `ENVPILOT_API_KEY`](/mcp/overview#set-envpilot-api-key) before configuring a client. ## Understand scope Every key has three independent scope dimensions: - **Projects** — `all` (every project in the org) or a specific list of projects - **Environments** — `all` (development, staging, production) or a specific list - **Resources** — which resource types the key can read: `variables`, `accounts`, `projects`, and optionally `requests` (the one write path — filing a variable request for a human to approve) A key also carries immutable **surfaces** — which faces it may use: the REST API (`rest_api`), the MCP server (`mcp_server`), and the GitHub Action (`github_action`). All three route through the same enforcement core; see [Architecture](/start/architecture). A key requesting a **project** outside its scope gets a `404` — identical to a project that genuinely doesn't exist, so a leaked key can't be used to probe which project slugs are real. A key requesting a **resource type, environment, or surface** outside its scope gets a `403` instead, since the project's existence is already implied by a URL the key can otherwise reach. This is deliberate: see [API Security](/api/authentication) for why. ## Pull your first variables ```bash curl https://www.envpilot.dev/api/v1/projects/backend/variables?environment=production \ -H "Authorization: Bearer envpk_your_key_here" ``` Response: ```json { "variables": [ { "key": "DATABASE_URL", "value": "postgres://...", "environments": ["production"], "isSensitive": true, "updatedAt": 1752192000000 }, { "key": "API_SECRET", "value": "sk_live_...", "environments": ["staging", "production"], "isSensitive": true, "updatedAt": 1752192000000 } ] } ``` `environment` is required unless you pass `metadata_only=true`. Each variable's `environments` array is its **full** scope — a key shared across staging and production shows both, even though you asked for `production` only. With `metadata_only=true`, the `value` field is omitted entirely (no vault round-trip happens). ## Filtering **Exact keys** — pull only the variables you name: ```bash curl "https://www.envpilot.dev/api/v1/projects/backend/variables?environment=production&keys=DATABASE_URL,API_SECRET" \ -H "Authorization: Bearer envpk_your_key_here" ``` **Prefix match** — useful for grabbing a related group, e.g. everything exposed to the client: ```bash curl "https://www.envpilot.dev/api/v1/projects/backend/variables?environment=production&prefix=NEXT_PUBLIC_" \ -H "Authorization: Bearer envpk_your_key_here" ``` **Metadata only** — list variable keys without decrypting any values (no vault round-trip, higher rate limit): ```bash curl "https://www.envpilot.dev/api/v1/projects/backend/variables?metadata_only=true" \ -H "Authorization: Bearer envpk_your_key_here" ``` **dotenv output** — get a ready-to-write `.env` file instead of JSON: ```bash curl "https://www.envpilot.dev/api/v1/projects/backend/variables?environment=production&format=env" \ -H "Authorization: Bearer envpk_your_key_here" # DATABASE_URL=postgres://... # API_SECRET=sk_live_... ``` ## Handling errors Every error response — across all v1 endpoints — has the same shape: `{ "error": "", "code": "" }`, plus an `x-request-id` header worth logging for support. | Status | Code | Meaning | | ------ | ------------------- | ------------------------------------------------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | Missing or malformed query params (e.g. no `environment` and no `metadata_only=true`) | | 401 | `MISSING_TOKEN` | No `Authorization: Bearer` header | | 401 | `INVALID_KEY` | Key is unknown, revoked, or expired — all three look identical to the caller | | 403 | `FORBIDDEN_SCOPE` | Key doesn't include this resource type or environment | | 403 | `FORBIDDEN_SURFACE` | Key isn't enabled for this surface (REST vs. MCP vs. GitHub Action) | | 403 | `TIER_GATE` | Organization's plan no longer includes the public API | | 404 | `NOT_FOUND` | Project outside the key's scope, or genuinely doesn't exist — see [Understand scope](#understand-scope) | | 422 | `OVERFLOW` | Project exceeds the 1000-row bounded-read cap — see [Bounded reads](#bounded-reads-no-pagination) | | 429 | `RATE_LIMITED` | Bucket exceeded — see [Rate limits](/limits/rate-limits) | | 503 | `DECRYPT_FAILED` | Vault couldn't decrypt one of the values in this pull | | 503 | `CONFIG_ERROR` | Server-side misconfiguration, not your key | | 500 | `INTERNAL_ERROR` | Unrecognized failure | ```bash curl -i "https://www.envpilot.dev/api/v1/projects/backend/variables?environment=production" \ -H "Authorization: Bearer envpk_a_revoked_key" # HTTP/1.1 401 Unauthorized # {"error":"Invalid or revoked API key","code":"INVALID_KEY"} ``` ## Bounded reads, no pagination There's no cursor or page parameter on any read endpoint — `keys`, `prefix`, and `metadata_only` are filters, not pagination. A pull is **all-or-nothing up to 1000 active rows** per project (variables and accounts each). If a project has more than that, the request fails outright with `422 OVERFLOW` and a message telling you to contact support to raise the limit — you will never get a silently-truncated partial page back. The same "loud, not partial" rule applies to decryption: if any single value in the pull fails to decrypt, the **whole request fails** with `503 DECRYPT_FAILED` naming the offending key. Nothing decrypted so far is returned — Envpilot would rather fail a pull than hand back a response with one variable silently missing. ## Retrying Envpilot's public API fails **closed**: every denial and every partial-data risk turns into a full request failure with a specific code, not a degraded response. That makes the retry decision mechanical: - **429 `RATE_LIMITED`** — respect the `Retry-After` header (seconds) before retrying the identical request. See [Rate limits](/limits/rate-limits) for the per-bucket windows. - **503 `DECRYPT_FAILED`** — retryable; vault errors are usually transient. If it persists across retries, the variable needs to be re-saved in Envpilot rather than pulled again. - **503 `CONFIG_ERROR` / 500 `INTERNAL_ERROR`** — retryable with backoff; these are server-side, not caused by your request. - **401 / 403 / 404 / 422** — not retryable as-is. These only change if you fix the actual cause: replace a revoked key, create a new key with the required immutable scope or surface, or shrink the project below the 1000-row cap. ## Next steps - [API Reference](/api/overview) — every endpoint, filter, and error code - [Architecture](/start/architecture) — the five client surfaces and the one auth core - [MCP Server](/mcp/overview) — create the right MCP key, configure `ENVPILOT_API_KEY`, and connect an AI agent directly to your variables - [Rate limits](/limits/rate-limits) — every per-key bucket and what happens when you exceed it - [API Security](/api/authentication) — key model, scoping, revocation, and audit ## Limits - Read-only. Every endpoint, every surface. - Pro plan (`public_api`), and the gate is re-checked on every call. - 120 requests/min for metadata, 30/min for value pulls, per key. - No pagination: a response that cannot be complete is an error, not a page. - No CORS — keys belong on a server, never in a browser bundle. ======================================================================== AUTHENTICATION ======================================================================== Source: https://docs.envpilot.dev/api/authentication # Authentication The REST API, MCP server, and GitHub Action all authenticate with the **same key system** and call the **same** underlying Convex actions — one enforcement core, three faces. Nothing about auth, scoping, or auditing is re-implemented per surface. ## Key model - A new key is generated as `envpk_...` and shown **exactly once**, at creation time. - Envpilot stores only a **SHA-256 hash** of the key, never the plaintext. If you lose a key, there's no way to recover it — revoke it and issue a new one. - Every key is scoped along three dimensions: **projects** (all or a specific list), **environments** (all or a specific list), and **resources** (`variables`, `accounts`, `projects`, optionally `files`, and optionally `requests`). A request for anything outside a key's scope is treated as if it doesn't exist. - Every key also declares its **surfaces** — which faces it may authenticate on: `rest_api`, `mcp_server`, `github_action`. All three route through one enforcement core (`_authorizeRequest`); see [Architecture](/start/architecture). - Keys can optionally have an **expiry date** (`expiresAt`). An expired key returns the same uniform "invalid or revoked" error as a bad or revoked key — the API never tells a caller _why_ a key stopped working. ## The `files` resource is never granted by default [Secret files](/platform/secret-files) are the highest-value material a key can read — a signing keystore is not recoverable by rotating a string. So `files` is an explicit opt-in when minting a key, never included by a "select all" default, and every file-content read is audited individually. ## Machine credentials never write secrets Every machine credential — REST API, MCP server, or GitHub Action key — is fundamentally **read-only**. No API key can create, edit, or delete a variable's value. The blast radius of a leaked key is bounded to reads within its scope, never a silent change to what a deploy pulls. There is exactly one escalation path, and it's **opt-in per key**: a key whose scope includes the `requests` resource can **file a variable request** — an ask, with a required justification, that lands in a human reviewer's dashboard queue. This is not a write. The agent never proposes a value; if the reviewer approves, **the reviewer supplies the value**. Keys without the `requests` resource (including every GitHub Action key) can't file requests at all. This is the trust model in one line: **read-only keys, human-approved escalation.** Request creation is additionally throttled per key — 5 per hour (burst 2), a standing cap of 5 open pending requests, and a 24-hour cooldown after a rejection — so a retry-looping agent can't flood reviewers. See [Rate limits](/limits/rate-limits) and [Architecture](/start/architecture). ## Revocation is immediate Revoking a key from the dashboard takes effect on the **next request** — there is no grace period and no cache to wait out. Every request re-checks the key's status (valid, scoped, not expired, not revoked, plan still entitled) before doing any work. ## Why 401 vs 403 vs 404 are deliberately uninformative - **401** covers every reason a key doesn't work — missing, malformed, invalid, expired, or revoked — as one message. This stops an attacker from using error responses to fingerprint _why_ a specific key failed. - **403** means the key is valid but the org's plan or the key's own resource scope doesn't cover this call (e.g. a `variables`-only key hitting `/accounts`, or the org isn't on a plan with `public_api`/`mcp_server` enabled). - **404** means the target doesn't exist **or** exists but is outside the key's scope — both return the same 404. A key is never allowed to confirm the existence of a project it can't see. ## Fail-loud, never partial If any part of a variable pull can't be completed safely, the whole request fails instead of returning something silently incomplete: - **No partial pulls.** If vault decryption fails for any variable in the response, the request aborts with `503` rather than returning the variables that did decrypt and omitting the ones that didn't. - **No sentinel values.** A variable that fails to decrypt is never represented as an empty string, `null`, or placeholder — that would look like a real (wrong) value to a deploy script. - **No silent truncation.** A project with more than 1000 matching variables returns `422` rather than a truncated list that looks complete but isn't. ## Audit trail Every value pull is logged — which key, which project/environment, which filters were used, and the source (`public-api`, `mcp`, or `cicd` for the GitHub Action). Metadata-only reads (key names without values) are **not** audited individually to avoid log noise, since they carry no secret exposure. Every **denied** request is also logged, including: - Reuse of a revoked key - A request outside a key's scope - A tier/plan gate rejection after a downgrade Revoked-key reuse is treated as the highest-signal event in this set — it's the strongest indicator of a leaked key still being used after the team responded. ## Rate limiting Rate limits are enforced per key with a token-bucket limiter — 120/min for metadata reads, 30/min for value pulls (see [API Reference](/api/overview) for the full table). Requests using a key hash that doesn't match anything on file are rate-limited separately, per hash, to slow down brute-force key guessing. ## Why there's no CORS The API and MCP endpoint deliberately do not send CORS headers, so browsers refuse cross-origin requests to them. API keys are bearer credentials with broad read access to your secrets — they must never be shipped in client-side JavaScript where anyone with dev tools open could read them out of a network request. If you need secrets in a browser context, that's a product decision to make deliberately, not something the public API should make easy by accident. ## CI runners are ephemeral GitHub-hosted (and most self-hosted) runners are torn down after each job. A pulled variable only exists in that job's process environment for the duration of the run — it isn't persisted anywhere by Envpilot's side, and the [GitHub Action](/action/overview) additionally masks every value in the job log before it's exported. ## Tier downgrades If an organization's plan is downgraded below the tier that grants `public_api`/`mcp_server`, existing keys aren't proactively deleted — the same per-request gate check simply starts returning `403` on the next call. There's no background sweep to race and no window where a downgraded org's keys keep working past the next request. ## Limits - **Scope is immutable.** Projects, environments, resources and surfaces are fixed at creation. - **The plaintext is shown once.** There is no recovery and no rotate-in-place — revoke and re-issue. - **Org-wide key creation is owner-only**; team leads can mint keys limited to specific projects. - **Pro plan required** (`public_api` / `mcp_server`), re-checked per request. - Keys are bearer credentials with no IP allowlist and no per-key CORS exemption. ======================================================================== ERRORS ======================================================================== Source: https://docs.envpilot.dev/api/errors # Errors Every non-2xx response is: ```json { "error": "human-readable message", "code": "machine_code" } ``` plus an `x-request-id` header. Quote that id to support — it identifies the exact request without you having to send anything sensitive. ## Status codes | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------- | | `400` | A required query parameter is missing or malformed | | `401` | The key is missing, malformed, invalid, expired, or revoked — one uniform answer for all five | | `403` | The key is valid, but its scope or the organization's plan does not cover this call | | `404` | The target does not exist, **or** exists outside the key's scope — the same response either way | | `422` | The response would exceed the bounded-read ceiling (1000 items) — refused rather than truncated | | `429` | Rate limit exceeded. Carries `Retry-After` in **seconds** | | `503` | A vault decrypt failed mid-request, or the service is misconfigured — the whole request aborts | | `500` | Something unexpected. The `x-request-id` is the thing to report | ## Why 401, 403 and 404 are vague - **401 never says why.** Missing, malformed, invalid, expired and revoked all read the same, so error responses cannot be used to fingerprint which key you hold or whether it once worked. - **403 means valid but not allowed** — a `variables`-only key hitting `/accounts`, or an organization whose plan no longer includes `public_api`. The gate is re-checked per request, so a downgrade shows up on the next call. - **404 covers both "no such thing" and "not yours."** A key can never confirm the existence of a project it cannot see. ## Retrying | Status | Retry? | | ------------ | ---------------------------------------------------------------------------- | | `429` | Yes, after `Retry-After` seconds. Do not retry sooner; the bucket is empty | | `503` | Yes, once or twice with backoff. If it persists, a secret needs re-uploading | | `4xx` others | No. Retrying a scope or parameter error produces the same answer | ## The failures that look like success elsewhere Two behaviours are worth building against explicitly: - **`422` instead of a short list.** Other APIs paginate; this one refuses. A partial `.env` starts a process that then misbehaves far from the cause. - **`503` instead of a null value.** A variable that cannot be decrypted is never returned as `""`, `null`, or a placeholder — those look like real values to a deploy script. ## See also - [Rate limits](/limits/rate-limits) - [Authentication](/api/authentication) ======================================================================== ORGANIZATION ======================================================================== Source: https://docs.envpilot.dev/api/organization # Organization Returns metadata for the organization the key belongs to. Works with **any** valid key regardless of resource scope — this is discovery-level information the key holder already has. ```bash curl https://www.envpilot.dev/api/v1/organization \ -H "Authorization: Bearer envpk_..." ``` ```json { "id": "org_abc123", "name": "Acme Inc", "slug": "acme", "plan": "pro" } ``` ## Limits - No parameters. There is nothing to filter. - Counts against the metadata bucket (120/min per key). - Not audited — no secret material is returned. - A key cannot see any organization other than its own. ======================================================================== PROJECTS ======================================================================== Source: https://docs.envpilot.dev/api/projects # Projects ## List projects Every project in the key's scope, with resource counts. Projects outside scope are silently omitted — an empty match is `200` with an empty list, never an error. ```bash curl https://www.envpilot.dev/api/v1/projects \ -H "Authorization: Bearer envpk_..." ``` ```json { "projects": [ { "id": "proj_1", "name": "Backend", "slug": "backend", "variableCount": 42, "accountCount": 3 } ] } ``` ## Get one project ```bash curl https://www.envpilot.dev/api/v1/projects/backend \ -H "Authorization: Bearer envpk_..." ``` A slug that does not exist and a slug outside the key's scope both return `404`. That is deliberate: a key must not be able to confirm the existence of a project it cannot read. ## Limits - Requires the `projects` resource. - Metadata bucket: 120 requests/min per key. - Counts are of **active** resources; trashed items are not included. - No pagination — see [bounded reads](/api/overview). ## Next - [Variables](/api/variables) · [Accounts](/api/accounts) · [Files](/api/files) ======================================================================== VARIABLES ======================================================================== Source: https://docs.envpilot.dev/api/variables # Variables ## Parameters | Query param | Required | What it does | | --------------- | --------------------------- | -------------------------------------------------------------------- | | `environment` | Yes, unless `metadata_only` | `development`, `staging`, or `production` | | `keys` | No | Comma-separated exact key names — `keys=A,B,C` | | `prefix` | No | Only keys starting with this prefix — `prefix=NEXT_PUBLIC_` | | `metadata_only` | No | `true` returns key names only: no decrypt, no values, no audit entry | | `format` | No | `json` (default) or `env` for dotenv text | ```bash curl "https://www.envpilot.dev/api/v1/projects/backend/variables?environment=production&prefix=NEXT_PUBLIC_" \ -H "Authorization: Bearer envpk_..." ``` ```json { "environment": "production", "variables": [ { "key": "NEXT_PUBLIC_API_URL", "value": "https://api.acme.com", "updatedAt": 1752192000000 } ] } ``` With `format=env` the same response is dotenv text, ready to redirect into a file. ## Filter before you pull `keys` and `prefix` are not conveniences — they narrow what gets decrypted, which narrows what appears in the audit log and what a compromised process could scrape. A build that needs three variables should ask for three. `metadata_only=true` is the right call whenever you are checking _whether_ something exists: it never touches the vault, uses the cheaper rate bucket, and is not audited individually. ## Legacy endpoint The original single-project endpoint used by the [GitHub Action](/action/overview). It still works and is not going anywhere, but new integrations should use `/v1/projects/{slug}/variables`. ## Limits - Requires the `variables` resource, and the project **and** environment must be in the key's scope. - **Rate**: metadata reads use the 120/min bucket; value pulls use the 30/min bucket. - **`422`** if more than 1000 variables match — refused, never truncated. - **`503`** if any variable in the response fails to decrypt — the whole request aborts, with no partial result and no sentinel values. - Values are current. There is no version-history endpoint; history lives in the dashboard. - Read-only. There is no write endpoint at any version. ## Next - [Accounts](/api/accounts) · [Files](/api/files) · [Errors](/api/errors) ======================================================================== SHARED ACCOUNTS ======================================================================== Source: https://docs.envpilot.dev/api/accounts # Shared accounts [Shared accounts](/platform/shared-accounts) are credentials for a third-party service that a team shares — distinct from variables, which are configuration your app reads. ## Parameters | Query param | Required | What it does | | --------------- | -------- | --------------------------------------------------- | | `environment` | No | Filter to one environment | | `metadata_only` | No | `true` returns account names only, no secret values | ```bash curl "https://www.envpilot.dev/api/v1/projects/backend/accounts?environment=production" \ -H "Authorization: Bearer envpk_..." ``` ## Limits - Requires the `accounts` resource; the project must be in the key's scope. - **Rate**: metadata reads use the 120/min bucket; value pulls use the 30/min bucket. - Value pulls are audited against the key. - Read-only — accounts are created and rotated by humans in the dashboard. - The same bounded-read and fail-loud rules apply as for [variables](/api/variables). ======================================================================== SECRET FILES ======================================================================== Source: https://docs.envpilot.dev/api/files # Secret files Project-scoped, unlike the variable endpoints: `project` is a **required query parameter** even when the key's scope resolves a single project. ## Parameters | Query param | Required | What it does | | -------------- | -------- | -------------------------------------------------------------------- | | `project` | Yes | Project slug | | `environment` | Yes | `development`, `staging`, or `production` | | `metadataOnly` | No | `1` or `true` — path, size, mode and checksum with nothing decrypted | | `path` | No | Repeatable. Restrict to these exact destination paths | ```bash # What does this project need, and does my copy match? curl "https://www.envpilot.dev/api/v1/files?project=mobile-app&environment=production&metadataOnly=1" \ -H "Authorization: Bearer envpk_..." # Fetch exactly one file's contents curl "https://www.envpilot.dev/api/v1/files?project=mobile-app&environment=production&path=android/app/upload.jks" \ -H "Authorization: Bearer envpk_..." ``` ## Response ```json { "project": { "slug": "mobile-app" }, "environment": "production", "files": [ { "name": "Play upload keystore", "path": "android/app/upload.jks", "mode": "0600", "size": 2842, "sha256": "9f2b…", "contentType": "application/octet-stream", "environments": ["production"], "updatedAt": 1752192000000, "content": "MIIK…" } ] } ``` `content` is base64 and is **omitted entirely** when `metadataOnly` is set. `sha256` is of the plaintext, so you can diff a local copy without fetching anything. ## Writing them out Write each file to its recorded `path`, relative to your workspace root, with its recorded `mode` (`0600` or `0400`). Re-validate containment yourself — refuse absolute paths, refuse `..`, refuse writing through a symlink. The [GitHub Action](/action/secret-files) does exactly this and is worth reading as a reference implementation. ## Limits - Requires the **`files` resource**, which is never granted by default. - Content fetches are **audited** individually; `metadataOnly` requests are not. - **Rate**: content reads refill at 60/min with a burst of 1000 (the per-project file ceiling); metadata reads use the 120/min bucket. - A single request refuses to return more than **8 MiB** of file content — batch by size, as the Action does. - **`422`** if the project holds more files than a complete listing allows — refused, never truncated. - **`503`** if any requested file fails to decrypt. - Read-only. Uploading is a human action in the CLI or dashboard. ## Next - [Secret files](/platform/secret-files) — paths, modes, storage - [GitHub Action: secret files](/action/secret-files) ======================================================================== MCP OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/mcp/overview # MCP overview MCP (Model Context Protocol) lets an assistant call tools against your real data instead of guessing at it. Envpilot runs a remote MCP server, so Claude Code, Codex, or Cursor can look up your projects, read the variables you allow, and fetch a secret file a build needs — through the **same** auth, scope, rate limits and audit trail as the REST API. Nothing is re-implemented for agents. Requires the **Pro** plan (`mcp_server`) and an [API key](/mcp/setup). ``` https://www.envpilot.dev/api/mcp ``` Transport is Streamable HTTP; auth is `Authorization: Bearer envpk_…`. ## What an agent can do | Can | Cannot | | ----------------------------------------------------- | ----------------------------------------------- | | List projects in the key's scope | Create, edit, or delete anything | | Read variables and shared accounts | Read outside the key's projects or environments | | Read secret files, with the `files` resource | Approve its own request | | Search project names and variable **keys** | See variable **values** through search | | File a variable request, with the `requests` resource | Propose a value for that request | That last row is the whole trust model: **read-only keys, human-approved escalation.** An agent states what it needs and why; a human decides and supplies the value. See [Agent requests](/mcp/agent-requests). ## Getting connected 1. [Mint a scoped key](/mcp/setup) and put it in the client's environment. 2. [Register the server](/mcp/clients) in Claude Code, Codex, Cursor, or any Streamable HTTP client. 3. Confirm with `/mcp` that the **tools** loaded, not merely that the server is listed. ## Limits - **Pro plan only**, re-checked on every call — a downgrade stops the agent immediately. - **Scope is immutable.** Projects, environments, resources and surfaces are fixed at creation; widening means a new key. - **Rate limits are shared with REST** for that key: 120/min metadata, 30/min value pulls, plus the file buckets. Request filing is 5/hour, burst 2. - **Every value-returning call is audited** against the key, including every secret-file fetch. - **Hosted Claude connectors are not supported** — they expect OAuth, and Envpilot uses a fixed bearer key. Use Claude Code. ## Next - [Setup](/mcp/setup) · [Clients](/mcp/clients) · [Tools](/mcp/tools) · [Agent requests](/mcp/agent-requests) - [Agent workflow guide](/guides/agents) ======================================================================== MCP SETUP ======================================================================== Source: https://docs.envpilot.dev/mcp/setup # MCP setup ## Create an MCP key Create a dedicated key in **Organization → Settings → API Keys → New API Key**: 1. Give the key a recognizable name, such as `codex-development`. 2. Select the **MCP server** surface. Deselect REST API and GitHub Action unless this same credential genuinely needs them. 3. Select only the projects the client needs. **All projects** also includes projects created in the future and is owner-only. 4. Select only the environments the client needs. A development-only agent should not receive production access. 5. Select the resources required by its tools: | Resource | MCP tools enabled | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `projects` | `envpilot_list_projects`; project-name and project-slug matches from `envpilot_search` | | `variables` | `envpilot_get_variables`, `envpilot_get_variable`; variable-key matches from `envpilot_search` (which also needs `projects` to enumerate) | | `accounts` | `envpilot_list_accounts` | | `files` | `envpilot_list_files`, `envpilot_get_file` | | `requests` | `envpilot_request_variable`, `envpilot_get_request_status` | | `docs` | `envpilot_search_docs`, `envpilot_get_doc`, `envpilot_create_doc`. **Cannot be combined with `files`** — see below | For the common project-discovery and variable-read workflow, select `projects` and `variables`. Most assistants begin with `envpilot_list_projects`; without `projects`, that opening call is denied. An accounts-only or requests-only key is still valid, but only for the corresponding tools. 6. Choose an expiry, create the key, and copy the `envpk_...` value immediately. `docs` and `files` are mutually exclusive: a key may carry one or the other, never both. Documentation is prose an agent reads into its context and `files` returns decrypted key material, so a single credential holding both is an exfiltration chain. An agent that needs each gets two keys. Project, environment, resource, and surface scopes are **immutable**. You cannot widen or edit them after creation; create a replacement key when the required access changes. The plaintext is also displayed **once**: Envpilot stores only its SHA-256 hash and cannot recover a lost key. ## Endpoint ``` https://www.envpilot.dev/api/mcp ``` Transport is Streamable HTTP (the current MCP spec transport for remote servers). Auth is a plain bearer token — the same `envpk_...` API key you'd use for the REST API: ``` Authorization: Bearer envpk_your_key_here ``` ## Set ENVPILOT_API_KEY Keep the plaintext out of MCP config files and repositories. Put it in the environment of the process that starts the client, then make the client reference the variable by name. ### macOS For the current Terminal session (zsh), enter the key at the hidden prompt so it does not appear in shell history: ```bash read -rs "ENVPILOT_API_KEY?Envpilot API key: "; echo export ENVPILOT_API_KEY ``` This value disappears when the shell exits. For future terminal sessions, have a password manager inject it or edit a user-only shell startup file (do not build the line with a shell command that would record the key in history): ```bash # ~/.zshrc export ENVPILOT_API_KEY='envpk_your_key_here' ``` A literal export in a startup file is persistent but is plaintext on disk; never put it in a repository, and restrict the file to your user: ```bash chmod 600 ~/.zshrc ``` Apps opened from Finder, the Dock, or Spotlight do not inherit variables from `~/.zshrc`. To make the current login session pass the already-exported value to **newly launched** GUI apps: ```bash launchctl setenv ENVPILOT_API_KEY "$ENVPILOT_API_KEY" ``` Fully quit and reopen Codex, Cursor, or the IDE afterward. `launchctl setenv` lasts only for the current login session; after logout or restart, set it again (preferably from a login automation that reads a keychain or password manager—do not embed the raw key in a LaunchAgent plist). Do not run `launchctl getenv ENVPILOT_API_KEY` without redirecting it, because that prints the secret. ### Linux For the current bash session: ```bash read -rsp "Envpilot API key: " ENVPILOT_API_KEY; echo export ENVPILOT_API_KEY ``` For future terminal sessions, have a secret manager inject it or edit the appropriate user-only login file (do not build the line with a shell command that would record the key in history): ```bash # ~/.profile export ENVPILOT_API_KEY='envpk_your_key_here' ``` A literal export is stored as plaintext, so keep the file private: ```bash chmod 600 ~/.profile ``` A desktop launcher inherits the graphical login session, not an interactive shell's `.bashrc`. The simplest safe option is to launch the app from a terminal that already has the variable. On systemd-based desktops, you can also import the current value into the user and D-Bus activation environments: ```bash systemctl --user import-environment ENVPILOT_API_KEY dbus-update-activation-environment --systemd ENVPILOT_API_KEY ``` Then fully quit and reopen the app. For persistence, configure the variable through your desktop login environment or secret manager and sign out and back in; do not put the raw key in a `.desktop` launcher. ### Windows PowerShell Set the key for the current PowerShell process without echoing the input: ```powershell $secret = Read-Host "Envpilot API key" -AsSecureString $pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret) try { $env:ENVPILOT_API_KEY = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } Remove-Variable secret, pointer ``` Child processes launched from that PowerShell window inherit it. To persist the current value for your Windows user: ```powershell [Environment]::SetEnvironmentVariable( "ENVPILOT_API_KEY", $env:ENVPILOT_API_KEY, "User" ) ``` The User-scope value is stored in your Windows profile and is available only to **new** processes. Fully quit and reopen terminals, Codex, Cursor, and IDE windows; already-running processes keep their old environment. ### WSL Windows and WSL have separate process environments. A Windows User-scope variable does not automatically configure clients running inside WSL, and a variable exported in WSL does not configure Windows GUI apps. Set `ENVPILOT_API_KEY` separately inside the WSL distribution using the Linux instructions, then start the WSL-hosted client from a new WSL shell. ### Check presence without printing the key Use a boolean check—never `echo`, `printenv`, `Get-ChildItem Env:`, or an unredirected `launchctl getenv` for a secret: ```bash if [ -n "${ENVPILOT_API_KEY:-}" ]; then echo "ENVPILOT_API_KEY is set" else echo "ENVPILOT_API_KEY is not set" fi ``` ```powershell if ([string]::IsNullOrEmpty($env:ENVPILOT_API_KEY)) { "ENVPILOT_API_KEY is not set" } else { "ENVPILOT_API_KEY is set" } ``` A repository `.env` or `.env.local` file does **not** automatically configure an MCP client. Those files are conventions loaded only by frameworks or commands that explicitly read them. Codex, Claude Code, Cursor, and desktop launchers inspect their own process environment; they do not source arbitrary repository dotenv files. Keeping the MCP credential outside the repository also prevents an agent from reading the very key that controls its access. ## Rotating or updating a key A key's projects, environments, resources, and surfaces are fixed at creation. To recover a lost key, rotate a credential, or grant different access (for example, adding the `projects` resource or MCP surface), create a new key with the complete intended scope. 1. Store the new value in the same environment-variable source. If you set both a persistent value and a current-session value, update both—an already-running shell or GUI process keeps the old value. 2. For macOS GUI clients, update `launchctl`; for Linux desktop clients, update the login/user environment; for Windows, update the User-scope value. WSL remains separate. 3. Fully quit and restart the client. The saved Claude Code, Codex, and Cursor configurations still reference the same variable name and need no secret edit. 4. Confirm `/mcp` or the client's MCP panel lists the Envpilot tools, then make a permitted tool call. 5. Revoke the old key in **Organization → Settings → API Keys**. Revocation is instant—anything still using the old key stops on its next call. If you previously pasted a key directly into Claude Code, remove and re-add the server (`claude mcp remove envpilot`) or edit the saved entry; `claude mcp add` does not overwrite an existing server name. ## Limits - **Scope cannot be widened.** Adding a project, environment or resource means a new key and a client restart. - **The key is shown once.** Store it in an environment variable, never in a config file you commit. - **Pro plan** (`mcp_server`), re-checked on every tool call. - Hosted Claude connectors cannot carry a fixed bearer key — see [Connecting a client](/mcp/clients). ======================================================================== CONNECTING A CLIENT ======================================================================== Source: https://docs.envpilot.dev/mcp/clients # Connecting a client Set `ENVPILOT_API_KEY` in the client's environment first, using the instructions above. ### Claude Code Reference the environment variable in an expanded `Authorization` header: ```bash claude mcp add --transport http --scope user \ envpilot https://www.envpilot.dev/api/mcp \ --header 'Authorization: Bearer ${ENVPILOT_API_KEY}' ``` The **single quotes matter** in POSIX shells: they keep `${ENVPILOT_API_KEY}` literal in the saved configuration, and Claude Code expands it from its environment at connection time. The secret never lands in the file. Choose the Claude Code configuration scope deliberately: - `--scope local` (the default) keeps the server private to you in the current project. - `--scope project` writes `.mcp.json` for the team. The environment-variable reference is safe to commit, but each user must set their own key and approve/trust the project server before tools load. - `--scope user` keeps the server private to your user and makes it available across projects. `claude mcp list` confirms the server is **registered** — a revoked or wrong key still shows as listed. To verify the connection is actually live and its tools loaded, run `/mcp` inside a session. Manage it later with `claude mcp get envpilot` / `claude mcp remove envpilot`. ### Codex CLI, desktop, and IDE extension Codex's local CLI, desktop app, and IDE extension share the same MCP configuration layers. Register the server from the CLI: ```bash codex mcp add envpilot --url https://www.envpilot.dev/api/mcp \ --bearer-token-env-var ENVPILOT_API_KEY ``` Or add it to the user-level `~/.codex/config.toml`: ```toml [mcp_servers.envpilot] url = "https://www.envpilot.dev/api/mcp" bearer_token_env_var = "ENVPILOT_API_KEY" ``` The `url` selects Streamable HTTP, while `bearer_token_env_var` makes Codex build the bearer header from the named variable. Do not replace it with the plaintext key. You may put the same block in `.codex/config.toml` to scope it to a repository, but Codex loads project configuration only after you trust that project. After changing the variable or configuration, start a new CLI session, select **Restart** for the server in Codex desktop, or restart the IDE extension. Run `/mcp` to confirm the server is connected and the Envpilot tools—not merely the saved server entry—are present. ### Cursor Add a remote server to the global `~/.cursor/mcp.json`, or to `.cursor/mcp.json` for a trusted project: ```json { "mcpServers": { "envpilot": { "url": "https://www.envpilot.dev/api/mcp", "headers": { "Authorization": "Bearer ${env:ENVPILOT_API_KEY}" } } } } ``` Cursor resolves `${env:ENVPILOT_API_KEY}` from the environment it inherited at startup. Fully quit and reopen Cursor after setting or rotating the variable, then confirm the tools under MCP settings. Do not commit a raw `envpk_...` value. ### Other Streamable HTTP clients Configure the endpoint as Streamable HTTP and send this header on every request: ```text Authorization: Bearer ``` Prefer a client-native secret store or environment-variable reference. Placeholder syntax is client-specific: do not assume a generic client expands `${ENVPILOT_API_KEY}` or `${env:ENVPILOT_API_KEY}`. ### Hosted Claude connectors are not compatible Claude's hosted custom connectors—used by Claude web and synced to hosted Claude Desktop and mobile—are different from Claude Code's local MCP configuration. Their standard setup accepts a remote URL and an OAuth connection flow, not a per-user arbitrary `Authorization` header. Envpilot currently uses a fixed `envpk_...` bearer API key and does not expose the OAuth flow those hosted connectors expect, so use **Claude Code** instead. Do not paste an Envpilot API key into an **OAuth Client Secret** field. That field authenticates an OAuth client during a token exchange; it does not become `Authorization: Bearer envpk_...` on MCP requests and cannot adapt Envpilot's API-key auth into OAuth. ## Troubleshooting A failed MCP tool call returns an error message in its result — no separate error code, since tool clients only see the text. The table lists each message (tool-specific variants may append detail, e.g. the request tools' `— filing requests needs the "requests" resource`). One exception: a missing or malformed `Authorization` header is rejected at the transport level with HTTP 401 before any tool runs. First separate client setup failures from Envpilot tool errors: | Symptom | Meaning / fix | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Server is registered but has no tools | Registration only proves the config was saved. Open `/mcp` or the client's MCP panel and inspect initialization. The most common cause is that the client process did not inherit `ENVPILOT_API_KEY`; set it in the parent environment and fully restart the client. | | Works in a terminal, not a GUI app | The terminal has a current-session variable, but the desktop launcher started from a different environment. Use `launchctl` on macOS, the graphical login/user environment on Linux, or Windows User scope, then launch a new process. | | Works on Windows but not in WSL | Windows and WSL environments are separate. Set the key inside the WSL distribution and restart the WSL-hosted client. | | Header contains `${...}` literally | The client did not expand the placeholder. Claude Code uses `${ENVPILOT_API_KEY}`; Cursor uses `${env:ENVPILOT_API_KEY}`; Codex uses `bearer_token_env_var = "ENVPILOT_API_KEY"`. Confirm the client supports that syntax and inherited the variable, then restart it. | | HTTP 401 before tools load | Ensure the header is exactly `Authorization: Bearer envpk_...`: one `Bearer` prefix, one space, no surrounding quotes in the value, no duplicated prefix, and no literal placeholder. Use a safe presence check instead of printing the key. | | Hosted Claude cannot authenticate | Hosted Claude web/Desktop/mobile custom connectors use their URL/OAuth flow and cannot supply Envpilot's fixed bearer key through the standard setup. Use Claude Code; do not put the API key in an OAuth client-secret field. | Once tools load, these are the server's error semantics: | Message | Meaning / fix | | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Invalid or revoked API key` | The key is wrong, revoked, or expired—create a replacement key. | | `That resource is not in this API key's scope` | The key is missing the resource a tool needs. Scopes are immutable: use the [Tools](/mcp/tools) table to create a replacement with the required resource. Request tools append `— filing requests needs the "requests" resource`. | | `That environment is not in this API key's scope` | The key is limited to other environments. Use one already in scope, or create a replacement key that includes the required environment. | | `Project not found` | The slug is wrong, or the project is outside the key's immutable scope—the two are indistinguishable by design; see [Understand scope](/api/quickstart#understand-scope). | | `This API key is not enabled for this surface` | The key was created without the MCP server surface. Surfaces are immutable: create a replacement with **MCP server** selected. | | `The public API is available on the Pro plan — this organization's plan no longer includes it` | The organization's plan no longer includes MCP access. | | `Rate limit exceeded — retry after Nms` | Hit a rate-limit bucket: 120/min metadata or 30/min value pulls (both shared with REST), or 5/hour (burst 2) for `envpilot_request_variable`—back off for the given delay. | | `Missing or invalid API key.` (HTTP 401) | Transport-level rejection before any tool runs—the `Authorization` header is missing or malformed. It must resolve to `Bearer envpk_...`, not a literal placeholder. | Older server builds returned an opaque `[Request ID] Server Error` for all of the above instead of the real message — if you see that instead of one of these strings, the connected server predates this fix. ## Limits - Streamable HTTP only. There is no stdio transport and no local proxy. - Placeholder syntax is client-specific — `${VAR}`, `${env:VAR}` and `bearer_token_env_var` are not interchangeable. - A client only inherits the environment it was launched with; changing the key means fully restarting it. - Hosted Claude web, Desktop and mobile connectors expect OAuth and cannot authenticate here. Use Claude Code. ======================================================================== TOOLS ======================================================================== Source: https://docs.envpilot.dev/mcp/tools # Tools Every tool runs against the same scope, rate limits and plan gate as the REST API, and every value-returning call is audited. ## Read tools | Tool | What it does | Resource | | ------------------------ | ----------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `envpilot_list_projects` | List every project in the key's scope | `projects` | | `envpilot_get_variables` | Variables for a project + environment, with optional `keys` / `prefix` / `metadata_only` | `variables` | | `envpilot_get_variable` | One variable by exact key | `variables` | | `envpilot_list_accounts` | Shared accounts for a project, optionally filtered by environment | `accounts` | | `envpilot_list_files` | Secret-file **metadata** for a project: path, size, mode, checksum. Never contents | `files` | | `envpilot_get_file` | The decrypted contents of **one** secret file, base64-encoded | `files` | | `envpilot_search` | Search project names/slugs and variable **keys** — never values. Bounded to 20 projects / 100 matches | `projects` (plus `variables` for key matches) | | `envpilot_search_docs` | Full-text search over **published** documentation — titles and page bodies. Never returns drafts | `docs` | | `envpilot_get_doc` | One **published** documentation page with its markdown body | `docs` | ## Write-adjacent tools A machine credential can perform exactly two mutations, and both end at a human: | Tool | What it does | Resource | | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ---------- | | `envpilot_request_variable` | File a request with a **required justification**. A human approves it and supplies the value | `requests` | | `envpilot_get_request_status` | Poll a filed request — `pending`, `approved`, `rejected` (with the reviewer's reason), or `canceled` | `requests` | | `envpilot_create_doc` | Propose a documentation page. Always creates a **draft**; a human publishes it | `docs` | Neither writes anything a reader sees on its own. A requested variable has no value until a person supplies one, and a proposed page is invisible to your team — and to every other agent — until a person publishes it. ## Secret file tools in detail ### `envpilot_list_files` | Parameter | Required | Notes | | ------------- | -------- | ------------------------------------------------------ | | `project` | Yes | Project slug | | `environment` | No | Omit to return files across every in-scope environment | Metadata only: destination path, size, mode, checksum, environments. Nothing is decrypted and no download is recorded, so an agent can safely explore what a build needs. ### `envpilot_get_file` | Parameter | Required | Notes | | ------------- | -------------------------------------------- | ------------------------------------------------- | | `project` | Yes | Project slug | | `path` | Yes | Exact destination path from `envpilot_list_files` | | `environment` | When the path exists in several environments | Otherwise the call fails rather than guessing | Returns real secret material. The tool description tells the agent as much: fetch only the file the task needs, never speculatively, never echo the contents back to the user, and write straight to the recorded path. If the path exists in more than one environment and no `environment` was given, the call fails with a message naming the count. Guessing which one a build wanted would be worse than asking. ## Documentation tools in detail ### Drafts are invisible `envpilot_create_doc` always writes a draft. Nothing on this surface can publish, and `envpilot_search_docs` / `envpilot_get_doc` return **published pages only** — a draft is not merely hidden from search, it cannot be fetched by id either. That gate is what stops a page one agent wrote from reaching another agent's context before a person has read it. ### Page count is capped by plan `envpilot_create_doc` fails once the project or the organization is at its page limit — 10 per project and 25 per organization on Free, unlimited on Pro. The caps are the same numbers the dashboard enforces, so an agent cannot use the MCP surface to walk around them. See [Plans & Limits](/limits/plans). ### Pages name variables, never values A page may reference `API_BASE_URL`; it never contains the value. Resolving it is the reader's own `envpilot_get_variables` call under its own `variables` and environment scope, so reading documentation can never become reading secrets. ### `docs` and `files` cannot share a key A key carrying `docs` may not also carry `files`, and the dashboard refuses to mint one. Documentation is prose an agent pulls into its context; `files` returns decrypted keystores and SSH keys. On one credential those two are an exfiltration chain, so they are kept apart at mint time. Use two keys. ### Content is scanned on write Every proposed page is checked before it is stored. Credential shapes (PEM blocks, provider key prefixes, connection strings with inline passwords) are rejected outright, and text instructing an agent to call a tool is refused. High-entropy strings that are normal in API documentation — sample JWTs, base64 payloads, commit SHAs — pass through untouched. ## Cost of each call | Tool | Bucket | Audited | | ------------------------------------------------------- | ------------------------------------- | ------- | | `list_projects`, `search`, `list_files`, metadata reads | 120/min metadata | No | | `get_variables`, `get_variable`, `list_accounts` | 30/min value pulls | Yes | | `get_file` | file reads: refill 60/min, burst 1000 | Yes | | `request_variable` | 5/hour per key, burst 2 | Yes | | `search_docs`, `get_doc` | 120/min metadata | No | | `create_doc` | 30/hour per key, burst 10 | Yes | ## Limits - A tool whose resource is missing from the key is denied with a message naming the resource — scopes are immutable, so the fix is a replacement key. - A project outside scope behaves exactly as if it does not exist. - `envpilot_search` never returns values, only names and keys. - There is no tool that writes a variable, uploads a file, publishes a documentation page, or approves a request. There is no plan on which one appears. - Documentation search is full-text: whole words, with only the last term prefix-matched. It is not a substring match and does not correct typos. ## Next - [Agent requests](/mcp/agent-requests) · [Setup](/mcp/setup) ======================================================================== AGENT REQUESTS ======================================================================== Source: https://docs.envpilot.dev/mcp/agent-requests # Agent requests A machine credential can never write a secret value. The one thing it can do beyond reading is **ask** — and only if its key carries the `requests` resource. `envpilot_request_variable` takes the variable key and a required justification, and files a request into a human reviewer's dashboard queue. The agent never proposes a value; if the reviewer approves, **the reviewer supplies it**. The loop for an agent is: 1. Call `envpilot_request_variable` with the key and a justification explaining why it's needed. 2. Poll `envpilot_get_request_status` until it returns `approved` or `rejected`. 3. On `approved`, read the value with `envpilot_get_variable`. On `rejected`, the status carries the reviewer's reason — report it back rather than retrying blindly. Request creation is deliberately strict: it is rate-limited to 5 per hour per key (burst 2), capped at 5 open pending requests per key, and a rejected request for a given variable can't be re-filed for 24 hours. Every created request emails a reviewer, so these limits stop an agent loop from becoming alert spam. See [Rate limits](/limits/rate-limits) for the full picture and [Architecture](/start/architecture) for how "agent requests, human approves" fits the wider trust model. ## Reviewing on the human side A reviewer sees machine-filed requests in the same queue as human ones — the project's Requests inbox, `envpilot requests`, or an email. Approving one prompts for the value: ```bash envpilot requests # find the id printf %s "$SECRET" | envpilot requests approve --value-stdin ``` Read the justification before approving. It is the only account of what the agent was doing when it asked, and it is required precisely so that a reviewer never has to guess. ## Limits - **Requests create variables.** An agent cannot request a change to an existing value. - **5 filings per hour per key**, burst 2, and at most **5 open pending** at a time. The sixth is refused until a human clears one. - **24-hour cooldown after a rejection** for the same key. A rejection is a decision. - The GitHub Action can never file a request, whatever its scope. ## See also - [Requests & approvals](/platform/requests) — the full model - [CLI: requests](/cli/requests) — the reviewer's side ======================================================================== DASHBOARD OVERVIEW ======================================================================== Source: https://docs.envpilot.dev/dashboard/overview # Dashboard overview The dashboard at [envpilot.dev](https://www.envpilot.dev) is the full management surface. Sign in through WorkOS AuthKit (Google, GitHub, or email), then use the sidebar to move between projects, team, audit trail, and settings. ## Home `/dashboard` shows a status summary (project, variable, team and event counts), a **Getting Started** checklist for new organizations, **Recent Projects**, a **Recent Activity** feed linking into the audit log, and a **Team Members** panel. Two widgets appear when your plan enables them: - **Expiring Secrets** — variables with a [rotation schedule](/platform/rotation) expiring within 7 days. - **Shared secrets** — a summary of your active [share links](/platform/sharing). ## Projects list `/dashboard/projects` lists every project you can reach. **Create Project** offers a framework template (Next.js, T3 Stack, Django, Rails, and around twenty more) that pre-fills a starter variable set with placeholders. ## Audit log `/dashboard/audit` is a searchable log of every recorded action — variable reads and writes, secret-file downloads, permission changes, authentication, team changes, billing and CI/CD events, across 14 categories. Filter by free text, category, and date range (24h / 7d / 30d / 90d), then export the filtered result as CSV or JSON. Retention follows your plan: **7 days on Free, 365 on Pro** (`audit_log_retention_days`). ## Analytics `/dashboard/analytics` (Project Manager and above) shows six panels: activity overview, project activity, variable changes by project, team activity, resource breakdown, and security insights — security-event, sensitive-access and permission-change counts. The 7/30/90-day selector is capped by your plan's analytics retention (7 days Free, 30 Pro). ## Limits - What you see is what your role allows; nothing hints at projects you cannot reach. - Search across variables is server-side and capped at the first 100 matches, with a banner when you hit it. - Analytics and audit ranges are truncated by tier retention, not just by the selector. ## Next - [Working in a project](/dashboard/project) - [Organization administration](/dashboard/organization) ======================================================================== WORKING IN A PROJECT ======================================================================== Source: https://docs.envpilot.dev/dashboard/project # Working in a project ## The variable table Opening a project lands on its variables, with tabs for **All** plus each of the three environments, a debounced server-side search, and a tag-filter row if tags are enabled. Each row supports edit, delete, reveal, [version history with rollback](/platform/variables), and — when sharing is enabled — sending a [share link](/platform/sharing). Selected rows can be bulk-deleted from a floating action bar. Deleting warns that the variable is recoverable for **7 days**, after which it and its vault value are purged. The create button reads **Add Variable** or **Request Variable** depending on your role. The same drawer has a **Bulk Paste** tab for a whole `.env` block. Rotation, if your plan includes it, is a checkbox in the same form with presets of 30, 60, 90 (default), 180, or 365 days. See [Rotation & expiry](/platform/rotation). ## Requests A project's requests list links through to the reviewer inbox at `/dashboard/requests`, tabbed by **Pending / Approved / Rejected / Canceled**. Only reviewers can act. Before accepting, a reviewer picks which environments to approve for. A **machine-filed request** — from an API key over MCP — shows a masked input reading _"Enter the value to approve"_: the reviewer types the secret, and Accept stays disabled until both an environment and a value are supplied. The value is encrypted before the approval runs. Rejection on this screen is a plain confirm dialog; the free-text reason field lives in the CLI's `requests reject --reason`. ## Files Project → **Files** manages [secret files](/platform/secret-files): keystores, SSH keys, certificates, service-account JSON. - **Upload** — drag and drop or pick a file, then set display name, destination path, environments, and mode (`0600 — owner read/write` or `0400 — owner read-only`). - **Replace contents** — swap the bytes while metadata stays locked, so a rotated keystore keeps its path and grants. - **Permissions** — per-file grants, the same model variables use. - **Trash** — soft-delete with the same 7-day retention. ## Compare environments The diff page compares two or more environments (default development vs production), classifying each key as matching, changed, or missing — based on vault references, not decrypted content, until you reveal. If two references differ but the decrypted values are byte-identical, a **content match** badge says so rather than reporting a false diff. Search, filter to sensitive-only, sort, reveal individually or all at once, and export as per-environment `.env` files or a JSON/Markdown report. For a key missing from one environment there is a **copy-from** helper — there is no one-click sync mutation. ## Shared variables The same credential often lives in several projects. Sharing keeps one row that every picked project reads, so a rotation is one edit. Sharing is off until an owner turns it on under **Organization → Settings → Shared variables**. Turning it off later stops new sharing only; groups that already exist keep working for every client. A row whose key also exists in other projects shows **same key in N projects**. Its **Share across projects** action opens a sheet listing every project you can manage: projects holding the same value are preselected and their copies are moved into the shared row (they land in each project's trash for 7 days); projects with a different value are listed but cannot be picked until that is resolved. Pick an existing group or name a new one and confirm. Shared rows appear pinned above the table in every project that reads them, with a **N projects** pill. Edit and delete work in place and state how many projects change before you confirm. You can edit a shared row only if you could edit variables in every project it reaches. If any of those projects protects an environment the row is in, the edit is filed as a change request instead of landing directly. **Stop sharing here** removes this project from the group. By default the current values are copied into the project first, so its next pull is unchanged. The last project to leave takes the group with it. The CLI, the editor extensions, the MCP server, the GitHub Action and the Docker image all receive shared rows with no update: resolution happens on the server. Pushing a shared key from a client is refused with a message naming the group. **Merge in one click.** The same settings tab lists every key that is identical across projects, and the dashboard shows the count. **Merge all** opens a sheet with three environment chips. Development and staging are on by default and merge on the spot. Production is off by default; when it is on, a key that reaches a project protecting production is not merged directly but filed as a change request for a second person to apply, exactly like a production edit. Keys with different values, or with an environment that is off, are held back with the reason and never touched. Groups are listed and renamed under **Organization → Settings → Shared variables**. Shared variables are a Pro feature; on the free plan the switch shows "Not available on this plan". ## Trash Soft-deleted variables and shared accounts are listed separately, each restorable individually. Retention is **7 days**, shown per row as "Deleted N days ago — M days left", turning red at one day or less. **Empty trash** purges everything immediately, destroying the vault values. Restoring re-runs the uniqueness check — a restore that would collide with a newer variable is rejected, not merged. ## Shared accounts `/accounts` holds [shared accounts](/platform/shared-accounts) — credential pairs for a shared Stripe login or a database admin account, kept separate from variables. Create with a name, optional URL, username, masked password, description, and environments. Edit, delete, reveal, manage permissions, and share, with the same 7-day trash retention. ## Sharing The Sharing page manages [share links](/platform/sharing): one-time or time-limited links that reveal a single value to someone outside the project, gated by email verification. It shows totals — active, viewed, expired, revoked — and each card's type, recipient status, and countdown. The only action here is **Revoke**. Links are created from the share button on a variable or account row. ## Members A project's Members page assigns existing organization members to the project. For environment-scoped roles, a checklist restricts which environments they see. Roles themselves are organization-level — see [Roles & permissions](/platform/rbac). ## Settings Two tabs: - **General** — name, description, icon and colour, and VS Code auto-unsync behaviour (Pro). - **Danger Zone** — transfer the project to another organization, or delete it. Deletion gives variables and accounts the same 7-day trash retention as a manual delete. There is no CI/CD Tokens tab. Legacy service tokens still work through a compatibility fallback, but new automation uses an [API key](/api/authentication) from Organization Settings. ## Limits - Environments are fixed at three. There is no way to add a fourth. - Variable search returns at most 100 matches at a time. - Diff compares vault references first — a reveal is what decrypts. - Trash is 7 days, then permanent. ## Next - [Organization administration](/dashboard/organization) ======================================================================== ORGANIZATION ADMINISTRATION ======================================================================== Source: https://docs.envpilot.dev/dashboard/organization # Organization administration ## Members Invite people, change roles, and remove them. Each row also carries **Suspend access** — [Security Hold](/platform/rbac#security-hold) — which freezes a member everywhere (web, CLI, extension, API) without touching their role or assignments, and optionally surfaces any API keys they created so you can revoke those too. A suspended row flips to **Reinstate**, restoring exactly what was frozen. Invitations expire after 7 days and are only redeemable by the email address they were sent to. ## Usage `/dashboard/usage` (owner only) is the plan page: - current tier, price, upgrade and billing links - an alert zone for anything at or near a limit - **Quotas & limits** meters: organizations, projects, team members, variables per project, invitations, active shares, rotation-enabled variables, shared accounts, secret files — plus your audit and analytics retention windows - **Plan features**, comparing your tier against Pro If tier enforcement is off (pre-alpha mode), a badge says so. `envpilot usage --json` gives the same numbers in the terminal. ## Settings Four tabs: **General**, **Tags**, **API Keys**, **Danger Zone**. General and Danger Zone are owner-only. ### Tags Full CRUD over the organization's colour-coded tag set — create, bulk-paste, edit, delete. Deleting a tag strips it from every variable that used it. ### API keys Create org-scoped [API keys](/api/authentication) for the REST API, MCP server, and GitHub Action. For each key you choose: - **surfaces** — `rest_api`, `mcp_server`, `github_action` - **projects** — specific ones, or all including future ones (owner-only) - **environments** - **resources** — `variables`, `accounts`, `projects`, `files`, `requests` Each row shows who created it, when it was last used, pull / denial / request counts, and **Revoke**. ### Danger zone Transfer the organization to another owner, or delete it. ## Billing Billing and plan upgrades live on the personal account settings page (`/dashboard/settings`), not in organization settings. ## Limits - Creating org-wide API keys is owner-only; team leads can create keys limited to specific projects. - "All projects" scope includes projects created later, which is why it is owner-only. - Security Hold requires the target to be strictly below your role, and Owners can never be suspended — transfer ownership first. - Plan limits are enforced server-side; this page reflects them, it does not define them. See [Plans & limits](/limits/plans). ======================================================================== SLACK & DISCORD NOTIFICATIONS ======================================================================== Source: https://docs.envpilot.dev/integrations/notifications # Slack & Discord Notifications When a secret changes in production, the team should find out in the channel they already watch — not by checking the dashboard the next morning. Envpilot posts organization activity to Slack and Discord channels: variable changes, access requests, membership changes, and security events. Notifications carry **key names, environments, and actor names — never secret values**. A notification channel is never a place where a secret can leak. > Slack & Discord notifications are a **Pro** feature. See [Plans](/limits/plans). ## Connect a channel Connecting takes one click — Envpilot uses each platform's OAuth flow, which creates the webhook for you: 1. Go to **Organization Settings → Integrations** (owner only). 2. Click **Connect Slack** or **Connect Discord**. 3. On the platform's consent screen, pick the channel to post to and approve. 4. You land back on the Integrations tab. Envpilot queues a test message and the row updates with the delivery result. There is nothing to copy out of Slack or Discord admin screens — the platform hands Envpilot the webhook URL during consent. The URL is encrypted in WorkOS Vault; Convex keeps only its opaque Vault reference and a masked preview. The OAuth access token is discarded, never stored. ## Add a webhook manually If the Connect buttons aren't available (a locked-down Slack workspace, or a self-hosted Envpilot without OAuth apps configured), paste a webhook URL instead: - **Slack**: in the target channel → **Integrations → Add an app → Incoming Webhooks** → create and copy the URL (`https://hooks.slack.com/services/…`). - **Discord**: channel settings → **Integrations → Webhooks → New Webhook** → **Copy Webhook URL** (`https://discord.com/api/webhooks/…`). Then **Organization Settings → Integrations → Advanced setup**, pick the platform, paste the URL, and choose project routing. All webhook URLs — manual or OAuth-connected — are encrypted as credentials, and the UI only ever shows a masked preview. ## Route projects to channels Every connected destination can receive activity from **all projects** or only a selected set of projects. Open **Manage** beside a destination to change its project routing and event groups. For example, connect the Slack `#envpilot` channel, choose **Selected projects**, and select the EnvPilot project. That channel then receives only EnvPilot project activity. Connect Slack or Discord again to route other projects to different channels. Organization-wide events that do not belong to a project, such as membership changes, are sent only to destinations configured for all projects. Existing destinations default to all projects. ## Event groups Each destination subscribes to event groups, editable through **Manage** at any time: | Group | Covers | Default | | --------- | ------------------------------------------------------------ | ------- | | Variables | created, updated, rotated, restored, rolled back, exported | on | | Requests | access requested, approved, rejected | on | | Members | invitations, removals, shared-account permission changes | off | | Security | attributable API denials, API keys, device/extension revokes | off | ## Delivery behavior - Delivery is fire-and-forget: the audited action only queues preparation and never waits for Slack or Discord. - Requests time out after 10 seconds. Network errors, HTTP 5xx responses, and rate limits retry up to three total attempts; provider `Retry-After` guidance is respected. - After **20 consecutive failures** a webhook auto-disables instead of hammering a dead endpoint. Re-enabling it (Resume) resets the counter. - **Send test** queues a test message on demand; every newly added webhook queues one too. The settings row reports the final delivery status. - Pausing, removing, or losing the feature gate stops already-queued deliveries because the endpoint is re-checked immediately before every post. - Up to 10 webhooks per organization. ## Self-hosting: enabling the Connect buttons The one-click Connect flow needs OAuth apps you register once per platform. Without these env vars the buttons hide and manual entry still works. **Slack** — [api.slack.com/apps](https://api.slack.com/apps) → Create New App: 1. Under **OAuth & Permissions**, add the redirect URL `https:///api/integrations/slack/callback` and the bot scope `incoming-webhook`. 2. To install the app in workspaces other than its development workspace, complete Slack's **Manage Distribution** checklist and activate public distribution. 3. Copy the **Client ID** and **Client Secret** into your web environment: ```bash SLACK_CLIENT_ID=... SLACK_CLIENT_SECRET=... ``` **Discord** — [discord.com/developers/applications](https://discord.com/developers/applications) → New Application: 1. Under **OAuth2**, add the redirect `https:///api/integrations/discord/callback`. 2. Copy the **Client ID** and **Client Secret**: ```bash DISCORD_CLIENT_ID=... DISCORD_CLIENT_SECRET=... ``` For local Discord testing, register this exact redirect: ```text http://localhost:3000/api/integrations/discord/callback ``` Slack requires OAuth redirect URLs to use HTTPS. Its PKCE exception for `http://localhost` is a desktop flow and cannot request the `incoming-webhook` bot scope. You can keep the app entirely local by running Next.js with local HTTPS: ```bash bun --cwd apps/web run dev -- --experimental-https ``` Accept/trust the generated local certificate in the test browser, set both `NEXT_PUBLIC_APP_URL` and `WORKOS_REDIRECT_URI` to the HTTPS localhost origin, and add `https://localhost:3000/callback` to the WorkOS development environment's allowed redirect URIs. Then register: ```text https://localhost:3000/api/integrations/slack/callback https://localhost:3000/api/integrations/discord/callback ``` Alternatively, use a temporary HTTPS tunnel or a trusted local HTTPS reverse proxy and register its callback URL instead. If the app must remain on plain `http://localhost:3000`, use **Add manually** with a disposable Slack incoming webhook; delivery, retries, status, pause/resume, and removal still run end to end. For Discord or manual-webhook testing, set `NEXT_PUBLIC_APP_URL=http://localhost:3000` in the web app's root `.env.local`. Set the same value in the Convex **development deployment** so notification links point back to localhost: ```bash bunx convex env set NEXT_PUBLIC_APP_URL http://localhost:3000 ``` The four OAuth client variables belong only in `.env.local`; do not put client secrets in Convex. `WORKOS_API_KEY` must already be configured in the Convex deployment because webhook URLs are encrypted in WorkOS Vault. The consent screens use the `incoming-webhook` (Slack) and `webhook.incoming` (Discord) scopes — channel-post capability only, no read access to anything. ## Limits - **Pro only** (`team_notifications`). Free organizations have the feature off and a channel limit of 0. - **Up to 10 webhooks** per organization (`team_notifications_limit`). - Delivery times out after **10 seconds** and retries up to **three total attempts**, respecting the provider's `Retry-After`. - Slack and Discord only, via incoming webhooks — post-only scopes, no read access to your workspace. - Manual **Send test** is rate limited to 5 per minute per organization. - Notifications carry event metadata and links, never secret values. ======================================================================== HOW TO SHARE ENVIRONMENT VARIABLES SECURELY ======================================================================== Source: https://docs.envpilot.dev/guides/migrate-from-dotenv # How to Share Environment Variables Securely with Your Team Every development team hits this moment: a new engineer joins, clones the repo, runs the app — and it crashes because they don't have the `.env` file. So someone DMs it to them on Slack. It works, everyone moves on, and your production database credentials now live in a chat log forever. This guide covers why that's a real problem, the common workarounds and their trade-offs, and how to set up secret sharing that's actually secure. ## Why sharing .env files over Slack is dangerous It feels harmless because it's so common. But pasting secrets into chat creates problems that compound over time: - **Secrets outlive their welcome.** Chat history is searchable and effectively permanent. A credential shared two years ago is still sitting there — long after the person who shared it left the company. - **No revocation.** When someone leaves the team, you can remove their Slack account, but every secret they ever received is still on their laptop, in their email, in their notes. - **No audit trail.** If a key leaks, you have no way to know who had access to it, when they got it, or which copy leaked. - **Drift.** The `.env` file in chat is a snapshot. Three weeks later someone rotates a key, and half the team is debugging errors caused by stale values. - **Compliance failure.** SOC 2, ISO 27001, and most security questionnaires explicitly ask how you distribute secrets. "We paste them in Slack" is a failing answer. The same applies to email, shared Google Docs, Notion pages, and committing `.env` to a private repo. Private is not the same as secure — repo access is rarely scoped to who should see production credentials, and git history never forgets. ## What good secret sharing looks like Whatever tool you use, secure team secret sharing has five properties: 1. **Encryption at rest** — secrets are stored encrypted (e.g. AES-256), not as plaintext in a database or document. 2. **Access control** — people see only the secrets they need. A frontend contractor doesn't need production database credentials. 3. **Revocation** — removing a person removes their access, immediately, without rotating every key they ever saw. 4. **Audit logging** — every read, write, and share is recorded with who/what/when. 5. **A single source of truth** — everyone pulls the current values from one place, so rotation propagates instead of causing drift. ## Common approaches, compared **Encrypted files in the repo (SOPS, git-crypt, dotenv-vault).** Better than plaintext, and it keeps secrets versioned next to code. But key distribution just moves the problem (now you share the _decryption_ key), revocation still means re-encrypting everything, and there's no per-secret access control or audit trail. **Cloud provider secret managers (AWS Secrets Manager, GCP Secret Manager).** Excellent for production workloads that already run in that cloud. But they're awkward for local development — every developer needs cloud IAM credentials, and the DX of pulling twenty variables into a local dev server is poor. **Password managers (1Password, Bitwarden).** Fine for a handful of shared credentials, and far better than chat. But they're built for humans logging into websites, not for injecting forty variables into a process at runtime — so developers end up copy-pasting values back into local `.env` files, recreating the drift problem. **A dedicated environment variable manager.** Purpose-built tools (Envpilot, Doppler, Infisical) store secrets encrypted, scope access by project/environment/role, keep an audit log, and inject variables directly into your process — so the `.env` file disappears entirely. ## The workflow with Envpilot [Envpilot](/) stores secrets AES-256-encrypted in an isolated vault, with [role-based access control](/platform/rbac) down to the individual variable. The day-to-day workflow looks like this: ```bash # One-time setup npm install -g @envpilot/cli envpilot login envpilot init # link the repo to a project # Run your app — variables are injected at runtime, no .env file written envpilot run -- npm run dev ``` When a teammate joins, you add them to the project in the dashboard and they run the same three commands. When someone leaves, you remove them — every secret stays where it is, and their access is gone. When a key rotates, everyone gets the new value on their next run automatically. For the full setup walkthrough, see [Getting Started](/start/quickstart). ## Checklist: migrating off shared .env files 1. Pick a single source of truth and import your current `.env` files into it. 2. Add `.env*` to `.gitignore` (and scrub any committed history with `git filter-repo` if needed). 3. **Rotate every secret that was ever shared over chat or email** — assume they're compromised, because you can't prove they aren't. 4. Scope access: production credentials to the people who deploy, staging to the team, local-dev to everyone. 5. Switch local development to runtime injection (`envpilot run -- `) so plaintext files stop being created at all. 6. Turn on audit logging and review it when offboarding. The rotation step is the one teams skip — and it's the most important. A secrets manager protects you going forward; it can't un-leak what's already in your chat history. ## Limits - Migrating does not invalidate the copies already circulating. Treat every previously shared secret as exposed and [rotate it](/platform/rotation). - Free tier caps you at 3 projects, 50 variables per project, and 3 members. - Bulk import is Pro-gated (`bulk_import`); on Free, paste variables through the dashboard's Bulk Paste tab or push them with the CLI. ======================================================================== NEXT.JS ENVIRONMENT VARIABLES BEST PRACTICES ======================================================================== Source: https://docs.envpilot.dev/guides/nextjs # Next.js Environment Variables: Best Practices Next.js has more environment variable behavior than almost any other framework — load order across multiple `.env` files, build-time inlining, the `NEXT_PUBLIC_` prefix, and different rules for server and client code. Most leaked frontend secrets trace back to misunderstanding one of these. This guide covers how it actually works and the practices that keep secrets safe. ## How Next.js loads .env files Next.js loads environment files in a strict order, where earlier files win: 1. `process.env` (already-set variables always win) 2. `.env.$(NODE_ENV).local` — e.g. `.env.development.local` 3. `.env.local` (skipped when `NODE_ENV` is `test`) 4. `.env.$(NODE_ENV)` — e.g. `.env.production` 5. `.env` Practical conventions that follow from this: - **`.env`** — defaults safe to commit (feature flags, public URLs). - **`.env.local`** — secrets and machine-specific overrides. Never commit it; Next.js's own `create-next-app` gitignores it for a reason. - **`.env.production` / `.env.development`** — committed, environment-specific _non-secrets_. If a variable mysteriously won't change, check the higher-priority files — a stale value in `.env.development.local` silently overrides everything below it. ## NEXT*PUBLIC* means "published to the world" This is the single most important rule: > Any variable prefixed with `NEXT_PUBLIC_` is **inlined into the JavaScript bundle at build time** and shipped to every visitor's browser. It is not "available on the client" in some managed, scoped sense — it is plain text in your public JS files, visible to anyone who opens DevTools. Treat the prefix as a publish button: ```bash # Fine — these are meant to be public NEXT_PUBLIC_APP_URL=https://www.envpilot.dev NEXT_PUBLIC_ANALYTICS_ID=abc123 # NEVER — this ships your key to every visitor NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_... # ✗ ``` If a third-party SDK asks for a `NEXT_PUBLIC_` key, make sure it's the _publishable_ key, not the secret one. Server-only variables (no prefix) are stripped from client bundles automatically — accessing them in client components just yields `undefined`. ## Build time vs runtime `NEXT_PUBLIC_` values are frozen **at build time**. Two consequences trip teams up: - **Changing the variable later does nothing** until you rebuild. Updating it in your hosting dashboard and restarting is not enough. - **One Docker image can't serve multiple environments** with different `NEXT_PUBLIC_` values — the values are baked in. If you need "build once, deploy anywhere", read public config at runtime instead: fetch it from an API route, or read server-side variables in a Server Component and pass them down as props. Server-side variables (no prefix) are read at **runtime** from `process.env`, so they can differ per deployment without rebuilding. ## Keeping secrets server-side in the App Router With the App Router, the server/client boundary is a file-level concern, and it's easy to drag a secret across it accidentally: - Read secrets in **Server Components, Route Handlers, and Server Actions** only. - Never pass a secret as a prop from a Server Component into a `"use client"` component — props are serialized into the HTML payload. - For belt-and-suspenders enforcement, use the `server-only` package: importing a module that contains secrets from client code then fails the build instead of leaking. ```ts // lib/payments.ts export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); ``` ## Validate variables at startup, not at first use A missing variable should fail the build or boot — not throw at 2 a.m. when the code path finally runs. Validate once with a schema: ```ts // env.ts const schema = z.object({ DATABASE_URL: z.string().url(), STRIPE_SECRET_KEY: z.string().startsWith("sk_"), NEXT_PUBLIC_APP_URL: z.string().url(), }); export const env = schema.parse(process.env); ``` Import `env` instead of touching `process.env` directly and typos become type errors. ## Stop distributing .env files to the team Everything above keeps secrets out of the _bundle_ — but most leaks happen earlier, in how teams pass `.env.local` files around (Slack, email, Notion). The fix is a managed source of truth that injects variables at runtime instead of living in files: ```bash envpilot run -- next dev # variables injected into the process, no .env.local on disk ``` With [Envpilot](/) the values are AES-256 encrypted, access is [role-scoped](/platform/rbac), every read is audited, and a rotated key reaches the whole team on their next run. See [How to Share Environment Variables Securely](/guides/migrate-from-dotenv) for the full migration checklist. ## Quick checklist 1. `.env*.local` in `.gitignore`; only non-secret defaults committed. 2. `NEXT_PUBLIC_` only on values you'd happily print on a billboard. 3. Secrets read in server code only; `server-only` package on secret-bearing modules. 4. Remember `NEXT_PUBLIC_` is baked at build time — rebuild to change it. 5. Validate `process.env` with a schema at boot. 6. Distribute team secrets through a secrets manager with runtime injection, not files in chat. ## Limits - `NEXT_PUBLIC_` values are compiled into the browser bundle. Nothing Envpilot does makes them private again. - Build-time values are frozen into the build; changing one in Envpilot needs a rebuild, not just a redeploy. - `envpilot run` injects into the process, so anything reading `process.env` at request time picks changes up on restart. ======================================================================== ANDROID KEYSTORE IN CI ======================================================================== Source: https://docs.envpilot.dev/guides/android-keystore-ci # Android keystore in CI Every Android team has the same artefact: an upload keystore that must reach the release build and must never reach the repository. The usual workarounds — base64 in a CI secret, a file in a private bucket, a Slack message from three years ago — all lose the same thing: who has it, and who last used it. This is the walkthrough for storing it as a [secret file](/platform/secret-files) instead. ## What you need - A project in Envpilot, with the keystore's environment (`production`) available - The CLI installed and linked: [`envpilot init`](/cli/link) - An API key for CI with the **GitHub Action** surface and the **`files`** resource ## Store the keystore ## Wire up Gradle Read the passwords from the environment rather than a checked-in `gradle.properties`: ```groovy android { signingConfigs { release { storeFile file("upload.jks") storePassword System.getenv("KEYSTORE_PASSWORD") keyAlias System.getenv("KEY_ALIAS") keyPassword System.getenv("KEY_PASSWORD") } } } ``` ## Local builds ```bash envpilot files pull -e production # writes android/app/upload.jks, mode 0600 envpilot run --env production -- ./gradlew bundleRelease ``` `files pull` gitignores the path before writing it, and refuses to overwrite a local copy that differs from the server unless you pass `--force`. ## The CI workflow ```yaml name: Release on: push: tags: ["v*"] jobs: bundle: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: rafay99-epic/envpilot-action@v1 with: token: ${{ secrets.ENVPILOT_TOKEN }} environment: production project: mobile-app files: true - uses: actions/setup-java@v4 with: distribution: temurin java-version: "17" - run: ./gradlew bundleRelease ``` One step brings both halves: the passwords are exported to `$GITHUB_ENV` and masked in the log, and the keystore is written to `android/app/upload.jks` with mode `0600`. `project` is required whenever `files: true` — the files endpoint is project-scoped. ## What you gained | Before | After | | ---------------------------------------- | ------------------------------------------------------- | | Keystore in a base64 CI secret | Encrypted at rest, envelope-encrypted across two stores | | No record of who downloaded it | Every fetch audited against a person or an API key | | Rotating means editing every repo secret | Replace contents once; path and grants stay put | | New hire needs someone to send it | `envpilot files pull` | ## Rotating the keystore Use **Replace contents** in Project → Files: the bytes change, the path, environments and per-file grants stay. Every client picks up the new file on its next pull, and the old one is unrecoverable. ## Limits - 8 MB per file on Pro, 256 KB on Free. A keystore is a few KB; a provisioning bundle might not be. - Every content fetch is audited and rate-limited — a workflow that pulls on every push produces an entry per run. - The Action overwrites the destination path without a conflict check. That is right for a runner, wrong for a laptop, which is why the CLI behaves differently. - The `files` resource is never granted by default. A key that does not carry it is refused, not partially served. ## See also - [Secret files](/platform/secret-files) · [CLI files](/cli/files) · [Action: secret files](/action/secret-files) ======================================================================== GIVING AN AGENT SECRETS SAFELY ======================================================================== Source: https://docs.envpilot.dev/guides/agents # Giving an agent secrets safely A coding agent that cannot see your environment variables writes code that guesses at them. An agent with your `.env` on disk is a credential you cannot revoke. The middle path is a scoped, audited, read-only key — plus a way for the agent to ask when it needs more. ## The shape of it ```mermaid flowchart LR A[Agent] -->|read, scoped| M[MCP server] A -->|ask| M M --> C[One enforcement core] C --> V[(Vault)] R[Human reviewer] -->|approve + supply value| C ``` The agent reads within its scope and files requests outside it. Every value it reads is logged against its key, and it can never write. ## Set it up ## What a good session looks like 1. The agent calls `envpilot_list_projects` to find the project. 2. It calls `envpilot_get_variables` with `metadata_only` to see which keys exist — no decrypt, no audit entry, no values in its context window. 3. It reads only the specific values it needs, by key. 4. Missing something? `envpilot_request_variable` with a justification. You approve in the dashboard or with `envpilot requests approve ` and type the value yourself. Step 2 is the habit worth encouraging in your prompt: **look at key names first, fetch values last**. Most code an agent writes needs to know a variable exists, not what it says. ## Guardrails you get for free | Risk | What stops it | | ---------------------------------- | ------------------------------------------------------------------- | | Agent edits a production secret | No machine credential can write. There is no such tool | | Agent invents a value and files it | Requests carry no value; the reviewer supplies it | | Agent loops and spams reviewers | 5 requests/hour, 5 open at a time, 24-hour cooldown after rejection | | Key leaks into a log | Revoke it; the next call fails, with no grace period | | You need to know what it read | Every value read is audited against the key | ## Guardrails you must add yourself - **Scope narrowly.** "All projects" includes projects that do not exist yet. - **Prefer development.** An agent rarely needs production to write code. - **Set an expiry.** An experiment from March should not still hold a key in November. - **Do not grant `files` casually.** A leaked string rotates in minutes; a leaked signing key does not. - **Watch the context window.** Once a value is in a transcript, it lives wherever that transcript lives. ## Limits - Pro plan (`mcp_server`), re-checked on every call. - Scope is immutable — widening access means a new key and a client restart. - Hosted Claude connectors expect OAuth and cannot carry a fixed bearer key; use Claude Code. - Tool calls draw on the same rate buckets as REST for that key. ## See also - [MCP overview](/mcp/overview) · [Tools](/mcp/tools) · [Agent requests](/mcp/agent-requests) - [Architecture](/start/architecture) — the trust model in full