feat(claude): strip Claude Code identity on API keys with cloak mode never or uncloak

- Strip incoming Claude Code billing attribution, CLI identity system blocks, metadata user IDs, and diagnostics when a non-CLI API key explicitly configures cloak mode as never or uncloak.
- Remove Claude Code CLI headers, beta flags, and user-agent strings on uncloaked API key requests while preserving capability betas and credential header overrides.
- Keep global disable-claude-cloak-mode passthrough behavior unchanged for credentials without explicit per-key cloak configuration.

Closes: #6762
This commit is contained in:
Luis Pater
2026-10-19 12:59:12 +08:00
parent d9451cb65d
commit 084b9ca717
7 changed files with 430 additions and 12 deletions

View File

@@ -728,7 +728,7 @@ upstream:
# cloak: # optional: explicitly enable request cloaking for non-Claude-Code clients
# mode: "auto" # "auto" (default inside this block): cloak only when client is not Claude Code
# # "always": cloak every unconfirmed client; confirmed native Claude Code still passes through
# # "never": never apply cloaking
# # "never" / "uncloak": never apply cloaking; on non-CLI API keys, also strip incoming Claude Code identity/billing attribution
# # This "cloak" block applies to this api-keys.claude key entry only. For Claude OAuth
# # credentials, set the same options in the auth/token JSON file via "cloak_mode" /
# # "cloak_strict_mode" / "cloak_sensitive_words" / "cloak_cache_user_id".

View File

@@ -564,11 +564,11 @@ type PayloadModelRule struct {
// CloakConfig configures request cloaking for non-Claude-Code clients.
// Cloaking disguises API requests to appear as originating from the official Claude Code CLI.
type CloakConfig struct {
// Mode controls cloaking behavior: "auto" (default), "always", or "never".
// Mode controls cloaking behavior: "auto" (default), "always", "never", or "uncloak".
// Supplying this CloakConfig explicitly enables cloaking for an unprofiled API key.
// - "auto": cloak unless strong request signals identify a verified native entrypoint
// - "always": cloak every unconfirmed client; confirmed native Claude Code remains passthrough
// - "never": never apply cloaking
// - "never" / "uncloak": never apply cloaking; on non-CLI API keys, also strip incoming Claude Code identity and billing attribution
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
// StrictMode controls how caller system prompts are handled when cloaking.

View File

@@ -1584,6 +1584,7 @@ type claudeWirePolicy struct {
ProfileClaudeCodeCLI bool // request fingerprint looks like Claude Code CLI
ConfirmedClaudeCode bool
Cloak bool
Uncloak bool
}
type claudeCloakSettings struct {
@@ -1592,6 +1593,79 @@ type claudeCloakSettings struct {
cacheUserID bool
}
func isClaudeCodeIdentitySystemText(text string) bool {
trimmed := strings.TrimSpace(text)
return trimmed == claudeCodeCLIIdentity ||
trimmed == "You are a Claude agent, built on Anthropic's Claude Agent SDK."
}
func stripClaudeCodeSystemBlocks(payload []byte) []byte {
payload = util.StripClaudeCodeAttributionSystem(payload)
system := gjson.GetBytes(payload, "system")
if !system.Exists() {
return payload
}
if system.Type == gjson.String {
if !isClaudeCodeIdentitySystemText(system.String()) {
return payload
}
if updated, errDelete := sjson.DeleteBytes(payload, "system"); errDelete == nil {
return updated
}
return payload
}
if !system.IsArray() {
return payload
}
kept := make([]string, 0, len(system.Array()))
removed := false
system.ForEach(func(_, block gjson.Result) bool {
if block.Get("type").String() == "text" && isClaudeCodeIdentitySystemText(block.Get("text").String()) {
removed = true
return true
}
if block.Raw != "" {
kept = append(kept, block.Raw)
}
return true
})
if !removed {
return payload
}
if len(kept) == 0 {
if updated, errDelete := sjson.DeleteBytes(payload, "system"); errDelete == nil {
return updated
}
return payload
}
if updated, errSet := sjson.SetRawBytes(payload, "system", []byte("["+strings.Join(kept, ",")+"]")); errSet == nil {
return updated
}
return payload
}
func uncloakClaudeCodePayload(payload []byte, confirmedClaudeCode bool) []byte {
payload = stripClaudeCodeSystemBlocks(payload)
if metadata := gjson.GetBytes(payload, "metadata"); metadata.Exists() && metadata.IsObject() {
if userID := metadata.Get("user_id"); userID.Exists() && (confirmedClaudeCode || helps.IsValidUserID(userID.String())) {
if updated, errDelete := sjson.DeleteBytes(payload, "metadata.user_id"); errDelete == nil {
payload = updated
if remaining := gjson.GetBytes(payload, "metadata"); remaining.Exists() && remaining.IsObject() && len(remaining.Map()) == 0 {
if updatedMeta, errDeleteMeta := sjson.DeleteBytes(payload, "metadata"); errDeleteMeta == nil {
payload = updatedMeta
}
}
}
}
}
if diag := gjson.GetBytes(payload, "diagnostics"); diag.Exists() && (confirmedClaudeCode || diag.Get("previous_message_id").Exists()) {
if updated, errDelete := sjson.DeleteBytes(payload, "diagnostics"); errDelete == nil {
payload = updated
}
}
return payload
}
func resolveClaudeWirePolicy(cfg *config.Config, auth *cliproxyauth.Auth, apiKey string, confirmedClaudeCode bool) (claudeWirePolicy, claudeCloakSettings) {
if auth != nil && auth.AuthKind() == cliproxyauth.AuthKindAPIKey {
cfg = cfg.ForAPIKey()
@@ -1608,12 +1682,14 @@ func resolveClaudeWirePolicy(cfg *config.Config, auth *cliproxyauth.Auth, apiKey
sensitiveWords: attrWords,
cacheUserID: attrCache,
}
explicitMode := strings.ToLower(strings.TrimSpace(attrMode))
if attrMode != "" {
cloakMode = attrMode
}
if cloakCfg != nil {
if mode := strings.TrimSpace(cloakCfg.Mode); mode != "" {
cloakMode = mode
explicitMode = strings.ToLower(mode)
}
if cloakCfg.StrictMode {
settings.strictMode = true
@@ -1628,23 +1704,25 @@ func resolveClaudeWirePolicy(cfg *config.Config, auth *cliproxyauth.Auth, apiKey
fp := resolveClaudeFingerprintPolicy(cfg, auth, apiKey)
cloakConfigured := cloakCfg != nil || attrMode != "" || attrStrict || len(attrWords) > 0 || attrCache
trimmedMode := strings.ToLower(strings.TrimSpace(cloakMode))
uncloak := !fp.ProfileClaudeCodeCLI && (explicitMode == "never" || explicitMode == "uncloak")
policy := claudeWirePolicy{
OAuth: fp.AuthIsOAuthToken,
ProfileClaudeCodeCLI: fp.ProfileClaudeCodeCLI,
ConfirmedClaudeCode: confirmedClaudeCode,
Cloak: (fp.ProfileClaudeCodeCLI || cloakConfigured) && !confirmedClaudeCode,
Cloak: (fp.ProfileClaudeCodeCLI || cloakConfigured) && !confirmedClaudeCode && !uncloak,
Uncloak: uncloak,
}
if confirmedClaudeCode {
// Native Claude Code is always a passthrough client. An operator-level
// "always" mode may cloak unknown callers, but must not overwrite a
// strongly confirmed CLI, sdk-cli, or claude-vscode fingerprint.
// Native Claude Code is always a passthrough client unless the
// credential explicitly opts out of Claude Code identity via never/uncloak.
policy.Cloak = false
return policy, settings
}
switch strings.ToLower(strings.TrimSpace(cloakMode)) {
switch trimmedMode {
case "always":
policy.Cloak = true
case "never":
case "never", "uncloak":
policy.Cloak = false
default:
// Auto applies the CLI cloak only to real Claude OAuth credentials,
@@ -1681,6 +1759,9 @@ func applyCloakingInternal(
) ([]byte, bool, error) {
policy, settings := resolveClaudeWirePolicy(cfg, auth, apiKey, confirmedClaudeCode)
if !policy.Cloak {
if policy.Uncloak {
payload = uncloakClaudeCodePayload(payload, confirmedClaudeCode)
}
return payload, false, nil
}
isExplicit := (len(explicitCacheMode) > 0 && explicitCacheMode[0]) || isExplicitPromptCacheModeContext(ctx)

View File

@@ -1144,7 +1144,7 @@ func claudeCredentialUsesOAuth(auth *cliproxyauth.Auth, apiKey string) bool {
return !hasAPIKeyAttr
}
func copyClaudeCallerFingerprintHeaders(dst, src http.Header, confirmedClaudeCode bool) {
func copyClaudeCallerFingerprintHeaders(dst, src http.Header, confirmedClaudeCode, uncloak bool) {
if dst == nil || src == nil {
return
}
@@ -1163,6 +1163,21 @@ func copyClaudeCallerFingerprintHeaders(dst, src http.Header, confirmedClaudeCod
if !confirmedClaudeCode && (strings.HasPrefix(lowerName, "x-claude-code-") || strings.HasPrefix(lowerName, "x-claude-remote-")) {
continue
}
if uncloak {
if lowerName == "x-app" ||
lowerName == "x-client-request-id" ||
lowerName == "x-client-app" ||
lowerName == "x-anthropic-additional-protection" ||
lowerName == "anthropic-dangerous-direct-browser-access" ||
strings.HasPrefix(lowerName, "x-stainless-") ||
strings.HasPrefix(lowerName, "x-claude-code-") ||
strings.HasPrefix(lowerName, "x-claude-remote-") {
continue
}
if lowerName == "user-agent" && len(values) > 0 && helps.IsClaudeCodeUserAgent(values[0]) {
continue
}
}
dst.Del(name)
for _, value := range values {
dst.Add(name, value)
@@ -1221,6 +1236,11 @@ func applyClaudeHeadersWithNativeProfile(
useAPIKey := !credentialUsesBearer
fp := resolveClaudeFingerprintPolicy(cfg, auth, apiKey)
wirePolicy, _ := resolveClaudeWirePolicy(cfg, auth, apiKey, confirmedClaudeCode)
if wirePolicy.Uncloak {
confirmedClaudeCode = false
helperProfile = false
sessionIDs = nil
}
applyCLIFingerprint := fp.ProfileClaudeCodeCLI || wirePolicy.Cloak
preserveCallerFingerprint := !applyCLIFingerprint && !confirmedClaudeCode
useOAuthBetas := fp.UseOAuthBetas
@@ -1356,6 +1376,21 @@ func applyClaudeHeadersWithNativeProfile(
}
}
applyBetaHeader := func() {
if wirePolicy.Uncloak {
baseBetas = withoutClaudeBeta(baseBetas, claudeCodeBeta)
baseBetas = withoutClaudeBeta(baseBetas, claudeOAuthBeta)
baseBetas = withoutClaudeBeta(baseBetas, claudeRedactThinkingBeta)
baseBetas = withoutClaudeBeta(baseBetas, claudeAFKModeBeta)
if !gjson.GetBytes(body, "diagnostics").Exists() {
baseBetas = withoutClaudeBeta(baseBetas, claudeCacheDiagnosisBeta)
}
if !gjson.GetBytes(body, "fallback_credit_token").Exists() {
baseBetas = withoutClaudeBeta(baseBetas, claudeFallbackCreditBeta)
}
if !helps.ClaudePayloadHas1hTTL(body) {
baseBetas = withoutClaudeBeta(baseBetas, claudeExtendedCacheTTLBeta)
}
}
// Enforce strict native Claude Code 2.1.280 model & turn beta gating:
if !claudeRequestSupportsEffort(body, nil) {
baseBetas = withoutClaudeBeta(baseBetas, claudeEffortBeta)
@@ -1412,7 +1447,7 @@ func applyClaudeHeadersWithNativeProfile(
defaultAccept = "text/event-stream"
defaultAcceptEncoding = "identity"
}
copyClaudeCallerFingerprintHeaders(r.Header, incomingHeaders, confirmedClaudeCode)
copyClaudeCallerFingerprintHeaders(r.Header, incomingHeaders, confirmedClaudeCode, wirePolicy.Uncloak)
if hd.ClientRequestID == config.ClaudeClientRequestIDOff {
r.Header.Del("X-Client-Request-Id")
}
@@ -1423,7 +1458,11 @@ func applyClaudeHeadersWithNativeProfile(
// sent none must not fall through to Go's transport default
// ("Go-http-client/1.1"), which upstreams read as a bot signature. Identify
// as CPA instead: honest about the hop, and not a fabricated client.
misc.EnsureHeader(r.Header, incomingHeaders, "User-Agent", "CLIProxyAPI/"+buildinfo.Version)
uaSourceHeaders := incomingHeaders
if wirePolicy.Uncloak && helps.IsClaudeCodeUserAgent(helps.HeaderValueCaseInsensitive(incomingHeaders, "User-Agent")) {
uaSourceHeaders = nil
}
misc.EnsureHeader(r.Header, uaSourceHeaders, "User-Agent", "CLIProxyAPI/"+buildinfo.Version)
applyBetaHeader()
sessionID := ""
for _, candidate := range sessionIDs {

View File

@@ -200,6 +200,8 @@ func (e *ClaudeExecutor) countTokensUpstream(ctx context.Context, auth *cliproxy
if len(settings.sensitiveWords) > 0 {
body = helps.ObfuscateSensitiveWords(body, helps.BuildSensitiveWordMatcher(settings.sensitiveWords))
}
} else if policy.Uncloak {
body = uncloakClaudeCodePayload(body, confirmedClaudeCode)
}
// Keep count_tokens requests compatible with Anthropic cache-control constraints too.

View File

@@ -1243,3 +1243,294 @@ func claudeFingerprintHeaderValue(headers http.Header, name string) string {
}
return ""
}
func TestClaudeExecutor_OfficialAPIKeyCloakNeverStripsClaudeCodeIdentity(t *testing.T) {
const sessionID = "11111111-2222-4333-8444-555555555555"
const userID = `{"device_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","account_uuid":"11111111-2222-4333-8444-555555555555","session_id":"11111111-2222-4333-8444-555555555555"}`
const billing = "x-anthropic-billing-header: cc_version=2.1.280.d7b; cc_entrypoint=cli; cch=00000; cc_turn_origin=human;"
incoming := http.Header{
"User-Agent": {"claude-cli/2.1.280 (external, cli)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,cache-diagnosis-2026-04-07"},
"Anthropic-Dangerous-Direct-Browser-Access": {"true"},
"X-Claude-Code-Session-Id": {sessionID},
"X-Claude-Code-Request-Class": {"main"},
"X-Stainless-Package-Version": {"0.112.1"},
"X-Stainless-Runtime-Version": {"v26.3.0"},
"X-Stainless-Os": {"MacOS"},
"X-Stainless-Arch": {"arm64"},
}
payload := []byte(
`{"model":"claude-opus-4-6","max_tokens":64,"system":[{"type":"text","text":"` + billing + `"},{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude.","cache_control":{"type":"ephemeral"}},{"type":"text","text":"You are an interactive CLI tool that helps users with software engineering tasks.","cache_control":{"type":"ephemeral"}}],"messages":[{"role":"user","content":"hello"}],"metadata":{"user_id":` + `"` + strings.ReplaceAll(userID, `"`, `\"`) + `"` + `},"diagnostics":{"previous_message_id":null},"thinking":{"type":"adaptive"}}`,
)
if detection := helps.DetectClaudeCodeRequest(incoming, payload, false); !detection.Confirmed {
t.Fatal("fixture must be classified as a confirmed native Claude Code request")
}
for _, tc := range []struct {
name string
mode string
customUA string
stream bool
countTokens bool
wantUserAgent string
wantBetaSubstr string
}{
{
name: "execute cloak never default user-agent",
mode: "never",
wantUserAgent: "CLIProxyAPI/",
wantBetaSubstr: "interleaved-thinking-2025-05-14",
},
{
name: "execute cloak never custom user-agent override",
mode: "never",
customUA: "my-api-client/1.0",
wantUserAgent: "my-api-client/1.0",
wantBetaSubstr: "interleaved-thinking-2025-05-14",
},
{
name: "stream cloak never custom user-agent override",
mode: "never",
customUA: "my-api-client/1.0",
stream: true,
wantUserAgent: "my-api-client/1.0",
wantBetaSubstr: "interleaved-thinking-2025-05-14",
},
{
name: "execute cloak uncloak mode",
mode: "uncloak",
customUA: "my-api-client/1.0",
wantUserAgent: "my-api-client/1.0",
wantBetaSubstr: "interleaved-thinking-2025-05-14",
},
{
name: "count_tokens cloak never",
mode: "never",
customUA: "my-api-client/1.0",
countTokens: true,
wantUserAgent: "my-api-client/1.0",
wantBetaSubstr: "token-counting-2024-11-01",
},
} {
t.Run(tc.name, func(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
transport := claudeFingerprintRoundTripperFunc(func(req *http.Request) (*http.Response, error) {
seenBody, _ = io.ReadAll(req.Body)
seenHeaders = req.Header.Clone()
if tc.countTokens {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"input_tokens":12}`)),
}, nil
}
if tc.stream {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader(
"event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
)),
}, nil
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(
`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`,
)),
}, nil
})
ctx := context.WithValue(
context.Background(),
"cliproxy.roundtripper",
http.RoundTripper(transport),
)
var customHeaders map[string]string
attrs := map[string]string{
"api_key": "sk-ant-api03-promotional-key",
}
if tc.customUA != "" {
customHeaders = map[string]string{"User-Agent": tc.customUA}
attrs["header:User-Agent"] = tc.customUA
}
cfg := &config.Config{ClaudeKey: []config.ClaudeKey{{
APIKey: "sk-ant-api03-promotional-key",
Cloak: &config.CloakConfig{Mode: tc.mode},
Headers: customHeaders,
}}}
auth := &cliproxyauth.Auth{
ID: "official-promotional-key",
Attributes: attrs,
}
exec := NewClaudeExecutor(cfg)
req := cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}
opts := cliproxyexecutor.Options{
Stream: tc.stream,
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: incoming.Clone(),
}
if tc.countTokens {
if _, errCount := exec.CountTokens(ctx, auth, req, opts); errCount != nil {
t.Fatalf("CountTokens() error = %v", errCount)
}
} else if tc.stream {
res, errStream := exec.ExecuteStream(ctx, auth, req, opts)
if errStream != nil {
t.Fatalf("ExecuteStream() error = %v", errStream)
}
for chunk := range res.Chunks {
if chunk.Err != nil {
t.Fatalf("stream chunk error = %v", chunk.Err)
}
}
} else {
if _, errExec := exec.Execute(ctx, auth, req, opts); errExec != nil {
t.Fatalf("Execute() error = %v", errExec)
}
}
if strings.Contains(string(seenBody), "x-anthropic-billing-header:") || strings.Contains(string(seenBody), "cch=") {
t.Fatalf("upstream body still contains Claude Code billing header: %s", seenBody)
}
if strings.Contains(string(seenBody), claudeCodeCLIIdentity) {
t.Fatalf("upstream body still contains Claude Code CLI identity block: %s", seenBody)
}
if got := gjson.GetBytes(seenBody, "system.#").Int(); got != 1 {
t.Fatalf("system block count = %d, want 1 preserved instruction block: %s", got, seenBody)
}
if got := gjson.GetBytes(seenBody, "system.0.text").String(); got != "You are an interactive CLI tool that helps users with software engineering tasks." {
t.Fatalf("system.0.text = %q, want preserved caller instructions", got)
}
if gjson.GetBytes(seenBody, "metadata").Exists() {
t.Fatalf("metadata = %s, want Claude Code metadata stripped", gjson.GetBytes(seenBody, "metadata").Raw)
}
if gjson.GetBytes(seenBody, "diagnostics").Exists() {
t.Fatalf("diagnostics = %s, want Claude Code diagnostics stripped", gjson.GetBytes(seenBody, "diagnostics").Raw)
}
ua := claudeFingerprintHeaderValue(seenHeaders, "User-Agent")
if strings.HasPrefix(strings.ToLower(ua), "claude-cli/") {
t.Fatalf("User-Agent = %q, must not forward claude-cli user-agent", ua)
}
if !strings.HasPrefix(ua, tc.wantUserAgent) {
t.Fatalf("User-Agent = %q, want prefix %q", ua, tc.wantUserAgent)
}
for _, forbiddenHdr := range []string{
"X-App",
"Anthropic-Dangerous-Direct-Browser-Access",
"X-Claude-Code-Session-Id",
"X-Claude-Code-Request-Class",
"X-Stainless-Runtime",
"X-Stainless-Lang",
"X-Stainless-Package-Version",
} {
if got := claudeFingerprintHeaderValue(seenHeaders, forbiddenHdr); got != "" {
t.Fatalf("header %s = %q, want stripped", forbiddenHdr, got)
}
}
betas := claudeFingerprintHeaderValue(seenHeaders, "Anthropic-Beta")
for _, forbiddenBeta := range []string{
"claude-code-20250219",
"oauth-2025-04-20",
"redact-thinking-2026-02-12",
"cache-diagnosis-2026-04-07",
} {
if strings.Contains(betas, forbiddenBeta) {
t.Fatalf("Anthropic-Beta = %q, want %q stripped", betas, forbiddenBeta)
}
}
if !strings.Contains(betas, tc.wantBetaSubstr) {
t.Fatalf("Anthropic-Beta = %q, want capability beta %q preserved", betas, tc.wantBetaSubstr)
}
})
}
}
func TestClaudeExecutor_GlobalDisableClaudeCloakModePreservesConfirmedCallerIdentityWhenKeyUnconfigured(t *testing.T) {
const sessionID = "11111111-2222-4333-8444-555555555555"
const userID = `{"device_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","account_uuid":"11111111-2222-4333-8444-555555555555","session_id":"11111111-2222-4333-8444-555555555555"}`
const billing = "x-anthropic-billing-header: cc_version=2.1.280.d7b; cc_entrypoint=cli; cch=00000; cc_turn_origin=human;"
incoming := http.Header{
"User-Agent": {"claude-cli/2.1.280 (external, cli)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219,interleaved-thinking-2025-05-14"},
"Anthropic-Dangerous-Direct-Browser-Access": {"true"},
"X-Claude-Code-Session-Id": {sessionID},
"X-Stainless-Package-Version": {"0.112.1"},
"X-Stainless-Runtime-Version": {"v26.3.0"},
"X-Stainless-Os": {"MacOS"},
"X-Stainless-Arch": {"arm64"},
}
payload := []byte(
`{"model":"claude-opus-4-6","max_tokens":64,"system":[{"type":"text","text":"` + billing + `"},{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude.","cache_control":{"type":"ephemeral"}}],"messages":[{"role":"user","content":"hello"}],"metadata":{"user_id":` + `"` + strings.ReplaceAll(userID, `"`, `\"`) + `"` + `}}`,
)
var seenBody []byte
var seenHeaders http.Header
transport := claudeFingerprintRoundTripperFunc(func(req *http.Request) (*http.Response, error) {
seenBody, _ = io.ReadAll(req.Body)
seenHeaders = req.Header.Clone()
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(
`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`,
)),
}, nil
})
ctx := context.WithValue(
context.Background(),
"cliproxy.roundtripper",
http.RoundTripper(transport),
)
cfg := &config.Config{
DisableClaudeCloakMode: true,
ClaudeKey: []config.ClaudeKey{{
APIKey: "sk-ant-api03-global-disable-passthrough",
}},
}
auth := &cliproxyauth.Auth{
ID: "official-global-disable-passthrough",
Attributes: map[string]string{
"api_key": "sk-ant-api03-global-disable-passthrough",
},
}
if _, errExec := NewClaudeExecutor(cfg).Execute(ctx, auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: incoming,
}); errExec != nil {
t.Fatalf("Execute() error = %v", errExec)
}
if got := gjson.GetBytes(seenBody, "system.0.text").String(); got != billing {
t.Fatalf("system.0.text = %q, want preserved caller billing header %q", got, billing)
}
if got := gjson.GetBytes(seenBody, "system.1.text").String(); got != claudeCodeCLIIdentity {
t.Fatalf("system.1.text = %q, want preserved caller CLI identity", got)
}
if got := gjson.GetBytes(seenBody, "metadata.user_id").String(); got != userID {
t.Fatalf("metadata.user_id = %q, want preserved %q", got, userID)
}
if got := claudeFingerprintHeaderValue(seenHeaders, "X-App"); got != "cli" {
t.Fatalf("X-App = %q, want preserved cli", got)
}
if got := claudeFingerprintHeaderValue(seenHeaders, "Anthropic-Beta"); !strings.Contains(got, "claude-code-20250219") {
t.Fatalf("Anthropic-Beta = %q, want preserved claude-code-20250219", got)
}
}

View File

@@ -496,6 +496,11 @@ func claudeJSONObjectHasKeys(raw []byte, want []string) bool {
return errClosing == nil && closing == json.Delim('}') && keyIndex == len(want)
}
// IsClaudeCodeUserAgent reports whether userAgent identifies a Claude Code CLI client.
func IsClaudeCodeUserAgent(userAgent string) bool {
return claudeCodeUserAgentPattern.MatchString(strings.TrimSpace(userAgent))
}
func plausibleClaudeCodeUserAgent(userAgent string, cfg *config.Config) bool {
userAgent = strings.TrimSpace(userAgent)
if !claudeCodeUserAgentPattern.MatchString(userAgent) || !claudeCodeNativeUserAgentPattern.MatchString(userAgent) {