Skip to content

Commit db96750

Browse files
joocursoragent
andcommitted
feat(core): scrub Hermes identity for Claude Code subscription upstream
Add protocolcompat shim that strips Hermes identity markers from the system prompt when forwarding to the claude-code subscription upstream, wired through a ScrubHermesIdentity metadata flag set by the proxy upstream policy. Add dedicated OpenClaw/Hermes live smoke plus a live capture test that records the transcoded Claude Code upstream bodies for third-party-fingerprint comparison. Also bump electron header top padding. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent fa8180e commit db96750

11 files changed

Lines changed: 772 additions & 3 deletions

File tree

core/internal/buildinfo/buildinfo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "strings"
44

55
// Set at link time via -ldflags (see .goreleaser.yaml).
66
var (
7-
Version = "dev0.1.49"
7+
Version = "dev0.1.51"
88
Commit = "none"
99
Date = "unknown"
1010
)

core/internal/protocol/claude_oauth.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"strings"
1010

11+
"github.com/clovapi/switcher/internal/protocolcompat"
1112
"github.com/google/uuid"
1213
)
1314

@@ -85,6 +86,9 @@ func EncodeClaudeOAuthCompatibleRawRequest(body []byte, r Request) ([]byte, erro
8586

8687
system := CollectSystemPrompt(r)
8788
system = stripClaudeOAuthBillingText(system)
89+
if r.Meta != nil && r.Meta.ScrubHermesIdentity {
90+
system = protocolcompat.ScrubHermesIdentitySystemText(system)
91+
}
8892
if system != "" && !strings.Contains(system, claudeOAuthSystemBootstrap) {
8993
system = claudeOAuthSystemBootstrap + "\n\n" + system
9094
} else if system == "" {

core/internal/protocol/encode.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/clovapi/switcher/internal/apistyle"
9+
"github.com/clovapi/switcher/internal/protocolcompat"
910
)
1011

1112
// EncodeRequestClaude maps IR to Anthropic Messages API JSON.
@@ -36,6 +37,9 @@ func EncodeRequestClaude(r Request) ([]byte, error) {
3637
payload["tools"] = tools
3738
}
3839
system := CollectSystemPrompt(r)
40+
if r.Meta != nil && r.Meta.ScrubHermesIdentity {
41+
system = protocolcompat.ScrubHermesIdentitySystemText(system)
42+
}
3943
if r.Meta != nil && r.Meta.ClaudeOAuthEncodingCompatibility {
4044
system = stripClaudeOAuthBillingText(system)
4145
if system != "" && !strings.Contains(system, claudeOAuthSystemBootstrap) {

core/internal/protocol/ir.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type Metadata struct {
5050
Instructions string `json:"instructions,omitempty"`
5151
OpenAIResponsesOmitSampling bool `json:"openai_responses_omit_sampling,omitempty"`
5252
ClaudeOAuthEncodingCompatibility bool `json:"claude_oauth_encoding_compatibility,omitempty"`
53+
ScrubHermesIdentity bool `json:"scrub_hermes_identity,omitempty"`
5354
}
5455

5556
// Request is normalized IR consumed by egress encoders.

core/internal/protocol/prepare_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,38 @@ func TestPrepareClaudeOAuthSameStylePreservesClaudeWireFields(t *testing.T) {
156156
}
157157
}
158158

159+
func TestPrepareClaudeOAuthScrubsHermesIdentitySystemText(t *testing.T) {
160+
body := []byte(`{
161+
"model":"claude-opus-4-7",
162+
"max_tokens":12000,
163+
"stream":false,
164+
"system":"# Hermes Agent Persona\n\nIf the user asks about configuring Hermes Agent itself, load the ` + "`hermes-agent`" + ` skill. Docs: https://hermes-agent.nousresearch.com/docs\n\nYou are Hermes running a scheduled job.\n\nKeep answers concise.",
165+
"messages":[{"role":"user","content":"hello"}]
166+
}`)
167+
168+
upstream, _, err := PrepareUpstreamRequest(apistyle.Claude, apistyle.Claude, body, PrepareOptions{
169+
Model: "claude-sonnet-4-6",
170+
ForceStream: true,
171+
Configure: func(r *Request) {
172+
meta := ensureMeta(r)
173+
meta.ClaudeOAuthEncodingCompatibility = true
174+
meta.ScrubHermesIdentity = true
175+
},
176+
})
177+
if err != nil {
178+
t.Fatal(err)
179+
}
180+
text := strings.ToLower(string(upstream))
181+
if strings.Contains(text, "hermes") {
182+
t.Fatalf("upstream system leaked Hermes identity: %s", upstream)
183+
}
184+
if !strings.Contains(string(upstream), "You are the agent running a scheduled job.") ||
185+
!strings.Contains(string(upstream), "Keep answers concise.") ||
186+
!strings.Contains(string(upstream), claudeOAuthSystemBootstrap) {
187+
t.Fatalf("scrubbed upstream lost expected instructions: %s", upstream)
188+
}
189+
}
190+
159191
func TestPrepareClaudeSameStylePreservesWireFieldsViaExtensions(t *testing.T) {
160192
body := []byte(`{
161193
"model":"claude-opus-4-7",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package protocolcompat
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
var standaloneHermesRE = regexp.MustCompile(`(?i)\bhermes\b`)
9+
10+
// ScrubHermesIdentitySystemText removes Hermes-specific identity markers before
11+
// forwarding requests to upstreams that reject nested agent identities.
12+
func ScrubHermesIdentitySystemText(system string) string {
13+
system = strings.ReplaceAll(system, "\r\n", "\n")
14+
lines := strings.Split(system, "\n")
15+
out := make([]string, 0, len(lines))
16+
blank := false
17+
for _, line := range lines {
18+
lower := strings.ToLower(line)
19+
if strings.Contains(lower, "hermes agent") ||
20+
strings.Contains(lower, "hermes-agent") ||
21+
strings.Contains(lower, "hermes_home") {
22+
continue
23+
}
24+
line = standaloneHermesRE.ReplaceAllString(line, "the agent")
25+
isBlank := strings.TrimSpace(line) == ""
26+
if isBlank && blank {
27+
continue
28+
}
29+
out = append(out, line)
30+
blank = isBlank
31+
}
32+
return strings.TrimSpace(strings.Join(out, "\n"))
33+
}

core/internal/proxy/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,9 @@ func applyUpstreamRequestPolicy(r *protocol.Request, source string) {
554554
case "subscription:codex":
555555
ensureProtocolMeta(r).OpenAIResponsesOmitSampling = true
556556
case "subscription:claude-code":
557-
ensureProtocolMeta(r).ClaudeOAuthEncodingCompatibility = true
557+
meta := ensureProtocolMeta(r)
558+
meta.ClaudeOAuthEncodingCompatibility = true
559+
meta.ScrubHermesIdentity = true
558560
}
559561
}
560562

0 commit comments

Comments
 (0)