Credential Lifecycle
The Agent’s Own Identity Problem
Sandboxing & Permissions addresses what an agent is allowed to do. This page addresses a prerequisite: how the agent proves who it is to the services it depends on — the model provider API, MCP servers, OAuth endpoints, Git hosts, and cloud platforms.
Credential management is not a configuration problem. Credentials expire, need rotation, require secure storage, and must be refreshed proactively — often mid-conversation. An agent that loses its API token during a multi-step task is not gracefully degraded; it is broken. The credential lifecycle is infrastructure, not plumbing.
The Credential Stack
An agent in a typical session holds multiple credentials simultaneously, 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 Type | Typical Lifetime | Refresh Mechanism | Storage Location |
|---|---|---|---|
| API key (Anthropic, OpenAI) | Indefinite (until rotated) | Manual rotation | ~/.agent/credentials or env var |
| OAuth access token | 1 hour | Refresh token exchange | ~/.agent/oauth/{provider}.json |
| OAuth refresh token | 30-90 days | Re-authentication | ~/.agent/oauth/{provider}.json |
| MCP server env vars | Indefinite | Manual update | .mcp.json references, resolved at runtime |
| Git PAT | 30-365 days | Manual regeneration | System credential store or env var |
| Cloud provider token | 1-12 hours | CLI tool refresh (gcloud auth, aws sso) | Provider-managed store |
OAuth for Agents
Most non-trivial agent deployments require OAuth for at least one service (GitHub for PRs, Google for Calendar, Slack for notifications). The agent harness must implement a complete OAuth flow — not delegate it to the user.
PKCE Authorization Code Flow
The standard flow for CLI agents is OAuth 2.0 with PKCE (Proof Key for Code Exchange), which does not require a client secret:
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 securely
Key implementation details:
- Localhost callback server: The harness starts a temporary HTTP server on a random port (e.g.,
http://127.0.0.1:PORT/callback) to receive the authorization code. The server shuts down immediately after receiving the callback. - PKCE is mandatory: Never use the implicit flow or store client secrets in CLI applications. PKCE is the standard for public clients (RFC 7636).
- Scope minimization: Request only the scopes the agent actually needs.
repoon GitHub is almost always too broad; preferread:org,public_repo, or fine-grained tokens.
Token Storage
Credentials must be stored securely on disk with appropriate file permissions:
// ~/.agent/oauth/github.json
{
"provider": "github",
"access_token": "gho_xxxxxxxxxxxx",
"refresh_token": "ghr_xxxxxxxxxxxx",
"token_type": "bearer",
"scope": "repo,read:org",
"expires_at": "2026-04-02T10:30:00Z",
"created_at": "2026-04-02T09:30:00Z"
}
- File permissions:
600(owner read/write only). - Directory permissions:
700on the parent~/.agent/oauth/. - Never store tokens in configuration files that are committed to version control.
.mcp.jsonuses${ENV_VAR}references precisely to avoid this. - Never log tokens. Redact credential values from traces, debug output, and error messages.
Proactive Refresh
The most common credential failure in agent systems is a token expiring mid-conversation. The model makes an API call, gets a 401, and either retries in a loop (burning tokens) or gives up (breaking the task). Both outcomes are preventable.
The Refresh Timeline
Token lifetime: ──────────────────────────────────────────► expiry
│
80% of TTL
REFRESH HERE
│
┌─────────────────┼──────────────┐
│ Safe zone │ Danger zone │
│ Token valid │ May expire │
│ No action │ mid-request │
└─────────────────┼──────────────┘
Refresh at 80% of TTL, not on expiry. For a 1-hour access token, refresh at the 48-minute mark. This provides a 12-minute buffer for network delays, clock skew, and retry attempts.
Refresh Implementation
def ensure_valid_token(credential):
now = current_time()
ttl = credential.expires_at - credential.created_at
refresh_threshold = credential.created_at + (ttl * 0.8)
if now < refresh_threshold:
return credential # Still in safe zone
# Attempt refresh
try:
new_tokens = oauth_refresh(
provider=credential.provider,
refresh_token=credential.refresh_token
)
save_credential(new_tokens)
return new_tokens
except RefreshError:
# Refresh token may be expired — require re-authentication
return prompt_reauthentication(credential.provider)
Call ensure_valid_token before every API request, not after a 401. This turns credential refresh from error handling into infrastructure.
MCP Server Credentials
Each MCP server may require its own credentials, configured via environment 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 harness resolves ${VAR} references at MCP server startup, not at config load time. This means:
- Environment variables can be set dynamically (e.g., from a secrets manager) before the server is spawned.
- Different MCP servers can use different credentials without sharing environment.
- If a variable is unset, the harness should emit a clear error (“GITHUB_TOKEN is not set — the GitHub MCP server cannot start”) rather than passing an empty string that produces cryptic downstream failures.
Credential Isolation
The agent harness manages credentials for itself and for the tools it connects to. These must be isolated:
| Credential | Who Uses It | Isolation Requirement |
|---|---|---|
| Model API key | Harness only | Never passed to MCP servers or tool processes |
| OAuth tokens | Harness + specific tools | Scoped per provider; only the relevant token is passed to each tool |
| MCP server env vars | Individual MCP servers | Each server sees only its own declared variables, not the full environment |
| Git credentials | Git operations only | Accessible only during bash(git *) calls, not arbitrary shell commands |
The two-phase runtime pattern (Sandboxing) extends to credentials: during the setup phase, the harness authenticates and stores tokens; during the execution phase, credentials are available only to the specific tool or server that needs them, not to the agent’s general-purpose execution environment.
Logout and Revocation
Users must be able to explicitly clear stored credentials. This is a safety requirement, not a convenience feature.
# Clear all stored credentials
agent logout
# Clear credentials for a specific provider
agent logout github
# Clear OAuth tokens only (keep API keys)
agent logout --oauth-only
On logout:
- Delete the credential file from disk.
- Attempt to revoke the token with the provider (if the provider supports token revocation — GitHub, Google, and Slack all do).
- Clear any in-memory cached credentials.
- Inform the user which services will stop working: “GitHub MCP server will require re-authentication on next use.”
Cross-Platform Credential Patterns
| Platform | API Auth | OAuth Support | Credential Storage | MCP Credentials |
|---|---|---|---|---|
| Claude Code | API key via ANTHROPIC_API_KEY env var, or OAuth PKCE for claude.ai accounts | Built-in OAuth with PKCE, browser-based login flow | ~/.claude/oauth/ with file permissions | ${ENV_VAR} references in .mcp.json, resolved at server spawn |
| OpenAI Codex | API key via OPENAI_API_KEY env var | OAuth via ChatGPT desktop app (implicit) | System keychain or env var | ${ENV_VAR} in codex config |
| Gemini CLI | Google Cloud auth (gcloud auth login) | Google OAuth via browser, managed by gcloud CLI | Google Cloud credential store | ${ENV_VAR} in MCP config |
| Google ADK | Application Default Credentials (ADC) | Separate agent-auth and user-auth flows | Provider-managed (Google Cloud IAM) | Service account keys or Workload Identity |
The pattern is consistent: API keys for the model provider, OAuth for third-party integrations, and environment variable references for MCP server credentials. The variation is in storage and refresh — some platforms handle refresh automatically (Google Cloud), while others require the harness to implement it (Claude Code, Codex).
Key Takeaways
- Credential management is infrastructure, not configuration. Tokens expire, refresh flows fail, and mid-conversation auth failures break tasks. Treat credentials with the same engineering rigor as context management.
- Implement proactive refresh at 80% of TTL. Never wait for a 401 to discover that a token has expired.
- Isolate credentials by scope. The model API key should never reach an MCP server. Each MCP server should see only its own environment variables.
- Store credentials in dedicated files with restricted permissions (
600). Never in configuration files, environment exports, or logs. - Provide explicit logout and revocation commands. Users must be able to clear credentials completely, and the harness should attempt server-side revocation when supported.
- PKCE is the standard for CLI-based OAuth. No client secrets. No implicit flow. No exceptions.