Book a call
LESSON18mVERIFIED 2026-08-05 · CLAUDE CODE 2.1.221 · CODEX CLI 0.146.0 · GITHUB COPILOT CLI 1.0.78 · ANTIGRAVITY CLI 1.1.10 · KIMI CODE CLI 0.31.1

Keyless CI: OIDC and workload identity before API keys

Most CI quickstarts end with "add your API key to repository secrets". Sometimes you don't have to — here's which harnesses can take a credential minted per run, and which can't.

The line at the end of every quickstart

It's what most of the headless-setup guides end with: storing the API key as a repository secret, like ANTHROPIC_API_KEY in Anthropic's GitHub Actions docs — "Add your API key as a repository secret named ANTHROPIC_API_KEY" — or CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} on Cursor's docs page. That's because it works and reflects the behaviour of the control on the settings page, but that's actually sth that deserves to be thought through for 20 minutes before you decide to go with it

Not because GitHub's secret store is not a good place for keeping secrets — it is; but because there are tools which make this step unnecessary and in which case excluding it from the setup reduces maintenance instead of introducing it. So it's about doing an audit: which tools allow for avoiding it, and admitting where they don't

What a key costs you that a token doesn't

The sceptical take is: it's one secret, in one repo, in an encrypted store, and if it leaks you rotate it. That's mostly fair, and the argument for the other way round is easiest to make from what GitHub says you get rather than from anything scary about keys:

  • Persistent — the secret survives until somebody acts; GitHub calls the thing you're avoiding "long-lived GitHub secrets", and contrasts them with a token "that is only valid for a single job, and then automatically expires". In other words: the time the key lives for is how long it takes for someone to notice and revoke it; it's up to you to reduce it

  • Not context-aware — the OIDC token GitHub mints has claims like repository, ref, environment, workflow_ref, job_workflow_ref or run_id. That's what makes GitHub's third advantage (the one about granular control) possible: a cloud-side policy which requires the token to have been exchanged from a particular repository on a particular branch. A key gives your cloud nothing to decide with

  • Credential for paying — the purpose of an API key is authorising a call which can be charged. In theory, if somebody finds it you'll notice, but not as easily as for deploy credentials; the thing is that utilising an API key is its natural behaviour, not a red flag

So, "keyless" is not perfect or magical, but it alters the setup in a way that instead of providing the model with a key, you give it a job description, based on which it issues a credential for this particular job, which expires when the job finishes; if there's nothing kept, there's nothing to leak

How keyless works, once

The mechanism is the same across platforms and can be understood as a single mental model:

  • CI platform becomes an OpenID Connect identity provider which on request signs a short-lived token describing the job — repository, ref, environment, workflow file. GitHub's issuer is https://token.actions.githubusercontent.com, and it signs a token containing claims like repository, ref, environment, workflow_ref, job_workflow_ref or run_id (and two dozen more)

  • Cloud provider is told to trust this issuer and — as a condition — what constitutes an acceptable job, and on receiving the token verifies its signature and the condition, providing it's valid with real credentials scoped to your defined role and limited in time by the provider's rules; for AWS it means an assumed role that lasts for one hour

And here are GitHub's three advantages, in their own words:

  • "You won't need to duplicate your cloud credentials as long-lived GitHub secrets."

  • "With OIDC, your cloud provider issues a short-lived access token that is only valid for a single job, and then automatically expires."

  • "You have more granular control over how workflows can use credentials, using your cloud provider's authentication (authN) and authorization (authZ) tools to control access to cloud resources."

For example, here's a workflow which requires one permission and one login step — id-token: write plus checkout and the AWS action responsible for setting up the credentials with a role to assume and region:

YAML
permissions:
  contents: read
  id-token: write

jobs:
  agent:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: us-east-1

The only opt-in needed to make it work is the id-token: write permission. Without it the job can't even request a token, but if it's there you get ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN in your environment and the login action runs the exchange

Just for clarity — the ARN of the role which you put into secrets is not a credential; AWS docs just call it the ARN of the role. What matters is the trust condition on the role, not the secrecy of its name

Similar to AWS, Google Cloud uses google-github-actions/auth with a workload_identity_provider and Azure — azure/login with client and tenant ids. All three write their result into the place their SDKs look for it, which is what this next section is about

The question that decides whether you can do this at all

Keyless is all about the prerequisites, and they're not mentioned in the guides for any of these tools as each of them covers only its own solution. What's more, out of the six AI coding harnesses none supports OIDC yet:

  • There's no --use-oidc option and there won't be

  • It's not about the harness — it's the cloud

The question is: can this harness be pointed at a model endpoint which receives its credentials through a cloud SDK credential chain rather than an API key? If yes, then it's automatically keyless as the login actions populate exactly these chains (the AWS SDK default one, Google's Application Default Credentials and Azure's DefaultAzureCredential). So you'd just need to point it at an endpoint by setting up an environment variable, and you can use the credential which the OIDC exchange deposited in that chain

Let's see it — here's Claude Code 2.1.221 in Bedrock mode with every AWS variable cleared and both credential files pointing to empty files:

TEXT
"terminal_reason": "api_error",
"result": "API Error: Could not load credentials from any providers",
"duration_api_ms": 0,
"total_cost_usd": 0

As you can see in the output, the call failed because it couldn't find credentials from any of the providers — and it cost zero dollars. What's important is that the Anthropic key wasn't requested at all; it looked for AWS credentials, didn't find any, and threw an error before any model call

What's also important, this way the only credential involved was an AWS one — which is sth OIDC is for

Just to be clear — Bedrock also has AWS_BEARER_TOKEN_BEDROCK, but that one doesn't involve the chain so should be avoided if you aim for keyless

If the harness doesn't have such a path, it can't do keyless — in which case you shouldn't waste time looking for it. The status today is:

HarnessKeyless in CIWhat it rides on
Claude CodeYes, three waysAWS, Google or Azure credential chain
GitHub Copilot CLIYes, without OIDCThe job's own GITHUB_TOKEN
Codex CLIPartlyBuilt-in Bedrock provider, or a token command you write
CursorNoCURSOR_API_KEY
Antigravity CLINoCredentials cached by an interactive login
Kimi Code CLINoA key in the config file, or KIMI_MODEL_*

Regarding the outliers — Copilot CLI is a positive outlier here, as GitHub provides both CI and the model so there's actually no need for federation at all; Antigravity is a negative outlier, as their documented headless route relies on credentials saved by an interactive login which is worse to introduce into CI than an API key

Scoping the trust, which is the part that actually breaks

So these are the prerequisites. The thing is, acquiring a token is trivial; restricting it to particular actors is what the security aspect of OIDC is about, and what makes it time-consuming to configure. GitHub is blunt about the floor: "To control how your cloud provider issues access tokens, you must define at least one condition, so that untrusted repositories can't request access tokens for your cloud resources." A trust policy with an issuer and no condition isn't a weaker version of this — it's a role any repository on GitHub can assume

To make sure only authorised actors can get tokens for your resources you require that the sub claim, which encodes what the job is, satisfies your condition; it usually looks like:

TEXT
repo:my-org/my-repo:ref:refs/heads/main
repo:my-org/my-repo:environment:production

But remember to use StringEquals instead of a wildcard, as the AWS action's README warns:

Avoid ForAllValues: in Allow statements. These operators return true when the claim is absent or misspelled, which can lead to unintended access. Instead, use StringEquals or StringLike operators to check for specific claim values.

The reason behind it is that a condition that silently passes on a typo is worse than none, because it looks like a condition

One more thing — the sub format changed this year:

  • Repositories created on github.com after 15 July 2026 (and older ones which opted in) append the immutable numeric IDs of the organisation and the repository to their subject claim

  • Subject strings before and after:

TEXT
repo:octo-org/octo-repo:ref:refs/heads/main
repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main

What it means is that a reused name of an organisation or repository can't lead to obtaining tokens which match an outdated trust policy. The consequence is that if you copy the policy from some 2025 write-up, it won't work with a newly created repository, and the error will say nothing about why

Like here for instance — a July 2026 post on r/devops where a user was stuck with a Not authorized to perform sts:AssumeRoleWithWebIdentity error despite providing a valid organisation, repository and environment name

They turned out to have created the role using the AWS console's web-identity wizard, typing "main" into the GitHub branch field which quietly added a ref: refs/heads/main condition to the trust policy. As they were releasing on tags, the pipeline was being run for tags instead

The point is that they had checked all the claims and they matched; it's just they hadn't checked the one they couldn't see

Always verify before incorporating any agent:

  • Create a workflow triggered only by workflow_dispatch and containing only the login action and a single identity call — aws sts get-caller-identity on AWS, gcloud auth list on Google Cloud, whichever one-liner prints "who am I" on yours

  • Run it; if it succeeds you can be sure the trust policy is set up properly, in ten seconds and with nothing else in the way

  • Only then add the agent

The artifact at the bottom of this lesson is that probe workflow — a minute to set up and invaluable in terms of distinguishing a broken trust policy from a broken agent invocation, which otherwise produce exactly the same red X

IN YOUR HARNESS

In Claude Code

There are three keyless routes available in Claude Code for the most popular cloud providers, one per provider; they all follow the same pattern — a single env var is being set to enable them and from that point on no Anthropic API key is queried by the tool at all.

VariableEndpointCredential it looks for
CLAUDE_CODE_USE_BEDROCK=1Amazon BedrockAWS SDK default chain
CLAUDE_CODE_USE_VERTEX=1Google Cloud's Agent PlatformApplication Default Credentials
CLAUDE_CODE_USE_FOUNDRY=1Microsoft FoundryAzure DefaultAzureCredential

The official docs confirm it for each provider. The Bedrock one: "Claude Code uses the default AWS SDK credential chain". The Foundry one: "When neither ANTHROPIC_FOUNDRY_API_KEY nor ANTHROPIC_FOUNDRY_AUTH_TOKEN is set, Claude Code automatically uses the Azure SDK default credential chain."

If we look at the CLI, in version 2.1.221 this is also described from the other side, in the help message under --bare: "Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials."

For instance, Anthropic's own GitHub Actions docs include a ready Bedrock setup — assuming an OIDC role and generating a GitHub App token to use with claude-code-action:

YAML
permissions:
  contents: write
  pull-requests: write
  issues: write
  id-token: write

jobs:
  claude:
    runs-on: ubuntu-latest
    env:
      AWS_REGION: us-west-2
    steps:
      - uses: actions/checkout@v4
      - name: Generate GitHub App token
        id: app-token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}
      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: us-west-2
      - uses: anthropics/claude-code-action@v1
        with:
          github_token: ${{ steps.app-token.outputs.token }}
          use_bedrock: "true"
          claude_args: '--model us.anthropic.claude-sonnet-4-6 --max-turns 10'

AWS_ROLE_TO_ASSUME stores a role ARN, which is not a credential.

So we still need to provide another token to the action for it to be able to read the PR and post comments. For third-party providers Anthropic recommends creating a GitHub App and then using actions/create-github-app-token to mint a token, storing APP_ID and APP_PRIVATE_KEY in secrets. The bottom line is that we replace a model key with an App's private key, which is still the better side of the trade as the App has only the permissions that were granted to it and can't be used to spend on models. It's not the zero-secrets solution, though.

If you use the CLI without the action, you don't need anything else — claude -p just picks up whatever the login step left in the environment.

If you're using the tool locally, you can define the settings entries awsAuthRefresh and gcpAuthRefresh which run a command when credentials expire, sparing SSO users from pasting keys.

In Codex CLI

0.146.0 doesn't support OIDC and doesn't have any kind of federation flag at all — we checked every option in codex --help, codex exec --help and codex login --help. Instead it gives two ways to provide a credential that isn't a stored key, and one of them is the most general mechanism among the six tools we examined.

The built-in Bedrock provider. Codex ships an amazon-bedrock provider, and the two settings the configuration reference documents for it are AWS ones — an AWS profile name and an AWS region:

TOML
[model_providers.amazon-bedrock.aws]
region = "us-east-1"

That's the shape of a provider which resolves AWS credentials rather than an OpenAI key, which is what we want. Two caveats worth knowing before you build on it, though. The reference only mentions those two properties, so it's hard to know which parts of the AWS credential chain it consults, and we haven't exercised it ourselves either (that needs a real AWS account with Bedrock access). And if you do reach for profile, note that configure-aws-credentials exports credentials into the environment by default and only writes a named profile when you ask for it — so a profile = "ci" line and an OIDC login step don't wire together on their own.

The token command, which is the interesting one. Any custom provider can point at a command that emits a bearer token on stdout:

TOML
[model_providers.internal]
name     = "Internal gateway"
base_url = "https://models.internal.example.com/v1"

[model_providers.internal.auth]
command             = "/usr/local/bin/mint-token"
refresh_interval_ms = 300000

The reference describes auth.command as the "command to run when Codex needs a bearer token. The command must print the token to stdout", with refresh_interval_ms controlling how often Codex proactively refreshes it — 300000 by default, and 0 meaning refresh only after an authentication retry. It's very powerful, as you can plug in any identity system you already run: a script exchanges the CI OIDC token for whatever your gateway accepts, and Codex never sees a stored secret. One rule — the reference says do not combine auth with env_key, experimental_bearer_token or requires_openai_auth.

Otherwise it's a key. env_key names an environment variable holding the provider API key; for OpenAI directly that's OPENAI_API_KEY or a ChatGPT session. Two things about the login flags matter in CI:

BASH
printenv OPENAI_API_KEY | codex login --with-api-key
printenv CODEX_ACCESS_TOKEN | codex login --with-access-token

Both read their value from stdin rather than the command line, which means it doesn't end up in shell history or a process listing. And --with-access-token is the enterprise path, for when something upstream has already obtained a token for you. codex login --device-auth is there for headless hosts, but it still needs a person at a browser — so it's for provisioning a box, not for a pipeline.

The gotcha. You can cache the ~/.codex/auth.json file that codex login writes, but we wouldn't: it puts a durable credential in your cache store, with none of the properties we came here for. Best to pass a key per run instead, so it dies with the runner.

In GitHub Copilot CLI

It's probably the simplest of the six, and it doesn't involve OIDC at all — both the CI and the model come from the same company, so there's no federation to arrange; the job's own token is the credential.

Since July 2nd, 2026 Copilot CLI authenticates in GitHub Actions with the built-in GITHUB_TOKEN; before that you had to create a PAT and store it. The whole change is one permission — here's a minimal workflow which reads the repository's contents, writes to copilot-requests, installs the CLI and runs it with GITHUB_TOKEN set as an environment variable:

YAML
name: Copilot CLI example
on: [push]

permissions:
  contents: read
  copilot-requests: write

jobs:
  copilot:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Install Copilot CLI
        run: npm install -g @github/copilot
      - name: Run Copilot
        run: copilot --yolo -p "Summarize the changes in this commit"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitHub says that "The GITHUB_TOKEN provided by GitHub Actions handles authentication automatically, no additional secrets are needed" — that's important, because just like in the other solutions presented in this lesson it's a token minted per job and expiring with it.

To use it you'll need:

  • the organisation policy "Allow use of Copilot CLI billed to the organisation" enabled (which is on by default if the Copilot CLI policy already is)

  • an up-to-date version of the CLI (copilot update, or a fresh install)

  • to accept that AI credits are charged to the organisation, so per-user budgets aren't applicable and the limit comes from a cost centre or a session cap

If you want Copilot to have different permissions than the rest of the job without disturbing the GITHUB_TOKEN your other steps are using, set COPILOT_GITHUB_TOKEN, as shown on the environment help page on 1.0.78 — "COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN (in order of precedence)".

The --yolo flag in the example above is necessary, not a decoration — it suppresses the interactive prompts that an environment without a terminal can't answer. Which also means the same workflow hands an agent a lot of power over the job, and GitHub says so plainly: "Invoking Copilot CLI directly in workflow steps gives it broad access to your workflow environment", and "Workflows triggered by pull requests from forks are particularly at risk." So gate the fork case before you ship this.

None of the above applies to the model call if you're on BYOK — COPILOT_PROVIDER_BASE_URL pointed at your own endpoint. But there COPILOT_PROVIDER_BEARER_TOKEN takes precedence over COPILOT_PROVIDER_API_KEY, which is the hook for a short-lived token from a gateway instead of a stored key.

In Cursor

In this tool, we can't get rid of secrets, so we'll just say it. The Cursor CLI supports two ways of authentication:

  • browser-based, with credentials stored on the machine

  • via API key

You can see in the docs that they encourage the latter for automation — "For automation, scripts, or CI environments, use API key authentication" — by setting an env var called CURSOR_API_KEY or using the --api-key flag. For instance, this is what the official GitHub Actions snippet looks like:

YAML
- name: Run Cursor Agent
  env:
    CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
  run: |
    agent -p "Your prompt here" --model gpt-5

To be sure we looked into the shipped 2026.07.23 build and saw CURSOR_API_KEY throughout the bundles; there's also no mention of ACTIONS_ID_TOKEN_REQUEST_URL, workloadIdentityPools or AssumeRoleWithWebIdentity. There's an --endpoint flag, but that's for the Cursor API (https://api2.cursor.sh by default), not a cloud model endpoint, which means credential chaining isn't available. So let's accept that as a dead end and focus on these four things instead:

  1. A key dedicated to this pipeline only, different from your local dev key, so we know where it lives and revoking it breaks exactly one thing

  2. Using the env var rather than the flag, so the key isn't visible in the process list of a shared runner and doesn't end up pasted into a debugging comment

  3. Limiting the agent's behaviour, since the key is long-lived — Cursor has a permissions config with allow and deny rules, so deny Write(.env*) and shell access to git, and allow only the paths this job actually needs. With a key like this, that config is doing the job an IAM role does elsewhere

  4. Writing down the rotation date, because nothing in this setup will remind you

Also, agent status prints the account a session is authenticated as — the fastest way to make sure the runner is using the pipeline's identity and not a personal login someone forgot in a cached home directory.

In Antigravity CLI

Antigravity — we would personally avoid setting up CI pipelines to run it in an automated way for now, and there's one specific reason for that.

There's no authentication flag and no auth subcommand. Run agy --help on 1.1.10 and the whole surface is agent, changelog, help, install, models, plugin and update, plus agents and plugins as aliases of two of those.

What is more, their docs for headless usage — which is what you need for CI — say plainly: "Headless mode uses your cached credentials. Authenticate once with an interactive agy session first." In a non-interactive environment with no terminal it fails with an authentication error rather than hanging, which is better, but given a CI scenario starts on a clean machine it means you'll need to store whatever that session cached and put it back on disk before the run.

What we don't like about this is that we don't know anything about that credential — they don't document it, so we can't know its scope or how long it stays valid, and we can't be confident it was properly revoked after use (if that's even possible). It's the opposite direction from everything else in this lesson.

That being said, running the CLI headlessly is fine in itself, and their own CI sample is a good pattern to keep:

BASH
result=$(agy -p "Name three popular version control systems, comma-separated." \
  --output-format json --print-timeout 10m)

Then inspect the status field and exit non-zero when it isn't a success, so the pipeline fails on such a run instead of quietly producing nothing.

On the API key you'll find suggested elsewhere. Blog posts and third-party wrappers name an ANTIGRAVITY_API_KEY. Looking into the 1.1.10 binary we can't find that string at all — what we do find is GEMINI_API_KEY, GOOGLE_API_KEY, GOOGLE_APPLICATION_CREDENTIALS and GOOGLE_GENAI_USE_VERTEXAI, along with the CLI's own warning text for having two of them set at once. None of it is in the vendor's CLI documentation, and a string in a binary doesn't prove which code path reads it. Undocumented auth is definitely not a good foundation for a pipeline.

If they add support for an API key in the future, it will be documented.

Generally speaking — use agy on your dev machine if you want to work with it interactively, and pick sth else for CI.

In Kimi Code CLI

No keyless path here, and one trap which can cost you significant time if you fall into it.

Auth flow — device-code login, via the kimi login command or /login inside the TUI. You have two options: Kimi Code OAuth, or an API key from the Moonshot AI Open Platform. After login, kimi provider list shows which provider is active, its type, how many models it has, where the credentials came from, and what the default model is:

TEXT
managed:kimi-code  type=kimi  models=4  source=oauth

Default model: kimi-code/k3

The trap — it ignores environment variables exported to the shell.

It's a very common mistake, and the official docs call it out by name: "many users run export KIMI_API_KEY=xxx in the shell expecting the CLI to pick it up automatically, but it does not."

Provider credentials resolve from [providers.<name>].api_key in the config file first, and only when that's empty from the matching key inside the [providers.<name>.env] sub-table.

The main thing is that the env sub-table is just a section of the config file. It doesn't read anything from your shell environment, so with KIMI_API_KEY=abc123 exported the CI run exits with a missing-credentials error even though env clearly shows the variable is set.

The exception — the KIMI_MODEL_* channel, and the recommended CI setup.

If you set KIMI_MODEL_NAME, the CLI takes a different route: it "synthesizes a temporary provider and model alias from the KIMI_MODEL_* variables in memory — nothing is written back to the config file". For example:

BASH
export KIMI_MODEL_NAME="kimi-k2.5"
export KIMI_MODEL_API_KEY="$MOONSHOT_KEY"
export KIMI_MODEL_PROVIDER_TYPE="kimi"
kimi -p "Summarise the changes on this branch"

You need to set both KIMI_MODEL_NAME and KIMI_MODEL_API_KEY. If you set only the name, the CLI fails immediately at startup with a clear error saying a required part is missing.

There are also a few optional variables: KIMI_MODEL_BASE_URL, KIMI_MODEL_PROVIDER_TYPE (kimi, anthropic or openai) and context and thinking settings.

So basically, if you want to run the CLI from CI, use this env-var channel. That way you don't need a config file holding credentials, and the secret is bound to the job environment rather than stored on the runner. If for some reason you really do need a config file, set KIMI_CODE_HOME to a per-job directory so it dies with the runner:

BASH
export KIMI_CODE_HOME="$RUNNER_TEMP/kimi"

One more thing to keep out of your artifacts. Kimi's own security note says session directories, wire files and task records "may contain user prompts, command output, repository paths, tool return values, or traces of credentials". All of those live under KIMI_CODE_HOME — so don't upload that directory as a build artifact.

What keyless doesn't buy you

So. If it comes to keyless, there are a few limitations:

  • During the run, the job has an actual credential — for its entire duration the runner can access the model endpoint, and an agent is a program that runs commands people write into issue comments; this way you shorten the window from indefinite to a session, but you don't stop the credential being used inside it. Prompt injection is a real path to the same endpoint and it doesn't care how the credential got there

  • Scope the role to the model and nothing else — this is where the granular control from GitHub's third bullet actually pays out. Anthropic publishes the minimum for Bedrock and it's four verbs, not one: bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream, bedrock:ListInferenceProfiles and bedrock:GetInferenceProfile — the last one so Claude Code can resolve an application inference profile to its model without an extra round-trip. On Google Cloud the documented role is roles/aiplatform.user. A role which can invoke a model and can't touch your buckets is a ruined afternoon; a role which got a broad managed policy because that was quicker is a different kind of story

  • Fork PRs are a pain — GitHub's own Copilot CLI guidance puts it plainly: "Workflows triggered by pull requests from forks are particularly at risk." A few ideas: checking who started the run, an environment with a required reviewer, or not running the credentialled job on fork PRs at all

  • If the harness can't do keyless, the boring answers are what's left: a key created only for this pipeline (so revoking it breaks exactly one thing), a cap set for the billing account (covered in the companion lesson about budgets and kill switches) and actually rotating it (not planning to). It's not revolutionary, but it covers most of the surface

Where that leaves you

So. If your harness can be pointed at Bedrock, Vertex or Foundry — spend 20 minutes on it: identity provider, role, StringEquals for the trust condition, workflow_dispatch probe to verify — and you'll have a pipeline without a model credential in it at all, and one less secret nobody remembers to rotate

If not, don't try to force it — keep the key in the secret store, but limit its use to this single pipeline, set up a spending cap on the billing account (like in the companion lesson on budgets and kill switches) and note the date you'll rotate it. All these measures are sensible, and it's a conscious choice rather than a line from a quickstart

THE FILE.github/workflows/verify-keyless.yml
CODE
# Proves the OIDC trust policy works, before any agent is wired to it.
# Run it by hand from the Actions tab; it makes no model calls and costs nothing.
# On Google Cloud, swap the login step for google-github-actions/auth and the
# identity check for `gcloud auth list`.

name: Verify keyless auth

on:
  workflow_dispatch:

# No checkout, so id-token is the only permission this needs.
permissions:
  id-token: write

jobs:
  verify:
    runs-on: ubuntu-latest
    # Include this only if your trust condition pins the environment claim.
    # environment: production
    steps:
      - name: Assume the role
        uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: us-east-1
          role-session-name: verify-keyless

      - name: Who are we
        run: |
          aws sts get-caller-identity | tee "$GITHUB_STEP_SUMMARY"

      - name: Can the role see Bedrock
        # Needs bedrock:ListInferenceProfiles, which is in the policy Anthropic
        # publishes. Drop this step if your role is scoped tighter than that.
        run: |
          aws bedrock list-inference-profiles \
            --region us-east-1 \
            --query 'inferenceProfileSummaries[].inferenceProfileId' \
            --output text
j / k to move between lessons