feat(claude): stabilize device fingerprint across mixed Claude Code and cloaked clients#2213
feat(claude): stabilize device fingerprint across mixed Claude Code and cloaked clients#2213TTTPOB wants to merge 6 commits intorouter-for-me:mainfrom
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces an optional feature to stabilize the Claude device fingerprint for outgoing Anthropic API requests. By learning and pinning a single, consistent device profile per upstream identity, it aims to prevent 'fingerprint flicker' caused by mixed traffic from genuine Claude Code clients and cloaked non-Claude clients. This stabilization helps reduce the risk of upstream bans by presenting a consistent client identity to Anthropic, addressing a known issue where fluctuating Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
The pull request introduces a new feature for stabilizing Claude device fingerprints, which is well-implemented with clear separation of concerns and good test coverage. The changes correctly integrate new configuration options, update the header application logic, and provide robust tests for various scenarios, including baseline usage, version upgrades, and disabled mode. The new claude_device_profile.go file encapsulates the core logic effectively, and the config.go updates ensure proper handling of the new settings. Overall, the code maintains a high standard of quality and addresses the stated motivation effectively.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0e337aeb9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| OS: hdrDefault(hd.OS, defaultClaudeFingerprintOS), | ||
| Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch), |
There was a problem hiding this comment.
Keep legacy OS/arch fallback runtime-derived
When stabilize-device-profile is unset or false, the legacy path still calls defaultClaudeDeviceProfile(), so non-Claude clients that omit X-Stainless-Os/X-Stainless-Arch now advertise the hard-coded MacOS/arm64 pair from here. Before this patch applyClaudeHeaders() used mapStainlessOS()/mapStainlessArch(), so Linux and Windows deployments will silently change their outgoing fingerprint even though the new feature is supposed to be opt-in.
Useful? React with 👍 / 👎.
| if isClaudeCodeClient(clientUA) { | ||
| r.Header.Set("User-Agent", clientUA) | ||
| return |
There was a problem hiding this comment.
Preserve configured User-Agent overrides for real Claude clients
This branch now runs after util.ApplyCustomHeadersFromAttrs() and unconditionally rewrites User-Agent back to the incoming claude-cli/... value whenever the caller is Claude Code. That means a claude-api-key[].headers.User-Agent override no longer works for official clients when stabilization is disabled, even though per-key headers used to win before this refactor; any upstream routing or allowlisting that depends on the configured UA will stop working for those requests.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd64adbeeb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if hasCandidate && (!cachedValid || candidate.Version.Compare(entry.profile.Version) > 0) { | ||
| newEntry := claudeDeviceProfileCacheEntry{ | ||
| profile: candidate, | ||
| expire: now.Add(claudeDeviceProfileTTL), | ||
| } | ||
| claudeDeviceProfileCacheMu.Lock() | ||
| claudeDeviceProfileCache[cacheKey] = newEntry |
There was a problem hiding this comment.
Re-check cached version under the write lock
resolveClaudeDeviceProfile decides whether to accept candidate from a read-lock snapshot and then writes it unconditionally after taking the write lock. If two requests for the same auth race on a cold cache (for example claude-cli/2.1.63 and claude-cli/2.1.62), both will see cachedValid == false, and the lower-version goroutine can arrive second and overwrite the newer profile. That makes the stabilized fingerprint nondeterministic under normal concurrent load and reintroduces the downgrade flicker this feature is supposed to prevent.
Useful? React with 👍 / 👎.
| baseline := defaultClaudeDeviceProfile(cfg) | ||
| candidate, hasCandidate := extractClaudeDeviceProfile(headers, cfg) | ||
|
|
||
| claudeDeviceProfileCacheMu.RLock() | ||
| entry, hasCached := claudeDeviceProfileCache[cacheKey] | ||
| cachedValid := hasCached && entry.expire.After(now) && entry.profile.UserAgent != "" | ||
| claudeDeviceProfileCacheMu.RUnlock() | ||
|
|
||
| if hasCandidate && (!cachedValid || candidate.Version.Compare(entry.profile.Version) > 0) { | ||
| newEntry := claudeDeviceProfileCacheEntry{ |
There was a problem hiding this comment.
Compare first learned profile against the configured baseline
On a cold start, this branch stores any official-looking Claude client profile without checking whether it is older than the configured claude-header-defaults baseline. In a deployment that pins a newer baseline (for cloaked traffic) and still has some older Claude Code clients, the first older client request will downgrade the cached fingerprint from the configured version to its older claude-cli/... tuple, even though the new feature describes the config values as the seeded pinned baseline.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8179d5a8a4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if stabilizeDeviceProfile { | ||
| applyClaudeDeviceProfileHeaders(r, deviceProfile) |
There was a problem hiding this comment.
Preserve custom fingerprint headers when stabilization is enabled
When stabilize-device-profile is true and an auth entry sets header:User-Agent or header:X-Stainless-*, util.ApplyCustomHeadersFromAttrs() writes those values first but this branch immediately overwrites all five fingerprint headers from deviceProfile. internal/util/header_helpers.go explicitly documents custom headers as overriding built-in defaults, so enabling stabilization now silently breaks per-key routing or allowlisting setups that pin a specific Claude fingerprint via claude-api-key[].headers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
in fact I don't know how should this feature be designed. it must be one overriding another if both configs are enabled.
| candidate, hasCandidate := extractClaudeDeviceProfile(headers, cfg) | ||
| if hasCandidate && !shouldUpgradeClaudeDeviceProfile(candidate, baseline) { | ||
| hasCandidate = false |
There was a problem hiding this comment.
Let the first same-version Claude client replace the seeded baseline
On a cold cache, a real Claude client whose claude-cli/<semver> exactly matches the configured baseline is discarded here because shouldUpgradeClaudeDeviceProfile(candidate, baseline) only accepts strictly newer versions. In the common case where operators seed the current Claude release but the first real client runs a different OS/arch or package/runtime, the proxy never learns that official profile and keeps advertising the synthetic baseline until some later version appears.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
since admin is the one who will be responsible for configuring a baseline, it would not be a problem.
Summary
This draft PR adds an optional Claude device-profile stabilization feature.
When enabled, CLIProxyAPI learns and pins a single outgoing fingerprint per upstream identity, so mixed traffic from:
does not cause
User-Agent/X-Stainless-*downgrade flicker at Anthropic.Motivation
Directly related external report:
Wei-Shaw/sub2api#580Bug: OAuth 模拟模式下 User-Agent 版本与真实 Claude Code 客户端不一致 Wei-Shaw/sub2api#580
That issue describes the same risk: one real Claude Code client upgrades the observed fingerprint, then a later cloaked request downgrades it back to an older/default one. The same upstream identity then appears to jump between client versions/environments, which may increase ban risk.
Related Upstream Work
This builds on earlier Claude cloaking / fingerprint work in this repo:
#868native Claude cloaking#1621discussion ofapplyClaudeHeaders()defaults#1628configurable Claude header defaults#1662cache-user-idtoggle#1750alignment with real Claude Code 2.1.63What This PR Does
User-AgentX-Stainless-Package-VersionX-Stainless-Runtime-VersionX-Stainless-OsX-Stainless-Archauth.IDclaude-cli/<semver>version is observedclaude-header-defaultswith:osarchstabilize-device-profileConfig
stabilize-device-profiledefaults tofalse.This is intentional: some personal/shared deployments may accept the risk of mixed fingerprints if all users are trusted. Operators must opt in explicitly to enable profile pinning.
Important Notes
if no official Claude fingerprint has been observed yet for a scope, the configured baseline is used.
if two official-looking clients report the same
claude-cli/<semver>version, first observed wins. Equal-version replacements are ignored to avoid environment flicker.this does not change the separation between device fingerprint and
metadata.user_id/ session behavior.Security Concern
This draft exists because fingerprint learning itself has trust implications.
If a client sends:
User-AgentandX-Stainless-*then pinning that profile could itself increase ban risk.
Two possible hardening directions:
Persistence Concern
This implementation is currently in-memory only.
It does not persist learned device profiles across process restarts yet. That is partly because the trust policy is still unresolved: persisting an unverified learned fingerprint would make a bad profile survive restarts.
There is existing per-account persistence infrastructure for file-backed/OAuth auth records, but not a single ready-made persistence path for both:
So persistence is better handled in a follow-up after the trust model is settled.
Tests
Added coverage for:
os,arch, andstabilize-device-profile