Wrapper cmd double-escapes env values: Ec() escapes for unquoted set, but ce() emits quoted set "KEY=..." so carets/%% survive into the child env (model names with () arrive as ^(self hosted^))

Author: kurtonthewayCreated Sep 14, 2026Updated Sep 14, 2026

Wrapper cmd double-escapes env values: Ec() escapes for unquoted set, but ce() wraps in quotes, so quoted set "KEY=..." never unescapes → model names with () arrive as ^(self hosted^)

Environment

  • Claude Code Router Desktop v3.1.0 (Windows, C:\Users\<user>\AppData\Local\Programs\Claude Code Router\Claude Code Router.exe)
  • Windows 10 19045 x64
  • Claude Code CLI 2.1.270
  • Profile default model: Habilynx/glm-5.3 (self hosted) (a provider model label containing parentheses — e.g. self-hosted provider model names)

Summary

The per-profile wrapper bin/ccr-claude-code-wrapper-<profile>.cmd (regenerated on every ccr-app "<Profile>" launch) escapes env values with Ec(), which encodes for unquoted set syntax — but the line is emitted by ce() as a quoted set "KEY=<escaped>". A quoted set never interprets caret/percent escapes, so the child process env literally contains ^ and %%.

For the profile default model this means:

cmd
set "ANTHROPIC_MODEL=Habilynx/glm-5.3 ^(self hosted^)"
set "CCR_CLAUDE_CODE_MODEL=Habilynx/glm-5.3 ^(self hosted^)"
set "CODEXL_CLAUDE_CODE_MODEL=Habilynx/glm-5.3 ^(self hosted^)"

So Claude Code receives ANTHROPIC_MODEL='Habilynx/glm-5.3 ^(self hosted^)' and prints on startup (for every session, query_source: generate_session_title):

[claude-code:unrecognized_model] {"model":"Habilynx/glm-5.3 ^(self hosted^)","query_source":"generate_session_title"}

Root cause (bundled cli.js, minified names from the shipped app.asar)

javascript
function ce(e, t = "") { return `${r}set "${e}=${Ec(t)}"` }        // quoted set
function Ec(e) {
  return e.replace(/\r?\n/g, " ")
    .replace(/\^/g, "^^")
    .replace(/%/g, "%%")
    .replace(/"/g, '^"')
    .replace(/[&|<>()]/g, "^$&");                                   // unquoted-set escaping
}

ce() uses Ec() (escapes for set KEY=value without quotes) inside a quoted set "KEY=...". Inside quotes, ^( / %% / ^" are never unescaped by cmd, so the value that reaches the child env still contains the escape characters.

Impact beyond the banner: any env value routed through the wrapper (model names with (), strings with %, values containing &|<>()) is corrupted the same way. In Claude Code, process env wins over settings.json env, so the clean settings.json value cannot recover it.

Minimal reproduction

powershell
# 1. Launch the profile
ccr-app "Claude Code"

# 2. Inspect the generated wrapper
type "%APPDATA%\claude-code-router\bin\ccr-claude-code-wrapper-<profile>.cmd"
# → set "ANTHROPIC_MODEL=<provider>/<model> ^(self hosted^)"   (model name containing parens)

# 3. Confirm the child env literally contains carets
echo 'Reply with one word: PING' | claude --print "test"   # with ANTHROPIC_BASE_URL pointed at CCR
# → [claude-code:unrecognized_model] {"model":"Habilynx/glm-5.3 ^(self hosted^)","query_source":"generate_session_title"}

Direct proof that the quoted set keeps the carets (cmd does not unescape inside quotes):

cmd
set "V=a ^(b^)"
echo %V%      → a ^(b^)          (carets preserved, not a(b)

Suggested fix

Either:

  1. Drop Ec() for quoted set: set "KEY=<value>" inside quotes only needs " doubling — a plain quoted set needs no caret/percent escaping at all (keep the \r?\n → space normalization), or
  2. Emit unquoted set KEY=<Ec(value)> lines, where the current Ec() escaping is actually correct, or
  3. Un-escape in the middleware before spawning the child (least desirable — every wrapper variant would need it).

Option 1 is the smallest change: inside set "...", only " needs handling ("" or ^"), and %/()/&|<> are literal.

Additional notes

  • Editing the wrapper by hand does not help: ccr-app "<Profile>" PREPARE regenerates wrapper/settings/api-key files from the CCR config store on every launch.
  • The ^(...) corruption is invisible in the UI (the profile model shows correctly) and only surfaces in the child process env.
  • Same generation path is used for the Codex stdio wrapper (ccr-codex-cli-stdio-<profile>.cmd), so CCR_CODEX_MODEL values with special characters are affected too.

Happy to provide the full reproduction transcript or a PR if the escaping change direction is agreed.

Source: musistudio/claude-code-router