Skip to content

CLI: support multiple identities per server (PAD_TOKEN env override + named credential profiles) #879

Description

@b4rk13

Problem

~/.pad/credentials.json (v2, per TASK-1228) holds exactly one credential per server URL.
That fits one human per machine, but breaks the increasingly common setup of multiple AI
coding agents sharing one machine and one CLI install
, where each agent should act as its
own pad user so comments and item changes are attributed correctly.

Concrete case: Claude Code and Cursor both drive pad on the same workstation against one
self-hosted server. Whichever user is logged in "wins" — both agents write as that user.
The only workaround inside the CLI is pad auth logout / pad auth login around every
run, which is unusable when agents run concurrently (the credentials file is a shared
mutable singleton).

The pieces are almost all there already:

  • The server accepts long-lived pad_ API tokens on every endpoint
    (internal/server/middleware_auth.go), with user-scoped mint/rotate/revoke endpoints and
    a web settings UI — but the CLI has no way to select a token other than the single
    stored login.
  • The v2 store comment in internal/cli/credentials.go explicitly anticipates this:
    "The v2 schema can be extended later if a use case shows up; the current map shape is
    forward compatible."
    This is that use case.

Workaround today (for anyone hitting this before a fix lands)

Credential entries are keyed by the literal URL string (normalizeServerURL strips only
whitespace and trailing slashes — no hostname canonicalization), so two different URLs
reaching the same server get independent entries. Combined with the PAD_URL env override
(internal/config/config.go), each agent can hold its own login:

  1. Add a hosts-file/DNS alias for the pad server, e.g. pad-cursor192.168.1.50.
  2. In the second agent's environment only: PAD_URL=http://pad-cursor:7777, then
    pad auth login -i as the second pad user (-i avoids the browser flow, which would
    authenticate as whoever is signed into the web UI).
  3. credentials.json now holds entries for both http://192.168.1.50:7777 and
    http://pad-cursor:7777; each agent resolves its own token by URL, concurrently, with
    no logout/login cycling.

This works because store reads are side-effect-free and login only Set()s the entry for
the URL being logged into. But it hides identity selection inside infrastructure config
(hosts/DNS), which is the gap this issue proposes to close properly.

Proposed fix (two independent PRs, smallest first)

1. PAD_TOKEN environment override

If PAD_TOKEN is set, the CLI uses it as the bearer token and skips the credential-store
lookup — same convention as gh's GH_TOKEN. Each agent process gets its own API token in
its environment; zero file contention, no schema change.

Touch points:

  • internal/cli/client.go — check PAD_TOKEN in NewClientFromURL before the
    LoadStore() block (~line 64).
  • cmd/pad/cmd_auth.gowhoami currently short-circuits on the store lookup before
    building a client; it should honor PAD_TOKEN and report the effective identity.
    login/logout keep managing the store but print a warning when PAD_TOKEN is set
    (again matching gh).

2. Named credential profiles + --profile flag

Bump the store to v3: each server key holds a map of named profiles, with default as the
implicit profile so single-user files migrate transparently (same probe-and-migrate pattern
LoadStore() already uses for v1 → v2):

{
  "version": 3,
  "credentials": {
    "https://app.getpad.dev": {
      "profiles": {
        "default": { "token": "...", "user_id": "..." },
        "cursor":  { "token": "...", "user_id": "..." }
      }
    }
  }
}
  • Selection is stateless, per invocation: --profile persistent flag >
    PAD_PROFILE env > default. No persisted "current profile" and no pad auth switch
    consistent with v2's deliberate rejection of a top-level default pointer
    (split-brain avoidance).
  • pad auth login --profile cursor, pad auth logout --profile cursor, whoami prints
    the active profile when non-default.
  • Store API: add GetProfile(url, name); Get(url) delegates to
    GetProfile(url, "default") so existing call sites are untouched.
  • Reads stay side-effect-free; single-profile users see zero behavior change.

Alternatives considered

  • Logout/login per invocation — racy under concurrency (motivating problem).
  • URL aliasing (the workaround above) — works today, but requires DNS/hosts control
    and hides identity in infrastructure config.
  • Flat composite keys ("<url>#profile") — pollutes the URL keyspace and breaks
    Get(url) semantics; nested profile map is cleaner to migrate.
  • .pad.toml agent_name / X-Pad-Agent — attribution metadata only; doesn't change
    the authenticated user.

Happy to open a draft PR for (1) first if the approach looks right, with (2) as a follow-up.


Suggested labels (cannot apply as outside contributor): area:cli, enhancement, effort:M

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions