ADR-0012: grm CLI names API keys per-device and revokes the old key on re-login

Status

Accepted

Tags

cli, grm, api-key, hostname, revocation, multi-device, authentication, better-auth

Decision

API keys created by grm auth login are named "grm-cli@<hostname>" using os.Hostname(), falling back to "grm-cli" if the hostname is unavailable. On re-login, the CLI lists all API keys for the authenticated user, filters for those matching the current device name, revokes them, and then creates a fresh key. Revocation is best-effort — login proceeds even if the delete call fails.

Why

A single developer may run grm from multiple machines sharing one account. A flat name like "grm-cli" would make it impossible to distinguish devices in the Better Auth dashboard and revocation on machine A would silently invalidate machine B’s key. Without any revocation, every grm auth login accumulates orphaned keys indefinitely since Better Auth’s apiKey plugin has no TTL. Per-device scoped revocation cleans up only the current machine’s stale key.

Known limitation

The hostname at key creation time is baked into the key name. If a machine is renamed, the old key becomes an orphan — it is not automatically cleaned up. The next grm auth login on the renamed machine creates a new key under the new hostname; the old one persists until manually revoked.

Rules for agents

  • Always use "grm-cli@<hostname>" as the key name — never a bare "grm-cli" unless hostname is unavailable
  • Revocation must filter by name match, not by the stored api_key value — another device may share the account
  • Do not block login on revocation failure — log a warning and continue

Bad pattern (do not generate)

// Revoking by stored key value — breaks multi-device accounts
if cfg.APIKey != "" {
    c.DeleteAPIKey(cfg.APIKey) // wrong — this is the key ID, and it may belong to another device
}

Good pattern

// Revoke only keys matching this device's name
name := deviceName() // "grm-cli@waynes-macbook"
if keys, err := existing.ListAPIKeys(); err == nil {
    for _, k := range keys {
        if k.Name == name {
            _ = existing.DeleteAPIKey(k.ID) // best-effort
        }
    }
}