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

# Environment variables in Celesto sandboxes

> Inject API keys, secrets, and config into Celesto sandboxes at boot or at runtime with one Python API that works on Linux and Windows guests.

You can pass configuration values, API keys, and other settings into a sandbox using environment variables. Celesto supports two approaches: setting variables when you create the sandbox, or adding them at runtime while the sandbox is running.

The same Python API (`env_vars=`, `vm.set_env_vars(...)`, `vm.unset_env_vars(...)`, `vm.list_env_vars(...)`) works for both Linux and Windows guests. On Linux the variables are written to `/etc/profile.d/smolvm_env.sh` and picked up by new login shells; on Windows they're written to `HKCU\Environment` and picked up by new processes. See [Windows guests](#windows-guests) below for the details specific to Windows.

## Setting variables at boot

Set environment variables when creating a VM:

```python theme={null} theme={null}
from celesto import Celesto, VMConfig
from celesto import ImageBuilder, SSH_BOOT_ARGS

builder = ImageBuilder()
kernel, rootfs = builder.build_alpine_ssh()

config = VMConfig(
    vm_id="env-vm",
    vcpu_count=1,
    memory=512,
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
    env_vars={  # Variables injected after boot
        "APP_MODE": "production",
        "DATABASE_URL": "postgres://localhost:5432/mydb",
        "API_KEY": "secret-key-value",
    },
)

with Celesto(config) as vm:
    # Variables are automatically injected during start()
    result = vm.run("echo $APP_MODE")
    print(result.output)  # "production"
```

### How it works

When `env_vars` is set in `VMConfig`:

1. VM boots normally
2. Celesto waits for SSH to become available
3. Variables are written to `/etc/profile.d/smolvm_env.sh`
4. All subsequent login shells source these variables

<Note>
  Environment variable injection requires an SSH-capable image. Use `ImageBuilder` or auto-config mode to ensure your image supports this feature.
</Note>

## Setting variables at runtime

You can also add, update, and remove variables while a sandbox is running:

```python theme={null} theme={null}
from celesto import Celesto

with Celesto() as vm:
    # Set environment variables
    vm.set_env_vars({"APP_MODE": "dev", "DEBUG": "1"})

    # Use them in commands
    result = vm.run("echo APP_MODE=$APP_MODE DEBUG=$DEBUG")
    print(result.output)  # "APP_MODE=dev DEBUG=1"

    # List current variables
    env_vars = vm.list_env_vars()
    print(env_vars)  # {"APP_MODE": "dev", "DEBUG": "1"}

    # Remove a variable
    removed = vm.unset_env_vars(["DEBUG"])
    print(removed)  # {"DEBUG": "1"}

    # Verify it's gone
    result = vm.run("echo DEBUG=$DEBUG")
    print(result.output)  # "DEBUG="
```

## Windows sandboxes

Environment variables work for Windows sandboxes too, using the same API. The only difference is where the values are stored.

```python theme={null}
from celesto import Celesto

with Celesto(
    os="windows",
    image="~/.smolvm/images/win11.qcow2",
    ssh_user="smolvm",
    ssh_password="smolvm",
    env_vars={"OPENAI_API_KEY": "sk-..."},
) as vm:
    vm.wait_for_ssh()

    # Fresh SSH session, so the new value is visible.
    print(vm.run("$env:OPENAI_API_KEY").stdout.strip())

    vm.set_env_vars({"DEBUG": "1"})
    print(vm.list_env_vars())
    vm.unset_env_vars(["DEBUG"])
```

How it works on Windows:

* Celesto runs `[Environment]::SetEnvironmentVariable(name, value, 'User')` over SSH, which writes the value into the `HKCU\Environment` registry hive — the standard per-user environment store on Windows.
* New processes inherit the updated environment automatically. The SSH session that issued the change does **not** — `vm.run(...)` opens a fresh session every call, so the next `vm.run(...)` sees the new value.
* Celesto tracks which keys it owns via a `SMOLVM_ENV_MANAGED_KEYS` sentinel value. `list_env_vars()` and `unset_env_vars()` only ever touch variables Celesto set; anything you configured inside Windows yourself is left untouched.
* Values are passed through verbatim — spaces, embedded quotes, and special characters are escaped safely by Celesto before the PowerShell call.

<Note>
  Variable changes are visible to new processes, not the current one. Inside a single SSH session you can verify the change by spawning a fresh process — for example `start-process powershell -wait`, or simply rely on the fact that the next `vm.run(...)` call will see the new value.
</Note>

## Method reference

### set\_env\_vars()

```python theme={null} theme={null}
def set_env_vars(
    self,
    env_vars: dict[str, str],
    *,
    merge: bool = True,
) -> list[str]:
    """Set environment variables on a running VM.

    Variables are persisted in /etc/profile.d/smolvm_env.sh and
    affect new SSH sessions/login shells.

    Args:
        env_vars: Key/value pairs to set.
        merge: If True (default), merge with existing variables.

    Returns:
        Sorted variable names present after update.
    """
```

**Example:**

```python theme={null} theme={null}
# Merge with existing variables (default)
vm.set_env_vars({"NEW_VAR": "value"})

# Replace all variables
vm.set_env_vars({"ONLY_VAR": "value"}, merge=False)
```

### list\_env\_vars()

```python theme={null} theme={null}
def list_env_vars(self) -> dict[str, str]:
    """Return Celesto-managed environment variables for a running VM."""
```

**Example:**

```python theme={null} theme={null}
env_vars = vm.list_env_vars()
for key, value in env_vars.items():
    print(f"{key}={value}")
```

### unset\_env\_vars()

```python theme={null} theme={null}
def unset_env_vars(self, keys: list[str]) -> dict[str, str]:
    """Remove environment variables from a running VM.

    Args:
        keys: Variable names to remove.

    Returns:
        Mapping of removed keys to their previous values.
    """
```

**Example:**

```python theme={null} theme={null}
# Remove multiple variables
removed = vm.unset_env_vars(["VAR1", "VAR2"])
print(f"Removed: {removed}")
```

## Complete example

From `examples/env_injection.py`:

```python theme={null} theme={null}
from celesto import Celesto

def main() -> int:
    with Celesto() as vm:
        print(f"VM started: {vm.vm_id}")

        print("\n1) Set environment variables")
        vm.set_env_vars({"APP_MODE": "dev", "DEBUG": "1"})
        print(vm.list_env_vars())

        print("\n2) Use env vars in a command")
        # vm.run() opens a fresh SSH session, so new values are available
        print(vm.run("echo APP_MODE=$APP_MODE DEBUG=$DEBUG").output)

        print("\n3) Remove one variable")
        removed = vm.unset_env_vars(["DEBUG"])
        print(f"Removed: {removed}")
        print(vm.list_env_vars())

    print("\nDone.")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
```

## Passing host environment into the sandbox

You can forward environment variables from your host machine into the sandbox. This is useful for sharing API keys without hardcoding them:

```python theme={null} theme={null}
import os
from celesto import Celesto, VMConfig
from celesto import ImageBuilder, SSH_BOOT_ARGS

def collect_host_env() -> dict[str, str]:
    """Collect API keys from host environment."""
    env_vars = {}

    if api_key := os.getenv("OPENAI_API_KEY"):
        env_vars["OPENAI_API_KEY"] = api_key

    if api_key := os.getenv("ANTHROPIC_API_KEY"):
        env_vars["ANTHROPIC_API_KEY"] = api_key

    return env_vars

builder = ImageBuilder()
kernel, rootfs = builder.build_alpine_ssh()

config = VMConfig(
    vm_id="host-env-vm",
    vcpu_count=1,
    memory=512,
    kernel_path=kernel,
    rootfs_path=rootfs,
    boot_args=SSH_BOOT_ARGS,
    env_vars=collect_host_env(),  # Inject host environment
)

with Celesto(config) as vm:
    result = vm.run("env | grep API_KEY")
    print(result.output)
```

This pattern is used in `examples/openclaw.py` to inject `OPENROUTER_API_KEY` and `OPENAI_API_KEY` from the host.

## Host-side variables Celesto reads

A few environment variables on your host machine change how Celesto itself behaves. Set these in your shell, not inside the sandbox.

### Celesto behavior

<ResponseField name="SMOLVM_BACKEND" type="string">
  Override the virtual machine monitor Celesto uses. Accepts `firecracker`, `qemu`, or `auto` (the default). On macOS the default is QEMU; on Linux it is Firecracker.
</ResponseField>

<ResponseField name="SMOLVM_FIRECRACKER_DIR" type="string">
  Folder containing the Firecracker binary on Linux. When set, Celesto installs to and looks for Firecracker only in this folder. When unset, `celesto setup` installs to `~/.smolvm/bin`, and Celesto discovery checks `PATH` first, then `~/.smolvm/bin`. Set this when you install Firecracker to a custom folder that is not on `PATH`:

  ```bash theme={null} theme={null}
  export SMOLVM_FIRECRACKER_DIR="$HOME/.local/bin"
  celesto setup
  ```

  The one-off equivalent is `celesto setup --firecracker-dir <dir>`, which overrides this variable for that run.
</ResponseField>

<ResponseField name="SMOLVM_USE_PUBLISHED" type="string" default="1">
  Controls the [published-image fast path](/smolvm/concepts/published-images). On by default. Set to `0`, `false`, or `no` to force Celesto to build images locally instead of downloading pre-built ones. Useful when you're testing changes to a preset's install script.
</ResponseField>

<ResponseField name="SMOLVM_VERBOSE_BOOT" type="string">
  Show the guest kernel's full boot log on the serial console. Off by default — Celesto appends `quiet` to the kernel command line so boots stay fast and clean. Set to `1`, `true`, `yes`, or `on` to drop `quiet` and surface every kernel message, which is the first thing to try when a sandbox hangs during start or panics before the agent answers.

  ```bash theme={null} theme={null}
  SMOLVM_VERBOSE_BOOT=1 celesto sandbox create --name verbose-boot
  ```

  This only affects sandboxes that use the default low-latency boot profile (`MICROVM_DIRECT`). It does not change behaviour after the guest is up.
</ResponseField>

### Coding-agent presets

When you launch a coding-agent preset, Celesto forwards a small list of host environment variables into the sandbox so the agent can authenticate without an extra login step.

<ResponseField name="OPENAI_API_KEY" type="string">
  Forwarded to presets that talk to OpenAI APIs, including `codex`, `opencode`, and `openclaw`.
</ResponseField>

<ResponseField name="OPENROUTER_API_KEY" type="string">
  Forwarded to the `openclaw` and `opencode` presets and any preset that supports OpenRouter.
</ResponseField>

<ResponseField name="ANTHROPIC_API_KEY" type="string">
  Forwarded to the `opencode` preset for Anthropic models.
</ResponseField>

<ResponseField name="GOOGLE_API_KEY" type="string">
  Forwarded to the `opencode` preset for Google models. `GEMINI_API_KEY` is forwarded as well.
</ResponseField>

<ResponseField name="AWS_ACCESS_KEY_ID" type="string">
  Forwarded to the `opencode` preset for Amazon Bedrock. `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, and `AWS_REGION` are forwarded together with it.
</ResponseField>

<ResponseField name="OPENCLAW_GATEWAY_TOKEN" type="string">
  Forwarded to the `openclaw` preset. OpenClaw rejects boot with a clear error message if neither this nor `OPENCLAW_GATEWAY_PASSWORD` is set, so populate one of the two before running `celesto openclaw start`.
</ResponseField>

<ResponseField name="OPENCLAW_GATEWAY_PASSWORD" type="string">
  Alternative to `OPENCLAW_GATEWAY_TOKEN` for the `openclaw` preset. Use whichever credential type matches your OpenClaw deployment.
</ResponseField>

<Tip>
  Set these variables in the shell that runs `celesto openclaw start`. Celesto forwards the selected values into that sandbox, but it does not copy `~/.openclaw/openclaw.json`, `~/.openclaw/.env`, or other OpenClaw state from your machine.
</Tip>

## Variable validation

Celesto validates environment variable keys:

```python theme={null} theme={null}
from celesto.env import validate_env_key

# Valid keys
validate_env_key("MY_VAR")      # OK
validate_env_key("_PRIVATE")    # OK
validate_env_key("VAR_123")     # OK

# Invalid keys
validate_env_key("")            # ValueError: cannot be empty
validate_env_key("123VAR")      # ValueError: must start with letter or _
validate_env_key("MY-VAR")      # ValueError: only [A-Za-z0-9_] allowed
```

Keys must match the pattern: `[A-Za-z_][A-Za-z0-9_]*`

## Persistence details

### File location

Variables are stored in `/etc/profile.d/smolvm_env.sh` inside the guest:

```bash theme={null} theme={null}
# Inside the VM
$ cat /etc/profile.d/smolvm_env.sh
#!/bin/sh
# Celesto managed environment variables

export APP_MODE='production'
export DATABASE_URL='postgres://localhost:5432/mydb'
```

### Atomic updates

All writes are atomic (write to temp file → `mv` into place) to prevent partial updates on failure.

### Quoting and escaping

Celesto uses `shlex.quote()` to safely handle special characters:

```python theme={null} theme={null}
vm.set_env_vars({
    "MESSAGE": "Hello, World!",
    "PATH_VAR": "/usr/local/bin:/usr/bin",
    "COMPLEX": "value with 'quotes' and spaces",
})

# All values are safely quoted in the generated shell script
```

## Use cases

### Configuration management

```python theme={null} theme={null}
config = VMConfig(
    vm_id="app-vm",
    # ... other config ...
    env_vars={
        "DATABASE_URL": "postgres://db:5432/app",
        "REDIS_URL": "redis://cache:6379",
        "LOG_LEVEL": "info",
    },
)
```

### Secret injection

```python theme={null} theme={null}
import os

with Celesto() as vm:
    # Inject secrets from host environment or secrets manager
    vm.set_env_vars({
        "API_KEY": os.getenv("API_KEY", "default-key"),
        "DB_PASSWORD": get_secret("db-password"),
    })

    vm.run("my-application")
```

### Dynamic configuration updates

```python theme={null} theme={null}
with Celesto() as vm:
    # Start in dev mode
    vm.set_env_vars({"APP_MODE": "dev"})
    vm.run("start-app")

    # Switch to production mode
    vm.set_env_vars({"APP_MODE": "production"})
    vm.run("restart-app")
```

## Troubleshooting

### Variables not available

If variables don't appear in your commands:

<Steps>
  <Step title="Verify SSH support">
    ```python theme={null} theme={null}
    if not vm.can_run_commands():
        print("VM does not support SSH - cannot inject variables")
    ```
  </Step>

  <Step title="Check the file was created">
    ```python theme={null} theme={null}
    result = vm.run("cat /etc/profile.d/smolvm_env.sh")
    print(result.output)
    ```
  </Step>

  <Step title="Ensure you're using a login shell">
    Variables are only available in login shells:

    ```python theme={null} theme={null}
    # This works - uses login shell (default)
    vm.run("echo $MY_VAR", shell="login")

    # This may not work - raw execution
    vm.run("echo $MY_VAR", shell="raw")
    ```
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Custom images" icon="box" href="/smolvm/guides/custom-images">
    Build images with pre-baked environment configuration
  </Card>

  <Card title="AI agent integration" icon="robot" href="/smolvm/guides/ai-agent-integration">
    Use environment variables for agent configuration
  </Card>

  <Card title="Port forwarding" icon="network-wired" href="/smolvm/features/port-forwarding">
    Expose services configured via environment variables
  </Card>
</CardGroup>


## Related topics

- [Windows sandboxes in Celesto](/smolvm/guides/windows-guests.md)
- [celesto sandbox env](/smolvm/cli/env.md)
- [Sandbox an OpenAI agent with Celesto or SmolVM](/cloud/openai-agents.md)
- [VM lifecycle management](/smolvm/guides/vm-lifecycle.md)
- [celesto ui](/smolvm/cli/ui.md)
