# Credentials
URL: /docs/core/concepts/credentials

Register credentials that get injected into outbound requests without the sandbox process ever holding them.



Sandboxes often need to call an authenticated API — GitHub, Slack, an internal service. The obvious way is to pass a token through `env`, but that puts it somewhere any code running in the sandbox can read, log, or accidentally echo back to you — including code an agent wrote itself.

`sb.credentials.set()` registers a credential and a rule for where it applies. From then on, matching outbound requests get the credential injected automatically, at the network layer — the value itself never enters the container's filesystem or environment.

## Enabling it [#enabling-it]

Credential injection rides on the same opt-in egress layer as [network policy](/docs/core/concepts/network-policy) — pass `credentialProxy: true` when creating the sandbox:

```ts
const sb = await client.sandbox({
  image: "node:22",
  resources: { cpu: "500m", memory: "512Mi" },
  networkPolicy: { defaultAction: "allow", egress: [] },
  credentialProxy: true,
});
```

A sandbox created without `networkPolicy`/`credentialProxy` has ordinary, unrestricted egress — nothing here applies to it, and `sb.credentials.*` throws if you call it on one. `networkPolicy` itself is a separate concern (which hosts the sandbox may reach at all, allow/deny style — see [Network policy](/docs/core/concepts/network-policy)); `credentialProxy` is what makes injection available. Setting `defaultAction: "allow"` with an empty `egress` list, as above, keeps egress wide open while turning injection on — tighten `egress` if you also want to restrict which hosts are reachable at all. `credentialProxy` also needs the server on `egress.mode = "dns+nft"` (the `alineo init` default).

## Registering a credential [#registering-a-credential]

```ts
await sb.credentials.set("github", process.env.GH_TOKEN!, {
  host: "api.github.com",
  injection: { type: "header", name: "Authorization" },
});
```

Any request the sandbox makes to `api.github.com` now gets that header added automatically — a plain `curl https://api.github.com/user` with no `Authorization` header of its own comes back authenticated. Requests to any other host are untouched. Run `env` inside the sandbox and the token isn't there.

| `CredentialBinding` field | Type                  | Description                                                 |
| ------------------------- | --------------------- | ----------------------------------------------------------- |
| `host`                    | `string`              | FQDN the credential applies to                              |
| `pathPrefix`              | `string` (optional)   | Narrows the binding to requests whose path starts with this |
| `injection`               | `CredentialInjection` | Where the value goes — see below                            |

## Injection modes [#injection-modes]

```ts
type CredentialInjection =
  | { type: "header"; name: string }
  | { type: "substitution"; placeholder: string; in: Array<"path" | "query" | "header" | "body"> };
```

### `header` (recommended) [#header-recommended]

Adds `name: <value>` to every matching outbound request. This is the default and the right choice for anything that takes a bearer token or API-key header.

```ts
injection: { type: "header", name: "Authorization" }
```

### `substitution` [#substitution]

For APIs that want the secret in the URL or body rather than a header. The sidecar replaces **every literal occurrence** of `placeholder` in the listed request surfaces with the real value.

```ts
await sb.credentials.set("openai", process.env.OPENAI_API_KEY!, {
  host: "api.example.com",
  injection: { type: "substitution", placeholder: "__API_KEY__", in: ["query"] },
});
```

The outbound request **must already contain `placeholder` verbatim** — the sidecar only substitutes, it doesn't append. Put it in the base URL you give your client:

```ts
// inside the sandbox
fetch("https://api.example.com/v1/data?key=__API_KEY__");
// leaves the sandbox as ...?key=<real value>
```

`sb.credentials.listBindings()` is lossy for substitution bindings (the vault doesn't echo the substitution config back), but `resume()` / `fork()` recover the full shape from the ledger.

<Callout type="warn" title="Migrating from query / path injection">
  The old `{ type: "query"; param }` and `{ type: "path"; segment }` shapes were removed in 0.4.0 (they only ever threw `UnsupportedInjectionError`). Replace `{ type: "query"; param: "k" }` with `{ type: "substitution"; placeholder: "__CRED__"; in: ["query"] }` and add `?k=__CRED__` to the request URL.
</Callout>

## Removing a credential [#removing-a-credential]

```ts
await sb.credentials.remove("github");
```

Requests to that host go out unauthenticated from then on.

## Where the value comes from — `source` [#where-the-value-comes-from--source]

`set()` takes an optional fourth argument describing how to re-derive the value later. It's not needed for a sandbox you register a credential on and close normally — only `resume()` and `fork()` (below) ever read it:

```ts
await sb.credentials.set(
  "github",
  process.env.GH_TOKEN!,
  { host: "api.github.com", injection: { type: "header", name: "Authorization" } },
  { type: "env", varName: "GH_TOKEN" },
);
```

* `{ type: "env", varName }` — re-read from `process.env` automatically, no extra code needed.
* `{ type: "external" }` (the default if you omit `source` entirely) — alineo has no way to reproduce this value on its own (a one-time minted token, something generated at call time). Resuming or forking a sandbox with one of these requires you to supply it explicitly — see below.

## resume() and fork() never drop a credential silently [#resume-and-fork-never-drop-a-credential-silently]

If a sandbox with bound credentials is resumed or forked and one of them can't be resolved, alineo throws rather than quietly continuing without it — a credential that silently stops being injected is a worse failure mode than a loud one.

```ts
const sb = await client.resume(sandboxId, {
  resolveCredential: (name, source) => {
    if (name === "one-time-token") return mintFreshToken();
    return undefined; // let anything else fall through to the env-var default, if it has one
  },
});
```

Anything registered with `{ type: "env" }` resolves on its own. Anything else needs `resolveCredential` to return a value for it, or the call throws naming exactly which credential it couldn't resolve.

`sb.fork()` carries over the parent's own bound credentials to the child automatically — the child starts out able to make the same authenticated requests the parent could, with no re-registration needed. The same resolution rules apply: pass `resolveCredential` to `fork()` if any of the carried-over credentials need one.

## Custom credential backends [#custom-credential-backends]

`sb.credentials.*` is backed by a `CredentialBroker` interface (the same shape as `IStorageAdapter` — see [Storage adapters](/docs/core/storage)). `@alineo-labs/vault`'s `OpenSandboxCredentialBroker` is the default, wired up automatically — nothing to configure for the common case. Pass `SandboxClientOptions.credentialBroker` to use a different implementation.
