Credentials
Runcell agents authenticate two ways: with an AI subscription you already pay for, or with provider API keys. Subscriptions are the fastest way to run your first agent. API keys are the path for production.
Which mode?
Pick the row that matches your situation and use its exact config:
| Your situation | Config |
|---|---|
| You have a provider API key in an environment variable | Omit credentials (the default reads env, see the table below) |
| You have a Claude/ChatGPT/Copilot subscription, local development | credentials: 'local' after a one-time interactive login |
| Your app loads secrets itself (vault, config service) | credentials: { type: 'apiKeys', keys: { anthropic: '...' } } |
| One Vercel AI Gateway key for all providers | Set AI_GATEWAY_API_KEY and omit credentials |
| Many deployments share refreshable credentials | credentials: { type: 'shared' } with @runcell/postgres-credentials |
| Credentials live in a specific directory (CI cache, mounted secret) | credentials: { type: 'agentDir', path: '...' } |
| A preferred source with a backup (for example shared logins, then API keys) | An array: credentials: [{ type: 'shared', ... }, { type: 'env' }]. See Fallback chain |
The default (env) passes through exactly these variables: any *_API_KEY, any *_BASE_URL, ANTHROPIC_AUTH_TOKEN, and VERCEL_OIDC_TOKEN. The variable name for a provider is its id upper-cased with _API_KEY appended:
| Provider (model prefix) | Environment variable |
|---|---|
anthropic/... | ANTHROPIC_API_KEY |
openai/... | OPENAI_API_KEY |
google/... | GOOGLE_API_KEY |
| Vercel AI Gateway | AI_GATEWAY_API_KEY |
For AI coding agents
If you are an AI agent setting up runcell, these rules prevent the common failure modes:
- The subscription login is interactive and requires a human.
npx piopens a terminal UI, and/loginopens a browser sign-in. You cannot complete this step. Ask the human to run it once, then usecredentials: 'local'. - Check for existing logins without the terminal UI: if
~/.pi/agent/auth.jsonexists, logins are present andcredentials: 'local'will work. Do not runnpx pito find out. - Never create or edit
~/.pi/agent/auth.jsonby hand. The login flow writes it, and Pi refreshes it automatically. A hand-made file will not authenticate. Never commit it. - Use the exact environment variable names from the table above. Do not invent variants such as
OPENAI_KEYorANTHROPIC_TOKEN. - If neither logins nor API keys exist, stop and ask. Tell the human to either run
npx piand/login(subscription) or supply an API key. There is no credential you can generate yourself.
To give these rules to your own coding agents, paste this into your project's AGENTS.md or CLAUDE.md:
## runcell credentials
- Default (no `credentials` option) reads env vars: `ANTHROPIC_API_KEY`,
`OPENAI_API_KEY`, etc. (exact names, pattern `<PROVIDER>_API_KEY`).
- `credentials: 'local'` needs a one-time interactive login that only a
human can run: `npx pi`, then `/login`. Check that `~/.pi/agent/auth.json`
exists before using `'local'`; do not launch `npx pi` yourself.
- Never create, edit, or commit `~/.pi/agent/auth.json`.
- No logins and no API keys means stop and ask; you cannot generate
credentials. Reference: https://runcell.run/credentialsThe same rules are machine-readable at runcell.run/llms.txt.
Use your subscription
credentials: 'local' runs agents on the provider logins stored on your machine, with no API key required. Supported subscription logins:
- Anthropic Claude Pro/Max
- OpenAI ChatGPT Plus/Pro (Codex)
- GitHub Copilot
Runcell bundles the Pi engine, so its CLI is already in your project. Log in once:
npx pi # then type /login and pick your providerThis step is interactive: the terminal UI plus a browser sign-in. A human runs it once per machine. AI coding agents cannot complete it (see For AI coding agents).
The browser opens, you sign in, and the OAuth tokens land in ~/.pi/agent/auth.json. From then on:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: 'local',
});Runcell refreshes the tokens automatically. result.usage.costUsd still reports what each run would have cost through the provider's API, so costs stay observable on a flat-rate subscription (see RunUsage).
Provider terms govern subscription use, and they differ per provider and change over time, so review yours before relying on it. Individual use of your own subscription is the commonly accepted pattern (Anthropic said so publicly). API keys are the provider-supported path for deployed and commercial work. For that reason, runcell refuses 'local' when NODE_ENV is production unless you opt in explicitly, for example a remote test box running under your own account:
credentials: { type: 'local', allowInProduction: true }Default behavior: environment variables
If you omit credentials, runcell uses environment variables:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
});This is equivalent to:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: { type: 'env' },
});Explicit API keys
Use this when your application already loaded secrets from its own vault or configuration layer:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: {
type: 'apiKeys',
keys: {
anthropic: process.env.ANTHROPIC_API_KEY!,
},
},
});Explicit credential directory
Use a specific local credential directory:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: { type: 'agentDir', path: '/path/to/agent-dir' },
});Fallback chain
Pass an array to try credential sources in order:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: [
{ type: 'shared', key: 'prod', store }, // preferred: shared subscription logins
{ type: 'env' }, // fallback: API keys from the environment
],
events: {
onCredentialFallback: ({ from, to, cause }) =>
log.warn('credentials fell back', { from, to, cause }),
},
});A run starts on the first source. If it fails with a credential error (a login that can no longer refresh, no key for the resolved provider, a provider 401), runcell retries the whole run on the next source and fires onCredentialFallback for the hop. The retried failure does not reach onError. Only a failure on the last source does.
Two limits apply:
- No fallback after a tool runs. A retry of the run would repeat the tool's side effects, so a credential error after any tool call propagates instead. Credential errors happen on the first request to the provider, before any tool exists, so in practice this rarely blocks a fallback.
- Only credential errors fall back. Model errors, tool errors, and timeouts propagate immediately.
Each source is a complete credential set: the chain picks one source per attempt, and does not mix providers across sources within a run. Runcell validates every source with the same rules as a single source, including the production guard on 'local'. onCredentialFallback shows that a preferred source is failing. Treat repeated fallbacks as an alert.
Shared credential store
For deployments that need shared OAuth state or refreshable credentials, supply a lockable store. The official Postgres implementation is @runcell/postgres-credentials:
import pg from 'pg';
import { createPostgresCredentialStore } from '@runcell/postgres-credentials';
const store = createPostgresCredentialStore({
pool: new pg.Pool({ connectionString: process.env.DATABASE_URL }),
encryptionKey: process.env.CREDENTIAL_STORE_SECRET, // optional, at-rest encryption
});It serializes concurrent refreshes across deployments on a Postgres row lock, so a rotated token is never clobbered. When the database is unreachable, it serves the last blob that the process read (cacheFallback, on by default). For all options (table, ensureTable, lockTimeoutMs, cacheFallback, encryptionKey) and the manual migration SQL, see the package README.
Pass the store as a shared source. To back the store with something else, implement the interface yourself:
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: {
type: 'shared',
key: 'prod-agent-default',
store,
},
});A CredentialStore implementation receives the current credential blob and returns the updated blob when tokens rotate. Pi may read credentials for multiple providers in parallel, so concurrent withLock calls for the same key must wait in a queue. Do not reject a call merely because another caller holds the lock.
type StoredCredential =
| {
type: 'api_key';
key?: string;
env?: Record<string, string>;
}
| {
type: 'oauth';
access: string;
refresh: string;
expires: number;
[key: string]: unknown;
};
type AuthBlob = Record<string, StoredCredential>;The optional API-key key supports keyless or environment-backed providers such as Bedrock. OAuth entries may include provider-specific fields such as accountId.
const store: CredentialStore = {
async withLock(key, fn) {
// Acquire a queueing lock for this key before reading or updating it.
return queueFor(key, async () => {
const current = await loadCredentialBlob(key);
const { result, next } = await fn(current);
if (next !== undefined) {
await saveCredentialBlob(key, next);
}
return result;
});
},
};See examples/07-shared-credential-store.ts for a minimal in-memory queue.