> ## Documentation Index
> Fetch the complete documentation index at: https://docs.auto.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets

> Store credentials as write-only, envelope-encrypted values and reference them by name from agent environment variables, environments, and webhook triggers.

Secrets are named values stored envelope-encrypted at the organization or project level and referenced by name from your `.auto/` configuration. The plaintext never appears in YAML, prompts, or session transcripts — you write a value once, and the platform decrypts it when a sandbox launches, when a webhook is verified, or when you reveal a secret you deliberately marked unprotected. This page covers creating secrets, referencing them, and the storage, expiry, rotation, and edge-injection machinery behind them.

## Creating and updating secrets

Secret names are resource names: 1–128 characters of `A-Za-z0-9_.-`.

### From the web app

The **Secrets** settings page in the web app manages secrets interactively at both scopes: set or rotate a value, edit the description and protection, configure expiry and edge injection, and reveal secrets marked non-protected. No write ever echoes a value back.

Alongside the value itself, every secret carries a description (up to 1024 characters), a **protected** flag (`true` by default — write-only; `false` allows deliberate plaintext reveal), the two [expiry](#expiry) settings, and an optional [edge-injection](#edge-injection-keeping-secrets-out-of-the-sandbox) configuration.

### From a session

Agents with the [local `auto` tool](/runtime/auto-tools) can create secrets with `auto.secrets.create`. It takes exactly one of `value` (a plaintext the agent already holds) or `generate` — a bounded regex pattern such as `[A-Za-z0-9]{48}` from which the platform draws a cryptographically random value server-side. A generated value is never returned, so it stays out of the session transcript entirely. Generate patterns support printable-ASCII literals, character classes, `\d`, `\w`, groups, alternation, and bounded quantifiers (`{n}`, `{n,m}`, `?`); unbounded quantifiers (`*`, `+`, `{n,}`) and `.` are rejected.

Creation from a session is not an upsert: without `overwrite: true`, it fails when a live secret with the same name exists in the scope, or when a project-scope create would shadow an inherited organization secret.

## Scoping and resolution

A secret lives at exactly one scope: the organization, or one project. At resolution time — when a session launches or a webhook trigger authenticates — the platform looks up the name at the project scope first, then falls back to the organization scope. A project secret with the same name shadows the organization one.

This gives you a natural layering: put shared credentials at the organization scope, then override per project where a project needs its own value.

## Referencing secrets

### Agent and environment `env`

The `env` map on an [agent](/reference/agent-file) accepts either a plain string or a `$secret` reference:

```yaml .auto/agents/chief-of-staff.yaml theme={null}
env:
  HERENOW_API_KEY:
    $secret: herenow-api-key
    optional: true
```

<ParamField path="env.<NAME>" type="string | object" required>
  Env var names must match `[A-Za-z_][A-Za-z0-9_]*`. A plain string is passed through literally; an object form references a secret.
</ParamField>

<ParamField path="env.<NAME>.$secret" type="string" required>
  The secret name to resolve at session launch.
</ParamField>

<ParamField path="env.<NAME>.optional" type="boolean">
  When `true` and the secret does not exist, the env var is omitted instead of failing the launch, and apply-time validation does not require the secret to be set. Absent or `false`, a missing or expired secret fails the launch with `Secret not found: <name>` or `Secret has expired: <name>`.
</ParamField>

An [environment's](/reference/environments) `env` map has exactly the same shape, so runtime fragments can carry their own secret references:

```yaml .auto/fragments/environments/agent-runtime.yaml theme={null}
environment:
  name: agent-runtime
  image:
    kind: preset
    name: node24
  env:
    TAILSCALE_AUTHKEY:
      $secret: tailscale-authkey
```

### Webhook trigger auth

Custom webhook [triggers](/reference/triggers) authenticate inbound requests against a secret named by `secretRef`, resolved with the same project-then-organization fallback:

```yaml .auto/agents/watchdog.yaml theme={null}
triggers:
  - event: webhook.watchdog.signal
    endpoint: watchdog-intake
    auth:
      kind: bearer_token
      secretRef: watchdog-webhook-secret
    routing:
      kind: spawn
```

`kind: hmac_sha256` verifies a request signature with the secret; `kind: bearer_token` compares the `Authorization: Bearer` value in constant time. See [Cron and webhooks](/reference/events/cron-and-webhooks).

## How values are stored

Secret values are envelope-encrypted with AES-256-GCM before they reach the database:

1. Each write generates a fresh random 256-bit data-encryption key (DEK) and encrypts the value with it.
2. The DEK is itself encrypted with the platform's key-encryption key (KEK) and zeroed from memory once wrapped.
3. Both encryptions bind additional authenticated data tying the ciphertext to its organization, secret name, and key version — a ciphertext copied to another org, name, or key version fails authentication rather than decrypting.

Metadata reads (list, get) return a fixed projection of non-secret columns; ciphertext fields are structurally excluded from every metadata response. OAuth tokens stored for [MCP OAuth tool connections](/reference/tools) are encrypted with the same envelope scheme.

## Launch-time delivery and auditing

When a session launches, the worker resolves every `$secret` reference in the merged agent and environment `env`, decrypts each value, and delivers it into the sandbox process environment. Every resolution is recorded: the secret's `lastAccessedAt` is stamped and a `secret.access` audit entry is written naming the secret, its scope, the session, and the delivery mode.

## Edge injection: keeping secrets out of the sandbox

By default a resolved secret lands in the sandbox as an ordinary env var, where the agent (and anything it runs) can read it. For HTTP API credentials there is a stronger option: mark the secret **injected**, and the plaintext never enters the sandbox at all. The env var carries the non-sensitive placeholder `auto-injected-secret:<name>`, and the sandbox provider's network edge adds the real value as an HTTP header on requests to the destination hosts — outside the sandbox trust boundary.

```json Injection configuration for herenow-api-key theme={null}
{
  "hosts": ["api.example.com"],
  "header": "Authorization",
  "format": "Bearer {value}"
}
```

<ParamField path="injection.hosts" type="string[]" required>
  1–16 exact lowercase hostnames. Matching is exact: no wildcards, no schemes, ports, or paths, and a parent domain does not match its subdomains — name every host.
</ParamField>

<ParamField path="injection.header" type="string" required>
  The HTTP header the value is injected into (a valid header-name token, up to 128 characters). `connection`, `content-length`, `host`, and `transfer-encoding` are refused — injecting into framing headers could break requests rather than authenticate them.
</ParamField>

<ParamField path="injection.format" type="string">
  Template for the header value around the plaintext, e.g. `Bearer {value}` (must contain the `{value}` placeholder, up to 256 characters). Absent means the raw value.
</ParamField>

Injection is applied to HTTPS requests, so TLS to the destination hosts is terminated by the provider's injecting edge. Two injected secrets cannot claim the same host + header pair — the launch fails with a clear error instead of letting one silently shadow the other. Sandbox providers without an injection edge (local development) fall back to delivering the plaintext inline, where the host is the trust boundary anyway. Audit entries distinguish `edge-injection` from `env` delivery.

## Write-only protection and reveal

Secrets are **protected** (write-only) by default: no surface returns the plaintext, ever. For secrets you deliberately mark non-protected, **Reveal** on the web Secrets page returns the decrypted value — gated on a dedicated permission, audited on every call, and never cached. Revealing a protected secret is refused outright.

Reveal addresses the exact scope you name: it does not follow the project-to-organization fallback that runtime resolution uses.

## Rotation

Rotating a secret replaces the value of an existing secret without echoing it and stamps `lastRotatedAt`. Rotation never creates a secret — rotating a missing name is a 404 — and leaves the description, protection flag, and injection config untouched. Sessions snapshot secret references, not values: the next session launch resolves the new value with no re-apply needed.

## Expiry

Two independent expiry mechanisms, settable when the value is written or edited later:

<ParamField path="expiresAt" type="ISO 8601 timestamp | null">
  An absolute deadline. `null` clears it.
</ParamField>

<ParamField path="idleExpirySeconds" type="integer | null">
  Expire after this long unused, from 1 second up to 10 years. "Used" means written or resolved — the idle clock restarts on every value write and every launch-time resolution. `null` clears it.
</ParamField>

An expired secret resolves as absent everywhere: required references fail the launch with `Secret has expired`, optional references are omitted, and the project-to-organization fallback applies as if the expired row did not exist. Expired secrets stay visible in listings (with a computed `expired: true`) so operators can see and remove them.

Writing a fresh value — a full `PUT` or a rotation — revives an expired secret: the idle clock resets, and an absolute deadline that has already passed is cleared. Editing expiry without a value is only permitted for non-protected secrets that have not expired yet; for a protected or already-expired secret, re-set it with the value.

## Permissions

Listing metadata, writing values, and revealing non-protected plaintexts are three separately gated permissions. Session-launched resolution is performed by the platform itself and audited as the `secret-resolver` system principal, not as a user.
