Appearance
Authentication
agent profile auth is the credential front door: one GitHub Copilot credential, resolved at fetch time. Agent configs never store a copy, and gh-cli holds no token of its own; static-key (below) is the one opt-out.
This page is about the credential and the profiles that carry one each. The wiring commands that ask for a credential are on the getting started page.
Providers
--provider | Where the credential comes from |
|---|---|
copilot | GitHub device flow (read:user scope) |
gh-cli | the machine's existing gh login, pinned to one account at auth time (below) |
gh-token | paste a GitHub token; --set <token> stores one non-interactively |
gh-env | copy a token from $COPILOT_GITHUB_TOKEN / $GH_TOKEN / $GITHUB_TOKEN |
copilotruns GitHub's device flow in copilot-env itself (VS Code Copilot's OAuth app,read:user), printing the URL and code on stderr; the token goes straight into the credential store, and no proxy-side token file is written.gh-clipins an account when you runagent profile auth:--gh-user <login>names it; without the flag, a single saved login or a headless run pins the active account, and a terminal asks when there are several logins or the only one comes from a token env var.- Only an
autoslot (that prompt's last option, or the sole-login fallback below) follows a latergh auth switchorgh auth login; a pinned account stays until you re-runagent profile auth. - A single saved login that gh cannot serve by name (a
hosts.ymlwritten before gh 2.40, with nousers:map) is recorded asautoinstead, with a line saying so: it is the same account while it is the only login. On gh 2.40 or newer,gh auth loginfor it rewriteshosts.ymlso the nextagent profile authpins it. - A pinned account resolves through
gh auth token --user <login> --hostname github.com(gh 2.40 or newer), anautoslot throughgh auth token --hostname github.com; neither follows aGH_HOSToverride. A pin gh cannot serve fails with gh's own refusal; it never falls back to another account's token. - The pin follows no
gh auth switch; it fails only when gh cannot serve that account by name (logged out, the pre-2.40hosts.ymllayout, or gh older than 2.40). An env token ($GH_TOKEN,$GITHUB_TOKEN) is never adopted for a pin, and a miss quotes the gh call and its stderr. gh-envin a terminal always shows the var and its GitHub account first: one set asks yes/no, several set get a menu. Headless takes the most specific (servers).- Every pasted or copied token is labelled with the account GitHub reports for it (GraphQL
viewer, noghneeded); a lookup miss only changes the label. --get/--del/--checkprint, clear, or check that a credential resolves.--dry-runruns no login: it plans the slot write from--setor--gh-user(a device flow is named, not run), so it needs--provider,--set <token>, or--gh-user <login>when no credential resolves.agent profile <name> authaddresses one profile's slot; bareagent profilelists every profile with its provider.
PAT passthrough
Classic and fine-grained PATs can't perform the proxy's editor token exchange. So agent start transparently enables a passthrough shim for PAT-shaped tokens, using the PAT as the bearer directly. Force it either way with agent profile set passthrough on|off (key).
Passthrough decides only the exchange; the proxy's client identity is the credential's whatever the decision.
Client identity
Copilot reads the client from two things on every request: the Copilot-Integration-Id header (or its absence) and the User-Agent. Together they decide which credentials are accepted and which catalog is served, so copilot-env resolves ONE client identity per credential and every mode sends it:
- Candidates, in probe order:
codex(the codex User-Agent with noCopilot-Integration-Id; the identity that lists the widest catalog), thencopilot-developer-cli, thencopilot-developer-sandbox, thenvscode-chat(copilot-api's former default). The firstGET /modelsthat answers 2xx on the host in use wins, so a later candidate is reached only when every earlier one answered without a 2xx. - Probed at a landing, stored in the slot: a credential landing (
agent profile [<name>] add,agent profile <name> auth) probes on the host in use and stores the halves the probe answered; a pinned identity or a literal host is an overlay, never stored. - Read back everywhere else: every Direct re-render and every daemon start read the slot under the
identitypin andhostliteral, with no request; a half still unknown is probed once and stored. - Direct bakes the header set into the agent configs (Codex
http_headers, ClaudeANTHROPIC_CUSTOM_HEADERS). - Proxy applies the same header set inside the daemon: a preload rewrites
User-AgentandCopilot-Integration-Idon every fetch and WebSocket to the Copilot API hosts, deleting the id forcodex, so the proxy serves the catalog Direct sees. The proxy's ownagent profile models --proxylist is copilot-api's trimmed view of that catalog.
text
$ agent profile identity
identity: auto
host: auto (api.githubcopilot.com in use)
* = in use: the pin, else the slot's probed identity; what every Direct re-render bakes and a daemon launch sends, on the host in use
identity api.githubcopilot.com (in use) api.enterprise.githubcopilot.com (account) note
------------------------- ------------------------------ ------------------------------------------ ---------------------------------------------------------
codex rejected (400) rejected (400) the default: no Copilot-Integration-Id header (auto only)
copilot-developer-cli accepted (5 models) * accepted (37 models) GitHub Copilot CLI; accepts fine-grained PATs
copilot-developer-sandbox accepted (2 models) rejected (400)
vscode-chat rejected (400) rejected (400) copilot-api's former default
codex on api.githubcopilot.com: 400 Personal Access Tokens are not supported for this endpoint
codex on api.enterprise.githubcopilot.com (account): 400 Personal Access Tokens are not supported for this endpoint
copilot-developer-sandbox on api.enterprise.githubcopilot.com (account): 400 Personal Access Tokens are not supported for this endpoint
vscode-chat on api.githubcopilot.com: 400 Personal Access Tokens are not supported for this endpoint
vscode-chat on api.enterprise.githubcopilot.com (account): 400 Personal Access Tokens are not supported for this endpoint- Rows are the identities: the four candidates, plus the pin and the slot's stored identity when they are neither. Every row is probed with exactly the header set every mode sends; the rows are probed concurrently, and the survey never stops at the first acceptance. The survey never reads the agent files and never stores a pair.
- Columns are the hosts:
api.githubcopilot.com, the API host the credential's account reports when it differs (api.enterprise.githubcopilot.comabove), and the host in use when it is neither: thehostliteral (taggedhost) or the slot's stored host (taggedstored). - Cells:
accepted (N models)is a 2xx with the/modelscatalog size;rejected (400)a 400/401;unclear (403)orunclear (network error)a non-definitive status or a blip;-an identity not probed on that host. - The
*marks the one identity in use for this credential, on the host in use: the pin, else the slot's stored identity, on the literal, else the slot's stored host. Nothing is marked*while either half is unknown. - The
>appears only while nothing is stored and no pin is set: it marks the identity the next landing would pick, the first candidate the host in use accepts. A row never carries both marks. - Lines under the table: the reason for every rejected or unclear cell (trimmed to 160 characters), and a note for each gap: nothing (or one half) is stored yet and one
agent profile add(oragent start) probes and stores what it lands on; a pin overlays the stored identity; a daemon is running and keeps its launch-time identity untilagent stop, thenagent start. - Width: the table fits the terminal by the terminal width rules; with the two hosts above the columns give way to one block per identity below about 105 columns.
agent profile set identity <id> pins one; the store is the identity key.
text
$ agent profile set identity copilot-developer-cli
identity = copilot-developer-cli (pinned; `agent profile set identity auto` restores probing).
$ agent profile set identity auto
identity = auto: the identity is probed per credential again.- The pin is probed once on every host with exactly the headers it would send. A definitive rejection on the host in use, or on every host, refuses the pin; a one-sided or unclear result pins with a warning naming what carried it; with no credential resolving, the pin is stored unverified and the warning says so.
set identity autoclears the pin and needs no credential.codexcannot be pinned: it is the absence of the header, and a pin is always sent as the header's value.autoalready selects it whenever the credential is accepted under it.- A pin overlays the stored identity: it applies to Direct at the next re-render and to the proxy at its next daemon launch.
agent profile <name> identitysurveys with that profile's credential and slot;agent profile <name> set identity <id>(oridentity --set <id>) pins that profile'sidentitykey.
User-Agent. Every mode sends codex_exec/<version>, and the version is resolved once per process, in this order: the installed codex --version, else npm's current @openai/codex release (npm view @openai/codex version), else the version baked into this copilot-env build. A machine without Codex and without network still sends a versioned header, since Copilot rejects some models for the bare shape.
Static key
static-key names which agent's config carries the credential value itself instead of a resolver command: none (the default), claude, codex, or all. An agent in scope runs no copilot-env process at request time.
bash
agent profile set static-key codex # bake Codex's config only; Claude keeps its helper
agent profile set static-key all # both agents and Claude Desktopstatic-key | Claude Code settings.json | Codex config.toml, managed provider | Claude Desktop entry |
|---|---|---|---|
none (default) | apiKeyHelper command | auth.command | helper script |
claude | env.ANTHROPIC_AUTH_TOKEN; no apiKeyHelper | auth.command | inferenceCredentialKind = "static" + inferenceGatewayApiKey; no helper script |
codex | apiKeyHelper command | http_headers.Authorization = "Bearer ..."; no auth | helper script |
all | env.ANTHROPIC_AUTH_TOKEN; no apiKeyHelper | http_headers.Authorization = "Bearer ..."; no auth | inferenceCredentialKind = "static" + inferenceGatewayApiKey; no helper script |
- Claude Desktop follows Claude's scope (
claudeorall). - Direct bakes the GitHub credential (
gh-cliis resolved once, at write time); proxy bakes the daemon's own API key. - The value does not follow a credential change. A named profile's
agent profile <name> authrewires both agents itself, so its baked value is fresh at once. The default'sagent authonly stores the credential: re-runagent initafter it, which notes that the agents in scope keep their baked value until that rewire. - Proxy mode loses the resolver's side effects for the agent in scope: the daemon is not auto-started and no idle heartbeat is recorded. Start it with
agent start, or launch throughcl/cx, which do. agent profile check(--claude/--codex) andagent healthreport the static shape as wired.agent healthalso warns when the baked value no longer matches the store (or the daemon's key), naming the rewire.
Profiles
A profile is an atomic unit: ONE credential + ONE mode (direct or proxy, never both), always wired into BOTH agents. Several sessions then run at once without touching the default setup.
bash
agent profile work add --proxy # the mode, then the credential step (it asks how)
agent profile work add --proxy --no-auth # headless: the mode alone, then
agent profile work auth --provider gh-env # the credential; wires both agents
cl --profile work # Claude under the profile (its own proxy daemon, own port)
cx --profile work # Codex under the same profile
agent list # NAME MODE PROVIDER DAEMON
agent profile work del --yes # stop its daemon, clear its credential, strip both agents' wiring- No fallback: a named profile hard-fails rather than falling back to the default credential. Re-authenticate one with
agent profile <name> auth. - Own daemon: a proxy-mode profile runs in an isolated home (
<copilot-api home>/profiles/<name>) on a stable reserved port, managed viaagent profile <name> start/stop. - Own files: Claude reads
~/.claude/settings-<name>.json; Codex reads~/.codex/<name>.config.toml, whose top-levelmodel_providerselects the[model_providers.copilot-env-<name>]table inconfig.toml(codex --profile <name>layers the file overconfig.toml). Your own keys in the profile file survive a rewire. Both files are in the write list. - Switch mode: re-run
addwith the other mode flag. - Web-search caveat: a DIRECT profile over a PROXY default has no search path in Claude. The builtin 400s on Direct, and the machine-global MCP server is only registered while the default wiring is direct (web search).
- Workaround: register the server by hand there, under a name other than
copilot-env. A hand-written entry under that name reads as foreign, so a later default wiring leaves it alone and cannot register its own.