Skip to content

Authentication Guide

Auth is where wrappers usually become untrustworthy.

This guide documents what deva.sh actually supports, what env vars it reads, and how credential files are mounted.

Rules First

  • Every agent has its own default auth home.
  • --auth-with <method> selects a non-default auth path.
  • --auth-with <file.json> is treated as an explicit credential file mount.
  • Non-default auth masks the agent's default credential file with a blank overlay unless the explicit credential file already occupies that path.
  • --dry-run is useful for mount and env inspection. It does not prove the credentials work.
  • Copilot --dry-run no longer starts the local proxy; it only shows the planned wiring.

Auth Matrix

Agent Default auth Other methods Main inputs
Claude claude api-key, oat, bedrock, vertex, copilot, credentials file .claude, .claude.json, ANTHROPIC_*, CLAUDE_CODE_OAUTH_TOKEN, AWS_*, gcloud, GH_TOKEN
Codex chatgpt api-key, copilot, credentials file .codex/auth.json, OPENAI_API_KEY, GH_TOKEN
Gemini oauth api-key, gemini-api-key, vertex, compute-adc, gemini-app-oauth, credentials file .gemini, GEMINI_API_KEY, gcloud, service-account JSON
Grok oauth api-key .grok/auth.json, XAI_API_KEY
Kimi oauth api-key .kimi-code (device-code), KIMI_CODE_API_KEY -> KIMI_MODEL_*
opencode oauth api-key .local/share/opencode/auth.json (device-code), OPENCODE_API_KEY

Claude

Default: --auth-with claude

Default Claude auth uses:

  • /home/deva/.claude
  • /home/deva/.claude.json

By default those come from the selected config home.

Example:

deva.sh claude
deva.sh claude -c ~/auth-homes/work

--auth-with api-key

This name is a little muddy because Claude supports more than one token shape here.

Accepted host inputs:

  • ANTHROPIC_API_KEY
  • ANTHROPIC_AUTH_TOKEN
  • CLAUDE_CODE_OAUTH_TOKEN

Optional endpoint override:

  • ANTHROPIC_BASE_URL

Examples:

export ANTHROPIC_API_KEY=sk-ant-...
deva.sh claude --auth-with api-key
export ANTHROPIC_BASE_URL=https://example.net/api
export ANTHROPIC_AUTH_TOKEN=token
deva.sh claude --auth-with api-key

If ANTHROPIC_API_KEY looks like a Claude OAuth token (sk-ant-oat01-...), deva auto-routes it as CLAUDE_CODE_OAUTH_TOKEN.

--auth-with oat

Requires:

  • CLAUDE_CODE_OAUTH_TOKEN

Optional:

  • ANTHROPIC_BASE_URL

Example:

export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...
deva.sh claude --auth-with oat

--auth-with bedrock

Uses AWS credentials from:

  • ~/.aws
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_SESSION_TOKEN
  • AWS_REGION

It also sets CLAUDE_CODE_USE_BEDROCK=1.

Example:

export AWS_REGION=us-west-2
deva.sh claude --auth-with bedrock

--auth-with vertex

Uses Google credentials from:

  • ~/.config/gcloud
  • GOOGLE_APPLICATION_CREDENTIALS when set to a host file path

It also sets CLAUDE_CODE_USE_VERTEX=1.

Example:

export GOOGLE_APPLICATION_CREDENTIALS=$HOME/keys/work-sa.json
deva.sh claude --auth-with vertex

--auth-with copilot

Requires either:

  • saved copilot-api token
  • GH_TOKEN
  • GITHUB_TOKEN

Deva starts the local copilot-api proxy, points Claude at the Anthropic-compatible endpoint, and injects dummy API key values where the CLI expects them.

Example:

export GH_TOKEN="$(gh auth token)"
deva.sh claude --auth-with copilot

--auth-with /path/to/file.json

Custom credential files are mounted directly to:

/home/deva/.claude/.credentials.json

Example:

deva.sh claude --auth-with ~/work/claude-prod.credentials.json

Bare names: the credentials store

A bare *.json name (no slash) resolves against the agent config home (~/.config/deva/claude/ by default) — a named credentials store:

deva.sh claude --auth-with claude-max.credentials.json
# -> ~/.config/deva/claude/claude-max.credentials.json

Resolution order: current directory first (compat), then the store. With an explicit --config-home root, the store is <root>/<agent>/. A missing bare name provisions into the store.

Provisioning new credentials

If the file does not exist, deva offers to create it via TUI login:

deva claude -Q --rm --auth-with ~/creds/xxx-claude-max.credentials.json -- --resume
# -> "Credentials file not found: .../xxx-claude-max.credentials.json"
# -> "Create it via TUI login? [y/N]"
# Log in via /login in the TUI, then exit when done.
# -> "Credentials captured: .../xxx-claude-max.credentials.json"
# -> "  subscription: max, expires in ~365d"
# -> "Reuse: deva claude --auth-with .../xxx-claude-max.credentials.json"

Use -Q (bare mode) to avoid polluting your default config home with the new account's identity. The placeholder file is removed automatically if no credentials are captured.

Codex

Default: --auth-with chatgpt

Uses:

  • /home/deva/.codex/auth.json

Usually from the selected config home.

--auth-with api-key

Requires:

  • OPENAI_API_KEY

Example:

export OPENAI_API_KEY=sk-...
deva.sh codex --auth-with api-key

--auth-with copilot

Requires either:

  • saved copilot-api token
  • GH_TOKEN
  • GITHUB_TOKEN

Deva points Codex at the OpenAI-compatible side of the proxy and defaults the model to gpt-5-codex unless you supplied one.

Example:

export GH_TOKEN="$(gh auth token)"
deva.sh codex --auth-with copilot

--auth-with /path/to/file.json

Custom credential files are mounted to:

/home/deva/.codex/auth.json

Example:

deva.sh codex --auth-with ~/work/codex-auth.json

Gemini

Default: --auth-with oauth

Uses:

  • /home/deva/.gemini

gemini-app-oauth is treated as the same app-style OAuth family.

--auth-with api-key or gemini-api-key

Requires:

  • GEMINI_API_KEY

When this mode is active and not running under --dry-run, deva makes sure the Gemini settings file in the chosen config home selects API-key auth. Gemini state can include both .gemini/ content and a top-level settings.json, depending on what the CLI has already written there.

Example:

export GEMINI_API_KEY=...
deva.sh gemini --auth-with api-key

--auth-with vertex

Uses:

  • ~/.config/gcloud
  • GOOGLE_APPLICATION_CREDENTIALS
  • GOOGLE_CLOUD_PROJECT
  • GOOGLE_CLOUD_LOCATION

Example:

export GOOGLE_CLOUD_PROJECT=my-project
export GOOGLE_CLOUD_LOCATION=us-central1
deva.sh gemini --auth-with vertex

--auth-with compute-adc

Uses Google Compute Engine application default credentials from the metadata server. That is mostly for workloads already running on GCP.

--auth-with /path/to/file.json

Custom service-account files are mounted to:

/home/deva/.config/gcloud/service-account-key.json

And GOOGLE_APPLICATION_CREDENTIALS is set to that container path.

Example:

deva.sh gemini --auth-with ~/keys/gcp-service-account.json

Grok

Default: --auth-with oauth

Uses:

  • /home/deva/.grok

Grok stores its session token in .grok/auth.json. First login opens a browser, which does not exist inside the container. Two ways in:

  • authenticate on the host once; deva auto-links ~/.grok and the mount carries auth.json into the container
  • run grok login --device-auth inside the container: it prints a URL and code you complete on any device

--auth-with api-key

Requires:

Grok's credential priority puts mounted config above the environment: model.api_key > model.env_key > session token > XAI_API_KEY. So in this mode deva mounts no ~/.grok at all — the exported key is the only credential in the container, and it is what gets billed. Session state lives container-local and does not persist across runs in this mode.

If you force a ~/.grok mount with -v anyway, deva still blank-overlays the default auth.json, but per-model keys in a mounted config.toml outrank XAI_API_KEY — don't mix the two.

Example:

export XAI_API_KEY=...
deva.sh grok --auth-with api-key

One more grok-specific wire

Grok keeps its real binary in ~/.grok/bin/ (its self-update dir) and the npm launcher resolves that path first. Inside the image, deva moves the binary to ~/.local/bin/grok and removes ~/.grok/bin, so a host-mounted ~/.grok (which may contain a macOS binary) never shadows it.

The reverse direction is guarded too: grok's self-updater writes into ~/.grok/bin/ and ~/.grok/downloads/. A mounted config.toml without the npm installer marker makes the updater treat the install as self-managed, so grok update would drop Linux binaries into the host mount — breaking a macOS host CLI via its npm launcher. Whenever a host dir is mounted at /home/deva/.grok, deva overlays those two paths with container-local tmpfs: in-container grok update works, its writes die with the container, and the host install stays intact. The image pin (GROK_CLI_VERSION) is the only version that matters.

Kimi

Default: --auth-with oauth

Mounts:

  • /home/deva/.kimi-code

Kimi Code stores config, sessions, and its OAuth token under ~/.kimi-code (overridable via KIMI_CODE_HOME). First login has no browser, so run the device-code flow inside the container:

  • run kimi then /login (or kimi login): it prints a URL + code; open it on any device to authorize, and
  • authenticate on the host once; deva auto-links ~/.kimi-code and the mount carries the token in.

--auth-with api-key

Inputs:

  • KIMI_CODE_API_KEY (from platform.kimi.com); KIMI_API_KEY is accepted as a fallback when KIMI_CODE_API_KEY is unset

Kimi is the odd one out: it reads no API key from the shell environment. Its docs are explicit — export KIMI_API_KEY=... gives no provider its key; credentials come only from ~/.kimi-code/config.toml. The single exception is the KIMI_MODEL_* family, which reads the shell and synthesizes an in-memory provider. So deva maps your KIMI_CODE_API_KEY onto that channel:

KIMI_MODEL_NAME=k3                                # DEVA_KIMI_MODEL to override
KIMI_MODEL_API_KEY=$KIMI_CODE_API_KEY
KIMI_MODEL_PROVIDER_TYPE=kimi
KIMI_MODEL_BASE_URL=https://api.kimi.com/coding/v1  # DEVA_KIMI_BASE_URL to override

The key is never written to config.toml (it lives in memory), so this mode mounts no ~/.kimi-code and nothing lands on disk. sk-kim… keys hit the Kimi Code coding endpoint above; for a direct Moonshot key (platform.moonshot.ai) set DEVA_KIMI_BASE_URL=https://api.moonshot.ai/v1.

export KIMI_CODE_API_KEY=...
deva.sh kimi --auth-with api-key
# pick a different model:
DEVA_KIMI_MODEL=kimi-for-coding deva.sh kimi --auth-with api-key

Unlike grok, kimi's npm bin is a plain symlink to dist/main.mjs (no self-update trampoline, no platform binary), so there is no host-mount shadowing to guard against. The image pin (KIMI_CODE_VERSION) is the only version that matters.

opencode

Default: --auth-with oauth

Mounts (opencode is XDG-native — three dirs instead of one dot-dir):

  • /home/deva/.config/opencode (config, plugins)
  • /home/deva/.local/share/opencode (auth.json, session db, logs)
  • /home/deva/.local/state/opencode (model prefs, prompt history)

~/.cache/opencode stays container-local on purpose: it only holds the models.json cache and the self-updater's bin dir, and the image pins the CLI version (OPENCODE_DISABLE_AUTOUPDATE=1 is set in the container).

First login has no browser, so use the device-code flow inside the container: run opencode auth login (or /connect in the TUI), open the printed URL on any device. Or authenticate on the host once; deva auto-links the three XDG dirs and the mount carries auth.json in.

opencode's permission model already allows everything inside the workspace; deva additionally unlocks the interactive asks (outside-workspace access, doom-loop guard, .env reads) via OPENCODE_PERMISSION — the container is the sandbox.

--auth-with api-key

Inputs:

The key travels as env only and authenticates the opencode gateway provider. This mode mounts none of the XDG dirs: a mounted auth.json outranks the env key and could silently bill another account (same no-mount contract as grok/kimi api-key). A blank overlay hides auth.json even if a user -v carries a data dir in.

export OPENCODE_API_KEY=sk-...
deva.sh opencode --auth-with api-key

BYO provider keys (Anthropic, OpenAI, OpenRouter, ...) are opencode config, not deva auth methods — wire them with -e / .deva ENV= entries and opencode's own opencode.jsonc.

Config Homes And Auth Isolation

Default homes live under:

~/.config/deva/claude
~/.config/deva/codex
~/.config/deva/gemini
~/.config/deva/grok
~/.config/deva/kimi
~/.config/deva/opencode

Use --config-home when you want a separate identity:

deva.sh claude -c ~/auth-homes/work
deva.sh codex -c ~/auth-homes/personal

Good reasons to split auth homes:

  • work vs personal accounts
  • OAuth vs API-key experiments
  • different org endpoints
  • reproducing auth bugs without contaminating your default state

Debugging Auth

Useful commands:

deva.sh --show-config
deva.sh claude --auth-with api-key --debug --dry-run
deva.sh shell

What to check in --dry-run:

  • the chosen auth label
  • expected env vars are present
  • unexpected auth env vars are absent
  • the explicit credential file mount points at the right container path
  • the blank overlay exists when non-default auth is active

What --dry-run cannot tell you:

  • whether the remote endpoint accepts the token
  • whether the agent CLI likes that token shape
  • whether your cloud credentials are actually authorized