Vectrum
Sign In

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.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Jjonicommitted 1 parent f158807commit3316c66

Showing 3 changed files with 72 additions and 3 deletions.

+7−0cmd/harness/main.goView File
@@ -78,6 +78,11 @@ func run() error {
7878 home = ""
7979 }
8080 var authNote string
81+ // systemPrefix carries the Claude Code identity block. It is set only when
82+ // the credential was borrowed from Claude Code's own login: an OAuth
83+ // request that omits it is rejected (as a 429, not a 401 — it reads like a
84+ // quota problem and is not one).
85+ var systemPrefix string
8186 if home != "" {
8287 src, note, err := resolveClaudeCode(client, home, *noClaudeCodeAuth)
8388 if err != nil {
@@ -86,6 +91,7 @@ func run() error {
8691 if src != nil {
8792 client = llm.New(append(opts, llm.WithTokenSource(src))...)
8893 authNote = note
94+ systemPrefix = agent.ClaudeCodeIdentity
8995 }
9096 }
9197 if !client.HasCredentials() {
@@ -172,6 +178,7 @@ func run() error {
172178 NoCache: *noCache,
173179 Stream: !*noStream,
174180 Skills: found,
181+ SystemPrefix: systemPrefix,
175182 })
176183 a.Observe = render.Handle
177184 a.Approve = policy
+26−3internal/agent/loop.goView File
@@ -39,6 +39,11 @@ do differently. Do not repeat a failing call unchanged.
3939
4040 Be concise. When the task is done, say what you changed and why, briefly.`
4141
42+ // ClaudeCodeIdentity is what an OAuth-authenticated request must present as
43+ // its first system block. It is a fixed string, so it costs no cache hit: the
44+ // prefix is still constant for the session.
45+ const ClaudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude."
46+
4247 // skillListing is appended to the system prompt when skills were discovered.
4348 // It is constant for the session, so the cached prefix still matches request to
4449 // request: what breaks caching is content that varies per request, not content
@@ -73,6 +78,10 @@ type Options struct {
7378 // descriptions are appended to the system prompt; the bodies are loaded on
7479 // demand by the skill tool.
7580 Skills []skills.Skill
81+ // SystemPrefix is prepended as a separate system block ahead of the
82+ // harness prompt. It carries the identity string an OAuth credential
83+ // requires, and is empty for every other credential.
84+ SystemPrefix string
7685 }
7786
7887 // EventKind identifies what happened, for an observer to render.
@@ -298,14 +307,12 @@ func (a *Agent) request() llm.Request {
298307 InputSchema: t.InputSchema(),
299308 })
300309 }
301- system := llm.SystemBlock{Type: "text", Text: a.opts.System}
302310 messages := a.state.History()
303311
304312 if !a.opts.NoCache {
305313 // Two breakpoints. The first caches the tools and system prompt, which
306314 // never change. The second rolls forward over the conversation each
307315 // turn, so every turn writes a small delta and reads a large prefix.
308- system.CacheControl = llm.Ephemeral()
309316 if n := len(messages); n > 0 {
310317 if blocks := messages[n-1].Content; len(blocks) > 0 {
311318 messages[n-1].Content = append([]llm.ContentBlock(nil), blocks...)
@@ -315,7 +322,7 @@ func (a *Agent) request() llm.Request {
315322 }
316323
317324 req := llm.Request{
318- System: []llm.SystemBlock{system},
325+ System: a.systemBlocks(),
319326 Messages: messages,
320327 Tools: defs,
321328 Thinking: llm.Adaptive(),
@@ -326,6 +333,22 @@ func (a *Agent) request() llm.Request {
326333 return req
327334 }
328335
336+ // systemBlocks builds the system prompt. The cache breakpoint goes on the last
337+ // block so everything ahead of it — tools, identity, prompt — is cached
338+ // together; on the first block it would cache the identity string and re-read
339+ // the rest every turn.
340+ func (a *Agent) systemBlocks() []llm.SystemBlock {
341+ var blocks []llm.SystemBlock
342+ if a.opts.SystemPrefix != "" {
343+ blocks = append(blocks, llm.SystemBlock{Type: "text", Text: a.opts.SystemPrefix})
344+ }
345+ blocks = append(blocks, llm.SystemBlock{Type: "text", Text: a.opts.System})
346+ if !a.opts.NoCache {
347+ blocks[len(blocks)-1].CacheControl = llm.Ephemeral()
348+ }
349+ return blocks
350+ }
351+
329352 // editedPath pulls the path out of an edit_file or write_file call.
330353 func editedPath(input json.RawMessage) string {
331354 var args struct {
+39−0internal/agent/loop_test.goView File
@@ -527,3 +527,42 @@ func TestSystemPromptIsStableAcrossRequests(t *testing.T) {
527527 t.Error("the system prompt changed between two requests in one session")
528528 }
529529 }
530+
531+ func TestSystemPrefixBecomesTheFirstBlock(t *testing.T) {
532+ a := New(nil, tools.NewRegistry(nil), NewState(), nil, Options{SystemPrefix: ClaudeCodeIdentity})
533+ blocks := a.systemBlocks()
534+ if len(blocks) != 2 {
535+ t.Fatalf("got %d system blocks, want 2", len(blocks))
536+ }
537+ if blocks[0].Text != ClaudeCodeIdentity {
538+ t.Errorf("first block = %q, want the identity string", blocks[0].Text)
539+ }
540+ if !strings.Contains(blocks[1].Text, "coding agent") {
541+ t.Errorf("second block should be the harness prompt, got %q", blocks[1].Text)
542+ }
543+ }
544+
545+ // The cache breakpoint has to sit on the last block, or only the identity
546+ // string gets cached and the prompt behind it is re-read every turn.
547+ func TestCacheBreakpointIsOnTheLastSystemBlock(t *testing.T) {
548+ a := New(nil, tools.NewRegistry(nil), NewState(), nil, Options{SystemPrefix: ClaudeCodeIdentity})
549+ blocks := a.systemBlocks()
550+ if blocks[0].CacheControl != nil {
551+ t.Error("the first block must not carry the breakpoint")
552+ }
553+ if blocks[len(blocks)-1].CacheControl == nil {
554+ t.Error("the last block must carry the breakpoint")
555+ }
556+ }
557+
558+ // Without OAuth nothing changes, including byte-for-byte.
559+ func TestNoPrefixMeansOneBlockAsBefore(t *testing.T) {
560+ a := New(nil, tools.NewRegistry(nil), NewState(), nil, Options{})
561+ blocks := a.systemBlocks()
562+ if len(blocks) != 1 {
563+ t.Fatalf("got %d system blocks, want 1", len(blocks))
564+ }
565+ if blocks[0].Text != defaultSystem {
566+ t.Error("the prompt must be unchanged when no prefix is set")
567+ }
568+ }