Skip to content

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 situationConfig
You have a provider API key in an environment variableOmit credentials (the default reads env, see the table below)
You have a Claude/ChatGPT/Copilot subscription, local developmentcredentials: '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 providersSet AI_GATEWAY_API_KEY and omit credentials
Many deployments share refreshable credentialscredentials: { 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 GatewayAI_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 pi opens a terminal UI, and /login opens a browser sign-in. You cannot complete this step. Ask the human to run it once, then use credentials: 'local'.
  • Check for existing logins without the terminal UI: if ~/.pi/agent/auth.json exists, logins are present and credentials: 'local' will work. Do not run npx pi to find out.
  • Never create or edit ~/.pi/agent/auth.json by 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_KEY or ANTHROPIC_TOKEN.
  • If neither logins nor API keys exist, stop and ask. Tell the human to either run npx pi and /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:

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/credentials

The 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:

bash
npx pi     # then type /login and pick your provider

This 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:

ts
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:

ts
credentials: { type: 'local', allowInProduction: true }

Default behavior: environment variables ​

If you omit credentials, runcell uses environment variables:

ts
const agent = createAgent({
  model: 'anthropic/claude-sonnet-4-5',
});

This is equivalent to:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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.

ts
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.

ts
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.