Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/trycua/cua/llms.txt

Use this file to discover all available pages before exploring further.

Secrets — API keys, database passwords, SSH private keys, tokens — must never be stored in an Image spec or committed to source control. The correct approach is to read them from the host environment at runtime and inject them into the sandbox only when needed. This page explains the injection patterns, what to avoid, and best practices for both cloud and local workflows.

The golden rule

Never pass secrets to Image.env(). Those values are stored in the image spec in plaintext and visible to anyone who can inspect the spec. Always inject secrets at runtime after the sandbox has started.

Inject secrets at runtime via shell

Read credentials from the host environment with os.environ, then pass them into the sandbox using sb.shell.run. The sandbox’s shell session holds the values only for as long as the sandbox is running — they are not written to disk or stored in any persistent spec.
import os
import asyncio
from cua import Sandbox, Image

async def main():
    db_url    = os.environ["DATABASE_URL"]
    gh_token  = os.environ["GITHUB_TOKEN"]

    async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
        await sb.shell.run(f"export DATABASE_URL='{db_url}'")
        await sb.shell.run(f"export GITHUB_TOKEN='{gh_token}'")
        await sb.shell.run("python /app/main.py")

asyncio.run(main())
Each sb.shell.run call is a fresh shell invocation. Environment variables set with export in one call are not available in the next call. Use a multi-command script (below) when you need them to persist across commands.

Multi-command script with persistent env vars

Combine all commands into a single shell script so the exported variables remain in scope for every step:
async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
    script = f"""
export DATABASE_URL='{db_url}'
export GITHUB_TOKEN='{gh_token}'
cd /app
python migrate.py
python main.py
"""
    result = await sb.shell.run(script)
    print(result.stdout)

Non-sensitive config with Image.env()

Use Image.env() only for values that are safe to store in plaintext — log levels, feature flags, port numbers, environment names. Never use it for credentials.
img = (
    Image.linux()
    .apt_install("python3")
    .env(
        LOG_LEVEL="info",
        APP_ENV="production",
        PORT="8080",
    )
)

Copy secret files into the sandbox at runtime

SSH private keys, TLS certificates, and similar secret files should be copied into the sandbox after it starts, not baked in with Image.copy(). Read the file contents on the host, write them inside the sandbox with appropriate permissions.
import os
import asyncio
from cua import Sandbox, Image

async def main():
    with open(os.path.expanduser("~/.ssh/id_rsa")) as f:
        key_content = f.read()

    async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
        await sb.shell.run("mkdir -p /root/.ssh && chmod 700 /root/.ssh")
        await sb.shell.run(
            f"cat > /root/.ssh/id_rsa << 'EOF'\n{key_content}\nEOF"
        )
        await sb.shell.run("chmod 600 /root/.ssh/id_rsa")
        await sb.shell.run("ssh -o StrictHostKeyChecking=no user@target.example.com 'echo ok'")

asyncio.run(main())
Non-secret files — app configs, static assets, seed data — are safe to bake in with Image.copy(). Reserve runtime injection for anything that grants access to external systems.

Using a .env file locally

For local development, keep secrets in a .env file (never committed to version control) and load them with python-dotenv before starting any sandboxes.
from dotenv import load_dotenv
import os, asyncio
from cua import Sandbox, Image

load_dotenv()   # reads .env into os.environ

async def main():
    api_key = os.environ["MY_API_KEY"]
    async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
        await sb.shell.run(f"export MY_API_KEY='{api_key}'")
        await sb.shell.run("python /app/main.py")

asyncio.run(main())
Add .env to .gitignore immediately:
echo ".env" >> .gitignore

Cloud vs local secret management

When running cloud sandboxes, store secrets in your CI/CD provider’s secret store (GitHub Actions secrets, GitLab CI variables, etc.) or a secrets manager (AWS Secrets Manager, HashiCorp Vault). Pull them into your runner’s environment and inject at runtime.
import os, asyncio
from cua import Sandbox, Image

async def main():
    # Secrets are injected by GitHub Actions as environment variables
    db_url   = os.environ["DATABASE_URL"]
    api_key  = os.environ["CUA_API_KEY"]

    async with Sandbox.ephemeral(
        Image.linux(),
        api_key=api_key,
        region="us-east-1",
    ) as sb:
        await sb.shell.run(f"export DATABASE_URL='{db_url}'")
        await sb.shell.run("python /app/run.py")

asyncio.run(main())

Best practices summary

✅ Do❌ Don’t
Read secrets from host os.environHardcode secrets in Python source
Inject at runtime via sb.shell.runStore secrets in Image.env()
Use .env + python-dotenv locallyCommit .env files to version control
Use your CI/CD provider’s secret storePass secrets as command-line arguments
Set strict file permissions on key filesLog or print secret values
Use Image.copy() only for non-secret filesBake credentials into image layers

Build docs developers (and LLMs) love