Credentials

Sandboxing addresses what the agent is allowed to do. Credentials address how the agent proves who it is — to the model provider, MCP servers, OAuth endpoints, Git hosts, and cloud platforms. Credentials expire. A mid-conversation 401 doesn’t gracefully degrade a task; it breaks it. Treat the lifecycle as infrastructure.


The Credential Stack

A session holds multiple credentials, each with different lifetimes, scopes, and refresh mechanisms.

graph TB
    subgraph credentials ["Active Credentials"]
        API["Model Provider API Key<br/>Anthropic, OpenAI, Google"]
        OAuth["OAuth Access Token<br/>GitHub, Slack, Calendar"]
        MCP["MCP Server Credentials<br/>Per-server env vars"]
        GIT["Git Credentials<br/>SSH keys, PATs"]
        CLOUD["Cloud Provider<br/>AWS, GCP, Azure tokens"]
    end

    subgraph lifecycle ["Lifecycle Operations"]
        ACQ["Acquire"]
        STORE["Store"]
        REFRESH["Refresh"]
        REVOKE["Revoke"]
    end

    credentials --> lifecycle
Credential TypeTypical LifetimeRefreshStorage
API key (Anthropic, OpenAI)IndefiniteManual rotationOS keychain or env var
OAuth access token15 min - 1 hr (drifting shorter)Refresh-token exchangeOS keychain
OAuth refresh token30-90 days (LinkedIn 1 yr; MS SPA 24 hr)Re-authOS keychain
MCP server env varsIndefiniteManual${ENV_VAR} in .mcp.json, resolved at spawn
Git PAT30-365 daysManual regenSystem credential store
Cloud provider token1-12 hoursCLI (gcloud auth, aws sso)Provider-managed

OAuth 2.1 Baseline

The reference point in 2026 is OAuth 2.1, consolidating RFC 7636 (PKCE), RFC 6749 BCP, and RFC 8252. The practical consequences:

PKCE Flow

sequenceDiagram
    participant A as Agent Harness
    participant B as User's Browser
    participant P as OAuth Provider

    A->>A: Generate code_verifier + code_challenge
    A->>B: Open authorization URL with code_challenge
    B->>P: User authenticates and approves scopes
    P->>A: Redirect to localhost callback with auth code
    A->>P: Exchange auth code + code_verifier for tokens
    P->>A: { access_token, refresh_token, expires_in }
    A->>A: Store tokens in OS keychain

The localhost callback runs on a random port and shuts down after receiving the code. For headless agents, swap the redirect leg for RFC 8628: the harness polls the token endpoint while the user completes the device-code grant in any browser.


Token Storage

Old default: ~/.agent/oauth/<provider>.json with 0600 permissions. Still widely shipped for compatibility, but now flagged as risk in every 2026 security review. chmod 600 is necessary, not sufficient.

2026 recommended baseline: the OS-native credential store — macOS Keychain, Windows Credential Manager (wincred), Linux libsecret / Secret Service — with file-fallback for headless servers without a daemon. Cross-platform wrappers handle the fork: keyring (Python), hrantzsch/keychain (C++), git-credential-manager, mcp-secrets-plugin. New MCP-server projects increasingly default to keychain-first.

Non-negotiables don’t change: never commit, never log, redact from traces. Tenant-multiplexed agents must also encrypt at rest and isolate per tenant.


Proactive Refresh

The most common credential failure is a token expiring mid-conversation. The model makes a call, gets a 401, and either retries in a loop or gives up. Both are preventable.

Token lifetime: ──────────────────────────────────────────► expiry

                                    ~75-80% of TTL
                                    REFRESH HERE

                        ┌─────────────────┼──────────────┐
                        │  Safe zone      │  Danger zone │
                        │  Token valid    │  May expire  │
                        └─────────────────┼──────────────┘

The “80% rule” is convention, not standard — no RFC prescribes a number; sources cite 70-80%, with Azure SDK and Scalekit both landing on ~75%. Pick a threshold in that range, combine with startup validation and a reactive 401 retry as safety net, and call ensure_valid_token before every API request — not after a failure. Refresh-token rotation is mandatory under OAuth 2.1, so persist the new refresh token on every successful refresh.

def ensure_valid_token(cred):
    ttl = cred.expires_at - cred.created_at
    if current_time() < cred.created_at + (ttl * 0.75):
        return cred
    try:
        new = oauth_refresh(cred.provider, cred.refresh_token)
        save_credential(new)  # MUST persist rotated refresh token
        return new
    except RefreshError:
        return prompt_reauthentication(cred.provider)

FAPI 2.0 and DPoP

For finance and health agents handling regulated data, the next tier above OAuth 2.1 is FAPI 2.0 (Final 2025) plus DPoP — proof-of-possession token binding that stops stolen tokens being replayed without the matching key. Singpass enforces FAPI 2.0 in production from Jan 2026. Most agents won’t need this; if you handle PHI or trade orders, it’s the baseline, and the DPoP pattern is leaking into general MCP server guidance.


MCP Credential Injection

Each MCP server may need its own credentials, configured via env-variable references in .mcp.json:

{
  "mcpServers": {
    "github":   { "env": { "GITHUB_TOKEN":   "${GITHUB_TOKEN}" } },
    "postgres": { "env": { "DATABASE_URL":   "${DATABASE_URL}" } },
    "slack":    { "env": { "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}" } }
  }
}

The host resolves ${...} at server-spawn time, not at config load — so values can come from a secrets manager invoked just before the spawn. Each MCP server sees only its declared variables. An unset variable should produce a clear error, not an empty string that fails downstream.

For production, prefer Vault / AWS Secrets Manager / Azure Key Vault over shell env. Cleartext literals in .mcp.json and accidental commits remain the two most-reported MCP credential incidents of 2026.


Credential Isolation

CredentialWho Uses ItIsolation Requirement
Model API keyHarness onlyNever reaches MCP servers or tool processes
OAuth tokensHarness + specific toolsPer-provider scoping; only relevant token per tool
MCP server env varsIndividual MCP serversEach server sees only its declared vars
Git credentialsGit operations onlyAvailable only during bash(git *) calls

The two-phase runtime (Sandboxing) extends to credentials: setup authenticates and stores, execution exposes credentials only to the specific tool or server that needs them.


Agent-Scoped OAuth: Current Reality

The widely-quoted scopes agent:read, agent:draft_only, agent:write are aspirational and vendor-specific, not ratified. No RFC or major provider defines them as a portable namespace; IETF work (draft-rosenberg-oauth-aauth-00, draft-klrc-aiagent-auth-01 introducing the agent_assertion grant type) is all pre-RFC. The real 2026 pattern is per-tool/per-action scopes (mcp:tool:read_file:read, calendar:create_event, gmail:draft) combined with On-Behalf-Of delegation via standard OAuth 2.0 extensions — Stytch, WorkOS, Scalekit, and Apideck all ship variants.


Logout and Revocation

Explicit logout is a safety requirement, not convenience.

agent logout                 # all providers
agent logout github          # one provider
agent logout --oauth-only    # keep API keys

On logout: delete the keychain entry (or file), attempt server-side revocation where supported (GitHub, Google, Slack all do), clear in-memory caches, and tell the user which services will require re-authentication.


Cross-Platform Patterns

PlatformStorageRefresh Mechanism
Claude CodeOS keychain; file fallback at ~/.claude/oauth/Harness PKCE refresh; device-code for headless
OpenAI CodexSystem keychain or OPENAI_API_KEY; ChatGPT OAuth via desktop appHarness-managed silent refresh
Gemini CLIGoogle Cloud credential store via gcloud authgcloud refreshes automatically
Google ADKProvider-managed (Cloud IAM, Workload Identity Federation)ADC / service account; no harness involvement

The shape is consistent: API key or device-code OAuth for the model provider, PKCE + refresh for third-party integrations, ${ENV_VAR} for MCP servers, OS keychain underneath. The variation is whether the platform refreshes for you (Google) or expects the harness to (Claude Code, Codex).