diff --git a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentClaudeCodeChatModelBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentClaudeCodeChatModelBuilder.java
index d8a5eca4..d3f83027 100644
--- a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentClaudeCodeChatModelBuilder.java
+++ b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/AgentClaudeCodeChatModelBuilder.java
@@ -38,9 +38,10 @@ import vip.mate.llm.model.ModelProviderEntity;
* {@code oauth-2025-04-20} or Anthropic's edge intermittently 500s.
* We push these via {@link AnthropicApi.Builder#anthropicBetaFeatures}
* so Spring AI's existing header-merging logic still applies.
- *
{@code User-Agent: claude-cli/ (external, cli)} and
- * {@code x-app: cli} masquerade as the Claude Code CLI — Anthropic
- * rejects unrecognised UAs on Bearer-auth requests with HTTP 400.
+ * {@code User-Agent: claude-cli/} (bare — no suffix) and
+ * {@code x-app: cli} masquerade as the Claude Code CLI. Suffix variants
+ * like {@code (external, cli)} are anti-abuse fingerprints; see
+ * {@link ClaudeCodeApiHeaders#userAgent()}.
*
*
* Token lifecycle
diff --git a/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeApiHeaders.java b/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeApiHeaders.java
index f3306cf8..60864e00 100644
--- a/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeApiHeaders.java
+++ b/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeApiHeaders.java
@@ -20,7 +20,7 @@ import java.util.List;
* Header set sent on OAuth requests
* | Header | Value | Why |
* | {@code Authorization} | {@code Bearer } | OAuth path uses Bearer; non-OAuth uses {@code x-api-key} |
- * | {@code User-Agent} | {@code claude-cli/ (external, cli)} | Anthropic routes OAuth by UA; spoof identity |
+ * | {@code User-Agent} | {@code claude-cli/} | bare form — suffix triggers anti-abuse fingerprint, see {@link #userAgent()} |
* | {@code x-app} | {@code cli} | Claude Code identity flag |
* | {@code anthropic-beta} | (comma-joined list — see {@link #allBetas()}) | OAuth-only + common feature betas |
*
@@ -58,12 +58,20 @@ public class ClaudeCodeApiHeaders {
/**
* User-Agent string Anthropic OAuth infrastructure expects.
- * Format: {@code claude-cli/ (external, cli)}.
- * The {@code (external, cli)} suffix is the canonical hermes / OpenCode /
- * Cline identity — drop it and Anthropic returns 400.
+ * Format: {@code claude-cli/} — bare, no suffix.
+ *
+ * History note: we previously appended {@code (external, cli)}
+ * after hermes-agent's pattern. That turned out to be wrong: Anthropic's
+ * anti-abuse gate uses the suffix to fingerprint third-party clients
+ * (hermes / OpenCode / Cline) and rate-limits them harder. Real Claude
+ * Code (Electron + Node + official Anthropic JS SDK) emits the bare
+ * {@code claude-cli/} form, which is what openclaw
+ * ({@code anthropic-transport-stream.ts:30,572}) also uses. Verified by
+ * reproducing 429 with the suffix and {@code anthropic-ratelimit-*}
+ * headers absent — the diagnostic signature of the anti-abuse path.
*/
public String userAgent() {
- return "claude-cli/" + versionDetector.get() + " (external, cli)";
+ return "claude-cli/" + versionDetector.get();
}
/** {@code x-app} header value. Constant. */
diff --git a/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeTokenRefresher.java b/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeTokenRefresher.java
index 504a3a9b..0ce3cc04 100644
--- a/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeTokenRefresher.java
+++ b/mateclaw-server/src/main/java/vip/mate/llm/anthropic/oauth/ClaudeCodeTokenRefresher.java
@@ -80,8 +80,11 @@ public class ClaudeCodeTokenRefresher {
.uri(endpoint)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+ // Bare UA — see ClaudeCodeApiHeaders.userAgent() javadoc
+ // for why the (external, cli) suffix would trip Anthropic's
+ // anti-abuse fingerprint.
.header(HttpHeaders.USER_AGENT,
- "claude-cli/" + versionDetector.get() + " (external, cli)")
+ "claude-cli/" + versionDetector.get())
.body(body)
.retrieve()
.body(String.class);