docs/security/STEALTH_GUIDE.md
Source of truth:
open-sse/utils/tlsClient.ts,open-sse/services/{chatgptTlsClient,claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts,open-sse/config/cliFingerprints.ts,src/mitm/Last updated: 2026-06-28 — v3.8.40 Audience: Engineers maintaining provider-specific stealth integrations.
OmniRoute integrates with providers whose edges actively fingerprint non-official clients (TLS JA3/JA4, header ordering, JSON body shape, integrity tokens). This page documents the stealth surfaces OmniRoute exposes and where they are implemented.
Stealth features exist so OmniRoute can act as a compatibility layer between user-owned official accounts (Claude Code CLI, ChatGPT Desktop/Web, Antigravity, Cursor, etc.) and OmniRoute's unified API. They are not for evading fraud detection, sharing credentials, or violating provider Terms of Service. The maintainers expect operators to comply with the upstream ToS they signed when creating accounts.
open-sse/utils/tlsClient.ts — wreq-js (Chrome 124)Lazy-loaded wreq-js session that impersonates Chrome 124 on macOS. Used as a generic JA3/JA4 wrapper for upstreams behind Cloudflare. Falls back to native fetch when wreq-js is not installed (available = false).
browser: "chrome_124", os: "macos"HTTPS_PROXY → HTTP_PROXY → ALL_PROXY (also lower-case)TLS_CLIENT_TIMEOUT_MS (inherits from FETCH_TIMEOUT_MS, default 600000)wreq-js Response is fetch-compatible (headers, text(), json(), clone(), body).open-sse/services/chatgptTlsClient.ts — tls-client-node (Firefox 148)Dedicated TLS impersonator for chatgpt.com. ChatGPT's Cloudflare config pins cf_clearance to JA3/JA4 + HTTP/2 SETTINGS frame ordering — undici's handshake gets cf-mitigated: challenge even with valid cookies.
firefox_148 (must match the Firefox 148 User-Agent sent)runtimeMode: "native" (koffi-loaded shared library; avoids managed sidecar HTTP)withRandomTLSExtensionOrder: truetlsFetchChatGpt(url, options) supports streaming (writes body to temp file, tailed as ReadableStream)raceWithTimeout + TlsClientHangError triggers resetClientCache() so the next call respawns the bindingproxyUrl → OMNIROUTE_TLS_PROXY_URL → HTTPS_PROXY/HTTP_PROXY/ALL_PROXY (the native binding does not read these envs itself; it must be threaded through)TlsClientUnavailableError (binary missing), TlsClientHangError (binding deadlocked)When cliCompatMode is on, OmniRoute reshapes outgoing Claude requests so they are indistinguishable from claude-cli traffic. Three modules collaborate:
claudeCodeFingerprint.tsComputes the 3-char cc_version fingerprint embedded in the billing header:
SHA256(SALT + msg[4] + msg[7] + msg[20] + version)[:3]
FINGERPRINT_SALT = "59cf53e54c78" (hardcoded; matches official client)claudeCodeCCH.ts (Client Content Hash)Server-side integrity check the official Claude Code CLI computes via Bun/Zig. OmniRoute reimplements with xxhash-wasm:
cch=00000; placeholderxxhash64(bytes, seed) & 0xFFFFFcch=00000; with the computed tokenConstants:
0x6e52736ac806831e/\bcch=([0-9a-f]{5});/claudeCodeObfuscation.tsInserts a Unicode zero-width joiner (U+200D) after the first character of "sensitive" client names so upstream filters cannot grep them. Default word list:
opencode, open-code, cline, roo-cline, roo_cline, cursor, windsurf,
aider, continue.dev, copilot, avante, codecompanion
Applied to: system blocks, all messages[].content, and tools[].description / tools[].function.description. Operator-overridable via setSensitiveWords().
claudeCodeCompatible.ts — anthropic-compatible-cc-* providersFor third-party Anthropic relays that only accept "real Claude Code" traffic:
CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.219 (external, sdk-cli)"CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.94.0"CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v26.3.0"anthropic-beta = "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24" by defaultredact-thinking-2026-02-12 when a CC Compatible upstream specifically requires redacted thinking streamsproviderSpecificData.requestDefaults.summarizeThinking and adds display: "summarized" to CC Compatible thinking requests that did not already set a display modeCONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07" (Opus/Sonnet 4.x family)/v1/messages?beta=trueSister modules in the same bundle:
claudeCodeConstraints.ts — temperature + cache-control rulesclaudeCodeToolRemapper.ts — tool-name remappingclaudeCodeExtraRemap.ts — extra payload normalizationAntigravity requests preserve caller text byte-for-byte. OmniRoute does not insert zero-width characters into prompts or rename/inject tools to imitate an IDE client.
antigravityHeaderScrub.tsStrips Stainless SDK markers (x-stainless-lang, x-stainless-package-version, x-stainless-os, x-stainless-arch, x-stainless-runtime, x-stainless-runtime-version, x-stainless-timeout, x-stainless-retry-count, x-stainless-helper-method) before forwarding.
ANTIGRAVITY_CREDITS=always (account-ban hot spot)ANTIGRAVITY_CREDITS=always (consumed by open-sse/executors/antigravity.ts) routes every request through Antigravity AI Credit Overages (paid Google credits) instead of letting Google's free-tier quota gate things. This is documented as a feature, but it is the single most common ToS-violation report we see — multiple Google Ultra accounts have been banned with 403 / "service disabled for ToS violation" / insufficient_quota after running for a few hours with =always.
The upstream enforcement is on Google's side, not anything OmniRoute can prevent. The env var name and the existing docs make it sound like a safe knob to flip; it isn't.
Why this draws abuse detection more aggressively than free-tier-only usage:
Recommended posture:
ANTIGRAVITY_CREDITS=off unless the operator explicitly accepts paid-credit and account-enforcement risk. retry sends the normal request first and injects credits at most once after an eligible quota 429; always injects credits on the first request.model: "auto" or kr/glm/etc-combo) instead of saturating a single Antigravity account.support.google.com → "Restore Workspace/Account access" with the exact quota_exceeded / service disabled response body Google sent. Restoration is not guaranteed.The environment reference documents the account and spend implications of each credits mode.
Touch points:
open-sse/executors/antigravity.ts — reads process.env.ANTIGRAVITY_CREDITSsrc/lib/oauth/providers/antigravity.ts — credential plumbingopen-sse/config/cliFingerprints.tsPer-provider table that pins exact header ordering and JSON body field ordering captured from mitmproxy traces of the official CLIs. Currently registered: codex, claude, plus runtime-derived profiles in providerHeaderProfiles.ts for antigravity and github.
interface CliFingerprint {
headerOrder: string[]; // case-sensitive
bodyFieldOrder: string[]; // top-level JSON keys
userAgent?: string | (() => string);
extraHeaders?: Record<string, string>;
}
Toggle per provider via env (see below). When disabled, headers/body keys appear in whatever order Node/JSON gave them — easy to fingerprint.
For CLIs whose binaries cannot be redirected via OPENAI_BASE_URL, OmniRoute runs a local TLS-terminating proxy. Endpoints live under src/app/api/cli-tools/antigravity-mitm/.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/cli-tools/antigravity-mitm | Status — running, pid, dnsConfigured, certExists |
| POST | /api/cli-tools/antigravity-mitm | Start MITM (requires apiKey + sudoPassword) |
| DELETE | /api/cli-tools/antigravity-mitm | Stop MITM |
| GET | /api/cli-tools/antigravity-mitm/alias | List model aliases |
| PUT | /api/cli-tools/antigravity-mitm/alias | Save model aliases for a tool |
Target intercepted host: daily-cloudcode-pa.googleapis.com (Antigravity's upstream).
src/mitm/manager.ts::startMitm)selfsigned (RSA-2048, SHA-256, 1y) — cert/generate.tscert/install.ts127.0.0.1 daily-cloudcode-pa.googleapis.com — dns/dnsConfig.tssrc/mitm/server.cjs with ROUTER_API_KEY + MITM_LOCAL_PORT (default 443)<DATA_DIR>/mitm/.mitm.pidcert/install.tsgetLinuxCertConfig() walks a priority list and picks the first existing directory:
| Distro family | Directory | Update command |
|---|---|---|
| Debian / Ubuntu | /usr/local/share/ca-certificates | update-ca-certificates |
| Arch / CachyOS / Manjaro | /etc/ca-certificates/trust-source/anchors | update-ca-trust |
| Fedora / RHEL / CentOS | /etc/pki/ca-trust/source/anchors | update-ca-trust |
| openSUSE | /etc/pki/trust/anchors | update-ca-certificates |
Cert filename: omniroute-mitm.crt. Fingerprint match via getCertFingerprint() (SHA-1 of DER).
Additionally, updateNssDatabases() installs into per-user NSS DBs when certutil is available: ~/.pki/nssdb, ~/snap/chromium/.../nssdb, all Firefox profiles (including snap), under the nickname OmniRoute MITM Root CA.
security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychaincertutil -addstore RootAll MITM endpoints require management auth (requireCliToolsAuth). The sudo password is cached in module scope (never globalThis) and cleared on stopMitm().
.env.example section 12)| Variable | Default |
|---|---|
CLAUDE_USER_AGENT | claude-cli/2.1.219 (external, cli) |
CODEX_USER_AGENT | codex-cli/0.142.0 (Windows 10.0.26200; x64) |
GITHUB_USER_AGENT | GitHubCopilotChat/0.54.0 |
ANTIGRAVITY_USER_AGENT | antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0 |
KIRO_USER_AGENT | AWS-SDK-JS/3.0.0 kiro-ide/1.0.0 |
QODER_USER_AGENT | Qoder-Cli |
CURSOR_USER_AGENT | Cursor/3.4 |
Consumed by open-sse/executors/base.ts::buildHeaders() via dynamic lookup. Bump these when providers release new CLI versions — stale UA strings start getting rejected as outdated clients.
.env.example section 13)| Variable | Effect |
|---|---|
CLI_COMPAT_CODEX=1 | Codex fingerprint |
CLI_COMPAT_CLAUDE=1 | claude-cli fingerprint |
CLI_COMPAT_GITHUB=1 | GitHub Copilot Chat fingerprint |
CLI_COMPAT_ANTIGRAVITY=1 | Antigravity fingerprint |
CLI_COMPAT_KIRO=1 | Kiro |
CLI_COMPAT_CURSOR=1 | Cursor |
CLI_COMPAT_KIMI_CODING=1 | Kimi Coding |
CLI_COMPAT_KILOCODE=1 | KiloCode |
CLI_COMPAT_CLINE=1 | Cline |
CLI_COMPAT_ALL=1 | Enable all of the above |
The provider IP is always preserved — the toggle only reshapes the request wire image, it does not switch IP egress.
OmniRoute scrubs inbound client headers before forwarding so a request that arrives from Cursor doesn't leak User-Agent: Cursor/X.Y.Z to a Claude upstream. See src/shared/constants/upstreamHeaders.ts for the denylist, kept in lockstep with the Zod schemas and unit tests.
mitmproxy (TLS interception + dump)CLI_FINGERPRINTS[...] entry*_USER_AGENT default in .env.examplechatgptTlsClient.ts::CHATGPT_PROFILE or wreq-js browser: optionchatgptTlsClient.test.ts and a manual canary against the live providerCHANGELOG.mdopen-sse/services/__tests__/chatgptTlsClient.test.ts — proxy resolution priority, abort handling, hang recoverytests/unit/anthropic-cache-fingerprint.test.ts — fingerprint determinismtests/unit/chatgpt-web.test.ts — end-to-end stealth path for ChatGPT403