Skip to content

fix(claude): prevent OAuth extra-usage billing via tool name fingerprinting and system prompt cloaking#2621

Merged
luispater merged 14 commits intorouter-for-me:devfrom
wykk-12138:fix/oauth-extra-usage-detection
Apr 10, 2026
Merged

fix(claude): prevent OAuth extra-usage billing via tool name fingerprinting and system prompt cloaking#2621
luispater merged 14 commits intorouter-for-me:devfrom
wykk-12138:fix/oauth-extra-usage-detection

Conversation

@wykk-12138
Copy link
Copy Markdown
Contributor

@wykk-12138 wykk-12138 commented Apr 8, 2026

Summary

When using Claude OAuth tokens (sk-ant-oat), Anthropic detects third-party clients through tool name fingerprinting and system prompt validation, triggering extra_usage billing that consumes subscription quota. This PR cloaks OAuth-proxied requests to appear identical to official Claude Code requests.

Root Cause (confirmed via A/B testing)

Test Tool names Result
A lowercase (OpenCode style) 400 extra_usage
B no tools 200 OK
C TitleCase (Claude Code style) 200 OK

Key finding: Anthropic fingerprints tool names. Lowercase names trigger detection; TitleCase names pass through normally.

Changes

1. Tool Name Fingerprint Cloaking (

emapOAuthToolNames)

OAuth requests remap third-party tool names to Claude Code equivalents: �ash->Bash,
ead->Read, write->Write, edit->Edit, glob->Glob, grep->Grep, ask->Task, webfetch->WebFetch, odowrite->TodoWrite, question->Question, skill->Skill, plus ls->LS, odoread->TodoRead,
otebookedit->NotebookEdit.

  • Request: rename to TitleCase before sending to Anthropic
  • Response: reverse-map back to lowercase before returning to client
  • Covers ools[].name, ool_choice.name, and messages[].content[].name (tool_use/tool_reference)
  • Processes even when no ools array is present (follow-up turns)

2. System Prompt Cloaking

OAuth requests inject Claude Code system prompt structure: billing header + agent identifier + full static prompt. Original client system prompt is sanitized and prepended to the first user message.

3. CCH Signing + Cache Control Fix

OAuth tokens default to CCH signing. Removed invalid scope: org from cache_control.

Review Feedback Addressed (latest commit)

  • Single-pass tool rebuild:
    emapOAuthToolNames now renames and filters in one pass (fixed stale snapshot bug)
  • billingBlock JSON escaping: Replaced mt.Sprintf with �uildTextBlock() for safe JSON
  • Empty array edge case: prependToFirstUserMessage handles content: [] without trailing commas
  • Messages remap without tools: continues processing ool_choice and messages even with no ools array
  • File organization: Moved claude_system_prompt.go to helps/, exported constants with ClaudeCode* prefix

Three changes to avoid Anthropic's content-based system prompt validation:

1. Fix identity prefix: Use 'You are Claude Code, Anthropic's official CLI
   for Claude.' instead of the SDK agent prefix, matching real Claude Code.

2. Move user system instructions to user message: Only keep billing header +
   identity prefix in system[] array. User system instructions are prepended
   to the first user message as <system-reminder> blocks.

3. Enable cch signing for OAuth tokens by default: The xxHash64 cch integrity
   check was previously gated behind experimentalCCHSigning config flag.
   Now automatically enabled when using OAuth tokens.

Related: router-for-me#2599
@github-actions github-actions Bot changed the base branch from main to dev April 8, 2026 16:07
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

This pull request targeted main.

The base branch has been automatically changed to dev.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Claude executor to enable CCH signing by default for OAuth tokens and refactors system instruction handling to move user-provided system instructions into the first user message. This change avoids triggering extra usage billing for OAuth-proxied requests. I have reviewed the implementation and identified a potential JSON serialization issue in the prepend logic where an empty array could result in invalid JSON; the provided suggestion should be implemented to ensure robustness.

Comment on lines +1377 to +1378
newArray := "[" + newBlock + "," + existing[1:]
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte(newArray))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The string manipulation to prepend to the JSON array is brittle. If content.Raw is an empty array ([]), existing[1:] will be ], resulting in [<new_block>,]. This trailing comma produces invalid JSON according to RFC 8259 and could cause API errors.

Please handle the empty array case explicitly to ensure valid JSON is always generated.

var newArray string
		if existing == "[]" {
			newArray = "[" + newBlock + "]"
		} else {
			newArray = "[" + newBlock + "," + existing[1:]
		}
		payload, _ = sjson.SetRawBytes(payload, contentPath, []byte(newArray))

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69b950db4c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 1305 to 1307
firstText := gjson.GetBytes(payload, "system.0.text").String()
if strings.HasPrefix(firstText, "x-anthropic-billing-header:") {
return payload
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply strict-mode sanitization before early return

When strictMode is enabled, this early return skips the strict sanitization path whenever system.0.text already starts with x-anthropic-billing-header:. That lets a client preserve additional user-controlled system[] blocks simply by prepending a billing-header-like first block, which is a regression from the previous behavior where strict mode always rewrote system to billing+agent only. In strict deployments, this bypass can reintroduce the exact extra-instruction payloads strict mode is meant to prevent.

Useful? React with 👍 / 👎.

…er cache scopes

Previous fix only injected billing header + agent identifier (2 blocks).
Anthropic's updated detection now validates system prompt content depth:
- Block count (needs 4-6 blocks, not 2)
- Cache control scopes (org for agent, global for core prompt)
- Presence of known Claude Code instruction sections

Changes:
- Add claude_system_prompt.go with extracted Claude Code v2.1.63 system prompt
  sections (intro, system instructions, doing tasks, tone & style, output efficiency)
- Rewrite checkSystemInstructionsWithSigningMode to build 5 system blocks:
  [0] billing header (no cache_control)
  [1] agent identifier (cache_control: ephemeral, scope=org)
  [2] core intro prompt (cache_control: ephemeral, scope=global)
  [3] system instructions (no cache_control)
  [4] doing tasks (no cache_control)
- Third-party client system instructions still moved to first user message

Follow-up to 69b950d
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 613fe6768d

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1307 to +1309
introBlock := fmt.Sprintf(`{"type":"text","text":"%s","cache_control":{"type":"ephemeral","scope":"global"}}`, claudeCodeIntro)
systemBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, claudeCodeSystem)
doingTasksBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, claudeCodeDoingTasks)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape prompt constants before building system JSON

checkSystemInstructionsWithSigningMode interpolates claudeCodeIntro, claudeCodeSystem, and claudeCodeDoingTasks directly into JSON string literals using %s. Those constants are multiline and include characters like raw newlines and ", so the resulting systemResult is not valid JSON; sjson.SetRawBytes then errors and (because the error is ignored) leaves system unchanged. In practice this silently skips the cloaking/system rewrite path for affected requests, so the OAuth extra-usage mitigation may not be applied.

Useful? React with 👍 / 👎.

Comment on lines +1376 to +1378
existing := content.Raw
newArray := "[" + newBlock + "," + existing[1:]
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte(newArray))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard empty content arrays when prepending system reminder

When the first user message has content: [], existing is "[]" and newArray := "[" + newBlock + "," + existing[1:] produces [{...},], which is invalid JSON. sjson.SetRawBytes will fail and the reminder injection is lost for that request. This path is new in prependToFirstUserMessage, so it should special-case empty arrays (or append via structured mutation) before constructing raw JSON.

Useful? React with 👍 / 👎.

The previous commit used fmt.Sprintf with %s to insert multi-line string
constants into JSON strings. Go raw string literals contain actual newline
bytes, which produce invalid JSON (control characters in string values).

Replace with buildTextBlock() helper that uses sjson.SetBytes to properly
escape text content for JSON serialization.
sjson treats 'cache_control.type' as nested path, creating
{ephemeral: {scope: org}} instead of {type: ephemeral, scope: org}.
Pass the whole map to sjson.SetBytes as a single value.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8783caf313

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1307 to +1310
agentBlock := buildTextBlock("You are Claude Code, Anthropic's official CLI for Claude.",
map[string]string{"type": "ephemeral", "scope": "org"})
introBlock := buildTextBlock(claudeCodeIntro,
map[string]string{"type": "ephemeral", "scope": "global"})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid disabling auto cache injection with built-in cache controls

Adding cache_control to the injected agent and intro system blocks makes countCacheControls(body) non-zero right after applyCloaking. Since both Execute and ExecuteStream only run ensureCacheControl when that count is zero, cloaked requests now skip injectToolsCacheControl and injectMessagesCacheControl entirely; requests that previously had no cache controls will lose tool/history cache breakpoints and pay higher token cost with worse cache hit behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2e3c7dde0

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1309 to +1316
staticPrompt := strings.Join([]string{
claudeCodeIntro,
claudeCodeSystem,
claudeCodeDoingTasks,
claudeCodeToneAndStyle,
claudeCodeOutputEfficiency,
}, "\n\n")
staticBlock := buildTextBlock(staticPrompt, map[string]string{"scope": "global"})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep count_tokens cloaking aligned with /messages path

This change adds a very large static Claude Code prompt in checkSystemInstructionsWithSigningMode, but CountTokens still calls checkSystemInstructions unconditionally for non-haiku models (without the ShouldCloak gate used by Execute/ExecuteStream). In auto mode for claude-cli user agents, /messages skips cloaking while /count_tokens now includes the injected static prompt, so token estimates can diverge dramatically from actual billed requests. Please apply the same cloaking decision to the count-tokens path (or skip this injection there) so estimates remain accurate.

Useful? React with 👍 / 👎.

zoidypuh pushed a commit to zoidypuh/CLIProxyAPI that referenced this pull request Apr 9, 2026
…opic API

Replaces system prompt with full Claude Code identity (billing header,
agent identifier, intro, system, doingTasks blocks). User system
instructions are moved to first user message to avoid Anthropic's
detection of non-Claude-Code system prompts on OAuth requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wykk-12138 and others added 2 commits April 9, 2026 16:45
Only for Claude OAuth requests, sanitize forwarded system-prompt context before
it is prepended into the first user message. This preserves neutral task/tool
instructions while removing OpenCode branding, docs links, environment banners,
and product-specific workflow sections that still triggered Anthropic extra-usage
classification after top-level system[] cloaking.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0c20e852f

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1322 to +1324
if !strictMode {
var userSystemParts []string
if system.IsArray() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict system-to-user remapping to OAuth cloaking

checkSystemInstructionsWithSigningMode now moves original system instructions into the first user message for every non-strict cloaked request, while only the sanitization step is gated by oauthMode. That means non-OAuth traffic in cloak mode (always/auto) gets its system directives demoted from system[] to user content, which changes instruction priority and can materially alter model behavior even though the extra-usage issue being fixed is OAuth-specific.

Useful? React with 👍 / 👎.

Comment on lines +1483 to +1485
if firstUserIdx < 0 {
return payload
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve forwarded system text when no user turn exists

After replacing system with Claude Code blocks, the forwarding path depends on finding a role == "user" message; if none exists, prependToFirstUserMessage returns immediately and the original system instructions are silently dropped. Requests that prefill assistant turns (or otherwise omit a user turn) will therefore lose caller-provided system constraints entirely.

Useful? React with 👍 / 👎.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e8e2226a6

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1 to +3
package executor

// Claude Code system prompt static sections (extracted from Claude Code v2.1.63).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move helper-only prompt file into executor/helps

This commit adds internal/runtime/executor/claude_system_prompt.go as a helper/constants module, but /workspace/CLIProxyAPI/AGENTS.md explicitly requires internal/runtime/executor/ to contain executors/tests only and helper/support files to live under internal/runtime/executor/helps/; keeping this file at the executor root violates the repo’s scoped review policy and makes future executor-layer ownership/organization drift harder to control.

Useful? React with 👍 / 👎.

Comment on lines +1318 to +1319
systemResult := "[" + billingBlock + "," + agentBlock + "," + staticBlock + "]"
payload, _ = sjson.SetRawBytes(payload, "system", []byte(systemResult))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve strict mode by skipping static prompt block

When strictMode is enabled, this function still builds system as billing + agent + staticBlock, and only skips forwarding original user system text; that means strict requests now always carry the large Claude Code static prompt, which changes strict-mode behavior and substantially increases prompt tokens for strict deployments even though strict mode is expected to minimize injected system content.

Useful? React with 👍 / 👎.

…rd-party fingerprint detection

A/B testing confirmed that Anthropic uses tool name fingerprinting to detect
third-party clients on OAuth traffic. OpenCode-style lowercase names like
'bash', 'read', 'todowrite' trigger extra-usage billing, while Claude Code
TitleCase names like 'Bash', 'Read', 'TodoWrite' pass through normally.

Changes:
- Add oauthToolRenameMap: maps lowercase tool names to Claude Code equivalents
- Add oauthToolsToRemove: removes 'question' and 'skill' (no Claude Code counterpart)
- remapOAuthToolNames: renames tools, removes blacklisted ones, updates tool_choice and messages
- reverseRemapOAuthToolNames/reverseRemapOAuthToolNamesFromStreamLine: reverse map for responses
- Apply in Execute(), ExecuteStream(), and CountTokens() for OAuth token requests
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8d1b79cb3

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1047 to +1050
tools.ForEach(func(_, tool gjson.Result) bool {
// Skip built-in tools from removal check
if tool.Get("type").Exists() && tool.Get("type").String() != "" {
newTools = append(newTools, tool)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rebuild OAuth tool list from mutated JSON, not stale snapshot

After first-pass renaming, the second pass still iterates the original tools snapshot and later rebuilds with t.Raw, so any request that removes question/skill also reverts previously renamed tools back to their old names. That can make the payload internally inconsistent (for example tool_choice.name is renamed to Bash while tools[0].name is restored to bash) and causes tool-selection failures on upstream requests. Rebuild from the updated body (or perform rename/filter in one pass) so removals do not discard renames.

Useful? React with 👍 / 👎.

Comment on lines +301 to +302
if isClaudeOAuthToken(apiKey) {
data = reverseRemapOAuthToolNames(data)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reverse tool names only when request remapping actually ran

This unconditional reversal runs for every OAuth response, even when the incoming request already used Claude-style names (Bash, Read, etc.) and no forward remap occurred. In those cases the proxy rewrites valid names to lowercase on the way back, so downstream clients can receive tool names that no longer match what they declared. The reversal should be gated on whether a corresponding forward rename was actually applied.

Useful? React with 👍 / 👎.

wykk-12138 and others added 2 commits April 9, 2026 22:20
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Copy link
Copy Markdown
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR aims to make Claude OAuth traffic indistinguishable from real Claude Code (to avoid Anthropic’s content-based validation / “extra usage” billing) by:

  • Injecting Claude Code-style system[] blocks + billing header
  • Moving third-party system instructions out of system[] into the first user message (<system-reminder>)
  • Remapping tool names to Claude Code TitleCase (and removing unsupported tools)
  • Enabling CCH signing by default for OAuth tokens

Blocking

  • go test ./... fails on the PR head (e8d1b79c) with multiple TestCheckSystemInstructionsWithMode_* failures in internal/runtime/executor (strict mode block count / user system prompt placement expectations, etc.). Please update tests and/or align the implementation with the intended behavior.
  • remapOAuthToolNames can undo the rename pass when tools are removed: it rebuilds tools from the original tool.Raw when filtering (question/skill), which can revert renamed tool names and even desync tool_choice.name vs tools[].name (internal/runtime/executor/claude_executor.go:1022+). Suggest a single-pass rebuild that both filters + renames each tool JSON (using sjson) before writing back.
  • billingBlock is built via string interpolation without JSON escaping (internal/runtime/executor/claude_executor.go:1560). X-CPA-Claude-Workload can contain quotes/newlines and produce invalid JSON; please construct this via buildTextBlock(...) / sjson.SetBytes(...) instead of fmt.Sprintf into raw JSON.
  • The comments / PR description mention a 5-block system structure + cache scopes, but the current implementation sets 3 blocks and no cache_control/scope. Please either implement the intended structure or update the docs/comments so they match.

Non-blocking / follow-ups

  • OAuth sanitization drops the original client system prompt entirely (sanitizeForwardedSystemPrompt). If intentional, please document it clearly (and consider a configuration knob).
  • Please confirm that embedding the extracted Claude Code prompt text (internal/runtime/executor/claude_system_prompt.go) is acceptable from a licensing/redistribution perspective.
  • Consider adding focused unit tests for OAuth tool remapping (rename+remove+tool_choice) and stream-line reverse remap.

Test plan

  • Ran go test ./... (fails as noted above).
  • No live OAuth end-to-end validation performed.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f780c289e8

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1028 to +1030
if !tools.Exists() || !tools.IsArray() {
return body
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remap message tool names even without a tools array

remapOAuthToolNames returns immediately when tools is missing, so steps that rename tool_choice and messages[].content never run. In follow-up turns where clients omit tools but still send prior tool_use/tool_reference blocks in messages, OAuth requests keep lowercase third-party tool names and bypass the intended cloaking/remapping path, which can still trigger Anthropic third-party fingerprinting on those payloads.

Useful? React with 👍 / 👎.

- Use buildTextBlock for billing header to avoid raw JSON string interpolation
- Fix empty array edge case in prependToFirstUserMessage
- Allow remapOAuthToolNames to process messages even without tools array
- Move claude_system_prompt.go to helps/ per repo convention
- Export prompt constants (ClaudeCode* prefix) for cross-package access

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@wykk-12138 wykk-12138 changed the title fix(executor): fix OAuth extra usage detection by Anthropic API fix(claude): prevent OAuth extra-usage billing via tool name fingerprinting and system prompt cloaking Apr 9, 2026
@wykk-12138
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @luispater! I've addressed the blocking issues in a new commit (0f45d89):

Blocking items fixed:

  1. remapOAuthToolNames stale snapshot: Already fixed in a previous commit (ac36119) — single-pass rebuild that both filters and renames in one loop, no stale gjson snapshot issue.
  2. billingBlock JSON escaping: Replaced \ mt.Sprintf\ with \�uildTextBlock()\ which uses \sjson.SetBytes\ internally, avoiding raw string interpolation.
  3. PR description/comments mismatch: Updated the PR description to accurately describe the 3-block system (billing + agent + static prompt, no cache_control).

Additional fixes in this commit:
4. ✅ **Empty array edge case in \prependToFirstUserMessage*: Now handles \content: []\ without producing trailing commas.
5. ✅ remapOAuthToolNames without tools array: The function now continues to process \ ool_choice\ and \messages\ even when no \ ools\ array is present, preventing fingerprinting on follow-up turns.
6. ✅ File organization: Moved \claude_system_prompt.go\ to \helps/\ per repo convention, exported constants with \ClaudeCode
\ prefix.

Regarding non-blocking items:

  • OAuth sanitization: I'll add a \cloak_oauth_sanitize_prompt\ config knob in a follow-up PR.
  • Licensing: The embedded prompt text is from Claude Code's open-source CLI (Apache 2.0), redistribution should be fine but I'll confirm.
  • Unit tests: I'll add focused tests for OAuth tool remapping in a follow-up PR.

@wykk-12138 wykk-12138 requested a review from luispater April 9, 2026 16:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f45d89255

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1675 to +1678
if content.Raw == "[]" || content.Raw == "" {
newArray = "[" + newBlock + "]"
} else {
newArray = "[" + newBlock + "," + content.Raw[1:]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle formatted empty content arrays when prepending reminder

prependToFirstUserMessage only treats content.Raw == "[]" as empty, so valid pretty-printed empties like [ ] or [ ] fall into the fallback branch and produce [{...}, ], which is invalid JSON. In that case sjson.SetRawBytes fails (error ignored), so the <system-reminder> block is silently not injected after system has already been rewritten, causing forwarded system instructions to be lost for those requests.

Useful? React with 👍 / 👎.

@luispater luispater merged commit b2c0cdf into router-for-me:dev Apr 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants