# How to use API keys in a sandbox without exposing them Store the key with `runtime secrets set NAME --host api.example.com`; sandboxes see a placeholder and Runtime adds the value in transit. **On Runtime the sandbox never holds the key: not in its environment, its memory or its disk, so a prompt injection or a stolen sandbox has nothing to leak.** Each secret names the hosts it may go to. The proxy on the host puts the value into HTTPS requests to those hosts and nowhere else, and code that sends the placeholder anywhere else sends something worthless. One stored secret serves every sandbox of the account, with up to 50 secrets per account. ## Store a secret ```bash no-run printf %s "$OPENAI_API_KEY" | runtime secrets set OPENAI_API_KEY --host api.openai.com runtime secrets ls ``` The CLI reads the value from standard input only, so it never lands in your shell history, and typed at a terminal it is not shown. `ls` shows names, hosts and placeholders, never values. ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.secrets.set("OPENAI_API_KEY", { value: process.env.OPENAI_API_KEY ?? "", hosts: ["api.openai.com"], }); ``` ```python check import os from withruntime import Runtime runtime = Runtime() runtime.secrets.set("OPENAI_API_KEY", value=os.environ.get("OPENAI_API_KEY", ""), hosts=["api.openai.com"]) ``` An agent connected through MCP uses `runtime_secrets_set`, `runtime_secrets_list` and `runtime_secrets_delete`. ## Use it from code in the sandbox Nothing changes in the code. Every sandbox has an environment variable of the secret's name, holding a placeholder such as `rtsec_3f9c…`, and an SDK that reads `OPENAI_API_KEY` sends it as usual: ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); const call = await sbx.exec( 'curl -sS https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"', { timeoutMs: 30_000 }, ); console.log(call.exitCode); const seen = await sbx.exec("printenv OPENAI_API_KEY"); console.log(seen.stdout); // rtsec_…, the placeholder, never the key ``` ```python check from withruntime import Sandbox with Sandbox.create() as sbx: call = sbx.exec('curl -sS https://api.openai.com/v1/models ' '-H "Authorization: Bearer $OPENAI_API_KEY"', timeout_ms=30_000) print(call.exit_code) print(sbx.exec("printenv OPENAI_API_KEY").stdout) # the placeholder ``` ## Set the header for the code With `header`, the proxy sets that header on every HTTPS request to the hosts, replacing one the sandbox sent, so code needs no placeholder at all. `format` says where the value goes: ```bash no-run runtime secrets set GITHUB_TOKEN --host api.github.com --header Authorization --format 'token {value}' < token.txt ``` ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.secrets.set("GITHUB_TOKEN", { value: process.env.GITHUB_TOKEN ?? "", hosts: ["api.github.com", "*.githubusercontent.com"], header: "Authorization", format: "token {value}", }); ``` A secret with header `Authorization` and format `AWS4-HMAC-SHA256 {value}` holds a bucket's `ACCESS_KEY_ID:SECRET_ACCESS_KEY` and signs S3 requests again with the real key, so the secret key is never sent at all ([mount your own bucket](/docs/storage#mount-your-own-bucket)). ## Limits | What | Limit | | --------------------- | -------------------------------------------------------------- | | Secrets per account | 50 | | Value | At most 8 KiB of visible ASCII | | Hosts per secret | 16, as names or `*.domain` | | Where it is added | URL and headers of HTTPS requests to the secret's hosts | | Never added | Plain HTTP, request bodies, any other host | | Protocol to its hosts | HTTP/1.1, WebSockets included; gRPC works to every other host | | Replacing a value | Keeps the placeholder; running sandboxes use it within seconds | ## Mistakes and how Runtime handles them - **Passing the key in `env` or the command line.** `env` values are never echoed back and journals keep only a hash, but the sandbox then holds the key. A command line is recorded as sent. Use a secret instead. - **Naming a host that echoes requests.** A host that reflects headers back would show the value to the sandbox. Name only hosts you trust with it. - **A program with pinned certificates.** The proxy opens the HTTPS connection to the secret's hosts itself and checks the real server's certificate. Python, Node, curl and git already trust Runtime's certificate through `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` and `NODE_EXTRA_CA_CERTS`; a program with its own list needs `/usr/local/share/ca-certificates/runtime-egress.crt` added to it. - **Deleting to revoke.** `runtime secrets rm` erases the value on Runtime; it does not revoke the key at its provider. Rotate it there too. - **A database password.** Only HTTPS requests are rewritten, so a Postgres password cannot be swapped this way. For your own cloud, an [identity token](/docs/identity-tokens) avoids storing a key at all. ## Lock it down further A secret stops the key from leaking; [network rules](/how-to/allow-only-some-hosts) stop the data. Allow only the secret's host and the sandbox can call that API and nothing else. For no network at all, [turn the internet off](/how-to/turn-off-sandbox-internet). ## Related - [Secrets sandboxes never see](/docs/security#secrets-sandboxes-never-see). - [Secrets in the CLI](/docs/cli#secrets). - [Run untrusted LLM code](/use-cases/run-untrusted-llm-code). Facts on this page were checked on 25 September 2026.