Route AI code reviews through Aperture
Last validated:
AI-powered code reviews generate API calls from every pull request, and each call needs an API key. When those keys live in GitHub secrets spread across repositories, you lose visibility into which team or workflow is spending what, and a single leaked key exposes your entire AI budget. Revoking or rotating keys means touching every repository that uses one.
In this guide, you route Claude Code GitHub Action requests through Aperture by Tailscale so that CI runners authenticate with their Tailscale identity instead of individual API keys. Every AI code review request flows through Aperture, where it is logged with the caller's identity, charged against a central budget, and forwarded to Anthropic using a single API key that never leaves the Aperture instance.
You configure three things: a tailnet policy file rule, an Aperture grant, and a GitHub Actions workflow.
How it works
The request flow works like this:
- The Tailscale GitHub Action runs at the start of your workflow and creates an ephemeral node tagged
tag:cion your tailnet. - The Claude Code Action starts a code review. Because the workflow sets
ANTHROPIC_BASE_URLtohttp://<aperture-hostname>, every API call goes to the Aperture device instead of directly to Anthropic. - Aperture receives the request, identifies the caller as
tag:ciusing Tailscale's WhoIs API, checks the grant, and forwards the request to Anthropic with the centrally managed API key. - Aperture logs the request with the caller's identity, model, token count, and cost. The response flows back to the runner.
This approach removes API keys from GitHub secrets and the runner environment. The runner authenticates through its Tailscale identity alone, and Aperture injects the Anthropic API key when forwarding requests.
Because all traffic between the runner and Aperture travels over your tailnet, it is encrypted with WireGuard. The http:// URL is safe because the underlying transport is already encrypted.
Prerequisites
- An Aperture instance with an Anthropic provider configured and at least one Anthropic model enabled.
- A Tailscale account with Owner, Admin, or Network admin permissions.
- A GitHub repository with GitHub Actions enabled and admin access to create repository secrets.
- Familiarity with GitHub Actions workflow syntax, Tailscale tags, and tailnet policy file grants.
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.
Step 1: Create a CI tag and grant access to Aperture
Before your CI runners can reach Aperture, define a tag for them, allow that tag to connect to the Aperture device, and grant it permission to use models.
Define the tag
Open the Access controls page in the admin console. 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 can apply the tag. The Tailscale GitHub Action applies this tag automatically when it creates its ephemeral node.
Allow network access
In the same tailnet policy file, add a grant that allows tag:ci devices to reach the Aperture device on port 80:
{
"grants": [
{
"src": ["tag:ci"],
"dst": ["ai"],
"ip": ["tcp:80"]
}
]
}
Replace "<aperture-hostname>" with the hostname or Tailscale IP address of your Aperture device if it differs from the default.
Grant model access
CI runners also need an Aperture grant that authorizes them to use specific models. Add the following grant to the tailnet policy file:
{
"grants": [
{
"src": ["tag:ci"],
"dst": ["<aperture-hostname>"],
"app": {
"tailscale.com/cap/aperture": [
{ "role": "user" },
{ "models": "anthropic/**" }
]
}
}
]
}
The models pattern anthropic/** allows access to all Anthropic models. Adjust the pattern to restrict CI runners to specific models, such as anthropic/claude-sonnet-4-5.
The grant must include both a role entry and a models entry. Without role, Aperture returns HTTP 403 even if the models pattern matches.
You can combine network access and model access into a single grant:
{
"grants": [
{
"src": ["tag:ci"],
"dst": ["<aperture-hostname>"],
"ip": ["tcp:80"],
"app": {
"tailscale.com/cap/aperture": [
{ "role": "user" },
{ "models": "anthropic/**" }
]
}
}
]
}
Save the tailnet policy file. The rules take effect immediately.
Step 2: Set up Tailscale authentication for GitHub Actions
The Tailscale GitHub Action needs credentials to join your tailnet as an ephemeral node.
Workload identity federation (recommended)
Workload identity federation uses GitHub's OIDC tokens to authenticate, eliminating secrets that can expire or leak.
- Create a federated identity in the Tailscale admin console. Set the issuer to GitHub Actions and configure the subject claim to match your repository. Assign the
auth_keysscope and thetag:citag. - Copy the Client ID and Audience values.
- In your GitHub repository, create two repository 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 in Step 3.
OAuth client
If your organization does not use workload identity federation, create an OAuth client instead.
- Create an OAuth client in the admin console with the
auth_keysscope and thetag:citag. - Copy the Client ID and Client secret.
- 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.
Step 3: Configure the GitHub Actions workflow
With access controls and authentication in place, configure your workflow to connect to the tailnet and route Claude Code requests through Aperture.
Create or update a workflow file (for example, .github/workflows/code-review.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.
name: AI code review
on:
pull_request:
permissions:
id-token: write
pull-requests: write
contents: read
jobs:
review:
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: <aperture-hostname>
- name: Run Claude Code review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: "-"
env:
ANTHROPIC_BASE_URL: "http://ai"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
Permissions
The permissions block sets the minimum GitHub token permissions the workflow needs:
| Permission | Purpose |
|---|---|
id-token: write | Required for workload identity federation. The Tailscale action uses it to request a GitHub OIDC token. 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 for the review. |
Tailscale GitHub Action
The tailscale/github-action@v4 step creates an ephemeral node on your tailnet:
tags: tag:ciassigns the tag that Aperture uses to identify and authorize the caller.ping: <aperture-hostname>waits up to three minutes for connectivity to the Aperture device before proceeding, preventing the Claude Code step from starting before the tailnet connection is ready.
Claude Code Action
The anthropics/claude-code-action@v1 step runs the AI code review:
anthropic_api_key: "-"is a placeholder that satisfies the action's non-empty check. Aperture manages the real API key, so this value is never sent to Anthropic.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 and fail without direct internet access.
anthropic_api_key is a with: input (action parameter), while ANTHROPIC_BASE_URL and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC are env: variables. This distinction matters because the action performs a truthy check on anthropic_api_key and refuses to start without it.
Step 4: Verify the pipeline
After committing the workflow file, open a pull request with a small code change to confirm the integration works end-to-end. The workflow runs automatically and the Claude Code Action posts review comments on the PR.
Check the GitHub Actions logs
Open the workflow run in your repository's Actions tab:
- In the Connect to tailnet step, confirm the Tailscale action connected and the
ping: <aperture-hostname>check succeeded. - In the Run Claude Code review step, confirm Claude Code received a response from Aperture. Find lines showing the API call to
http://<aperture-hostname>.
Check the Aperture dashboard
Open the Aperture dashboard at http://<aperture-hostname>/ui/ from a device on your tailnet:
- Go to the Logs page.
- Find the request from your test PR. The request shows
tag:cias the caller identity. - Verify the logged model name, token count, and cost match what you expect.
If the request appears with the tag:ci identity, the integration is working. Every future pull request generates auditable records in Aperture.
Troubleshoot common issues
If the workflow fails, check these common causes:
| 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 Aperture 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. |
Export logs for compliance (optional)
For long-term retention, you can export Aperture session logs to S3-compatible storage. Each exported record includes the caller identity, model, token counts, cost, and full session context, which you can feed into your existing compliance, cost-allocation, or analytics pipelines.
For setup instructions, refer to export usage data to S3.
Next steps
With AI code reviews flowing through Aperture, consider:
- Set per-user spending limits to cap how much your CI pipelines spend on AI reviews per day or month.
- Restrict CI runners to specific models by narrowing the
modelspattern in the grant, for exampleanthropic/claude-sonnet-4-5instead ofanthropic/**. - Apply the same pattern to other AI-powered CI tools by setting their base URL to
http://<aperture-hostname>and connecting through the tailnet. - Build a custom webhook to receive real-time notifications when CI runners make AI requests.