Skip to content

Commit 85d2fad

Browse files
authored
fix(claude): preserve native subagent and environment headers (router-for-me#4982) (router-for-me#5084)
1 parent 8aa6868 commit 85d2fad

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

internal/runtime/executor/claude_executor_request.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,10 @@ func copyClaudeCallerFingerprintHeaders(dst, src http.Header) {
624624
lowerName != "x-app" && lowerName != "x-client-request-id" &&
625625
!strings.HasPrefix(lowerName, "anthropic-") &&
626626
!strings.HasPrefix(lowerName, "x-stainless-") &&
627-
!strings.HasPrefix(lowerName, "x-claude-code-") {
627+
!strings.HasPrefix(lowerName, "x-claude-code-") &&
628+
!strings.HasPrefix(lowerName, "x-claude-remote-") &&
629+
lowerName != "x-client-app" &&
630+
lowerName != "x-anthropic-additional-protection" {
628631
continue
629632
}
630633
dst.Del(name)
@@ -880,6 +883,19 @@ func applyClaudeHeadersWithNativeProfile(
880883
}
881884
identityHeader("X-Claude-Code-Session-Id", sessionID)
882885
}
886+
// Preserve native Claude Code subagent and environment headers when present in the incoming request.
887+
for _, hdr := range []string{
888+
"X-Claude-Code-Agent-Id",
889+
"X-Claude-Code-Parent-Agent-Id",
890+
"X-Claude-Remote-Container-Id",
891+
"X-Claude-Remote-Session-Id",
892+
"X-Client-App",
893+
"X-Anthropic-Additional-Protection",
894+
} {
895+
if val := helps.HeaderValueCaseInsensitive(incomingHeaders, hdr); val != "" {
896+
r.Header.Set(hdr, val)
897+
}
898+
}
883899
// Per-request UUID, matches Claude Code's x-client-request-id for first-party API.
884900
// identityHeader prefers the incoming value for a confirmed client, so a confirmed
885901
// helper keeps its own native request ID and this fresh UUID only covers a caller

internal/runtime/executor/claude_executor_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6319,3 +6319,100 @@ func TestClaudeExecutor_CacheTTLIsPairedWithExtendedCacheTTLBeta(t *testing.T) {
63196319
})
63206320
}
63216321
}
6322+
6323+
func TestClaudeExecutor_PreservesNativeAgentAndEnvironmentHeaders(t *testing.T) {
6324+
tests := []struct {
6325+
name string
6326+
incomingHeaders http.Header
6327+
wantHeaders map[string]string
6328+
wantAbsent []string
6329+
}{
6330+
{
6331+
name: "preserves canonical agent and parent agent headers",
6332+
incomingHeaders: http.Header{
6333+
"X-Claude-Code-Agent-Id": {"subagent-001"},
6334+
"X-Claude-Code-Parent-Agent-Id": {"parent-agent-root"},
6335+
},
6336+
wantHeaders: map[string]string{
6337+
"X-Claude-Code-Agent-Id": "subagent-001",
6338+
"X-Claude-Code-Parent-Agent-Id": "parent-agent-root",
6339+
},
6340+
},
6341+
{
6342+
name: "preserves lowercased agent and environment headers",
6343+
incomingHeaders: http.Header{
6344+
"x-claude-code-agent-id": {"agent-xyz"},
6345+
"x-claude-remote-container-id": {"container-123"},
6346+
"x-claude-remote-session-id": {"remote-sess-456"},
6347+
"x-client-app": {"custom-sdk"},
6348+
"x-anthropic-additional-protection": {"true"},
6349+
},
6350+
wantHeaders: map[string]string{
6351+
"X-Claude-Code-Agent-Id": "agent-xyz",
6352+
"X-Claude-Remote-Container-Id": "container-123",
6353+
"X-Claude-Remote-Session-Id": "remote-sess-456",
6354+
"X-Client-App": "custom-sdk",
6355+
"X-Anthropic-Additional-Protection": "true",
6356+
},
6357+
},
6358+
{
6359+
name: "does not fabricate agent header when absent",
6360+
incomingHeaders: http.Header{
6361+
"User-Agent": {"test-client"},
6362+
},
6363+
wantAbsent: []string{
6364+
"X-Claude-Code-Agent-Id",
6365+
"X-Claude-Code-Parent-Agent-Id",
6366+
"X-Claude-Remote-Container-Id",
6367+
"X-Claude-Remote-Session-Id",
6368+
"X-Client-App",
6369+
"X-Anthropic-Additional-Protection",
6370+
},
6371+
},
6372+
}
6373+
6374+
for _, tt := range tests {
6375+
t.Run(tt.name, func(t *testing.T) {
6376+
var seenHeaders http.Header
6377+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6378+
seenHeaders = r.Header.Clone()
6379+
w.Header().Set("Content-Type", "application/json")
6380+
_, _ = w.Write([]byte(`{"id":"msg_agent","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
6381+
}))
6382+
defer server.Close()
6383+
6384+
executor := NewClaudeExecutor(&config.Config{})
6385+
auth := &cliproxyauth.Auth{
6386+
ID: "agent-header-test",
6387+
Attributes: map[string]string{
6388+
"api_key": "sk-ant-test-key",
6389+
"base_url": server.URL,
6390+
"cloak_mode": "always",
6391+
},
6392+
Metadata: claudeOAuthTestMetadata(),
6393+
}
6394+
6395+
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
6396+
Model: "claude-opus-4-6",
6397+
Payload: []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`),
6398+
}, cliproxyexecutor.Options{
6399+
SourceFormat: sdktranslator.FormatClaude,
6400+
Headers: tt.incomingHeaders,
6401+
})
6402+
if errExecute != nil {
6403+
t.Fatalf("Execute() error = %v", errExecute)
6404+
}
6405+
6406+
for wantKey, wantVal := range tt.wantHeaders {
6407+
if got := seenHeaders.Get(wantKey); got != wantVal {
6408+
t.Errorf("header %s = %q, want %q", wantKey, got, wantVal)
6409+
}
6410+
}
6411+
for _, absentKey := range tt.wantAbsent {
6412+
if got := seenHeaders.Get(absentKey); got != "" {
6413+
t.Errorf("header %s = %q, want absent", absentKey, got)
6414+
}
6415+
}
6416+
})
6417+
}
6418+
}

internal/runtime/executor/helps/claude_code_session.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ func claudeCodeHeader(ctx context.Context, headers http.Header, name string) str
5656
return ""
5757
}
5858

59+
// HeaderValueCaseInsensitive returns the first non-empty header value matching name case-insensitively.
60+
func HeaderValueCaseInsensitive(headers http.Header, name string) string {
61+
return headerValueCaseInsensitive(headers, name)
62+
}
63+
5964
func headerValueCaseInsensitive(headers http.Header, name string) string {
6065
if headers == nil {
6166
return ""

0 commit comments

Comments
 (0)