> ## 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 SmolVM sandboxes

> Inject API keys, secrets, and config into SmolVM 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. SmolVM 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 smolvm import SmolVM, VMConfig
from smolvm.build 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 SmolVM(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. SmolVM 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 smolvm import SmolVM

with SmolVM() 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 smolvm import SmolVM

with SmolVM(
    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:

* SmolVM 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.
* SmolVM tracks which keys it owns via a `SMOLVM_ENV_MANAGED_KEYS` sentinel value. `list_env_vars()` and `unset_env_vars()` only ever touch variables SmolVM 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 SmolVM 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 SmolVM-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 smolvm import SmolVM

def main() -> int:
    with SmolVM() 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 smolvm import SmolVM, VMConfig
from smolvm.build 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 SmolVM(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 SmolVM reads

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

### SmolVM behavior

<ResponseField name="SMOLVM_BACKEND" type="string">
  Override the virtual machine monitor SmolVM uses. Accepts `firecracker`, `qemu`, or `auto` (the default). On macOS the default is QEMU; on Linux it is Firecracker.
</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 SmolVM 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 — SmolVM 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 smolvm run "echo hello"
  ```

  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, SmolVM 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 (for example `codex`).
</ResponseField>

<ResponseField name="OPENROUTER_API_KEY" type="string">
  Forwarded to the `openclaw` preset and any preset that supports OpenRouter.
</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 `smolvm 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 in your shell profile (`~/.zshrc`, `~/.bashrc`) so every `smolvm openclaw start` picks them up automatically. They never get baked into a published image — they only travel into a sandbox you boot.
</Tip>

## Variable validation

SmolVM validates environment variable keys:

```python theme={null} theme={null}
from smolvm.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
# SmolVM 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

SmolVM 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 SmolVM() 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 SmolVM() 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>
