Secrets and credential custody

August 27, 2026 · View on GitHub

Secret values are never DotAgents resources. Portable repositories contain names and policy only; values live in a platform-backed store or encrypted headless store.

flowchart LR
  META[Portable bundle names and policy] --> R[Run resolution]
  STORE[(Platform or encrypted store)] --> B[Secrets broker]
  R --> B
  B --> ENV[Child-only environment]
  ENV --> H[Harness process]
  B --> AUDIT[Value-free audit metadata]

Two boundaries

Storage protection answers where plaintext rests. Materialization protection answers whether a value enters agent-visible stdout, environment, files, or transcripts. They are separate guarantees.

Injection passes named values directly into a child environment without printing them. Materialization deliberately reveals a value and therefore requires the policy and human gate defined by the current command contract. All materializing paths must agree; a command-specific exception cannot contradict the system threat model.

The daemon hosts the lightweight broker so repeated launches do not trigger repeated platform prompts. Expensive or failure-prone work remains outside the daemon's critical loop. Remote use transports values on demand to an authenticated target and never turns them into synced plaintext.

The reserved auth bundle is file-backed by construction: it holds long-lived Claude setup-tokens that usage/probe and unattended workers read without Touch ID. Creating it on the keychain or vault backend fails loud. The daemon's auth-sync service pushes a local file-backed auth bundle to pinned fleet devices that lack it, always with the file backend so each destination auto-provisions its own machine-local key.

The usage-read credential is role-gated (USAGE-READ-1/2). By default a usage read resolves only this file-based setup-token, never the interactive login (RUSH-1822) — the guarantee every background caller (daemon usage warm, auth-health probe, watchdog) keeps, since the fleet-logout revocation came from an unattended loop firing the interactive token at Anthropic. The setup-token itself lacks the user:profile scope a usage read requires (RUSH-2392), so on a worker/unmarked device — or any --json or piped reader — an account signed in interactively and nothing else reports usage unavailable (no usage credential). agents view names that state precisely instead of folding it into the generic bucket, which used to send operators back to claude setup-token for a remedy that cannot work (#2987); a cache that has not been read yet reports the distinct usage pending.

The one exception is a foreground human agents view on a personal device (selfConfiguredDeviceRole() === 'personal' and process.stdout.isTTY): the read falls through to the interactive OAuth login — the only credential carrying user:profile — so agents view --refresh repopulates a live session (5h) + week (7d) bar for every signed-in account. This mirrors the exec-credential role gate (EXEC-2a): the personal box authenticates from its interactive login; unattended loops and machine readers never touch it. A usage read never refreshes an access token — an expired interactive login reports expired-credential, not a silent refresh.

Actors, audit events, and usage counters contain metadata only. Redaction is defense in depth, not permission to publish raw transcripts.

Linux: headless servers and the encrypted-file fallback

Off macOS there is no platform keychain, so the encrypted-file store is the backend. Its data key is unwrapped from a machine-local key file at ~/.agents/.secrets-key/passphrase — mode 0600, generated on first use, never synced and never a DotAgents resource. That file is the store: a machine that has it can read every bundle on it.

Resolution is entirely non-interactive: the daemon-hosted broker reads the key file directly, and there is no TTY step anywhere in this list. A command that appears to wait for a passphrase is waiting for the transport passphrase of secrets push / secrets pull / --to-file, which is AGENTS_SYNC_PASSPHRASE — a different secret with a different lifetime. Headless sync is configured with that transport variable.

Never place the master key in a shell startup file. AGENTS_SECRETS_PASSPHRASE overrides the key file, so exporting it from ~/.zshenv, ~/.bashrc, or any other rc file leaves the plaintext key readable by every process the account starts — including agents — and agents doctor reports it as an env-secret-export warning. This is not hypothetical: it is what RUSH-1968 was, on seven machines at once, because an earlier revision of this page recommended it.

agents secrets export --device --remote-backend file never forwards AGENTS_SECRETS_PASSPHRASE. The remote auto-provisions its own machine-local key so headless reads work. Forwarding that env var used to key destination ciphertext to a secret the remote daemon did not hold, while import still printed success.