RSS Amplifier

Zest · Nov 28, 2025

Stop Paying Twice: Connecting Claude’s Subscription to OpenCode

0
Sign in to vote or save

Haseeb Ahmed · Zest

I checked my API bill last week, and I didn’t like what I saw.

If you’ve been following my series Vibe Coding to Agentic Development, you know I’ve been advocating for moving away from “vibes” and toward structured, agentic workflows. But the truth is that agents can be expensive.

As soon as you start chaining prompts — planning, proposals, implementations, reviews — you scale from sending a single message to dozens, and your bill scales with it. And if you’re using high-end models like Claude Sonnet, the meter moves fast.

The irony is that I’m already paying Anthropic for a Claude Pro subscription. Yet when I use OpenCode Agents on GitHub, I’m forced to supply an API key and pay again for every token.

After digging into the OpenCode documentation and GitHub issues, I found a workaround. If you have a Claude Pro/Max subscription, you don’t actually need to use an API key. You only need to understand how OpenCode authentication works.

OpenCode stores credentials for multiple providers in a local auth.json file. We can leverage this file by injecting your Claude Code OAuth tokens into the GitHub workflow so that OpenCode can authenticate using your subscription instead of an API key.

Before you begin, you should already have:

  1. A Claude Pro or Claude Max subscription

  2. OpenCode installed locally

  3. OpenCode GitHub integration configured

If you have those in place, start by logging into your Claude subscription from the OpenCode CLI:

opencode auth login

Then:

  • Select Anthropic

  • Select Claude Pro/Max

  • Authorize the request in the browser window

  • Copy the auth code and paste it back into the terminal

At this point, your local OpenCode installation has fresh access and refresh tokens. OpenCode stores these in the following file:

~/.local/share/opencode/auth.json

It should look roughly like this:

{
  ...
  “anthropic”: {
    “type”: “oauth”,
    “refresh”: “********”,
    “access”: “********”,
    “expires”: 1764258797353
  }
  ...
}

Copy the refresh, access, and expires values from the anthropic section.

A critical detail: Anthropic generates a new refresh token each time one is used. If both your local machine and GitHub use the same refresh token, whichever environment refreshes it first will invalidate the other. That’s why GitHub needs its own synced copy, and why your tokens in secrets must stay fresh. I would recommend that once you’ve copied the values, rerun the opencode auth login flow to get a new refresh token for your local machine.

Next, create GitHub secrets with values that you copied before:

  1. ANTHROPIC_ACCESS_TOKEN

  2. ANTHROPIC_REFRESH_TOKEN

  3. ANTHROPIC_EXPIRES

You also need a GitHub Personal Access Token (Classic) with full “repo” scope stored as GH_PAT. This is required because the workflow updates secrets whenever tokens refresh.

Finally, update your opencode.yaml workflow with the following:

name: OpenCode
on:
  issue_comment:
    types: [created]
jobs:
  opencode:
    runs-on: self-hosted
    timeout-minutes: 15
    permissions:
      id-token: write
      contents: write
      pull-requests: write
      issues: write
    if: |
      contains(github.event.comment.body, ‘ /oc’) ||
      startsWith(github.event.comment.body, ‘/oc’) ||
      contains(github.event.comment.body, ‘ /opencode’) ||
      startsWith(github.event.comment.body, ‘/opencode’)
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Refresh Anthropic OAuth tokens if needed
        env:
          GH_TOKEN: ${{ secrets.GH_PAT }}
        run: |
          CURRENT_TIME=$(($(date +%s) * 1000))
          TOKEN_EXPIRES=${{ secrets.ANTHROPIC_EXPIRES }}
          # Check if token needs refresh (expires in less than 1 hour)
          if [ $((TOKEN_EXPIRES - CURRENT_TIME)) -lt 3600000 ]; then
            echo “🔄 Token expiring soon, refreshing...”
            RESPONSE=$(curl -s -w “\n%{http_code}” -X POST https://console.anthropic.com/v1/oauth/token \
              -H “Content-Type: application/json” \
              -d “{
                \”grant_type\”: \”refresh_token\”,
                \”refresh_token\”: \”${{ secrets.ANTHROPIC_REFRESH_TOKEN }}\”,
                \”client_id\”: \”9d1c250a-e61b-44d9-88ed-5944d1962f5e\”
              }”)
            HTTP_CODE=$(echo “$RESPONSE” | tail -1)
            BODY=$(echo “$RESPONSE” | head -n -1)
            if [ “$HTTP_CODE” -eq “200” ]; then
              echo “✅ Token refresh successful!”
              NEW_ACCESS=$(echo “$BODY” | jq -r ‘.access_token’)
              NEW_REFRESH=$(echo “$BODY” | jq -r ‘.refresh_token’)
              EXPIRES_IN=$(echo “$BODY” | jq -r ‘.expires_in’)
              NEW_EXPIRES=$(($(date +%s) * 1000 + EXPIRES_IN * 1000))
              # Update GitHub Secrets
              echo “📝 Updating GitHub secrets...”
              gh secret set ANTHROPIC_ACCESS_TOKEN --body “$NEW_ACCESS”
              gh secret set ANTHROPIC_REFRESH_TOKEN --body “$NEW_REFRESH”
              gh secret set ANTHROPIC_EXPIRES --body “$NEW_EXPIRES”
              echo “✅ Secrets updated successfully”
              # Export new tokens for use in subsequent steps
              echo “ANTHROPIC_ACCESS_TOKEN=$NEW_ACCESS” >> $GITHUB_ENV
              echo “ANTHROPIC_REFRESH_TOKEN=$NEW_REFRESH” >> $GITHUB_ENV
              echo “ANTHROPIC_EXPIRES=$NEW_EXPIRES” >> $GITHUB_ENV
            else
              echo “❌ Token refresh failed with status $HTTP_CODE”
              echo “$BODY”
              exit 1
            fi
          else
            echo “✅ Token still valid, skipping refresh”
          fi
      - name: Configure authentication
        run: |
          mkdir -p ~/.local/share/opencode
          # Use refreshed tokens if available, otherwise use secrets
          ACCESS_TOKEN=”${ANTHROPIC_ACCESS_TOKEN:-${{ secrets.ANTHROPIC_ACCESS_TOKEN }}}”
          REFRESH_TOKEN=”${ANTHROPIC_REFRESH_TOKEN:-${{ secrets.ANTHROPIC_REFRESH_TOKEN }}}”
          EXPIRES=”${ANTHROPIC_EXPIRES:-${{ secrets.ANTHROPIC_EXPIRES }}}”
          cat > ~/.local/share/opencode/auth.json <<EOF
          {
            “anthropic”: {
              “type”: “oauth”,
              “refresh”: “$REFRESH_TOKEN”,
              “access”: “$ACCESS_TOKEN”,
              “expires”: $EXPIRES
            },
            “opencode”: {
              “type”: “api”,
              “key”: “${{ secrets.OPENCODE_API_KEY }}”
            }
          }
          EOF
          chmod 600 ~/.local/share/opencode/auth.json
          # Verify with smart masking (first half + 8 asterisks)
          echo “Auth configuration:”
          jq ‘{
            anthropic: {
              type: .anthropic.type,
              refresh: (.anthropic.refresh[:(.anthropic.refresh|length)/2] + “********”),
              access: (.anthropic.access[:(.anthropic.access|length)/2] + “********”),
              expires: .anthropic.expires
            },
            opencode: {
              apiKey: (.opencode.apiKey[:(.opencode.apiKey|length)/2] + “********”)
            }
          }’ ~/.local/share/opencode/auth.json
      - name: Run opencode
        uses: sst/opencode/github@latest
        with:
          model: anthropic/claude-sonnet-4-5

This workflow handles token refresh automatically and syncs updated tokens back into GitHub Secrets. When the workflow runs OpenCode, it reconstructs the auth.json file on the runner using the latest tokens.

I could explain it line by line, but where’s the fun in that? Paste it into your LLM of choice and let it walk you through the logic.

This setup works well, but it forces a comparison between OpenCode and the built-in Claude Code environment that comes with your subscription.

Using this method effectively turns OpenCode into a third-party client for your Claude subscription. So why not just use Anthropic’s official Claude Code?

If you operate entirely within Anthropic’s ecosystem and prefer a guided, safety-first experience, Claude Code is hard to beat. But the real strength of OpenCode is provider agnosticism. If Claude throttles, goes down, or hits your limit, you can switch to OpenAI or even a local model without changing your workflow. You’re not locked in, which for many developers — myself included — is a compelling reason to use OpenCode.

Yes. It’s ideal for the SDD (Spec-Driven Development) workflows I’ll cover in Part 3. It encourages more experimentation with agents without fearing runaway API costs. And when heavy lifting is required — long autonomous loops, sweeping refactors — you can still fall back to raw API usage when it makes sense.

Thanks for reading Zest! This post is public so feel free to share it.

Share

PS: This approach should also work with other OAuth-based subscriptions (e.g., GitHub Copilot). You’ll need to adapt the refresh-token logic to match that provider’s OAuth flow, but the structure stays the same.

Read the original on zestbyhaseeb.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.