Store secrets and give sandboxes credentials
Store an API key or token once, choose whether the sandbox can read it or only the network proxy can, and rotate it without restarting running sandboxes.
Store a secret and use it
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
await client.secrets.create("STRIPE_TEST_KEY", process.env.STRIPE_TEST_KEY!);
const box = await client.create({ secrets: ["STRIPE_TEST_KEY"] });
try {
const { stdout } = await box.exec("test -n \"$STRIPE_TEST_KEY\" && echo set");
console.log(stdout.trim());
} finally {
await box.delete();
}Select secrets at create with secrets: ["NAME", ...], or secrets: "all" for every secret you own. Each entry is a bare name; the binding stored with the secret decides how the sandbox receives it.
Values never come back out: list() returns names and bindings, not values.
for (const s of await client.secrets.list()) console.log(s.name, s.hosts, s.updatedAt);
await client.secrets.delete("STRIPE_TEST_KEY");Choose who holds the value
| Custody | The sandbox sees | Use it when |
|---|---|---|
| Box | The real value as an environment variable. | A tool in the sandbox needs the raw value: a database URL, a signing key. |
| Proxy | A placeholder. The egress proxy swaps in the real value on HTTPS requests to the hosts you name. | The value is an API credential sent in a header or query parameter, and code in the sandbox should never hold it. |
A secret stored with no hosts and no proxy policy is box custody. exposeInBox: true records that choice explicitly; it cannot be combined with hosts or a proxy policy.
Proxy custody with hosts
Store the secret with the hosts it may go to. The proxy adds it to the Authorization header unless you name another header.
await client.secrets.create("HF_TOKEN", process.env.HF_TOKEN!, { hosts: ["huggingface.co"] });
await client.secrets.create("ACME_KEY", process.env.ACME_KEY!, {
hosts: ["api.acme.dev"],
header: "X-Api-Key",
});
const box = await client.create({
secrets: ["HF_TOKEN", "ACME_KEY"],
egressPolicy: { mode: "strict", allowDomains: ["huggingface.co", "api.acme.dev"] },
});
// $HF_TOKEN holds a placeholder; only an HTTPS request to huggingface.co redeems it.
await box.exec(`curl -s -H "Authorization: Bearer $HF_TOKEN" https://huggingface.co/api/whoami-v2`);- A host is one public DNS name: no scheme, port, path, wildcard or IP address. Tangle’s own hosts are refused.
- A
headerneeds at least one host. - A strict egress policy does not open the host for you. List it in
allowDomains. - A plaintext
http://request carries the placeholder and does not authenticate.
Proxy custody with a policy
A proxySecrets policy controls how the value is presented and which requests get it.
await client.secrets.create("GIT_TOKEN", process.env.GIT_TOKEN!, {
proxySecrets: [
{
envName: "GIT_TOKEN",
secretRef: { kind: "secret-ref", key: "GIT_TOKEN" },
presentation: { type: "basic", username: "git" },
hosts: ["git.acme.dev"],
methods: ["GET", "POST"],
paths: ["/acme/*"],
},
],
});
const box = await client.create({ secrets: ["GIT_TOKEN"] });presentation | The proxy sends |
|---|---|
{ type: "bearer" } | Authorization: Bearer <value>. |
{ type: "basic", username } | HTTP Basic auth with that username and the value as password. |
{ type: "header", name } | The value in the named header. |
{ type: "query", name } | The value in the named query parameter. |
envName is the variable the sandbox sees, secretRef.key must be the secret’s own name, and hosts are exact names. methods, paths and ttlSeconds narrow it further. Store the policy on the secret as above and select the secret by name, or pass proxySecrets directly at create.
An object in secrets is refused: secrets entries are secret names: save a proxySecrets policy on the secret and select it by bare name.
In the CLI, --secret takes bare names for box custody, and --proxy-secret takes one binding as JSON or @file with an array:
tangle sandbox create --proxy-secret '{"envName":"HF_TOKEN","secretRef":{"kind":"secret-ref","key":"HF_TOKEN"},"presentation":{"type":"bearer"},"hosts":["huggingface.co"]}'
tangle sandbox create --proxy-secret @bindings.jsonWhat the proxy masks
When a proxy-held value comes back in a response, the proxy masks it in response headers, streaming bodies, HTTP trailers and decoded WebSocket messages, so the sandbox does not learn it from an echo. A response compressed in a way the proxy cannot decode is refused rather than passed through unmasked. A value re-encoded by the remote service, for example as hex, cannot be recognized, so name only hosts you trust.
Rotate a secret
update replaces the value and re-keys running sandboxes without a restart.
const [current] = (await client.secrets.list()).filter((s) => s.name === "HF_TOKEN");
await client.secrets.update("HF_TOKEN", newValue, {
hosts: ["huggingface.co"],
expectedUpdatedAt: current.updatedAt.toISOString(),
});expectedUpdatedAt refuses the update if someone changed the secret since you read it. A 503 Live re-key not acknowledged means the value is stored but a running sandbox did not confirm the change. A retry with the old expectedUpdatedAt then meets its own write and answers 409, so read the secret again before you retry. delete(name, { expectedUpdatedAt }) takes the same guard. The CLI has no update command: delete and create the secret again, or use the SDK.
Team secrets
A team secret is available to sandboxes created in the team.
await client.teams.upsertSecret(teamId, "SENTRY_DSN", dsn);
console.log(await client.teams.listSecrets(teamId));
const { value } = await client.teams.revealSecret(teamId, "SENTRY_DSN");
await client.teams.deleteSecret(teamId, "SENTRY_DSN");tangle team secret set SENTRY_DSN --value-stdin < dsn.txt
tangle team secret list
tangle team secret reveal SENTRY_DSN
tangle team secret delete SENTRY_DSNupsertSecret takes the same hosts, header and proxySecrets options as a personal secret.
Credentials for coding agents
Your Claude or ChatGPT plan
Store the token from claude setup-token (or a Codex auth.json), select it, and point the backend at it with cliAuth:
await client.secrets.create("CLAUDE_TOKEN", process.env.CLAUDE_CODE_OAUTH_TOKEN!);
const box = await client.create({
git: { url: "https://github.com/tangle-network/sandbox-demo" },
backend: {
type: "claude-code",
model: { cliAuth: { account: "me", secretEnv: "CLAUDE_TOKEN", format: "token" } },
},
secrets: ["CLAUDE_TOKEN"],
});format is token for Claude Code, and bundle or files for Codex. The secret in secretEnv must be listed in secrets, or the create fails with Native authentication secret CLAUDE_TOKEN must be listed in secrets. The CLI form is --backend claude-code --cli-auth CLAUDE_TOKEN. Full walkthroughs: Claude Code and Codex.
To give a running sandbox a stored Claude token, use grantNativeCredential:
await box.grantNativeCredential({
harness: "claude-code",
cliAuth: { account: "me", secretEnv: "CLAUDE_TOKEN", format: "token" },
});Your own provider key
backend.model.apiKeyEnv names the variable that holds your provider key. It must name a selected secret or a proxy alias, or the create fails with backend.model.apiKeyEnv names OPENAI_KEY, which is not a selected secret or proxy alias.
const box = await client.create({
backend: { type: "opencode", model: { provider: "openai", model: process.env.OPENAI_MODEL!, apiKeyEnv: "OPENAI_KEY" } },
secrets: ["OPENAI_KEY"],
});Names you cannot use
Names starting with TANGLE_, plus GITHUB_TOKEN, GH_TOKEN and the platform’s model proxy tokens, are reserved.
Python and REST
Python’s client.secrets has create(name, value, options), list(), update(name, value, options) and delete(name, expected_updated_at). options is a dict of the same binding fields:
client.secrets.create("HF_TOKEN", os.environ["HF_TOKEN"], {"hosts": ["huggingface.co"]})
box = client.create(secrets=["HF_TOKEN"])Python has no team secrets. Call the team route with client.fetch, which returns the raw response and does not raise on an error status:
client.fetch("PUT", f"/v1/teams/{team_id}/secrets/SENTRY_DSN", json={"value": dsn})Next
- How Tangle keeps credentials out of the agent: the threat model and a measured boundary test.
- Control network access
- Run Claude Code on your Claude plan
- Isolation, privacy and compliance