A cloud agent sooner or later runs into a system that is not its own: an image registry, a database to migrate, object storage, an internal company service. The habitual answer is to put an access key into the launch environment variables and consider the question closed. Cursor proposes a different order. A local socket listens inside the working machine, code can ask it for a short-lived signed identity token, and that token is exchanged for temporary credentials of a cloud provider. There is no long-lived secret in the machine, because none was put there.
Keeping a secret in the environment looks sensible not out of carelessness. Build systems have worked this way for twenty years: put the key into a protected variable, read it in a build step, get access. The model is clear, it is debugged in a minute, it moves between tools without rework and demands nothing from the side being called. As long as a human starts the run and looks at its result, the weak spots of this model stay theoretical.
Autonomous work makes them practical. A key in the environment is a long-lived value that works the same yesterday, today and half a year from now. It copies: everything that once read the variable carried away full access, and revoking it is only possible together with the key itself. It is bound to nothing: the provider's log will show that access happened but will not say which run, in which repository and on whose task performed it. And it is rotated by hand, which means in reality it is not rotated until an incident happens.
The mechanism is simple. The address of the local socket lives in the CURSOR_AGENT_SOCKET variable and defaults to /run/cursor/api.sock. Code sends a POST there to /v1/tokens/oidc and must name the audience - the name of the system the token is meant for, up to 512 printable characters. The response is an RS256-signed web token and an expiry stamp in epoch seconds. The verifying side stores nothing at all: it fetches the public keys at api.cursor.com/keys, finds the right one by the key identifier from the header, checks the signature, the issuer api.cursor.com, its own audience and the validity window with a tolerance for clock skew. All the addresses are gathered in the discovery document at the standard path .well-known/openid-configuration.
The value of the mechanism is not in the signature itself but in the composition of the claims. Every token always carries the issuer, the owner subject as user with an identifier or service_account with an identifier, the requested audience, three time marks, a unique token identifier, the cloud agent identifier and the runtime marker. Depending on circumstances it adds the owner's email and identifier, the team identifier, the identifier of the current run and its start time, the repository address or a list of addresses with their count, the branch name, the environment identifier, the launch source and the automation identifier. The trust rule on the provider's side is therefore written not as access for whoever presented a key, but as access for an agent of a given team working in a given repository.
Two optional details of the request close practical corners. The nonce value binds the token to a specific request and prevents replaying an intercepted response. The sub_claim parameter projects a chosen claim straight into the subject as name:value, and the team identifier is supported today. This is needed because many verifiers can match on the subject and cannot match on arbitrary fields: without such a projection the trust policy would have to be written more coarsely than one would like.
Next the token is exchanged for temporary credentials. In AWS this means creating an identity provider with the issuer api.cursor.com, setting the audience to sts.amazonaws.com, describing in the trust policy a match on the subject and, if needed, on the team identifier, and calling the exchange by web identity. GCP does the same through workload identity federation, Azure through federated credentials, Vault through JWT login, and an in-house service through any standard verifier, because there is nothing non-standard in the token. Below is that sequence in full:
The price of the mechanism is measurable. The token lives five minutes and there is no refresh: once expired, mint a new one, which means long operations must either fit into that window or run on the provider's temporary credentials, which live longer. The rate is capped: thirty tokens per minute per machine with bursts of up to ten, no more than eight simultaneous connections to the socket, a request body of up to four kilobytes. Codes 429, 503, 500, 502 and 504 are worth retrying with a backoff, while retrying 403 is pointless - it means this machine is not permitted to mint. And a separate line of cost is setting up trust on each provider's side, done by hand once and maintained afterwards.
The trust boundary should be read literally: the token attests to the run as a whole, not to an individual process inside it. Any code that reaches the socket will get a token in the agent's name, so role permissions are granted by the minimum the task needs rather than by convenience; the host side closes something else - a guest cannot forge an identity or mint for a different agent. The result is verified not by the fact that the call succeeded: on the first production run the payload is decoded and the subject, the audience, the agent identifier and the expiry are read by eye; the provider's log is checked to confirm the session belongs to this very run; and the denial is verified separately - ask the role for something it should not have and confirm the provider refuses.
The typical failures repeat. Leaving the old long-lived key next to the new mechanism in case it does not work, and forgetting to remove it. Writing the trust policy on the issuer and audience only, without narrowing by subject - then the role is available to any agent, not to yours. Minting a token per request in a loop and hitting the rate cap instead of reusing the temporary credentials already obtained. Treating a signature over identity as trust in the content of the agent's work. And granting the role broad permissions on the assumption that nobody extra will reach the local socket.
# the socket address lives in an environment variable, default /run/cursor/api.sock
SOCKET="${CURSOR_AGENT_SOCKET:-/run/cursor/api.sock}"
# the token is minted for one audience and lives five minutes, there is no refresh
TOKEN=$(curl -sS --unix-socket "$SOCKET" \
-X POST http://localhost/v1/tokens/oidc \
-H 'Content-Type: application/json' \
-d '{"aud":"sts.amazonaws.com"}' | jq -r .token)
# exchange the signed token for the role's temporary credentials
aws sts assume-role-with-web-identity \
--role-arn "$ROLE_ARN" \
--role-session-name cursor-cloud-agent \
--web-identity-token "$TOKEN"