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-runis useful for mount and env inspection. It does not prove the credentials work.- Copilot
--dry-runno 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_KEYANTHROPIC_AUTH_TOKENCLAUDE_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:
~/.awsAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKENAWS_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/gcloudGOOGLE_APPLICATION_CREDENTIALSwhen 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-apitoken GH_TOKENGITHUB_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-apitoken GH_TOKENGITHUB_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/gcloudGOOGLE_APPLICATION_CREDENTIALSGOOGLE_CLOUD_PROJECTGOOGLE_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
~/.grokand the mount carriesauth.jsoninto the container - run
grok login --device-authinside the container: it prints a URL and code you complete on any device
--auth-with api-key¶
Requires:
XAI_API_KEY(from console.x.ai)
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
kimithen/login(orkimi login): it prints a URL + code; open it on any device to authorize, and - authenticate on the host once; deva auto-links
~/.kimi-codeand the mount carries the token in.
--auth-with api-key¶
Inputs:
KIMI_CODE_API_KEY(from platform.kimi.com);KIMI_API_KEYis accepted as a fallback whenKIMI_CODE_API_KEYis 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:
OPENCODE_API_KEY(service-account key from console.opencode.ai)
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