Identity and MCP governance / Connect GitHub Actions jobs with workload identity
Connect GitHub Actions jobs with workload identity
Give a GitHub Actions job an agent identity's plugins, MCP servers, and hooks with the job's own OIDC token and no stored Speakeasy secret.
The speakeasy-api/device-agent-action step signs a GitHub Actions job in as an agent identity with the job’s own GitHub OIDC token, so no Speakeasy secret is stored. Later steps run Claude Code or Codex with the agent’s plugins, MCP servers, and hooks.
This guide covers:
- Access requirements
- How it works
- Prerequisites
- Set up the agent
- Inputs
- Outputs
- Verify the job
- Troubleshooting
- Limits
- Revoke access
Access requirements
Section titled “Access requirements”The setup needs workload:write, and permission to give the agent the device agent permissions listed under prerequisites. Organization admins have both. See workload identity.
How it works
Section titled “How it works”- The setup trusts the GitHub OIDC issuer for the organization and adds an access rule that admits the repository’s jobs as the agent.
- In the job, the action installs
speakeasydand writes a managed configuration that holds no secret, only the organization and the hooks project. - The action’s one-time sync requests a GitHub OIDC token, exchanges it at the organization’s device agent endpoint for a 15-minute Speakeasy session, and fetches the agent’s plugins.
- Each Speakeasy MCP server is written into Claude Code and Codex with a header helper,
speakeasyd mcp-token, which gets a fresh token every time the tool connects. No MCP token is written to disk or to the job log. - The action leaves
speakeasydrunning for the rest of the job, so tools and hooks in later steps can get tokens even when the step has no GitHub OIDC variables. For example,anthropics/claude-code-actionremoves those variables. The runner stopsspeakeasydwhen the job ends.
Prerequisites
Section titled “Prerequisites”| Requirement | Detail |
|---|---|
| Runner | A Linux or macOS runner, x64 or ARM64, on github.com. Windows runners and GitHub Enterprise Server are not supported. |
| AI tool | Claude Code 2.1.238 or later, or Codex 0.152.0 or later, installed in an earlier step of the same job. Cursor and OpenClaw have no header helper, so a job gets no Speakeasy MCP servers in them. |
| Device agent | speakeasyd 0.1.54 or later. The action installs the latest stable release unless you pin one with version. |
| Job permissions | id-token: write, so the job can request its OIDC token, and contents: read. |
| Agent permissions | org:device_agent_sync for the whole organization, to fetch plugins, and org:hooks_ingest on the hooks project, to send hook events. |
| Plugins | Every MCP server the job should call is in a plugin delivered to the agent. The job gets its MCP servers only from these plugins. See what the job gets. |
| MCP access | The agent may connect to each of those servers, selected under MCP Selection when you create or edit the agent. |
A job can call an MCP server only when both are true: the server is in a plugin delivered to the agent, and the agent may connect to it. Selecting a server under MCP Selection covers only the second. It grants mcp:connect, which makes the server reachable through a gateway but doesn’t put it in the job.
Set up the agent
Section titled “Set up the agent”Open Identity > Agents and select New agent identity. Name the agent and, under MCP Selection, select the MCP servers the job may call. Each one must also be in a plugin delivered to the agent: see what the job gets. On the Agent sign-in step, choose GitHub Actions.
Repository and jobs
Section titled “Repository and jobs”Enter the repository’s Owner and Repository with the same capital letters GitHub shows. Speakeasy compares them exactly. Repositories created, renamed, or transferred on github.com since July 15, 2026 carry immutable IDs in their tokens. For those, add @ and the ID to both names, which the setup shows how to look up.
Choose Which jobs the rule admits:
| Choice | Rule | Admits |
|---|---|---|
| Whole repository (default) | repo:OWNER/REPO:* | Every branch, tag, environment, and pull request job of the repository. |
| One branch | repo:OWNER/REPO:ref:refs/heads/BRANCH | Jobs on that branch that use no environment. Pull request jobs are refused. |
| One environment | repo:OWNER/REPO:environment:ENVIRONMENT | Only jobs that use that environment. GitHub starts them only after the environment’s protection rules pass. |
| Pull requests | repo:OWNER/REPO:pull_request | Only pull request jobs that use no environment. A pull request job runs the workflow as its branch has it, so nobody reviews it first. |
Anyone who can push a branch to the repository can run a job that the whole-repository or pull request rule admits, and use the agent’s access. Pull requests from forks get no OIDC token on the pull_request event, but a pull_request_target workflow with id-token: write is admitted, so do not use that event. To put a review in front of the agent’s access, choose One environment and set its protection rules under the repository’s Settings > Environments before adding the workflow.
Select Connect GitHub Actions. The setup checks that the agent can be given both permissions, then creates the agent, trusts the GitHub issuer at the organization level, saves the rule, and grants the permissions.
What the job gets
Section titled “What the job gets”This step lists the plugins installed for Everyone in the project, and the MCP servers in them. The list is partial: it leaves out plugins from roles the agent holds and from other projects. A job receives:
- Plugins delivered to the agent: those assigned to Everyone or to a role the agent holds, from any project, as Required or On by default. A plugin that is only Available is never installed on a job, because nobody there can turn it on. Plugins can’t be assigned to an agent directly, so assign the plugin to a role and give the agent that role. See plugin assignments.
- The Speakeasy-hosted MCP servers in those plugins, and no others. Each is named
speakeasy-and the server’s slug in the tool’s configuration, such asspeakeasy-linear-1a2b, with a number added when two servers share a name. A server selected under MCP Selection that is in none of these plugins doesn’t reach the job. - The Speakeasy observability plugin, which sends the job’s hook events, unless the project disabled it.
The step also names the servers the job can’t use. A server in these plugins that the agent may not connect to shows in the job’s tools as needing authentication: go back to MCP Selection and add it. For the servers a job skips, see limits.
Add the workflow
Section titled “Add the workflow”The Add the workflow step generates the workflow for Claude Code or Codex. Copy it from the dashboard. For a branch rule, the workflow runs on pushes to that branch only. For an environment rule, it adds environment: to the job. For the pull request rule, it runs on pull requests only. The example below uses the whole-repository rule, with ORGANIZATION_ID and PROJECT_SLUG as placeholders for the values the dashboard fills in.
For Claude Code, add ANTHROPIC_API_KEY to the repository’s Actions secrets. Speakeasy access uses the job’s GitHub token separately.
name: Speakeasy
on: push: branches: [main] pull_request:
jobs: agent: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false runs-on: ubuntu-latest permissions: id-token: write # lets the job request its GitHub OIDC token contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false # Install the tool and create its configuration directory before Speakeasy syncs. - name: Install Claude Code run: | npm install -g @anthropic-ai/claude-code@2.1.314 mkdir -p "$HOME/.claude" # Installs the latest stable speakeasyd; add version: <x.y.z> to pin one. - name: Connect to Speakeasy id: speakeasy uses: speakeasy-api/device-agent-action@v0.1 with: organization-id: ORGANIZATION_ID project: PROJECT_SLUG # hook events go to this project # Lists the agent's Speakeasy MCP servers; each one connects with a fresh token. - run: claude mcp list - name: Run Claude Code uses: anthropics/claude-code-action@6d5ae526d25ce88daa2200be073e1a373167f064 # v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ github.token }} prompt: Describe this repository and list the Speakeasy MCP tools available to you. Do not modify files.@v0.1 follows the latest v0.1.x release of the action. Teams that require commit pins can pin a release’s commit instead, with a # v0.1.x comment.
To let Claude call a Speakeasy tool without a permission prompt, add claude_args: --allowedTools mcp__SERVER_NAME to the last step, using a server name that claude mcp list prints.
For Codex, add OPENAI_API_KEY to the repository’s Actions secrets. The workflow differs in three steps: Codex gets a CODEX_HOME shared with the device agent, the list step runs codex mcp list, and the last step runs openai/codex-action:
- name: Install Codex run: | npm install -g @openai/codex@0.166.0 echo "CODEX_HOME=$RUNNER_TEMP/speakeasy-codex" >> "$GITHUB_ENV" mkdir -p "$RUNNER_TEMP/speakeasy-codex" # ...Connect to Speakeasy, unchanged... - run: codex mcp list - name: Run Codex uses: openai/codex-action@bdf19a4a223ec2549a3e2274a0cf61556bc07675 # v1 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} codex-home: ${{ env.CODEX_HOME }} prompt: Describe this repository and list the Speakeasy MCP tools available to you. Do not modify files.Keep the steps in this order, in one job. The action writes configuration only for tools installed before it, and the tool steps after it rely on the speakeasyd it leaves running.
Inputs
Section titled “Inputs”| Input | Required | Description |
|---|---|---|
organization-id | Yes | The Speakeasy organization ID. The dashboard fills it in. |
project | No | The project slug that receives the job’s hook events. The agent needs org:hooks_ingest on it. Leave it empty to turn hook events off. |
control-plane-url | No | Defaults to https://ai.speakeasy.com. The token audience is built from it, so set it only for a deployment that serves the device agent API at another address. |
version | No | The speakeasyd release to install. Defaults to latest, the latest stable release. Set a stable version, 0.1.54 or later, for reproducible runs: pinning the action to a commit does not pin speakeasyd. |
speakeasyd-binary | No | Path to a speakeasyd already on the runner, used instead of a release. For testing unreleased builds only. |
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
report | Path to the JSON report from speakeasyd sync --once: the control plane, the agent the job signed in as, and each tool’s status. |
skipped | Set to true when the step skipped, for example on a pull request from a fork. |
daemon | Set to true when speakeasyd is running for later steps to get tokens from. |
version | The speakeasyd version installed, such as X.Y.Z. Empty when speakeasyd-binary is set. |
Read them in later steps as steps.speakeasy.outputs.NAME, using the id the generated workflow gives the step.
Verify the job
Section titled “Verify the job”- Sign-in and plugin sync. The Connect to Speakeasy step log prints the same summary as
report, one line per tool. A status ofarmedmeans the tool is configured. - MCP servers. The
claude mcp listorcodex mcp liststep lists eachspeakeasy-server. - An MCP call. Ask the tool to use one of the agent’s tools. On the agent’s page, Overview counts the call under Tool calls and lists it under Tool logs.
- Hook events. With
projectset, the job’s session appears under Agent sessions on the agent’s page, and in that project’s Observability > Agent Sessions.
Troubleshooting
Section titled “Troubleshooting”| What the job shows | What to do |
|---|---|
| Notice: skipped on a pull request from a fork | Nothing. GitHub gives fork pull requests no OIDC token. |
| Error: the job has no GitHub OIDC token | Add id-token: write to the job’s permissions. |
| Error: Speakeasy refused this job’s GitHub token | Compare the subject the error prints with the agent’s access rule, including capital letters and IDs. A job that uses an environment is named by the environment, not its branch. Speakeasy answers every refusal the same way, so also check that the GitHub issuer is trusted for the organization, not only for one project. |
Error naming org:device_agent_sync | Give the agent that permission. The agent’s owner must hold it too. |
| Error: did not pick up the GitHub identity | The installed speakeasyd predates GitHub Actions support. Use latest, or set version to 0.1.54 or later. |
Error: a speakeasyd version is not a stable release, or is blocked | Set version to another stable release, or to latest. |
Error: a speakeasyd SHA-256 or size doesn’t match | The download doesn’t match the Speakeasy release records. Contact Speakeasy rather than working around it. |
| Warning: a tool is not set up | Install the tool before Connect to Speakeasy, or read the report output for its status. |
| Warning: hook events are off | Set the project input. |
Warning: speakeasyd did not start | Later steps that can’t request a GitHub token, such as anthropics/claude-code-action, can’t reach Speakeasy. Read the daemon log the warning names. |
| An MCP server shows as needing authentication | Edit the agent and add that server under MCP Selection. A server that signs in upstream with a person’s account may not work in a job. See limits. |
| An expected server is missing | Put the server in a plugin delivered to the agent. Selecting it under MCP Selection isn’t enough. See what the job gets and limits. |
Limits
Section titled “Limits”- No MCP gateways yet. Gateway endpoints and the organization gateway don’t accept workload sessions, and the agent’s own gateway takes only an agent API key. The job skips the gateways in its plugins and the servers behind them. It gets a server only when a plugin lists that server directly.
- Servers need a Speakeasy authorization server. The job gets each server’s token from one, so it skips a server without one.
- No sign-ins during a job. Nobody in a job can complete an upstream OAuth sign-in. A server that signs in upstream with a person’s account can use only an account the agent’s owner already connected, so plan on it not working in a job. See servers with per-user OAuth. A server with a shared credential, such as a service account, needs no person.
- Hook events need the current hooks. They need version 0.3.41 or later of the observability plugin’s hooks, which Speakeasy rolls out to organizations in stages.
- Codex telemetry is off. Codex can’t send a per-connection token to its OpenTelemetry exporter, so on a job only its hook events are sent.
Revoke access
Section titled “Revoke access”On the agent’s page, under Credentials, select Remove on GitHub Actions under Federated access to stop the repository’s jobs from signing in as the agent. To stop a job that is already running, suspend the agent. See change or remove access.
Next steps
Section titled “Next steps”- Read Workload identity for how trusted platforms, access rules, and token exchange work.
- Read Authorizing agents to scope the agent the job acts as.
- Read the Device agent reference for how
speakeasydinstalls plugins and hooks on developer machines and cloud sandboxes.