# Workspace secrets

> Give an agent a credential it can use inside its sandbox, without putting it in a prompt.

An agent with a sandbox has a shell and network access, so it can already run any command line tool. A workspace secret is how you give it something to authenticate with, without putting the credential in a prompt or a file in your repository.

## What this protects, and what it does not

A stored value is encrypted at rest and no route returns it. There is no read-back endpoint, so nothing has to guard one.

The value does not appear in a response, an event, a log line, a stored record, or anything sent to the model provider. Pantheon never puts it anywhere the model reads.

> **Warning**
>
> **The agent can disclose its own credential.** It has a shell in its own sandbox, so it can read its own environment and its own files and print the value into its own output. That is inherent to handing a credential to a program you then ask to use it. Pantheon keeps the value out of everything it controls; it cannot stop an agent from reading what it was deliberately given.
>
> So store narrow, short lived credentials: one scope, one mailbox, one repository, an hour rather than a year. Revoke at the source when the work is done. Deleting the secret here removes Pantheon's copy; it revokes nothing at the provider that issued it, and a sandbox already running still holds what it was handed.

## Store a credential

Names are unique within your workspace. Two workspaces may each hold a `ZOHO_OAUTH_TOKEN` and they are different entries that never meet.

```bash title="Terminal"
curl -sS -X POST "$PANTHEON_BASE_URL/v1/secrets" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ZOHO_OAUTH_TOKEN",
    "value": "1000.the-access-token",
    "description": "Zoho Mail, read only, expires in one hour"
  }'
```

```json title="Example JSON response"
{
  "id": "0f2c1a5e-9f1d-4a0a-9b1c-2f3e4d5a6b7c",
  "name": "ZOHO_OAUTH_TOKEN",
  "description": "Zoho Mail, read only, expires in one hour",
  "fingerprint": "3b9f1c77ad0e42a1",
  "created_by": "key:8f1d2c3b",
  "updated_by": "key:8f1d2c3b",
  "created_at": "2026-09-12T09:14:02Z",
  "updated_at": "2026-09-12T09:14:02Z"
}
```

| Value                      | Meaning                                                                                                                                                           |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                     | Unique in your workspace, and the default environment variable name inside the sandbox. Letters, digits and underscores, starting with a letter or an underscore. |
| `value`                    | The credential. Written once and never returned by any route.                                                                                                     |
| `description`              | What this credential is for. Never put the value here.                                                                                                            |
| `fingerprint`              | A short keyed digest. Two entries with the same fingerprint hold the same value. It does not reveal the value and cannot be turned back into one.                 |
| `created_by`, `updated_by` | The signed-in user, or the API key, that last wrote this entry.                                                                                                   |

Rotating with `PUT` keeps the id and the name, so a session that already names this secret picks up the new value the next time its sandbox is provisioned.

| Route                         | What it does                                                            |
| ----------------------------- | ----------------------------------------------------------------------- |
| `POST` `/v1/secrets`          | Store a credential. Needs `secrets:write`.                              |
| `GET` `/v1/secrets`           | List names, descriptions and fingerprints. Needs `secrets:read`.        |
| `GET` `/v1/secrets/{name}`    | One entry's metadata. Needs `secrets:read`.                             |
| `PUT` `/v1/secrets/{name}`    | Rotate the value, edit the description, or both. Needs `secrets:write`. |
| `DELETE` `/v1/secrets/{name}` | Forget Pantheon's copy. Needs `secrets:write`.                          |

## Deliver it to a sandbox

A session names which secrets its sandbox receives. They arrive in that sandbox and in no other, and a name your workspace does not hold is refused when the session is created rather than discovered missing at run time.

**Terminal**

```bash title="Terminal"
curl -sS -X POST "$PANTHEON_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_slug": "mail-reader",
    "secrets": [{"name": "ZOHO_OAUTH_TOKEN"}]
  }'
```

**TypeScript**

```typescript title="TypeScript"
import { PantheonClient } from "@alethi/pantheon-chat";

const pantheon = new PantheonClient({
  baseUrl: process.env.PANTHEON_BASE_URL!,
  apiKey: process.env.PANTHEON_API_KEY!,
});

await pantheon.createSecret({
  name: "ZOHO_OAUTH_TOKEN",
  value: process.env.ZOHO_OAUTH_TOKEN!,
  description: "Zoho Mail, read only, expires in one hour",
});

const session = await pantheon.createSession({
  agentSlug: "mail-reader",
  secrets: [{ name: "ZOHO_OAUTH_TOKEN" }],
});
```

**Python**

```python title="Python"
from pantheon_chat import SyncPantheonClient, SecretBinding

pantheon = SyncPantheonClient(base_url, api_key)

pantheon.create_secret(
    name="ZOHO_OAUTH_TOKEN",
    value=zoho_oauth_token,
    description="Zoho Mail, read only, expires in one hour",
)

session = pantheon.create_session(
    agent_slug="mail-reader",
    secrets=[SecretBinding(name="ZOHO_OAUTH_TOKEN")],
)
```

By default the secret becomes an environment variable of the same name. The agent's shell sees `$ZOHO_OAUTH_TOKEN` and nothing else changes.

## Tools that read a file instead

Plenty of command line tools do not read a credential from the environment. They read a file, often under a directory named by an environment variable. Both are supported, and a binding can do either or both.

**Terminal**

```bash title="Terminal"
curl -sS -X POST "$PANTHEON_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_slug": "mail-reader",
    "secrets": [{
      "name": "ZOHO_OAUTH_TOKEN",
      "env_var": null,
      "file_path": "/home/daytona/.config/zog/token.json"
    }],
    "env": {"ZOG_CONFIG_DIR": "/home/daytona/.config/zog"}
  }'
```

**TypeScript**

```typescript title="TypeScript"
const session = await pantheon.createSession({
  agentSlug: "mail-reader",
  secrets: [{
    name: "ZOHO_OAUTH_TOKEN",
    envVar: null,
    filePath: "/home/daytona/.config/zog/token.json",
  }],
  env: { ZOG_CONFIG_DIR: "/home/daytona/.config/zog" },
});
```

**Python**

```python title="Python"
from pantheon_chat import file_secret

session = pantheon.create_session(
    agent_slug="mail-reader",
    secrets=[file_secret("ZOHO_OAUTH_TOKEN", "/home/daytona/.config/zog/token.json")],
    env={"ZOG_CONFIG_DIR": "/home/daytona/.config/zog"},
)
```

| Value                  | Meaning                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                 | Which stored secret to deliver.                                                                                                                   |
| `env_var`              | Environment variable to set. Defaults to `name` when no `file_path` is given. Send `null` with a `file_path` to deliver the value only as a file. |
| `file_path`            | Absolute path inside the sandbox. The file is created with mode 600 before the first run.                                                         |
| `env` (on the session) | Plain environment variables. Stored as written and returned by `/v1/sessions`, so this is for settings, not credentials.                          |

Two rules keep this unambiguous. Two secrets may not set the same variable or write the same file, and a plain `env` entry may not shadow a secret. Either is refused with HTTP 422 rather than resolved by list order.

The file is written with mode 600 before the first run, so only the sandbox user can read it. The path must be absolute and may not contain `..`.

## What is recorded

Every mutation writes an audit row in the same transaction as the change, readable at `/v1/audit/records`: `secret.created`, `secret.updated`, `secret.deleted`, and `secret.attached` when a session names one.

Each row carries the name and the fingerprint. None of them carries the value. The sandbox record shows which secrets a sandbox received, by name, for the same reason.

## Scopes

`secrets:read` lists and reads metadata. `secrets:write` creates, rotates and deletes. Neither can read a stored value, because no route returns one.

A write scope does not imply the matching read, as everywhere else in this API. Naming secrets on a session needs `sessions:write` and `secrets:read` together.

## Pitfalls

> **Warning**
>
> - **HTTP 503 SECRETS_NOT_CONFIGURED:** The deployment has no PANTHEON_SECRETS_ENCRYPTION_KEY. On Pantheon Cloud this should never happen; if you self-host, generate a Fernet key and set it. See [Self-host](https://pantheon-todo.alethiconsulting.com/docs/self-host).
> - **HTTP 400 SECRET_NOT_FOUND when creating a session:** The session named a secret this workspace does not hold. Store it first, and check for a typo in the name.
> - **HTTP 409 SECRET_EXISTS:** A secret of that name already exists. Rotate it with PUT rather than creating a second one.
> - **HTTP 422 on a session:** Two bindings claim the same variable or file, or a plain env entry names the same variable as a secret. Choose one source per target.
> - **The variable is missing inside the sandbox:** The secret was deleted after the session named it. Resolution skips a name that no longer exists rather than failing the run; store it again and the next sandbox will have it.
> - **The agent printed the credential:** Expected, and unavoidable: it has a shell in that sandbox. Rotate the credential at its source and store a narrower one.
