Use Claude Code Action with Aperture · Tailscale Docs

Documentation

Use Claude Code Action with Aperture

Last validated: Jun 16, 2026 | Copy as Markdown | View as Markdown

Aperture by Tailscale is currently in beta.

Configure the Claude Code GitHub Action to send requests through Aperture by Tailscale so that CI runners use their Tailscale identity for authentication instead of per-repository API keys.

Prerequisites

To avoid unexpected TLS issues, use http:// for the Aperture URL when configuring LLM clients. All connections remain encrypted using WireGuard, even when HTTPS is not used.

Create a CI tag and grant access to Aperture

CI runners need a Tailscale tag, network access to the Aperture device, and a grant that authorizes model usage.

  1. Open the Access controls page in the admin console.
  2. In the tagOwners section, add tag:ci if it does not already exist:
    "tagOwners": {
         "tag:ci": []
    }
    ```

An empty owner list means [Owners, Admins, and Network admins](/content/docs/reference/user-roles/index.html) can apply the tag.
3. Add a grant that gives `tag:ci` devices network access to the Aperture device and permission to use Anthropic models:

```json
    {
         "grants": [
           {
             "src": ["tag:ci"],
             "dst": ["<aperture-hostname>"],
             "ip": ["tcp:80"],
             "app": {
               "tailscale.com/cap/aperture": [
                 { "role": "user" },
                 { "models": "anthropic/**" }
               ]
             }
           }
         ]
    }
    ```

Replace `"<aperture-hostname>"` with the hostname or Tailscale IP address of your Aperture device if it differs from the default. Adjust the `models` pattern to restrict CI runners to specific models, such as `anthropic/claude-sonnet-4-6`.
4. Save the tailnet policy file. The rules take effect immediately.

The grant must include both a `role` entry and a `models` entry. Without `role`, Aperture returns HTTP 403 even if the `models` pattern matches.

For more information on grant syntax and model patterns, refer to [grant access to models](/content/docs/aperture/how-to/grant-model-access/index.html). Because this grant lives in the tailnet policy file, it includes an explicit `dst` field. For how grant requirements differ between the tailnet policy file and the Aperture configuration, refer to [Aperture grants versus tailnet policy file grants](/content/docs/aperture/reference/aperture-vs-tailnet-grants/index.html).

## [Set up Tailscale authentication for GitHub Actions](/content/docs/aperture/how-to/use-claude-code-action#set-up-tailscale-authentication-for-github-actions/index.html)

The [Tailscale GitHub Action](/content/docs/integrations/github/github-action/index.html) needs credentials to join your tailnet as an ephemeral node.

### [Workload identity federation (recommended)](/content/docs/aperture/how-to/use-claude-code-action#workload-identity-federation-recommended/index.html)

Workload identity federation uses GitHub's OIDC tokens to authenticate, eliminating secrets that can expire or leak.

1. Create a [federated identity](/content/docs/features/workload-identity-federation/index.html) in the Tailscale admin console. Set the issuer to GitHub Actions and configure the subject claim to match your repository. Assign the `auth_keys` scope and the `tag:ci` tag.
2. Copy the **Client ID** and **Audience** values.
3. In your GitHub repository, create two [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets):
   - `TS_OAUTH_CLIENT_ID`: your federated identity Client ID.
   - `TS_AUDIENCE`: your federated identity Audience.

Workload identity federation requires the `id-token: write` permission in your GitHub Actions workflow, which you add when you configure the workflow file.

### [OAuth client](/content/docs/aperture/how-to/use-claude-code-action#oauth-client/index.html)

If your organization does not use workload identity federation, create an [OAuth client](/content/docs/features/oauth-clients/index.html) instead.

1. Create an [OAuth client](/content/docs/features/oauth-clients/index.html) in the admin console with the `auth_keys` scope and the `tag:ci` tag.
2. Copy the **Client ID** and **Client secret**.
3. In your GitHub repository, create two repository secrets:
   - `TS_OAUTH_CLIENT_ID`: your OAuth Client ID.
   - `TS_OAUTH_SECRET`: your OAuth Client secret.

For detailed instructions on either option, refer to the [Tailscale GitHub Action documentation](/content/docs/integrations/github/github-action/index.html).

## [Add Claude Code Action to the workflow](/content/docs/aperture/how-to/use-claude-code-action#add-claude-code-action-to-the-workflow/index.html)

Create or update a workflow file (for example, `.github/workflows/claude.yml`) with the following content. This example uses workload identity federation. If you use an OAuth client, replace `audience` with `oauth-secret` and reference `secrets.TS_OAUTH_SECRET`.

```yaml
name: Claude Code
on:
  pull_request:

permissions:
  id-token: write
  pull-requests: write
  contents: read

jobs:
  claude:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

- name: Connect to tailnet
        uses: tailscale/github-action@v4
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          audience: ${{ secrets.TS_AUDIENCE }}
          tags: tag:ci
          ping: ai

- name: Run Claude Code
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: "-"
        env:
          ANTHROPIC_BASE_URL: "http://ai"
          CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"

Key settings

Setting Purpose
tags: tag:ci Assigns the tag that Aperture uses to identify and authorize the caller.
ping: <aperture-hostname> Waits for connectivity to the Aperture device before proceeding. Prevents the Claude Code step from starting before the tailnet connection is ready.
anthropic_api_key: "-" Placeholder that satisfies the action's non-empty check. Aperture manages the real API key.
ANTHROPIC_BASE_URL: "http://<aperture-hostname>" Redirects all API calls to the Aperture device. MagicDNS resolves <aperture-hostname> to the Aperture device's Tailscale IP address.
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" Prevents Claude Code from sending telemetry or update checks that would bypass Aperture.

anthropic_api_key is a with: input (action parameter), while ANTHROPIC_BASE_URL and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC are env: variables. The action performs a truthy check on anthropic_api_key and refuses to start without it.

Permissions

The permissions block sets the minimum GitHub token permissions the workflow needs:

Permission Purpose
id-token: write Required for workload identity federation. Omit if you use an OAuth client.
pull-requests: write Lets the Claude Code Action post review comments on the pull request.
contents: read Lets the Claude Code Action read repository files.

Verify the connection

  1. Open a pull request with a small code change to trigger the workflow.
  2. In the workflow run's Actions tab, confirm:
    • The Connect to tailnet step connected and the ping: <aperture-hostname> check succeeded.
    • The Run Claude Code step completed and shows API calls to http://<aperture-hostname>.
  3. Open the Aperture dashboard at http://<aperture-hostname>/ui/ from a device in your tailnet. Go to the Logs page (admin only) and confirm the request appears with tag:ci as the caller identity.

Troubleshoot common issues

Symptom Cause Fix
Connection timeout in the Tailscale step tag:ci does not exist in tagOwners, or the runner cannot reach the coordination server. Verify the tag exists in your tailnet policy file and your GitHub secrets are correct.
HTTP 403 from Aperture The grant is missing a role entry, or the models pattern does not match the requested model. Verify the grant includes both { "role": "user" } and a matching { "models": "..." } entry.
Claude Code reports an API key error anthropic_api_key is empty or missing. Set anthropic_api_key: "-" in the with: block. The value must be non-empty.
ping: <aperture-hostname> times out The network grant does not allow tag:ci to reach the Aperture device on port 80. Check the dst and ip fields in your network access grant.

Next steps