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
harness; import paths are harness/internal/..../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.httptest for everything.client.IsDefaultEndpoint() is true and no flag or env var supplied one. This is a security property, not a preference.gofmt -w on every file you touch before committing.go build ./... && go test ./... before every commit.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:
/tmp/oauth-probe.sh (throwaway, not committed)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.
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.
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.
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."
internal/claudecode — parse and expiryPure functions over a file. No network, no refresh.
Files:
internal/claudecode/claudecode.gointernal/claudecode/claudecode_test.goInterfaces:
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) boolclaudecode.ErrNotFound — sentinel, matched with errors.Isclaudecode.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.
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.
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)
}
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w internal/claudecode/ && go test ./internal/claudecode/...
Expected: PASS, all seven tests.
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."
llm.Client resolves its credential per requestToday 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:
internal/llm/client.go:56-58 (struct fields), :74-80 (WithAuthToken), :116 (HasCredentials), :150-155 (CreateMessage), :190-205 (attempt)internal/llm/stream.go:81-93 (streamAttempt), and StreamMessage's loop above itinternal/llm/client_test.go (add tests)Interfaces:
Consumes: nothing from earlier tasks.
Produces:
llm.WithTokenSource(func(context.Context) (string, error)) Optionauth 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.
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/llm/...
Expected: FAIL — WithTokenSource and the tokenSource field are undefined.
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
}
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.
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.
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."
The feature, end to end, without refresh. An expired token fails with an instruction rather than a 401.
Files:
internal/claudecode/claudecode.go (add Source)internal/claudecode/claudecode_test.go (add tests)cmd/harness/main.go:38-72 (flag, client construction, fallback), :296 (errNoCredentials)cmd/harness/auth_test.goInterfaces:
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.ErrExpiredresolveClaudeCode(client *llm.Client, home string, disabled bool) (func(context.Context) (string, error), string, error) in package mainStep 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.
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/claudecode/...
Expected: FAIL — NewSource and ErrExpired undefined.
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.
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")
}
}
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
}
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./cmd/... ./internal/claudecode/...
Expected: FAIL — resolveClaudeCode undefined.
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.
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w cmd/ internal/ && go build ./... && go test ./...
Expected: PASS.
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.
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."
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:
internal/agent/loop.go:57-77 (Options), :301 and :318 (request assembly)internal/agent/loop_test.go (add tests)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.
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/agent/...
Expected: FAIL — SystemPrefix, ClaudeCodeIdentity and systemBlocks undefined.
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.
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
gofmt -w internal/agent/ && go test ./internal/agent/...
Expected: PASS, including the existing TestSystemPromptIsStableAcrossRequests.
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.
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.
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."
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:
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.
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.
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.
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.
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."
Only if Task 6 reported NOT ROTATED. Otherwise this task does not exist yet.
Files:
internal/claudecode/claudecode.go (add Refresh, make Source refresh)internal/claudecode/claudecode_test.gocmd/harness/main.go (give NewSource an HTTP client)Interfaces:
Credentials, Source, ErrExpired, ErrNotFound.claudecode.Refresh(ctx context.Context, h *http.Client, c *Credentials) (*Credentials, error)claudecode.TokenURL and claudecode.ClientID constantsNewSource gains a second parameter: NewSource(c *Credentials, h *http.Client) *SourceBreaking 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.
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.
cd /home/joni/.t3/worktrees/coding-harness/t3code-8b127a46
go test ./internal/claudecode/...
Expected: FAIL — Refresh undefined, and NewSource takes one argument.
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.
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.
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.
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."
Files:
README.md:10-30 (Quick start and the auth paragraph), and the flag list at :31-32Interfaces:
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.
Append -no-claude-code-auth to the "Useful flags" line at README.md:31.
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.
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."
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.-base-url http://127.0.0.1:9 reports no credentials, not a connection error.go test ./... and go test -race ./internal/claudecode/... pass.