In this article
August 31, 2026
August 31, 2026

Keeping credentials out of an AI agent's context with Relay

Relay proxies an agent's third-party API calls and injects the credential at the boundary, so prompt injection has no token to steal and nowhere to send it.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

An agent that reads a web page, a Jira comment, or an inbox is reading attacker-controlled text. If a Slack token sits in the same process, the token is part of that attack surface. Before you get to sandboxes, output filters, or where the agent runs, answer a smaller question: at the moment the agent makes an outbound call, where does the credential live?

Relay is WorkOS's answer, shipped August 6, 2026. It lets you call third-party APIs on behalf of your users without directly handling secrets: requests proxy through WorkOS, where the third-party credential is swapped in based on the user and organization context. The agent holds nothing worth stealing. Relay is in early access, enabled per environment by contacting support, so everything below is what it does today.

What a prompt injection is actually after

Christian Posta puts the failure mode in one sentence: "Prompt injection, exfiltration, and accidental disclosure sound like three different attacks, but they're just delivery mechanisms for the same failure mode: the bytes of a sensitive credential leave the agent and still work". Bearer credentials are the reason. A GitHub PAT, an API key, and the overwhelming majority of SaaS OAuth tokens do not check sender constraints, so whoever holds the bytes is the holder.

You do not need an attacker for this to go wrong. Posta's other observation is that a goal-oriented, stochastic system may hand its credentials over unprompted, because it believes it is solving your problem.

And the model has no reliable way to tell your instructions from the ones in the content it just read. Simon Willison: "LLMs are unable to reliably distinguish the importance of instructions based on where they came from. Everything eventually gets glued together into a sequence of tokens and fed to the model".

Willison's lethal trifecta names the combination that turns that into data loss: access to private data, exposure to untrusted content, and the ability to externally communicate. The third leg is the cheap one. Any tool that can make an HTTP request, to an API, to load an image, even to hand the user a link to click, can carry stolen information back out. Vendors who fixed real injection exploits mostly did it by locking down that exfiltration vector, not by teaching the model better judgment.

So there are two things to take away from the agent: the credential, and the open road out.

Three places the credential can live

The IETF draft Credential Broker for Agents (CB4A), published March 29, 2026 by K. Hartman of the SANS Institute, enumerates the options cleanly. It defines three credential proxy models alongside a threat model, which the abstract counts as ten threats though the appendix enumerates eleven.

Three rows, each showing the agent process as a dashed container. In Model A, the proxy gateway, the container is empty and the credential sits outside it, travelling broker to target only; blast radius minimal. In Model B, short-lived token minting, a credential with a dashed border sits inside the agent container for the length of its TTL; blast radius bounded by the TTL. In Model C, credential wrapping with revocation, a solid credential sits inside the agent container until revoked; blast radius full until revoked.
Models B and C differ only in how long. Model A differs in kind.
  • Model A, proxy gateway. The broker exposes a proxy endpoint, validates the agent's session token, injects the real credential, forwards the request, and returns the response. Blast radius: minimal.
  • Model B, short-lived token minting. The broker mints a narrow, short-lived token and hands it to the agent. This is the draft's recommended primary model, and its listed weakness is that the agent holds a real, if temporary, credential in memory. Blast radius: bounded by the TTL.
  • Model C, credential wrapping with scheduled revocation. Hand over the long-lived credential and schedule its revocation. The draft calls this the weakest model, does not recommend it for new integrations, and puts its blast radius at full until revoked.

The gap between A and B is smaller on paper than in practice. A Model B token lives in the same context window that just read a Jira comment written by a stranger, and for its TTL it is a working bearer credential for anyone who gets the bytes out. A Model A request has nothing to get out. CB4A's own framing: the credential travels from the broker to the target service, but never to the agent.

Posta argues that for most enterprises today the draft's ordering is backwards, and Model A is the primary approach. The practical reason is that minting requires the target to cooperate, and plenty of targets do not. There is no exchange path from your corporate IdP to a GitHub token: GitHub wants its own OAuth token, obtained by that specific user consenting in a browser, which you get once and then have to keep.

Relay matches the Model A shape for the providers Pipes supports, with the credential going broker-to-target and never to the agent. Vending a credential and using it yourself is the other option in the same product, and the docs draw the line where you would expect: access tokens for trusted infrastructure and long-running syncs, relay for agents, sandboxes, and untrusted runtimes.

What the call looks like when the agent holds nothing

The relay base URL is api.workos.com/relay, and any HTTP method is accepted. Converting a direct provider call is four edits: keep the method, body, and content headers; send it to the relay base URL with the original URL in X-Relay-URL; put your WorkOS API key in Authorization instead of the provider token; and name the user in X-Relay-User, adding X-Relay-Organization for organization-scoped connections. The provider is resolved from the host in X-Relay-URL, so you usually send no provider header at all, though X-Relay-Provider is there for when the host is ambiguous or you would rather not depend on inference.

  
async function postDigestToSlack(
  userId: string,
  organizationId: string | undefined,
  channel: string,
  text: string,
) {
  const res = await fetch('https://api.workos.com/relay', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.WORKOS_API_KEY}`,
      'Content-Type': 'application/json',
      'X-Relay-URL': 'https://slack.com/api/chat.postMessage',
      'X-Relay-User': userId,
      // Only when the connection is organization-scoped. Sending it otherwise is a 402.
      ...(organizationId ? { 'X-Relay-Organization': organizationId } : {}),
    },
    body: JSON.stringify({ channel, text }),
  });

  if (res.status === 402) {
    const { authorization_url } = await res.json();
    // Nobody is around to click this now. Queue it for the user's next login.
    return { reconnect: authorization_url ?? null };
  }

  return { upstream: res.headers.get('X-Relay-Upstream-Status') };
}
  
Three zones left to right. The agent process, drawn with a dashed border, holds your request and your WorkOS API key. WorkOS relay resolves the user, fetches the credential, and injects it at the edge. The provider API receives the credential and returns the response. A band beneath the first zone reads "no provider token," and a band spanning the other two reads "the provider credential exists here, and only here."
The agent holds your WorkOS API key and nothing else. The provider credential is attached after the boundary.

Behind that request, WorkOS verifies the key and resolves the environment, resolves the user from X-Relay-User, looks up the connected account, fetches the credential and refreshes it if expired, strips the relay control headers along with cookies and forwarding metadata, injects the provider credential into Authorization, and streams the provider's status, headers, and body back verbatim. Your WorkOS API key is never forwarded upstream. Refresh happens on both the vend and relay paths, so the credential you never see is also never stale.

The injection point is the whole design. There is no window in which the provider token exists inside the agent's process, no env var to read, no context to leak it from.

The allowlist is the half people skip

Removing the credential handles one leg of the trifecta. Relay narrows the third one too, because a proxy that injects credentials has to decide which destinations are legitimate.

X-Relay-URL must be HTTPS, since credentials are never injected into a plaintext request. Requests can only target a supported provider's allowed hosts, and redirects are returned rather than followed. A URL that is not HTTPS, is malformed, or names a host outside the provider's allowlist comes back as 400 relay_invalid_url. For Google, Slack, and Jira, the allowed hosts are www.googleapis.com, slack.com, and api.atlassian.com, published in the supported providers table alongside each provider's slug.

That is an egress rule with credentials attached. An injected instruction telling the agent to POST the contents of a document to attacker.example/collect does not fail because a classifier caught the phrasing. It fails because the host is not on any provider's allowlist, and because the credential the agent would need is not in the agent. The design patterns paper Willison cites sets the bar this way: once an LLM agent has ingested untrusted input, it must be constrained so that it is impossible for that input to trigger any consequential actions. A host allowlist is one of the few controls that can honestly claim impossible rather than usually.

Two more things get cleaned on the way through. Outbound, relay strips hop-by-hop headers, Cookie, forwarding metadata such as X-Forwarded-*, Via, and X-Real-IP, your Authorization header, and anything prefixed X-Relay- or X-WorkOS-. Inbound, it strips hop-by-hop headers and Set-Cookie, and since compressed bodies are decoded in transit, Content-Encoding and Content-Length may disappear. Bodies forward byte-for-byte up to 5 MB, GET and HEAD send none, and the upstream timeout is 30 seconds.

If you use path routing, where /relay/github/user proxies to the GitHub API's /user path, it can only reach the provider's default host. URL routing reaches any allowed host for that provider and keeps real URLs in your code and logs.

What injection can still do

A proxy boundary bounds the damage. It does not eliminate it, and the honest list of what remains is short enough to read in one sitting.

  • Everything inside the granted scope. Pipes scopes are configured per provider integration and granted at connect time, not per request. Connect Google with a read-only Drive scope and every relayed Drive call can read every file that user can read, so an agent talked into listing and summarizing the whole drive gets exactly that, faithfully credentialed. Narrow the integration's scopes instead of trusting the runtime to behave. The scope question and the credential-location question are different problems, which is why intent-based access control is a separate layer of work for us.
  • Your WorkOS API key. Relay removes provider tokens from the calling environment, not this key, which authenticates every WorkOS API call for that environment and should be injected at request time rather than baked into agent-visible code or prompts.
  • Any other egress path the runtime has. Relay governs the requests that go through relay. A runtime that can also open arbitrary sockets or render a markdown image from any URL still has a channel out. Guardrail products advertising 95% detection do not close it either. As Willison puts it, in web application security 95% is very much a failing grade.
  • Grants that outlive your cleanup. Deleting a user's connected account disconnects it, removes the stored access and refresh tokens, returns 204 No Content, and makes the next relayed call return 402. It does not revoke access on the provider side, and a provider token that already leaked stays a valid bearer credential until it expires or the user revokes the grant at the provider. Never handing the token out is what keeps that scenario hypothetical.

The 402 is the branch worth writing code for. A user who never connected, whose grant was revoked, or whose token can no longer be refreshed gets a 402 with relay_authorization_required and an authorization_url:

  
{
  "code": "relay_authorization_required",
  "connection": "slack",
  "message": "User has not authorized provider \"slack\"",
  "authorization_url": "https://slack.com/oauth/v2/authorize?client_id=..."
}
  

It is 402 rather than 401 or 403 specifically so it cannot be confused with a WorkOS auth failure, which is a 401, or with the provider's own 401 or 403 passed through from upstream. Note that authorization_url is null for API key and client credentials connections, which have no authorization screen, so fall back to the widget. An organization scope mismatch in either direction returns the same 402, so check the header against the connected account before asking a connected user to reauthorize. When you need to tell an upstream failure from a relay failure, the presence of X-Relay-Upstream-Status means the request reached the provider.

The broker is now the target

The tradeoff in Model A is concentration, and pretending otherwise would be dishonest. CB4A's real-world motivation is the March 2026 TeamPCP campaign, which poisoned the supply chains of Trivy, Checkmarx, and LiteLLM, an AI gateway proxy used by thousands of enterprises to route requests to LLM providers. The draft credits the campaign with exfiltrating an estimated 300+ GB of compressed credentials affecting roughly 500,000 corporate identities, though that figure originates in the threat actor's own extortion claim rather than in forensics.

The mechanism is worth being precise about, because it is not quite the story the headline suggests. The backdoored packages carried a generic host credential stealer, which harvested SSH keys, cloud credentials, Kubernetes secrets, database credentials, and .env files from any machine that installed them. It was not an attack on LiteLLM's stored provider keys. The lesson generalizes anyway, and the draft draws it: when AI infrastructure concentrates long-lived credentials in a single process or configuration, compromise of that process yields catastrophic access. Its threat model rates broker compromise, TM-1, as CRITICAL, the only threat at that severity, and requires the broker be hardened with no shell access, restricted network, and credential zeroing after minting.

Posta reaches the same place from the implementation side, and he aims it at the architecture rather than at any one vendor, his own employer included: a credential broker is a database that holds every user's GitHub token, every Slack token, every upstream credential in the environment. It does not eliminate the credentials so much as create a high-value target.

Relay moves that target to WorkOS. That is the actual purchase decision, and it deserves to be evaluated as one. The draft's structural advice is worth holding any broker to, ours included: the component that decides "yes" should never touch credential material, and the component that dispenses credentials should never make policy decisions. The operational costs are real too. CB4A lists Model A's weaknesses as the extra hop's latency, the proxy as a single point of failure, throughput pressure at scale, and the requirement that the proxy understand the request and response format of every target API it mediates, which is the constraint behind both the host allowlist and the per-provider configuration. We have not published latency numbers for the relay hop, so measure it on your own path before you route a high-volume sync through it, and note that the upstream timeout is 30 seconds.

Where that leaves the design

Posta gives three options for an agent credential: make the credential expire fast, make it non-transferable, or don't give the agent a credential at all. The first two shrink the blast radius of a leak; the third removes the thing that leaks. Sender constraints would be the elegant answer if both ends implemented them, and the ends that matter mostly do not.

If you run an agent today that reads untrusted text and calls a provider API, the change with the most effect available to you is not another guardrail. It is deleting the line of code that puts a provider token in that process, and letting the token get attached at the boundary instead. Relay is in early access, so contact support to enable it for your environment, and the relay docs plus the scheduled-worker walkthrough cover the wiring. Then go read your provider integration's scope list, because that is the reach an injection still has.