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

# Secrets

Manage encrypted secrets that agents receive through environment bindings or fetch on demand.

## Agent List and Fetch

These routes require the current run-bound agent JWT. They are not available to
long-lived agent keys, low-trust review agents, task-bridge keys, or skill-test
tokens.

List the secrets accessible to the current run without materializing values:

```
GET /api/agents/me/secrets
```

```json theme={null}
{
  "secrets": [
    {
      "key": "github_token",
      "secretRef": "11111111-1111-4111-8111-111111111111",
      "name": "GitHub token",
      "description": null,
      "delivery": "env",
      "projectionClass": "unclassified",
      "latestVersion": 2,
      "versionSelector": "latest",
      "resolvedVersion": 2
    }
  ]
}
```

`delivery` is `env`, `api`, or `both`. `secretRef` is a stable opaque handle,
not secret material or a capability; every consuming route re-authorizes it.
The list never returns values, the internal `secretId` field, binding IDs, or
config paths. An `env.*` binding implies read access through this API; an
`access.*` binding grants API access without environment injection.

Fetch a value only when it is needed. The request has no body and the response
uses `Cache-Control: no-store`:

```
POST /api/agents/me/secrets/github_token/value
```

```json theme={null}
{
  "key": "github_token",
  "value": "decrypted-secret-value",
  "version": 2
}
```

Prefer env injection when the adapter or its child processes need the value on
every run. Prefer on-demand fetch for values used only on some runs, large or
structured values, or skills and tools that do not inherit adapter env. Every
successful or failed value fetch is audited in both `secret_access_events` and
`activity_log`; agents must not log or paste fetched values into issues,
comments, or documents.

## Agent Secret Proposals

These routes use the same current run-bound agent JWT as the list and fetch
routes:

```
POST /api/agents/me/secret-proposals
GET /api/agents/me/secret-proposals
DELETE /api/agents/me/secret-proposals/{proposalId}
```

An agent can ask Paperclip to bind an existing secret under a new path without
knowing a secret ID. Set `kind` to `binding` and identify the source by the
agent's own existing `env.*` or `access.*` config path:

```json theme={null}
POST /api/agents/me/secret-proposals
{
  "kind": "binding",
  "sourceConfigPath": "access.openai_api_key",
  "configPath": "access.evals_openai_api_key",
  "justification": "Use the existing OpenAI credential under the eval-specific alias"
}
```

`sourceConfigPath` must resolve from the proposing agent's own binding. An
unknown path or another agent's path returns `404`. A binding request must
provide exactly one of `sourceConfigPath`, `secretId`, or `secretProposalId`.
Omit `targetAgentId` to target the proposing agent; under the default policy a
manager may instead target one of its reports. `configPath` accepts
`env.<KEY>` for environment injection or `access.<ALIAS>` for API-only access.

For a run with a checked-out origin issue, a successful proposal automatically
creates a human-only **Confirm secret binding** card in that issue. API clients
must not create a second interaction. The card contains only non-secret
metadata: the source label, target agent, new config path, justification, and
expiry.

Selecting **Create binding** accepts the card and then triggers a separate,
freshly authorized binding write. Card acceptance is not execution. Read
`result.secretProposal.status` for the actual outcome:

* `executed` means the binding write completed.
* `failed` means the card was accepted but execution failed. The card renders
  **FAILED**, exposes a non-secret `errorCode`, and Paperclip posts a **Secret
  binding execution failed** comment with `Binding created: no`.
* `rejected`, `withdrawn`, or `expired` means no binding was created.

The card wakes the issue assignee after resolution. The wake payload includes
`secretProposal.configPath`, `decision`, `executionStatus`, and instructions.
After any secret card, call `GET /api/agents/me/secrets` again and confirm the
expected secret metadata and delivery before using the new binding. Acceptance
is not execution; a failed wake or missing metadata means the alias must be
treated as unavailable until a fresh proposal executes successfully.

## List Secrets

```
GET /api/companies/{companyId}/secrets
```

Returns secret metadata (not decrypted values).

## Create Secret

```
POST /api/companies/{companyId}/secrets
{
  "name": "anthropic-api-key",
  "value": "sk-ant-..."
}
```

The value is encrypted at rest. Only the secret ID and metadata are returned.

To link a provider-owned secret without copying the value into Paperclip, create
an external-reference secret:

```json theme={null}
{
  "name": "prod-stripe-key",
  "provider": "aws_secrets_manager",
  "managedMode": "external_reference",
  "externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/stripe",
  "providerVersionRef": "version-id-or-label"
}
```

Paperclip stores the provider reference and a non-sensitive fingerprint only.
The value is resolved, when the provider is configured, through the server
runtime path that enforces binding context and records access events.

## Provider Health

```
GET /api/companies/{companyId}/secret-providers/health
```

Returns provider setup diagnostics, warnings, and local backup guidance. Health
responses must not include secret values or provider credentials.

For `aws_secrets_manager`, an unready health response names the missing
non-secret provider environment variables, the AWS SDK default credential source
expected by the server runtime, and the custody rule that AWS bootstrap
credentials must not be stored in Paperclip `company_secrets`.

The equivalent CLI check is:

```sh theme={null}
npx paperclipai secrets doctor --company-id {companyId}
```

## Provider Vaults

Provider vaults are named, company-scoped configurations that route secret
material to one of the supported provider backends. See the
[secrets deploy guide](/deploy/secrets#provider-vaults) for the operator model
and custody rules.

All routes below require board auth and company access. Mutating routes emit
`secret_provider_config.*` activity-log entries. No route in this surface
returns provider credential values; submitting credential-shaped fields in
`config` is rejected at validation time.

### List Vaults

```
GET /api/companies/{companyId}/secret-provider-configs
```

Returns every vault for the company (including disabled rows for audit), each
with id, provider, displayName, status, isDefault, non-sensitive `config`,
latest health snapshot (`healthStatus`, `healthCheckedAt`, `healthMessage`,
`healthDetails`), `disabledAt`, and audit columns.

### Create Vault

```
POST /api/companies/{companyId}/secret-provider-configs
{
  "provider": "aws_secrets_manager",
  "displayName": "Prod US-East",
  "isDefault": true,
  "config": {
    "region": "us-east-1",
    "namespace": "paperclip",
    "secretNamePrefix": "paperclip",
    "kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/abcd-...",
    "environmentTag": "production"
  }
}
```

Per-provider `config` shapes:

* `local_encrypted`: optional `backupReminderAcknowledged: boolean`.
* `aws_secrets_manager`: required `region`; optional `namespace`,
  `secretNamePrefix`, `kmsKeyId`, `ownerTag`, `environmentTag`.
* `gcp_secret_manager` (coming soon): optional `projectId`, `location`,
  `namespace`, `secretNamePrefix`.
* `vault` (coming soon): optional origin-only HTTPS `address`, `namespace`,
  `mountPath`, `secretPathPrefix`. `address` values with embedded credentials,
  paths, query strings, or fragments are rejected.

`status` defaults to `ready` for `local_encrypted` and `aws_secrets_manager`,
and to `coming_soon` for `gcp_secret_manager` and `vault`. Coming-soon and
disabled vaults cannot be marked `isDefault`. Setting `isDefault: true` clears
the previous default for the same provider in the same transaction.

### Get Vault

```
GET /api/secret-provider-configs/{id}
```

### Update Vault

```
PATCH /api/secret-provider-configs/{id}
{
  "displayName": "Prod US-East-2",
  "config": {
    "region": "us-east-2",
    "kmsKeyId": "arn:aws:kms:us-east-2:123456789012:key/abcd-..."
  }
}
```

`config` is replaced wholesale on update — pass the full provider config
payload, not a partial diff. Status transitions for `gcp_secret_manager` and
`vault` are constrained to `coming_soon` and `disabled` until their runtime
modules ship.

### Disable Vault

```
DELETE /api/secret-provider-configs/{id}
```

Soft-deletes the vault: status flips to `disabled`, `isDefault` clears, and
`disabledAt` is stamped. Disabled vaults remain in `GET` results for audit
purposes but are no longer offered in the secret create/rotate flow.

### Set Default

```
POST /api/secret-provider-configs/{id}/default
```

Marks the target vault as the default for its provider family and clears the
previous default. Returns 422 when the target is `coming_soon` or `disabled`.

### Run Health Check

```
POST /api/secret-provider-configs/{id}/health
```

Runs a provider-specific health probe and persists the result on the vault.
Response shape:

```json theme={null}
{
  "configId": "<uuid>",
  "provider": "aws_secrets_manager",
  "status": "ready" | "warning" | "error" | "coming_soon" | "disabled",
  "message": "Provider vault is ready to handle managed writes",
  "details": {
    "code": "provider_ready",
    "message": "...",
    "guidance": ["..."]
  },
  "checkedAt": "2026-05-06T14:00:00.000Z"
}
```

Health responses never include provider credentials or secret values. For AWS
vaults, `details.guidance` may include missing non-secret env names and the
expected AWS SDK credential source; coming-soon vaults always return
`status: "coming_soon"` with `code: "runtime_locked"` and never call into
provider modules.

### Selecting A Vault When Creating Or Rotating Secrets

`POST /api/companies/{companyId}/secrets` and
`POST /api/secrets/{secretId}/rotate` both accept an optional
`providerConfigId` field that pins the secret to a specific vault. When
omitted (or null), the operation runs through the deployment-level provider
configuration — the same path existing installs already use. The board UI
preselects the company's default vault for the chosen provider before
submitting, so callers should usually send an explicit `providerConfigId`.
Coming-soon and disabled vaults are rejected with a 422; a vault that does not
match the secret's provider is rejected the same way.

```json theme={null}
POST /api/companies/{companyId}/secrets
{
  "name": "prod-stripe-key",
  "provider": "aws_secrets_manager",
  "providerConfigId": "<vault-uuid>",
  "managedMode": "external_reference",
  "externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/stripe"
}
```

### Response Redaction Rules

Every route in this surface enforces the same redaction contract:

* Secret values are never returned. The board UI never has a "reveal value"
  affordance; resolution happens server-side at runtime under a binding.
* Provider credential values are never accepted, stored, returned, logged, or
  echoed in error messages. Submitting credential-shaped fields fails
  validation with a non-leaking error.
* Activity log entries record vault id, provider, displayName, status, and
  isDefault transitions — never `config` payloads or health detail bodies.

## Remote Import From AWS Secrets Manager

Remote import links existing AWS Secrets Manager entries into Paperclip as
`external_reference` secrets. Import stores provider reference metadata only; it
does not copy the remote secret plaintext into Paperclip.

The routes are board-only and company-scoped. `providerConfigId` must point to
a same-company AWS provider vault with status `ready` or `warning`. Disabled,
coming-soon, non-AWS, and cross-company vaults are rejected. Imported secrets
resolve later through the selected vault, so runtime reads still need
`secretsmanager:GetSecretValue` and any required KMS decrypt permission on the
selected external secret.

### Preview Remote Import Candidates

```
POST /api/companies/{companyId}/secrets/remote-import/preview
{
  "providerConfigId": "<aws-vault-uuid>",
  "query": "stripe",
  "nextToken": "opaque-provider-token",
  "pageSize": 50
}
```

`query` is optional and is passed to AWS Secrets Manager inventory filtering.
Treat it as non-secret metadata because AWS may record list request parameters
in CloudTrail. `nextToken` is an opaque AWS cursor; callers must pass it back
unchanged and must not synthesize offsets. `pageSize` is optional, defaults to
50 in the UI, and is capped at 100.

Preview uses AWS `ListSecrets` only. It must not call `GetSecretValue` or
`BatchGetSecretValue`, must not request `SecretString`, and must not require KMS
decrypt. The response contains sanitized metadata for display and conflict
decisions:

```json theme={null}
{
  "providerConfigId": "<aws-vault-uuid>",
  "provider": "aws_secrets_manager",
  "nextToken": null,
  "candidates": [
    {
      "externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
      "remoteName": "prod/stripe",
      "name": "prod/stripe",
      "key": "prod-stripe",
      "providerVersionRef": null,
      "providerMetadata": {
        "createdDate": "2026-05-06T00:00:00.000Z",
        "lastChangedDate": "2026-05-06T00:00:00.000Z",
        "hasDescription": true,
        "hasKmsKey": true,
        "tagCount": 3
      },
      "status": "ready",
      "importable": true,
      "conflicts": []
    }
  ]
}
```

Candidate statuses:

* `ready`: the row can be selected for import.
* `duplicate`: a Paperclip secret already links the same canonical provider
  reference for the same provider vault.
* `conflict`: the row has a name/key collision or provider guardrail failure.

Conflict types are `exact_reference`, `name`, `key`, and
`provider_guardrail`. AWS refs under Paperclip's own managed namespace are
blocked as external references; use the Paperclip-managed secret flow for those
resources instead.

### Import Selected Remote References

```
POST /api/companies/{companyId}/secrets/remote-import
{
  "providerConfigId": "<aws-vault-uuid>",
  "secrets": [
    {
      "externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
      "name": "Stripe production key",
      "key": "stripe-production-key",
      "description": "Stripe key used by production checkout",
      "providerVersionRef": null,
      "providerMetadata": {
        "createdDate": "2026-05-06T00:00:00.000Z"
      }
    }
  ]
}
```

The `secrets` array accepts 1-100 rows. Each row may override the suggested
Paperclip `name`, `key`, optional Paperclip `description`,
`providerVersionRef`, and sanitized `providerMetadata`. Blank descriptions are
stored as `null`; AWS provider descriptions are not copied into Paperclip
descriptions. The backend re-checks duplicate refs and name/key conflicts at
submit time; a stale preview does not bypass those checks.

The import response is row-level:

```json theme={null}
{
  "providerConfigId": "<aws-vault-uuid>",
  "provider": "aws_secrets_manager",
  "importedCount": 1,
  "skippedCount": 1,
  "errorCount": 0,
  "results": [
    {
      "externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
      "name": "Stripe production key",
      "key": "stripe-production-key",
      "status": "imported",
      "reason": null,
      "secretId": "<paperclip-secret-id>",
      "conflicts": []
    }
  ]
}
```

Row statuses:

* `imported`: Paperclip created an active `external_reference` secret and one
  metadata-only version row.
* `skipped`: the row had an exact-reference duplicate or name/key conflict.
* `error`: the provider rejected the reference or the row failed validation.

Activity logs for preview/import store aggregate counts, provider id, and vault
id only. They must not store remote secret names, ARNs, descriptions, tags,
plaintext values, provider credentials, or raw AWS error blobs.

## Rotate Secret

```
POST /api/secrets/{secretId}/rotate
{
  "value": "sk-ant-new-value..."
}
```

Creates a new version of the secret. Agents referencing `"version": "latest"`
automatically get the new value on next heartbeat. Pin to a specific version
when a bad `latest` rollout would affect many agents at once.

## Using Secrets in Agent Config

Reference secrets in agent adapter config instead of inline values:

```json theme={null}
{
  "env": {
    "ANTHROPIC_API_KEY": {
      "type": "secret_ref",
      "secretId": "{secretId}",
      "version": "latest"
    }
  }
}
```

The server resolves and decrypts secret references at runtime, injecting the
real value into the agent process environment. Paperclip's custody guarantees
end at injection: the agent process can read, log, or forward the value, so
treat any secret bound to an agent as exposed to that agent. See the custody
boundaries note in the [secrets deploy guide](/deploy/secrets#custody-boundaries).

User-specific env bindings use a definition key instead of a concrete
`secretId`. The concrete value is resolved for the run's responsible user:

```json theme={null}
{
  "env": {
    "GITHUB_TOKEN": {
      "type": "user_secret_ref",
      "key": "github_api_token",
      "version": "latest",
      "required": true,
      "allowMissingOverride": false
    }
  }
}
```

`required` defaults to `true` and `allowMissingOverride` defaults to `false`.
Missing required user-secret values must fail closed before adapter dispatch.
Optional missing values omit the environment variable; they must not inject an
empty string or another user's value. Paperclip records value-free access
events with `secretScope`, `responsibleUserId`, `credentialOwnerUserId`, and
`userSecretDefinitionId`.

## Portability

Company export/import APIs represent agent and project environment requirements
as declarations in the package manifest. Exports omit secret values, secret IDs,
provider references, and encrypted provider material. Use:

```sh theme={null}
npx paperclipai secrets declarations --company-id {companyId}
```

to inspect the declarations that an export would emit before moving a package.
