> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Environments and secrets

> Named sets of variables and secrets that sandboxes read. Secret values stay on the host and never enter the VM.

An environment is a named set of **variables** and **secrets** in your workspace. Each sandbox reads one environment. Every workspace has an environment called `default`, which cannot be deleted.

* A **variable** is a plain value. Commands in the sandbox see it, and members see it in the console and API.
* A **secret** is write-only. No API route returns its value, and the sandbox never sees it. Commands see a placeholder, and the host swaps in the real value on HTTPS requests to the hosts you allow.

You manage environments in the console (**Environment**), the API and the TypeScript SDK. There is no `zode env` command in the CLI.

## Set up an environment

```ts TypeScript theme={null}
import { Zode, Sandbox } from "@zodedev/sdk";

const zode = new Zode();
await zode.environments.create("staging");
await zode.environments.setVariable("staging", "APP_ENV", "staging");
await zode.environments.setSecret("staging", "STRIPE_KEY", {
  value: process.env.STRIPE_KEY!,
  allowedHosts: ["api.stripe.com"],
});

const sbx = await Sandbox.create({ environment: "staging" });
```

```bash curl theme={null}
API=https://api.zode.dev
AUTH="Authorization: Bearer $ZODE_API_KEY"
JSON="Content-Type: application/json"

ENV=$(curl -sS -X POST $API/v1/environments -H "$AUTH" -H "$JSON" \
  -d '{"name":"staging"}' | jq -r .id)

curl -sS -X PUT $API/v1/environments/$ENV/variables/APP_ENV -H "$AUTH" -H "$JSON" \
  -d '{"value":"staging"}'

curl -sS -X PUT $API/v1/environments/$ENV/secrets/STRIPE_KEY -H "$AUTH" -H "$JSON" \
  -d "{\"value\":\"$STRIPE_KEY\",\"allowed_hosts\":[\"api.stripe.com\"]}"

curl -sS -X POST "$API/v1/sandboxes?environment=staging" -H "$AUTH" -H "$JSON" \
  -d '{"vcpus":2}'
```

The SDK takes an environment's name or `env_` id. The API's environment routes take the `env_` id; `?environment=` on create takes either.

Owners and admins can create and delete environments and set variables and secrets. Members can read variables and secret names.

## Which environment a sandbox reads

You choose it when you create the sandbox: `?environment=<name or id>` on `POST /v1/sandboxes`, or the `environment` option of `Sandbox.create`. Leave it out and the sandbox reads `default`. An unknown environment returns 400 `unknown_environment` and nothing is created.

* `zode new` has no environment flag, so sandboxes made with the CLI read `default`.
* A fork reads its parent's environment.
* You cannot move a sandbox to another environment after it is made.
* Deleting an environment unlinks its sandboxes; their next commands get nothing from it.

## When values are read

A sandbox reads its environment at the start of every exec or attach session: each `zode exec`, `zode ssh`, SDK `exec()` and API exec call. A change applies to the next command. Processes that are already running keep what they started with.

Each secret's placeholder stays the same when you change its value, and the host gets the new value within about 30 seconds. Rotating a secret does not need a restart.

## Per-command variables

You can also pass variables to one command. They win over the environment's variables and secrets when names clash.

```bash CLI theme={null}
zode exec -e DEBUG=1 -e AWS_REGION demo -- npm test   # AWS_REGION alone passes your local value
zode ssh --env-file .env demo
```

```ts TypeScript theme={null}
await sbx.exec("npm test", { env: { DEBUG: "1" } });
```

```bash curl theme={null}
curl -sS -X POST https://api.zode.dev/v1/sandboxes/$ID/exec \
  -H "Authorization: Bearer $ZODE_API_KEY" -H "Content-Type: application/json" \
  -d '{"command":"npm test","env":["DEBUG=1"]}'
```

`--env-file` reads `KEY=VALUE` lines, skips blank lines and `#` comments, drops a leading `export` and strips quotes around values. Flags go before the sandbox name. Over attach, pass variables in the `X-Zode-Env` header; see [Attach](https://docs.zode.dev/api-reference/attach.md).

## How secrets work

1. You store a secret with a value and `allowed_hosts`, for example `api.stripe.com` or `*.example.com` (any subdomain, not the apex).
2. Commands in the sandbox see `STRIPE_KEY=zode_vault_...`, a placeholder that is the same in every session.
3. Your code uses it as usual, for example `Authorization: Bearer $STRIPE_KEY`.
4. TCP traffic from the sandbox to ports 443 and 80 leaves through an egress proxy on the host. For hosts on a secret's allowlist, the proxy decrypts the request, finds the placeholder in the URL, headers or body, and swaps in the real value.

To make step 4 work, the sandbox trusts a certificate authority for your workspace, and sessions set `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` so Node, Python and curl trust it too.

What happens to a request carrying the placeholder:

| Request | Result |
| - | - |
| HTTPS to an allowed host | The real value is sent. |
| HTTPS to a host no secret allows | Passed through unread. The placeholder goes out unchanged and is worthless to the receiver. |
| HTTPS to a host another secret allows, or to an AI provider the proxy meters | Blocked with 403 `secret_not_allowed_for_host`. |
| The placeholder base64-, hex- or URL-encoded, to an inspected host | Blocked with 403 `encoded_secret`. |
| Plain HTTP on port 80, any host | Blocked with 403 `secret_over_plain_http`. The real value never goes out unencrypted. |

### What this protects against

The real value is never in the VM: not in the environment, files, memory, shell history, logs, snapshots or forks. An agent that prints its environment, a dependency that uploads `env` to its own server, or a prompt injection that asks for your keys can only leak the placeholder.

### What it does not protect against

* **Use of the secret.** Code in the sandbox can still call the allowed hosts with your key, and do anything the key permits. Give sandboxes keys with the narrowest scope your provider offers.
* **Hosts that echo requests.** If an allowed host returns request headers or bodies in its responses, the real value comes back into the sandbox. Allow only the API hosts that need the key.
* **Variables.** Variables are plain values inside the sandbox. Put anything sensitive in a secret.

### Limits

* Only bearer tokens and header keys for HTTPS APIs. Request-signed credentials and connection strings are refused with 400 `secret_unsupported`: names starting with `AWS_SECRET_ACCESS_KEY` or `AWS_SESSION_TOKEN`, and values starting with `postgres://`, `postgresql://`, `mysql://`, `mongodb://`, `mongodb+srv://`, `redis://` or `-----BEGIN`. Use short-lived credentials for those.
* Swapping works on port 443 only. Other ports are not proxied, so a placeholder sent to them goes out unchanged.
* Clients that pin certificates fail against allowed hosts, because the proxy presents its own certificate.
* Requests to allowed hosts use HTTP/1.1, and request bodies over 32 MiB are refused with 413.
* No secrets reach [custom image](https://docs.zode.dev/concepts/images.md) builds.

## Limits

| | Limit |
| - | - |
| Environments per workspace | 20 |
| Environment names | `a-z`, `0-9` and `-`, 1 to 63 characters |
| Variables per environment | 100, with 32 KB of values in total |
| Variable names | `^[A-Za-z_][A-Za-z0-9_]{0,127}$`, not starting with `ZODE_` |
| Variable value | 4096 bytes, UTF-8, no NUL |
| Secrets per environment | 50 |
| Secret value | 1 byte to 16 KB |
| Allowed hosts per secret | 1 to 20, host names without scheme or port |
| Per session, all variables together | 200 entries and 64 KB |

A variable and a secret in the same environment cannot share a name (409 `name_taken`).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.