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:
A Claude Pro or Claude Max subscription
OpenCode installed locally
OpenCode GitHub integration configured
If you have those in place, start by logging into your Claude subscription from the OpenCode CLI:
opencode auth loginThen:
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.jsonIt 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 loginflow to get a new refresh token for your local machine.
Next, create GitHub secrets with values that you copied before:
ANTHROPIC_ACCESS_TOKENANTHROPIC_REFRESH_TOKENANTHROPIC_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-5This 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.
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.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.