Vectrum
Sign In
1690 lines · 56.0 KB

Claude Code OAuth Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: If you are logged into Claude Code, ./harness starts with no arguments and no environment.

Architecture: A new internal/claudecode package parses ~/.claude/.credentials.json and hands out a bearer token. llm.Client gains WithTokenSource so the credential is resolved per request instead of frozen at construction, and both request paths share one apply instead of duplicating header logic. main.go consults the file only when no other credential was supplied and the endpoint is Anthropic's.

Tech Stack: Go 1.26.8, stdlib only (encoding/json, net/http, net/http/httptest, sync). No new dependencies.

Spec: docs/superpowers/specs/2026-09-17-claude-code-oauth-design.md

Global Constraints

  • Go module is harness; import paths are harness/internal/....
  • Working root for every command is /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46. Do not cd to the original checkout.
  • internal/claudecode imports stdlib only. It must not import internal/llm — llm is the consumer, and the dependency runs that way or not at all.
  • No test makes a real network call. httptest for everything.
  • Discovered credentials are used only when client.IsDefaultEndpoint() is true and no flag or env var supplied one. This is a security property, not a preference.
  • New exported identifiers get a doc comment, matching the existing house style.
  • Run gofmt -w on every file you touch before committing.
  • Run go build ./... && go test ./... before every commit.
  • Tasks 1 and 6 are probes. They end in a recorded finding, not a feature.
  • Task 6 has a STOP gate. Do not implement Task 7 if Task 6 reports rotation. Report and wait.

Task 1: Probe — does an OAuth request need the Claude Code identity block?

The spec leaves this open. It costs one request to close, and it touches only the access token, so it carries none of Task 6's risk.

Files:

  • Create: /tmp/oauth-probe.sh (throwaway, not committed)
  • Modify: docs/superpowers/specs/2026-09-17-claude-code-oauth-design.md (record the finding)

Interfaces:

  • Consumes: nothing.

  • Produces: a decision that gates Task 5. Either "identity block required" or "not required".

  • Step 1: Send a minimal request with the stored token and no identity block

cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
TOK=$(python3 -c "import json;print(json.load(open('$HOME/.claude/.credentials.json'))['claudeAiOauth']['accessToken'])")
curl -sS -o /tmp/probe-plain.json -w '%{http_code}\n' https://api.anthropic.com/v1/messages \
  -H "authorization: Bearer $TOK" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: oauth-2025-04-20" \
  -H "content-type: application/json" \
  -d '{"model":"claude-opus-5","max_tokens":16,"system":[{"type":"text","text":"You are a coding agent operating on a local repository through tools."}],"messages":[{"role":"user","content":"Say OK."}]}'
cat /tmp/probe-plain.json

Expected: either 200 (no identity block needed) or 401/403 with a message about the client.

  • Step 2: If it failed, confirm the identity block is the difference

Only run this if Step 1 was non-200. Identical request, identity string as the first system block:

curl -sS -o /tmp/probe-identity.json -w '%{http_code}\n' https://api.anthropic.com/v1/messages \
  -H "authorization: Bearer $TOK" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: oauth-2025-04-20" \
  -H "content-type: application/json" \
  -d '{"model":"claude-opus-5","max_tokens":16,"system":[{"type":"text","text":"You are Claude Code, Anthropic'"'"'s official CLI for Claude."},{"type":"text","text":"You are a coding agent operating on a local repository through tools."}],"messages":[{"role":"user","content":"Say OK."}]}'
cat /tmp/probe-identity.json

A 200 here with a non-200 in Step 1 proves the identity block is required.

  • Step 3: Record the finding in the spec

Replace the first paragraph of the spec's ## The system prompt section with what you measured. State the status codes observed. If the block is not required, say so and note that Task 5 is skipped.

  • Step 4: Commit the finding
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add docs/superpowers/specs/2026-09-17-claude-code-oauth-design.md
git commit -m "Design: settle whether OAuth needs the Claude Code identity block

Probed with one request. Recorded the status codes rather than the
inference, so the next person does not have to re-run it."

Task 2: internal/claudecode — parse and expiry

Pure functions over a file. No network, no refresh.

Files:

  • Create: internal/claudecode/claudecode.go
  • Create: internal/claudecode/claudecode_test.go

Interfaces:

  • Consumes: nothing.

  • Produces:

    • claudecode.Credentials{AccessToken, RefreshToken string; ExpiresAt, RefreshExpiresAt time.Time; Scopes []string; SubscriptionType string}
    • claudecode.Load(home string) (*Credentials, error)
    • (*Credentials).Expired(now time.Time) bool
    • (*Credentials).RefreshExpired(now time.Time) bool
    • claudecode.ErrNotFound — sentinel, matched with errors.Is
    • claudecode.Filename = ".claude/.credentials.json"
  • Step 1: Write the failing tests

Create internal/claudecode/claudecode_test.go:

package claudecode

import (
	"errors"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// write builds a fake home directory holding one credentials file.
func write(t *testing.T, body string) string {
	t.Helper()
	home := t.TempDir()
	dir := filepath.Join(home, ".claude")
	if err := os.MkdirAll(dir, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, ".credentials.json"), []byte(body), 0o600); err != nil {
		t.Fatal(err)
	}
	return home
}

const sample = `{"claudeAiOauth":{
  "accessToken":"sk-ant-oat01-access",
  "refreshToken":"sk-ant-ort01-refresh",
  "expiresAt":1789668269460,
  "refreshTokenExpiresAt":1791404074460,
  "scopes":["user:inference","user:profile"],
  "subscriptionType":"team"}}`

func TestLoadReadsEveryField(t *testing.T) {
	c, err := Load(write(t, sample))
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if c.AccessToken != "sk-ant-oat01-access" {
		t.Errorf("AccessToken = %q", c.AccessToken)
	}
	if c.RefreshToken != "sk-ant-ort01-refresh" {
		t.Errorf("RefreshToken = %q", c.RefreshToken)
	}
	if c.SubscriptionType != "team" {
		t.Errorf("SubscriptionType = %q", c.SubscriptionType)
	}
	if len(c.Scopes) != 2 || c.Scopes[0] != "user:inference" {
		t.Errorf("Scopes = %v", c.Scopes)
	}
}

// The file stores epoch milliseconds. Reading them as seconds puts expiry
// 55,000 years out and the token never looks stale.
func TestLoadTreatsTimestampsAsMilliseconds(t *testing.T) {
	c, err := Load(write(t, sample))
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	want := time.UnixMilli(1789668269460)
	if !c.ExpiresAt.Equal(want) {
		t.Errorf("ExpiresAt = %s, want %s", c.ExpiresAt, want)
	}
	if want := time.UnixMilli(1791404074460); !c.RefreshExpiresAt.Equal(want) {
		t.Errorf("RefreshExpiresAt = %s, want %s", c.RefreshExpiresAt, want)
	}
}

// Not logged in is not a failure. It is a fall-through to the next source.
func TestLoadMissingFileIsErrNotFound(t *testing.T) {
	_, err := Load(t.TempDir())
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

// A file that exists but does not parse is a broken install, and saying so
// with the path beats falling through to "no credentials".
func TestLoadMalformedJSONNamesThePath(t *testing.T) {
	home := write(t, "{not json")
	_, err := Load(home)
	if err == nil || errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want a parse error", err)
	}
	if !strings.Contains(err.Error(), ".credentials.json") {
		t.Errorf("error should name the file, got %q", err)
	}
}

// An empty access token is a file we cannot use, and it fails at parse time
// rather than as a puzzling 401 later.
func TestLoadEmptyAccessTokenIsAnError(t *testing.T) {
	_, err := Load(write(t, `{"claudeAiOauth":{"accessToken":""}}`))
	if err == nil || errors.Is(err, ErrNotFound) {
		t.Fatalf("err = %v, want an error about the missing token", err)
	}
}

func TestExpiredUsesASkewMargin(t *testing.T) {
	base := time.Unix(1_700_000_000, 0)
	c := &Credentials{ExpiresAt: base}
	if c.Expired(base.Add(-5 * time.Minute)) {
		t.Error("a token with five minutes left is not expired")
	}
	// Inside the skew margin: it would expire during the request we are about
	// to send, so it counts as expired now.
	if !c.Expired(base.Add(-30 * time.Second)) {
		t.Error("a token expiring in 30s should count as expired")
	}
	if !c.Expired(base.Add(time.Minute)) {
		t.Error("a token past its expiry is expired")
	}
}

func TestRefreshExpiredIsTerminal(t *testing.T) {
	base := time.Unix(1_700_000_000, 0)
	c := &Credentials{RefreshExpiresAt: base}
	if c.RefreshExpired(base.Add(-24 * time.Hour)) {
		t.Error("a refresh token with a day left is not expired")
	}
	if !c.RefreshExpired(base.Add(time.Hour)) {
		t.Error("a refresh token past its expiry is expired")
	}
}

Add "strings" to that file's imports.

  • Step 2: Run the tests to verify they fail
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/claudecode/...

Expected: FAIL — the package does not build, Load and Credentials are undefined.

  • Step 3: Write the implementation

Create internal/claudecode/claudecode.go:

// Package claudecode reads the OAuth credential Claude Code stores on disk, so
// a harness run can borrow a login that already exists.
//
// This is Claude Code's private storage, not a documented interface: the
// schema can change without notice. Everything that knows its shape lives
// here, and parses strictly, so a change surfaces as a clear error rather than
// as a puzzling 401 somewhere else.
package claudecode

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"
)

// Filename is the credential store, relative to the home directory.
const Filename = ".claude/.credentials.json"

// skew is how long before its stated expiry a token starts counting as
// expired. A token with seconds left would expire during the request we are
// about to send.
const skew = 60 * time.Second

// ErrNotFound means there is no credentials file: not logged in, rather than
// broken. Callers fall through to whatever source is next.
var ErrNotFound = errors.New("claudecode: no credentials file")

// Credentials is the claudeAiOauth object, with its millisecond timestamps
// converted to something Go can compare.
type Credentials struct {
	AccessToken      string
	RefreshToken     string
	ExpiresAt        time.Time
	RefreshExpiresAt time.Time
	Scopes           []string
	SubscriptionType string
}

// stored mirrors the on-disk JSON. Timestamps are epoch milliseconds.
type stored struct {
	ClaudeAIOAuth struct {
		AccessToken           string   `json:"accessToken"`
		RefreshToken          string   `json:"refreshToken"`
		ExpiresAt             int64    `json:"expiresAt"`
		RefreshTokenExpiresAt int64    `json:"refreshTokenExpiresAt"`
		Scopes                []string `json:"scopes"`
		SubscriptionType      string   `json:"subscriptionType"`
	} `json:"claudeAiOauth"`
}

// Load reads and parses the credential store. home is passed in rather than
// looked up so tests can point it at a fixture, matching skills.Discover.
func Load(home string) (*Credentials, error) {
	path := filepath.Join(home, Filename)
	raw, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("claudecode: reading %s: %w", path, err)
	}
	var s stored
	if err := json.Unmarshal(raw, &s); err != nil {
		return nil, fmt.Errorf("claudecode: parsing %s: %w", path, err)
	}
	o := s.ClaudeAIOAuth
	if o.AccessToken == "" {
		return nil, fmt.Errorf("claudecode: %s has no access token", path)
	}
	return &Credentials{
		AccessToken:      o.AccessToken,
		RefreshToken:     o.RefreshToken,
		ExpiresAt:        time.UnixMilli(o.ExpiresAt),
		RefreshExpiresAt: time.UnixMilli(o.RefreshTokenExpiresAt),
		Scopes:           o.Scopes,
		SubscriptionType: o.SubscriptionType,
	}, nil
}

// Expired reports whether the access token is past its expiry, or close enough
// that it would lapse mid-request.
func (c *Credentials) Expired(now time.Time) bool {
	return !now.Before(c.ExpiresAt.Add(-skew))
}

// RefreshExpired reports whether the refresh token is past its expiry. That is
// terminal: no amount of refreshing fixes it, only logging in again.
func (c *Credentials) RefreshExpired(now time.Time) bool {
	return !now.Before(c.RefreshExpiresAt)
}
  • Step 4: Run the tests to verify they pass
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w internal/claudecode/ && go test ./internal/claudecode/...

Expected: PASS, all seven tests.

  • Step 5: Commit
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add internal/claudecode/
git commit -m "claudecode: parse Claude Code's credential store

Timestamps are epoch milliseconds; reading them as seconds puts expiry
55,000 years out and the token never looks stale. A missing file means
not logged in and falls through; a file that will not parse names its
path, because that is a broken install rather than an absent one."

Task 3: llm.Client resolves its credential per request

Today authToken is frozen at construction and both request paths repeat the same header logic. Resolve once per call, before the retry loop, and share one apply.

Files:

  • Modify: internal/llm/client.go:56-58 (struct fields), :74-80 (WithAuthToken), :116 (HasCredentials), :150-155 (CreateMessage), :190-205 (attempt)
  • Modify: internal/llm/stream.go:81-93 (streamAttempt), and StreamMessage's loop above it
  • Modify: internal/llm/client_test.go (add tests)

Interfaces:

  • Consumes: nothing from earlier tasks.

  • Produces:

    • llm.WithTokenSource(func(context.Context) (string, error)) Option
    • unexported auth struct with apply(http.Header), and (*Client).resolveAuth(context.Context) (auth, error)
    • WithAuthToken and WithAPIKey keep today's behaviour exactly.
  • Step 1: Write the failing tests

Append to internal/llm/client_test.go:

// A token source is consulted per request, so a credential that rotates
// mid-session reaches the wire without rebuilding the client.
func TestTokenSourceIsCalledPerRequest(t *testing.T) {
	var got []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got = append(got, r.Header.Get("authorization"))
		io.WriteString(w, okResponse)
	}))
	defer srv.Close()

	n := 0
	c := New(WithBaseURL(srv.URL), WithTokenSource(func(context.Context) (string, error) {
		n++
		return fmt.Sprintf("tok-%d", n), nil
	}))
	for i := 0; i < 2; i++ {
		if _, err := c.CreateMessage(context.Background(), Request{}); err != nil {
			t.Fatalf("CreateMessage: %v", err)
		}
	}
	if len(got) != 2 || got[0] != "Bearer tok-1" || got[1] != "Bearer tok-2" {
		t.Errorf("authorization headers = %v, want Bearer tok-1 then tok-2", got)
	}
}

func TestTokenSourceGetsTheOAuthBetaHeader(t *testing.T) {
	var got http.Header
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got = r.Header.Clone()
		io.WriteString(w, okResponse)
	}))
	defer srv.Close()

	c := New(WithBaseURL(srv.URL), WithTokenSource(func(context.Context) (string, error) {
		return "tok", nil
	}))
	if _, err := c.CreateMessage(context.Background(), Request{}); err != nil {
		t.Fatalf("CreateMessage: %v", err)
	}
	if got.Get("anthropic-beta") != oauthBeta {
		t.Errorf("anthropic-beta = %q, want %s", got.Get("anthropic-beta"), oauthBeta)
	}
	if got.Get("x-api-key") != "" {
		t.Errorf("x-api-key should be unset, got %q", got.Get("x-api-key"))
	}
}

// A credential that cannot be resolved is permanent. Retrying it three times
// with backoff just delays the error, so resolution happens before the loop.
func TestTokenSourceErrorIsNotRetried(t *testing.T) {
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		hits++
		io.WriteString(w, okResponse)
	}))
	defer srv.Close()

	var calls int
	c := New(WithBaseURL(srv.URL), WithTokenSource(func(context.Context) (string, error) {
		calls++
		return "", errors.New("login expired")
	}))
	c.RetryDelay = time.Millisecond
	_, err := c.CreateMessage(context.Background(), Request{})
	if err == nil || !strings.Contains(err.Error(), "login expired") {
		t.Fatalf("err = %v, want the source's error", err)
	}
	if calls != 1 {
		t.Errorf("token source called %d times, want 1", calls)
	}
	if hits != 0 {
		t.Errorf("server hit %d times, want 0 — no request should have been sent", hits)
	}
}

func TestHasCredentialsCountsATokenSource(t *testing.T) {
	c := New(WithBaseURL("http://127.0.0.1:1"))
	c.apiKey, c.authToken = "", ""
	if c.HasCredentials() {
		t.Fatal("no credentials expected before the source is set")
	}
	c.tokenSource = func(context.Context) (string, error) { return "tok", nil }
	if !c.HasCredentials() {
		t.Error("a token source is a credential")
	}
}

// The streaming path must authenticate identically to the buffered one.
func TestStreamUsesTheTokenSource(t *testing.T) {
	var got http.Header
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got = r.Header.Clone()
		w.Header().Set("content-type", "text/event-stream")
		io.WriteString(w, streamOK)
	}))
	defer srv.Close()

	c := New(WithBaseURL(srv.URL), WithTokenSource(func(context.Context) (string, error) {
		return "stream-tok", nil
	}))
	if _, err := c.StreamMessage(context.Background(), Request{}, func(StreamEvent) {}); err != nil {
		t.Fatalf("StreamMessage: %v", err)
	}
	if got.Get("authorization") != "Bearer stream-tok" {
		t.Errorf("authorization = %q", got.Get("authorization"))
	}
	if got.Get("anthropic-beta") != oauthBeta {
		t.Errorf("anthropic-beta = %q, want %s", got.Get("anthropic-beta"), oauthBeta)
	}
}

streamOK does not exist. stream_test.go:15 defines sse(events ...string) string, which formats events the way the API does — build the payload with it. A message_stop event alone is enough for a stream that ends cleanly:

	streamOK := sse(`{"type":"message_stop"}`)

Put that line at the top of the test body. If readStream rejects a stream with no message_start, prepend the message_start and message_delta events that stream_test.go's existing happy-path test uses — copy them verbatim from there rather than inventing a shape.

Add "errors", "fmt" and "time" to client_test.go's imports if they are not already present.

  • Step 2: Run the tests to verify they fail
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/llm/...

Expected: FAIL — WithTokenSource and the tokenSource field are undefined.

  • Step 3: Add the field, the option, and the resolver

In internal/llm/client.go, extend the struct's credential fields:

	apiKey      string
	authToken   string
	tokenSource func(context.Context) (string, error)

Add the option next to WithAuthToken:

// WithTokenSource sets a bearer credential resolved per request, so a token
// that expires mid-session can be replaced without rebuilding the client. A
// nil function is ignored.
func WithTokenSource(f func(context.Context) (string, error)) Option {
	return func(c *Client) {
		if f != nil {
			c.tokenSource, c.apiKey, c.authToken = f, "", ""
		}
	}
}

Teach HasCredentials about it:

// HasCredentials reports whether the client can authenticate.
func (c *Client) HasCredentials() bool {
	return c.apiKey != "" || c.authToken != "" || c.tokenSource != nil
}

Add the resolver and the shared header logic:

// auth is one request's resolved credential. Resolving returns a value rather
// than mutating the client so concurrent requests cannot interleave.
type auth struct {
	apiKey string
	bearer string
}

// apply writes the credential headers. An OAuth bearer also needs the beta
// flag; an API key does not.
func (a auth) apply(h http.Header) {
	if a.apiKey != "" {
		h.Set("x-api-key", a.apiKey)
		return
	}
	h.Set("authorization", "Bearer "+a.bearer)
	h.Set("anthropic-beta", oauthBeta)
}

// resolveAuth produces the credential for one request. It runs before the
// retry loop: a credential that will not resolve is permanent, and the loop
// only short-circuits on a non-retryable *APIError, so resolving inside
// attempt would retry a dead credential three times with backoff.
func (c *Client) resolveAuth(ctx context.Context) (auth, error) {
	if c.apiKey != "" {
		return auth{apiKey: c.apiKey}, nil
	}
	if c.tokenSource != nil {
		tok, err := c.tokenSource(ctx)
		if err != nil {
			return auth{}, err
		}
		return auth{bearer: tok}, nil
	}
	return auth{bearer: c.authToken}, nil
}
  • Step 4: Thread it through both request paths

In CreateMessage, resolve before the retry loop — directly after the HasCredentials check:

	a, err := c.resolveAuth(ctx)
	if err != nil {
		return nil, err
	}

Change attempt's signature to func (c *Client) attempt(ctx context.Context, body []byte, a auth) (*Response, error), update its call site to c.attempt(ctx, body, a), and replace its switch/default credential block with:

	a.apply(httpReq.Header)

Do the same in stream.go: resolve once in StreamMessage before its retry loop, change streamAttempt to take an auth, update the call site, and replace its if c.apiKey != "" { … } else { … } block with a.apply(httpReq.Header).

StreamMessage needs the same up-front HasCredentials check as CreateMessage if it does not already have one — check, and add it if absent.

  • Step 5: Run the full test suite
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w internal/llm/ && go build ./... && go test ./...

Expected: PASS. TestOAuthTokenUsesBearerAndBetaHeader and TestNoCredentialsIsAClearError must still pass untouched — the old options are now the constant case of the new one, and that is the point.

  • Step 6: Commit
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add internal/llm/
git commit -m "llm: resolve the credential per request

A frozen authToken cannot outlive its own expiry. WithTokenSource makes
the bearer a function; WithAuthToken is now its constant case.

Resolution happens before the retry loop because a credential error is
not an *APIError, and the loop only returns early for those — resolving
inside attempt would retry a dead credential three times with backoff.
Both request paths now share one apply instead of repeating the headers."

Task 4: Fall back to the Claude Code login

The feature, end to end, without refresh. An expired token fails with an instruction rather than a 401.

Files:

  • Modify: internal/claudecode/claudecode.go (add Source)
  • Modify: internal/claudecode/claudecode_test.go (add tests)
  • Modify: cmd/harness/main.go:38-72 (flag, client construction, fallback), :296 (errNoCredentials)
  • Create: cmd/harness/auth_test.go

Interfaces:

  • Consumes: claudecode.Load, (*Credentials).Expired, llm.WithTokenSource, llm.Client.HasCredentials, llm.Client.IsDefaultEndpoint.

  • Produces:

    • claudecode.NewSource(*Credentials) *Source
    • (*Source).Token(context.Context) (string, error)
    • claudecode.ErrExpired
    • resolveClaudeCode(client *llm.Client, home string, disabled bool) (func(context.Context) (string, error), string, error) in package main
  • Step 1: Write the failing Source tests

Append to internal/claudecode/claudecode_test.go:

func TestSourceReturnsTheAccessToken(t *testing.T) {
	s := NewSource(&Credentials{
		AccessToken: "tok",
		ExpiresAt:   time.Now().Add(time.Hour),
	})
	got, err := s.Token(context.Background())
	if err != nil {
		t.Fatalf("Token: %v", err)
	}
	if got != "tok" {
		t.Errorf("Token = %q, want tok", got)
	}
}

// Until refresh lands, an expired token is a clear instruction rather than a
// 401 from the API.
func TestSourceExpiredTokenSaysWhatToDo(t *testing.T) {
	s := NewSource(&Credentials{
		AccessToken: "tok",
		ExpiresAt:   time.Now().Add(-time.Hour),
	})
	_, err := s.Token(context.Background())
	if !errors.Is(err, ErrExpired) {
		t.Fatalf("err = %v, want ErrExpired", err)
	}
	if !strings.Contains(err.Error(), "claude") {
		t.Errorf("the error should say to run claude, got %q", err)
	}
}

Add "context" to the imports.

  • Step 2: Run the tests to verify they fail
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/claudecode/...

Expected: FAIL — NewSource and ErrExpired undefined.

  • Step 3: Implement Source

Append to internal/claudecode/claudecode.go:

// ErrExpired means the stored access token has lapsed. Task 7 may replace this
// with a refresh; until then it is the whole story.
var ErrExpired = errors.New("claudecode: your Claude Code login has expired, run `claude` to refresh it")

// Source hands out the access token for as long as it is valid. It satisfies
// the shape llm.WithTokenSource wants.
type Source struct {
	creds *Credentials
}

// NewSource wraps credentials in a token source.
func NewSource(c *Credentials) *Source { return &Source{creds: c} }

// Token returns the access token, or ErrExpired once it has lapsed.
func (s *Source) Token(context.Context) (string, error) {
	if s.creds.Expired(time.Now()) {
		return "", ErrExpired
	}
	return s.creds.AccessToken, nil
}

Add "context" to the file's imports.

  • Step 4: Write the failing precedence tests

Create cmd/harness/auth_test.go:

package main

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"harness/internal/llm"
)

// fakeHome writes a credentials file with a token valid for an hour.
func fakeHome(t *testing.T) string {
	t.Helper()
	home := t.TempDir()
	dir := filepath.Join(home, ".claude")
	if err := os.MkdirAll(dir, 0o700); err != nil {
		t.Fatal(err)
	}
	// expiresAt is 2035, comfortably in the future.
	body := `{"claudeAiOauth":{"accessToken":"file-token","refreshToken":"r",
	  "expiresAt":2058430000000,"refreshTokenExpiresAt":2058430000000,
	  "scopes":["user:inference"],"subscriptionType":"team"}}`
	if err := os.WriteFile(filepath.Join(dir, ".credentials.json"), []byte(body), 0o600); err != nil {
		t.Fatal(err)
	}
	return home
}

func TestClaudeCodeFillsInAMissingCredential(t *testing.T) {
	c := llm.New(llm.WithBaseURL(llm.DefaultBaseURL))
	c.ClearCredentialsForTest()
	src, note, err := resolveClaudeCode(c, fakeHome(t), false)
	if err != nil {
		t.Fatalf("resolveClaudeCode: %v", err)
	}
	if src == nil {
		t.Fatal("expected a token source")
	}
	tok, err := src(context.Background())
	if err != nil {
		t.Fatalf("token: %v", err)
	}
	if tok != "file-token" {
		t.Errorf("token = %q, want file-token", tok)
	}
	if note == "" {
		t.Error("using a discovered credential should be announced")
	}
}

// An explicit credential wins. We never override what the user typed.
func TestClaudeCodeIsSkippedWhenACredentialExists(t *testing.T) {
	c := llm.New(llm.WithBaseURL(llm.DefaultBaseURL), llm.WithAPIKey("sk-ant-explicit"))
	src, _, err := resolveClaudeCode(c, fakeHome(t), false)
	if err != nil {
		t.Fatalf("resolveClaudeCode: %v", err)
	}
	if src != nil {
		t.Error("an explicit credential must not be replaced by the file")
	}
}

// The security property: a credential we found is never sent to an endpoint
// the user pointed us at. "We did not send it" is not observable from outside,
// so it is a test rather than a comment.
func TestClaudeCodeIsNeverSentToANonDefaultEndpoint(t *testing.T) {
	c := llm.New(llm.WithBaseURL("http://gateway.example"))
	c.ClearCredentialsForTest()
	src, _, err := resolveClaudeCode(c, fakeHome(t), false)
	if err != nil {
		t.Fatalf("resolveClaudeCode: %v", err)
	}
	if src != nil {
		t.Fatal("a discovered credential must never go to a non-default endpoint")
	}
}

func TestClaudeCodeCanBeDisabled(t *testing.T) {
	c := llm.New(llm.WithBaseURL(llm.DefaultBaseURL))
	c.ClearCredentialsForTest()
	src, _, err := resolveClaudeCode(c, fakeHome(t), true)
	if err != nil {
		t.Fatalf("resolveClaudeCode: %v", err)
	}
	if src != nil {
		t.Error("-no-claude-code-auth must skip discovery entirely")
	}
}

// Not logged in is not an error. It falls through to errNoCredentials.
func TestClaudeCodeMissingFileIsNotAnError(t *testing.T) {
	c := llm.New(llm.WithBaseURL(llm.DefaultBaseURL))
	c.ClearCredentialsForTest()
	src, _, err := resolveClaudeCode(c, t.TempDir(), false)
	if err != nil {
		t.Fatalf("a missing file is not an error, got %v", err)
	}
	if src != nil {
		t.Error("no file means no source")
	}
}
  • Step 5: Add the test helper the tests need

llm.New reads ANTHROPIC_API_KEY from the environment, so a developer with that set would see these tests pass for the wrong reason. Add to internal/llm/client.go:

// ClearCredentialsForTest drops any credential read from the environment, so a
// test exercising credential precedence is not at the mercy of the developer's
// own shell.
func (c *Client) ClearCredentialsForTest() {
	c.apiKey, c.authToken, c.tokenSource = "", "", nil
}
  • Step 6: Run the tests to verify they fail
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./cmd/... ./internal/claudecode/...

Expected: FAIL — resolveClaudeCode undefined.

  • Step 7: Implement the fallback in main.go

Add the flag beside -no-skills:

	noClaudeCodeAuth := flag.Bool("no-claude-code-auth", false, "do not fall back to the OAuth login in ~/"+claudecode.Filename)

Add the resolver function to cmd/harness/main.go:

// resolveClaudeCode returns a token source borrowed from Claude Code's login,
// or nil when one should not be used. The second return is a line for the
// user: a borrowed credential is worth announcing, the same way a non-default
// endpoint is.
//
// Two conditions gate it. The client must have no credential already, because
// we never override what the user typed. And the endpoint must be Anthropic's,
// because a credential we found on disk is not one the user chose to send —
// forwarding it to a -base-url someone else operates would leak it to a third
// party because a file happened to exist.
func resolveClaudeCode(client *llm.Client, home string, disabled bool) (func(context.Context) (string, error), string, error) {
	if disabled || client.HasCredentials() || !client.IsDefaultEndpoint() {
		return nil, "", nil
	}
	creds, err := claudecode.Load(home)
	if errors.Is(err, claudecode.ErrNotFound) {
		return nil, "", nil
	}
	if err != nil {
		return nil, "", err
	}
	note := fmt.Sprintf("using your Claude Code login (%s plan) from ~/%s", creds.SubscriptionType, claudecode.Filename)
	return claudecode.NewSource(creds).Token, note, nil
}

Rework the client construction to allow a second pass:

	opts := []llm.Option{
		llm.WithModel(*model),
		llm.WithBaseURL(*baseURL),
		llm.WithAPIKey(*apiKey),
		llm.WithAuthToken(*authToken),
	}
	client := llm.New(opts...)

	home, err := os.UserHomeDir()
	if err != nil {
		// No home directory is not fatal: an explicit credential still works.
		home = ""
	}
	var authNote string
	if home != "" {
		src, note, err := resolveClaudeCode(client, home, *noClaudeCodeAuth)
		if err != nil {
			return err
		}
		if src != nil {
			client = llm.New(append(opts, llm.WithTokenSource(src))...)
			authNote = note
		}
	}
	if !client.HasCredentials() {
		return errNoCredentials
	}

notice is defined further down, after useTUI is known, so report the note there:

	if authNote != "" {
		notice("%s", authNote)
	}

Put that next to the existing if !client.IsDefaultEndpoint() notice.

The existing skills block calls os.UserHomeDir() too — reuse the home you just computed rather than calling it twice, and delete the now-duplicate lookup.

Extend errNoCredentials with the new route:

var errNoCredentials = errors.New(`no credentials. Supply one of:
    export ANTHROPIC_API_KEY=sk-ant-...      (sent as x-api-key)
    export ANTHROPIC_AUTH_TOKEN=...          (sent as Authorization: Bearer)
    -api-key / -auth-token                   (flags override the environment)
    log in with Claude Code                  (borrowed automatically, Anthropic endpoint only)
To use a non-Anthropic endpoint, set ANTHROPIC_BASE_URL or -base-url as well.`)

Add "harness/internal/claudecode" to the imports.

  • Step 8: Run the full suite
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w cmd/ internal/ && go build ./... && go test ./...

Expected: PASS.

  • Step 9: Verify by hand that it actually starts
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go build -o /tmp/harness ./cmd/harness
env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN /tmp/harness -C /tmp -no-tui -p "reply with the single word: ok"

Expected: stderr says harness: using your Claude Code login (team plan) …, and the model replies. If Task 1 found the identity block is required, this fails with a 401 until Task 5 lands — that is expected, and it is the point of doing Task 5 next. Record which you saw.

Also confirm the guard holds:

env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN /tmp/harness -C /tmp -no-tui -base-url http://127.0.0.1:9 -p "hi"

Expected: no credentials — not a connection error. The file was never read.

  • Step 10: Commit
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add cmd/harness/ internal/claudecode/ internal/llm/
git commit -m "harness: borrow the Claude Code login when nothing else is set

Logged into Claude Code means ./harness starts with no arguments.

Two gates. An explicit credential is never overridden, and the file is
read only when the endpoint is Anthropic's: a credential we found on
disk is not one the user chose to send, and forwarding it to a -base-url
someone else runs would leak it because a file happened to exist.

No refresh yet — an expired token says to run claude."

Task 5: The Claude Code identity block

Skip this task entirely if Task 1 found the identity block is not required. If you skip it, say so in the final report.

Files:

  • Modify: internal/agent/loop.go:57-77 (Options), :301 and :318 (request assembly)
  • Modify: internal/agent/loop_test.go (add tests)
  • Modify: cmd/harness/main.go:149-157 (pass it)

Interfaces:

  • Consumes: resolveClaudeCode's non-nil source as the signal that the credential is OAuth-sourced.

  • Produces: agent.Options.SystemPrefix string and agent.ClaudeCodeIdentity.

  • Step 1: Write the failing tests

Append to internal/agent/loop_test.go:

func TestSystemPrefixBecomesTheFirstBlock(t *testing.T) {
	a := New(nil, tools.NewRegistry(nil), NewState(), nil, Options{SystemPrefix: ClaudeCodeIdentity})
	blocks := a.systemBlocks()
	if len(blocks) != 2 {
		t.Fatalf("got %d system blocks, want 2", len(blocks))
	}
	if blocks[0].Text != ClaudeCodeIdentity {
		t.Errorf("first block = %q, want the identity string", blocks[0].Text)
	}
	if !strings.Contains(blocks[1].Text, "coding agent") {
		t.Errorf("second block should be the harness prompt, got %q", blocks[1].Text)
	}
}

// The cache breakpoint has to sit on the last block, or only the identity
// string gets cached and the prompt behind it is re-read every turn.
func TestCacheBreakpointIsOnTheLastSystemBlock(t *testing.T) {
	a := New(nil, tools.NewRegistry(nil), NewState(), nil, Options{SystemPrefix: ClaudeCodeIdentity})
	blocks := a.systemBlocks()
	if blocks[0].CacheControl != nil {
		t.Error("the first block must not carry the breakpoint")
	}
	if blocks[len(blocks)-1].CacheControl == nil {
		t.Error("the last block must carry the breakpoint")
	}
}

// Without OAuth nothing changes, including byte-for-byte.
func TestNoPrefixMeansOneBlockAsBefore(t *testing.T) {
	a := New(nil, tools.NewRegistry(nil), NewState(), nil, Options{})
	blocks := a.systemBlocks()
	if len(blocks) != 1 {
		t.Fatalf("got %d system blocks, want 1", len(blocks))
	}
	if blocks[0].Text != defaultSystem {
		t.Error("the prompt must be unchanged when no prefix is set")
	}
}

This is the constructor shape TestSystemPromptListsSkills at loop_test.go:494 already uses, so tools and skills are already imported in that file.

  • Step 2: Run the tests to verify they fail
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/agent/...

Expected: FAIL — SystemPrefix, ClaudeCodeIdentity and systemBlocks undefined.

  • Step 3: Implement it

Add to internal/agent/loop.go, next to defaultSystem:

// ClaudeCodeIdentity is what an OAuth-authenticated request must present as
// its first system block. It is a fixed string, so it costs no cache hit: the
// prefix is still constant for the session.
const ClaudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude."

Add the field to Options:

	// SystemPrefix is prepended as a separate system block ahead of the
	// harness prompt. It carries the identity string an OAuth credential
	// requires, and is empty for every other credential.
	SystemPrefix string

Extract the block assembly out of the request builder at loop.go:301:

// systemBlocks builds the system prompt. The cache breakpoint goes on the last
// block so everything ahead of it — tools, identity, prompt — is cached
// together; on the first block it would cache the identity string and re-read
// the rest every turn.
func (a *Agent) systemBlocks() []llm.SystemBlock {
	var blocks []llm.SystemBlock
	if a.opts.SystemPrefix != "" {
		blocks = append(blocks, llm.SystemBlock{Type: "text", Text: a.opts.SystemPrefix})
	}
	blocks = append(blocks, llm.SystemBlock{Type: "text", Text: a.opts.System})
	if !a.opts.NoCache {
		blocks[len(blocks)-1].CacheControl = llm.Ephemeral()
	}
	return blocks
}

Then in the request builder, replace the system := llm.SystemBlock{…} line and its system.CacheControl = llm.Ephemeral() inside the if !a.opts.NoCache block with a call to a.systemBlocks(), and set System: a.systemBlocks() in the llm.Request. Leave the messages-side breakpoint in that if exactly as it is.

  • Step 4: Run the tests to verify they pass
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w internal/agent/ && go test ./internal/agent/...

Expected: PASS, including the existing TestSystemPromptIsStableAcrossRequests.

  • Step 5: Pass it from main.go

Track whether the credential came from Claude Code, and set the prefix:

	a := agent.New(client, reg, state, log, agent.Options{
		MaxTurns:      *maxTurns,
		Effort:        *effort,
		ContextWindow: *contextWindow,
		CompactAt:     *compactAt,
		NoCache:       *noCache,
		Stream:        !*noStream,
		Skills:        found,
		SystemPrefix:  systemPrefix,
	})

where systemPrefix is set to agent.ClaudeCodeIdentity in the same branch that set authNote, and is otherwise empty. There is a second agent.Options literal at main.go:185 — set SystemPrefix there too, or the resumed-session path authenticates differently from the fresh one.

  • Step 6: Verify end to end
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go build -o /tmp/harness ./cmd/harness
env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN /tmp/harness -C /tmp -no-tui -p "reply with the single word: ok"

Expected: the model replies. This is the request that 401'd at the end of Task 4.

  • Step 7: Commit
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w cmd/ internal/ && go build ./... && go test ./...
git add cmd/harness/ internal/agent/
git commit -m "agent: the Claude Code identity block for OAuth requests

An OAuth credential requires the identity string as the first system
block. It is static, so the cached prefix survives — but the breakpoint
moves to the last block, or only the identity string would be cached
and the real prompt re-read every turn."

Task 6: Probe — does the token endpoint rotate refresh tokens?

This task ends in a report, not code. It has a STOP gate.

The risk: if the endpoint returns a new refresh token, then refreshing without writing it back consumes the one Claude Code has on disk. Its copy goes dead and claude asks for a fresh login. That is damage outside this repo, so it gets measured before anything depends on it.

Files:

  • Modify: docs/superpowers/specs/2026-09-17-claude-code-oauth-design.md (record the finding)

Interfaces:

  • Consumes: nothing.

  • Produces: the gate for Task 7.

  • Step 1: Warn the user before spending the refresh token

Tell them, in these terms: this probe consumes one refresh. If the endpoint rotates, their Claude Code login will need claude run once to recover. Get a yes before Step 2. They approved the probe in principle when they chose "find out first" — this is the confirmation that it is happening now.

  • Step 2: Refresh once and compare
OLD=$(python3 -c "import json;print(json.load(open('$HOME/.claude/.credentials.json'))['claudeAiOauth']['refreshToken'])")
curl -sS -o /tmp/refresh.json -w '%{http_code}\n' https://console.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"$OLD\",\"client_id\":\"9d1c250a-e61b-44d9-88ed-5944d1962f5e\"}"
python3 - <<'EOF'
import json, os
r = json.load(open('/tmp/refresh.json'))
old = json.load(open(os.path.expanduser('~/.claude/.credentials.json')))['claudeAiOauth']['refreshToken']
new = r.get('refresh_token')
print('keys:', sorted(r))
print('returned a refresh_token:', new is not None)
print('ROTATED' if new and new != old else 'NOT ROTATED')
EOF

If the endpoint 404s or rejects the client id, that is itself the finding: the refresh route is not what we assumed. Record it and stop — do not go hunting for another endpoint without asking.

  • Step 3: Record the finding in the spec

Replace the spec's ## Refresh, and an open question that gates it open-question paragraphs with what you measured: the status code, whether a refresh_token came back, and whether it differed.

  • Step 4: STOP if it rotated, or if the endpoint was not what we assumed

Report to the user. Do not start Task 7. The remaining choice — write-back with atomic rename, or no refresh at all — is theirs, and the spec says so explicitly.

If it did not rotate, continue to Task 7.

  • Step 5: Commit the finding
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add docs/superpowers/specs/2026-09-17-claude-code-oauth-design.md
git commit -m "Design: settle whether the token endpoint rotates refresh tokens

Measured rather than assumed, because guessing wrong here breaks the
user's Claude Code login rather than this harness."

Task 7: Refresh on expiry

Only if Task 6 reported NOT ROTATED. Otherwise this task does not exist yet.

Files:

  • Modify: internal/claudecode/claudecode.go (add Refresh, make Source refresh)
  • Modify: internal/claudecode/claudecode_test.go
  • Modify: cmd/harness/main.go (give NewSource an HTTP client)

Interfaces:

  • Consumes: Credentials, Source, ErrExpired, ErrNotFound.
  • Produces:
    • claudecode.Refresh(ctx context.Context, h *http.Client, c *Credentials) (*Credentials, error)
    • claudecode.TokenURL and claudecode.ClientID constants
    • NewSource gains a second parameter: NewSource(c *Credentials, h *http.Client) *Source

Breaking change to fix as you go: Task 4's TestSourceReturnsTheAccessToken and TestSourceExpiredTokenSaysWhatToDo call NewSource with one argument, and will stop compiling. Pass nil as the second argument in both — neither test refreshes, and NewSource substitutes http.DefaultClient for a nil client.

  • Step 1: Write the failing tests

Append to internal/claudecode/claudecode_test.go:

func TestRefreshExchangesTheToken(t *testing.T) {
	var body map[string]any
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		json.NewDecoder(r.Body).Decode(&body)
		io.WriteString(w, `{"access_token":"new-access","refresh_token":"new-refresh","expires_in":28800}`)
	}))
	defer srv.Close()
	t.Setenv("CLAUDECODE_TOKEN_URL", srv.URL)

	old := &Credentials{AccessToken: "old", RefreshToken: "old-refresh"}
	got, err := Refresh(context.Background(), srv.Client(), old)
	if err != nil {
		t.Fatalf("Refresh: %v", err)
	}
	if got.AccessToken != "new-access" {
		t.Errorf("AccessToken = %q", got.AccessToken)
	}
	if body["grant_type"] != "refresh_token" || body["refresh_token"] != "old-refresh" {
		t.Errorf("request body = %v", body)
	}
	if !got.ExpiresAt.After(time.Now().Add(7 * time.Hour)) {
		t.Errorf("expires_in should have become an absolute time, got %s", got.ExpiresAt)
	}
}

// A refresh token the server rejects is terminal: only a fresh login fixes it.
func TestRefreshRejectedSaysToLogInAgain(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		io.WriteString(w, `{"error":"invalid_grant"}`)
	}))
	defer srv.Close()
	t.Setenv("CLAUDECODE_TOKEN_URL", srv.URL)

	_, err := Refresh(context.Background(), srv.Client(), &Credentials{RefreshToken: "dead"})
	if err == nil {
		t.Fatal("a rejected refresh must be an error")
	}
	if !strings.Contains(err.Error(), "claude") {
		t.Errorf("the error should say to log in again, got %q", err)
	}
}

func TestRefreshTransportErrorIsReported(t *testing.T) {
	t.Setenv("CLAUDECODE_TOKEN_URL", "http://127.0.0.1:1")
	_, err := Refresh(context.Background(), &http.Client{Timeout: time.Second}, &Credentials{RefreshToken: "r"})
	if err == nil {
		t.Fatal("a transport error must be reported")
	}
}

// The whole point: an expired token becomes a working one without the caller
// knowing anything happened.
func TestSourceRefreshesOnExpiry(t *testing.T) {
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		hits++
		io.WriteString(w, `{"access_token":"fresh","refresh_token":"r2","expires_in":28800}`)
	}))
	defer srv.Close()
	t.Setenv("CLAUDECODE_TOKEN_URL", srv.URL)

	s := NewSource(&Credentials{
		AccessToken:      "stale",
		RefreshToken:     "r1",
		ExpiresAt:        time.Now().Add(-time.Hour),
		RefreshExpiresAt: time.Now().Add(24 * time.Hour),
	}, srv.Client())

	for i := 0; i < 3; i++ {
		tok, err := s.Token(context.Background())
		if err != nil {
			t.Fatalf("Token: %v", err)
		}
		if tok != "fresh" {
			t.Fatalf("Token = %q, want fresh", tok)
		}
	}
	if hits != 1 {
		t.Errorf("refreshed %d times, want 1 — the result should be cached", hits)
	}
}

// An expired refresh token cannot be refreshed. Fail with instructions rather
// than sending a request that is certain to be rejected.
func TestSourceExpiredRefreshTokenDoesNotCallOut(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Error("no request should be sent for a dead refresh token")
	}))
	defer srv.Close()
	t.Setenv("CLAUDECODE_TOKEN_URL", srv.URL)

	s := NewSource(&Credentials{
		AccessToken:      "stale",
		RefreshToken:     "r",
		ExpiresAt:        time.Now().Add(-time.Hour),
		RefreshExpiresAt: time.Now().Add(-time.Hour),
	}, srv.Client())
	if _, err := s.Token(context.Background()); !errors.Is(err, ErrExpired) {
		t.Fatalf("err = %v, want ErrExpired", err)
	}
}

func TestSourceConcurrentTokensRefreshOnce(t *testing.T) {
	var mu sync.Mutex
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		hits++
		mu.Unlock()
		io.WriteString(w, `{"access_token":"fresh","refresh_token":"r2","expires_in":28800}`)
	}))
	defer srv.Close()
	t.Setenv("CLAUDECODE_TOKEN_URL", srv.URL)

	s := NewSource(&Credentials{
		AccessToken:      "stale",
		RefreshToken:     "r1",
		ExpiresAt:        time.Now().Add(-time.Hour),
		RefreshExpiresAt: time.Now().Add(24 * time.Hour),
	}, srv.Client())

	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if _, err := s.Token(context.Background()); err != nil {
				t.Error(err)
			}
		}()
	}
	wg.Wait()
	if hits != 1 {
		t.Errorf("refreshed %d times under concurrency, want 1", hits)
	}
}

Add "encoding/json", "io", "net/http", "net/http/httptest" and "sync" to the imports.

  • Step 2: Run the tests to verify they fail
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/claudecode/...

Expected: FAIL — Refresh undefined, and NewSource takes one argument.

  • Step 3: Implement Refresh and make Source use it

In internal/claudecode/claudecode.go:

// TokenURL is the OAuth token endpoint. The environment variable exists so
// tests can point it at an httptest server; nothing else should set it.
var TokenURL = func() string {
	if u := os.Getenv("CLAUDECODE_TOKEN_URL"); u != "" {
		return u
	}
	return "https://console.anthropic.com/v1/oauth/token"
}

// ClientID identifies Claude Code to the token endpoint.
const ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"

// Refresh exchanges the refresh token for a new access token. It returns new
// credentials and writes nothing: the file belongs to Claude Code, and two
// writers racing over it is a worse problem than a token resolved twice.
func Refresh(ctx context.Context, h *http.Client, c *Credentials) (*Credentials, error) {
	body, err := json.Marshal(map[string]string{
		"grant_type":    "refresh_token",
		"refresh_token": c.RefreshToken,
		"client_id":     ClientID,
	})
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, TokenURL(), bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("content-type", "application/json")
	resp, err := h.Do(req)
	if err != nil {
		return nil, fmt.Errorf("claudecode: refreshing: %w", err)
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("claudecode: reading refresh response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("claudecode: refresh rejected (%d): run `claude` to log in again: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	var out struct {
		AccessToken  string `json:"access_token"`
		RefreshToken string `json:"refresh_token"`
		ExpiresIn    int64  `json:"expires_in"`
	}
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, fmt.Errorf("claudecode: decoding refresh response: %w", err)
	}
	if out.AccessToken == "" {
		return nil, errors.New("claudecode: refresh returned no access token")
	}
	next := *c
	next.AccessToken = out.AccessToken
	if out.RefreshToken != "" {
		next.RefreshToken = out.RefreshToken
	}
	next.ExpiresAt = time.Now().Add(time.Duration(out.ExpiresIn) * time.Second)
	return &next, nil
}

Replace Source with the refreshing version:

// Source hands out an access token, refreshing it when it lapses. The refresh
// is kept in memory for the process lifetime and never written back: Claude
// Code owns that file and stays its only writer.
type Source struct {
	mu    sync.Mutex
	creds *Credentials
	http  *http.Client
}

// NewSource wraps credentials in a token source. h is the client used for
// refreshes.
func NewSource(c *Credentials, h *http.Client) *Source {
	if h == nil {
		h = http.DefaultClient
	}
	return &Source{creds: c, http: h}
}

// Token returns a valid access token, refreshing first if the current one has
// lapsed. The lock means concurrent requests — streaming and buffered can be
// in flight together — produce one refresh rather than a thundering herd.
func (s *Source) Token(ctx context.Context) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	now := time.Now()
	if !s.creds.Expired(now) {
		return s.creds.AccessToken, nil
	}
	if s.creds.RefreshToken == "" || s.creds.RefreshExpired(now) {
		return "", ErrExpired
	}
	next, err := Refresh(ctx, s.http, s.creds)
	if err != nil {
		return "", err
	}
	s.creds = next
	return s.creds.AccessToken, nil
}

Add "bytes", "io", "net/http", "strings" and "sync" to the imports.

  • Step 4: Update the call site

In cmd/harness/main.go, resolveClaudeCode now needs an HTTP client:

	return claudecode.NewSource(creds, client.HTTP).Token, note, nil

client.HTTP is already a *http.Client with a 10-minute timeout. That is long for a token exchange but harmless, and reusing it keeps one connection pool.

  • Step 5: Run the full suite
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w cmd/ internal/ && go build ./... && go test ./... && go test -race ./internal/claudecode/...

Expected: PASS, including under -race — the concurrency test is there to be run that way.

  • Step 6: Commit
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add cmd/harness/ internal/claudecode/
git commit -m "claudecode: refresh the access token in memory

Measured first: this endpoint does not rotate refresh tokens, so using
one does not invalidate the copy Claude Code holds. Nothing is written
back — Claude Code owns that file and stays its only writer.

One refresh per expiry, mutex-guarded, because streaming and buffered
requests can be in flight together."

Task 8: Document it

Files:

  • Modify: README.md:10-30 (Quick start and the auth paragraph), and the flag list at :31-32

Interfaces:

  • Consumes: everything above.

  • Produces: nothing code depends on.

  • Step 1: Rewrite the Quick start opening

The current text opens by telling you to export an API key. Lead with the case that now needs nothing:

## Quick start

```bash
go build -o harness ./cmd/harness

./harness -C /path/to/a/throwaway/clone          # full-screen TUI
./harness -C /path/to/repo -p "why does TestFoo fail?"   # one prompt

If you are logged into Claude Code, that is all the setup there is: the harness borrows the OAuth credential from ~/.claude/.credentials.json when nothing else supplied one. Otherwise, or to use a different account:

export ANTHROPIC_API_KEY=sk-ant-...

- [ ] **Step 2: Extend the credentials paragraph**

After the existing `-auth-token` / `-api-key` sentences, add:

```markdown
The Claude Code login is the last resort, tried only when no flag and no
environment variable supplied a credential, and only against Anthropic's own
endpoint — a credential found on disk is not one you chose to send, so it is
never forwarded to a `-base-url` you pointed the harness at. `-no-claude-code-auth`
skips it entirely. Note that the credential is issued to Claude Code; using it
from another client is outside Anthropic's terms for it, which API keys and
gateway tokens do not carry.

On macOS Claude Code keeps this in the Keychain rather than a file, so there is
nothing to borrow there.

If Task 7 shipped, add: An expired access token is refreshed in memory; the file is never written. If it did not, add: An expired access token is not refreshed — run claude and the harness picks up the new one.

  • Step 3: Add the flag to the list

Append -no-claude-code-auth to the "Useful flags" line at README.md:31.

  • Step 4: Verify the README matches reality
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go build -o /tmp/harness ./cmd/harness && /tmp/harness -h 2>&1 | grep -A1 claude-code

Expected: the flag exists with the help text the README implies.

  • Step 5: Commit
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
git add README.md
git commit -m "README: document borrowing the Claude Code login

Leads with the no-setup case, and says plainly why the credential is
never sent to a non-default endpoint and what its terms are."

Done when

  • env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN ./harness -C /tmp -no-tui -p "say ok" works on a machine logged into Claude Code.
  • The same command with -base-url http://127.0.0.1:9 reports no credentials, not a connection error.
  • go test ./... and go test -race ./internal/claudecode/... pass.
  • Both probe findings are recorded in the spec, with the status codes observed.
  • If Task 6 found rotation, Tasks 7 is unstarted and the user has been told why.