Skip to content

Agent authentication

A human always owns the account and the data. An agent — Claude Code, Cursor, Codex, Gemini CLI, a CI bot — never holds an account of its own. It holds an agent token: a delegated credential minted by a human, bound to that human and to exactly one repository, restricted to an explicit scope list, revocable at any time from the dashboard, and visibly distinct from the human in every audit trail. Reviews recorded through an agent token are attributed "<label> (agent, on behalf of <owner>)" — never silently as the owner.

Agent tokens look like dbat_… and are sent as a Bearer header on every /api/v1 endpoint (alongside the existing HTTP Basic scheme for CI push tokens, which is unchanged):

sh
curl -H "Authorization: Bearer dbat_…" \
  https://dungbeetle.dev/api/v1/repository

Get a token with the CLI

The easiest path — dungbeetle login runs the device flow for you:

sh
dungbeetle login \
  --label "Claude Code on MacBook" \
  --scopes runs:read baselines:read analytics:read reviews:write
First, copy your one-time code: XXXX-XXXX
Then approve this agent in your browser: https://dungbeetle.dev/ui/connect?code=XXXX-XXXX
Waiting for approval…
Connected as "Claude Code on MacBook" to repository my-app.
Scopes: runs:read baselines:read analytics:read reviews:write
Credential stored at ~/.config/dungbeetle/credentials.json.

Open the printed URL signed in as yourself, check the label and requested scopes, pick the repository the agent may touch, and approve. The CLI stores the token at $XDG_CONFIG_HOME/dungbeetle/credentials.json (mode 0600, keyed by server origin), where push / push-baselines pick it up automatically. --label defaults to dungbeetle CLI on <hostname>; omitting --scopes requests the Triage preset. dungbeetle logout revokes the token on the server and deletes it locally. The CLI talks to the hosted cloud by default — self-hosting? Point it at your server with --server <url> or DUNGBEETLE_SERVER_URL.

The approval can happen from any browser on any machine — the code is what links them — so headless boxes and cloud sandboxes work too.

The device flow, raw

For a custom agent, the same flow is two public HTTP endpoints (RFC 8628 shaped; no auth header — the agent has no credential yet).

1. Start an authorization:

sh
curl -X POST https://dungbeetle.dev/auth/device/code \
  -H 'content-type: application/json' \
  -d '{"label": "My custom agent", "scopes": ["runs:read", "reviews:write"]}'
json
{
  "device_code": "…high-entropy secret, keep private…",
  "user_code": "XXXX-XXXX",
  "verification_uri": "https://dungbeetle.dev/ui/connect",
  "verification_uri_complete": "https://dungbeetle.dev/ui/connect?code=XXXX-XXXX",
  "expires_in": 600,
  "interval": 5
}

Show the human the user_code and verification_uri_complete. scopes is optional and defaults to the Triage preset; an unknown scope is a 400.

2. Poll for the token every interval seconds while the human approves at /ui/connect:

sh
curl -X POST https://dungbeetle.dev/auth/device/token \
  -H 'content-type: application/json' \
  -d '{"device_code": "…"}'

Until approval, the poll returns an RFC 8628 error in the standard { "error": … } envelope:

ErrorStatusMeaning
authorization_pending400Not approved yet — keep polling.
slow_down429Polling too fast — add 5s to the interval (honor Retry-After).
expired_token400The 10-minute window passed — start over.
access_denied400The human denied the request.
invalid_grant400Unknown or already-redeemed device_code.

On approval, the next poll mints and returns the token — codes are single-use, so exactly one poll can ever redeem an authorization:

json
{
  "token": "dbat_…",
  "token_type": "bearer",
  "label": "My custom agent",
  "scopes": ["runs:read", "reviews:write"],
  "repository": { "id": "repo_…", "name": "my-app" }
}

The plaintext token is delivered exactly once — store it securely.

Scopes

Scopes are granted at approval time and enforced deny-by-default: a request outside the token's scopes gets 403 {"error": "This agent token lacks the '<scope>' scope."}, and endpoints not on this map at all (there is no agent-reachable account, team, billing, or token-minting surface) return 403 for any agent token.

ScopeGrants
runs:readList runs, fetch a run + report, fetch run screenshots.
runs:writeIngest runs, probe/upload screenshot artifacts.
baselines:readList baselines, read version history and snapshots.
baselines:writeUpload baseline versions.
reviews:writeApprove/reject/reopen a run, including promoting candidates to baselines.
analytics:readTrend, flakiness, and usage-vs-plan-limits reads.

GET /api/v1/repository (identity probe) and DELETE /api/v1/token (self-revoke) work with any valid agent token, regardless of scopes.

Presets offered at approval:

PresetScopesUse for
Triage (recommended)runs:read baselines:read analytics:read reviews:writeCoding agents that triage and approve — they read diffs but can't push runs or rewrite baselines directly.
Read-onlyruns:read baselines:read analytics:readReporting and dashboards.
FullAll sixAgents that also capture and push.

Manage and revoke

  • Dashboard/ui/agents lists your agent tokens (label, repository, scopes, created, last used, optional expiry) and pending device codes, with one-click Revoke. Revocation is immediate: the next request gets 401.
  • Self-revoke — a Bearer token may always revoke itself: curl -X DELETE -H "Authorization: Bearer dbat_…" …/api/v1/token. dungbeetle logout does this and removes the stored credential.
  • Expiry — optional, set at approval; expired tokens 401 and are swept by retention pruning.

Security notes

  • Hashed at rest. Tokens are stored as a peppered HMAC-SHA-256 hash, never plaintext; verification is constant-time. The high-entropy device_code is hashed the same way; the short user_code only routes a signed-in human to the pending request and is useless without a session.
  • One repository per token. A token can never see or touch anything outside the repository the human picked at approval — and the server re-checks that the approver actually has access to it.
  • Owned, not autonomous. Deleting the owning user (or the repository) cascades the token away; an agent credential cannot outlive its owner.
  • Audited. Every mint, use, denial, and revocation is written to the structured audit log with the agent's label; reviews carry "<label> (agent, on behalf of <owner>)" in the append-only trail.
  • Single-use redemption. Device codes expire in 10 minutes, are rate-limited per IP, and can be redeemed exactly once.

Next: wire the token into your agent — Claude Code, Cursor, Codex, Gemini CLI, or any MCP client via Setup.

Source-available: CLI under FSL-1.1-ALv2, cloud server under BUSL-1.1. See Licensing.