Jul 15, 2026 · 5 min · Dev Guides

Claude Code is steganographically marking requests

Claude Code is steganographically marking requests

Claude Code is steganographically marking requests

A request can look like this in your gateway log:

{
  "role": "user",
  "content": "Fix the failing authentication test."
}

Yet the actual string may be 47 bytes longer than the visible text. The extra bytes can be Unicode characters that render with zero width or modify the preceding character without producing an obvious visual difference. Your terminal, JSON viewer, and observability dashboard may all display the same sentence while the model receives a distinguishable request.

That is the important engineering fact behind Claude Code’s prompt marking: request identity can be carried inside apparently ordinary prompt text rather than an explicit field such as:

{"client": "claude-code"}

This is steganography in the practical sense. Information is hidden in the representation of the prompt, not necessarily encrypted, and normal visual inspection will miss it.

What the Technique Actually Does

Unicode gives a client several ways to create strings that look identical to a human but remain different to software:

For a simplified example, these two Python strings do not compare equal:

plain = "Run the tests"
marked = "Run\u200b the tests"

print(plain)
print(marked)
print(plain == marked)
print(len(plain), len(marked))

Both lines usually render as:

Run the tests

But the comparison returns False, and the marked version contains an additional code point.

A richer marker can encode multiple bits. Suppose a client chooses between a normal space and a zero-width-space-plus-space sequence at eight predetermined positions. Those choices can represent one byte without visibly changing the prompt.

Claude Code requests have demonstrated this broader property: content associated with the client can carry machine-detectable distinctions that are not obvious in rendered prompt text. The exact encoding should be treated as implementation detail. It can change between releases, and the presence of a marker does not by itself establish its purpose. Plausible uses include client recognition, abuse analysis, routing validation, telemetry correlation, and identifying prompts that have passed through an unofficial intermediary.

The observable behavior is more defensible than speculation about intent.

How to Inspect the Request You Actually Send

In practice, inspecting the terminal output is insufficient. You need the serialized request body before TLS encryption, ideally at an API gateway or inside the SDK transport.

Log Escaped Strings, Not Rendered Strings

This Python function reports non-ASCII and format characters without silently hiding them:

import json
import unicodedata
from typing import Any


def inspect_string(value: str, path: str) -> None:
    suspicious = []

    for index, char in enumerate(value):
        category = unicodedata.category(char)

        if ord(char) > 127 or category in {"Cf", "Mn", "Me"}:
            suspicious.append({
                "index": index,
                "char": ascii(char),
                "codepoint": f"U+{ord(char):04X}",
                "name": unicodedata.name(char, "UNKNOWN"),
                "category": category,
            })

    if suspicious:
        print(json.dumps({
            "path": path,
            "python_repr": repr(value),
            "utf8_hex": value.encode("utf-8").hex(),
            "characters": suspicious,
        }, indent=2))


def walk(value: Any, path: str = "$") -> None:
    if isinstance(value, str):
        inspect_string(value, path)
    elif isinstance(value, list):
        for index, item in enumerate(value):
            walk(item, f"{path}[{index}]")
    elif isinstance(value, dict):
        for key, item in value.items():
            walk(item, f"{path}.{key}")


with open("request.json", "r", encoding="utf-8") as request_file:
    walk(json.load(request_file))

Run it against a captured payload:

python inspect_prompt.py

For Run\u200b the tests, the useful part of the output is:

{
  "index": 3,
  "char": "'\\u200b'",
  "codepoint": "U+200B",
  "name": "ZERO WIDTH SPACE",
  "category": "Cf"
}

Do not flag every non-ASCII character as malicious. Accented names, CJK text, mathematical notation, and emoji are legitimate prompt content. The scanner is a diagnostic tool, not a production policy.

Compare Bytes Across Clients

To establish whether a marker is client-generated, send equivalent prompts through:

  1. Claude Code
  2. A minimal direct API script
  3. Your normal gateway
  4. Another model provider’s compatible endpoint

Store the exact UTF-8 request body at each controlled boundary. Then compare hashes and escaped content:

sha256sum claude-code-body.json direct-body.json
xxd -g 1 claude-code-body.json | less
diff -u \
  <(jq -S . claude-code-body.json) \
  <(jq -S . direct-body.json)

A common gotcha is JSON escaping. One logger may store an actual U+200B, while another stores the six visible characters \u200b. Parse the JSON and compare decoded strings, not only files.

Also exclude unrelated differences first:

Otherwise, ordinary client behavior can be mistaken for hidden marking.

Why This Matters for API Gateways

Many teams put Claude, GPT, and Gemini behind one OpenAI-compatible interface. The gateway translates message schemas, tool definitions, streaming events, and authentication. Prompt-level markers complicate that architecture because they survive some transformations but not others.

Gateway operationMarker likely preserved?Main risk
Forward raw UTF-8 stringsYesHidden data crosses trust boundaries
Parse and re-serialize JSONUsuallySerializer behavior may differ
Apply Unicode NFC normalizationSometimesMarker may survive or be altered
Strip all format charactersNo, for many encodingsLegitimate text can be damaged
Summarize or rewrite promptsUnlikelyMeaning and tool instructions may change
Convert messages to another provider schemaDependsAttribution becomes ambiguous

What actually happens when a proxy forwards marked content to another provider is easy to overlook: the receiving model may see Claude-specific system instructions and hidden distinctions even though the outbound request is labeled as GPT-5.5 or Gemini 3.

This creates three practical issues.

Attribution Becomes Unreliable

A provider or internal detector may classify the request as originating from Claude Code even after it has traversed your gateway. That classification could be correct at the prompt level but misleading at the transport level.

Keep explicit provenance separately:

{
  "request_id": "req_01J...",
  "source_client": "claude-code",
  "inbound_provider": "anthropic-compatible",
  "outbound_provider": "gemini",
  "transformations": [
    "tool_schema_conversion",
    "unicode_nfc"
  ]
}

Do not put sensitive provenance into the model-visible prompt. Store it in gateway logs or trusted metadata fields.

Cache Keys Can Fragment

Two visually identical prompts with different byte sequences produce different hashes:

import hashlib

def prompt_key(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

assert prompt_key("Run the tests") != prompt_key("Run\u200b the tests")

If your semantic or exact-match cache uses raw bytes, hidden markers lower the hit rate. If you normalize too aggressively, distinct user input can collapse into one key.

A reasonable design uses two hashes:

Normalization must be versioned. Changing it invalidates cache assumptions.

Token Billing May Change

Invisible does not mean free. Tokenizers can assign tokens to hidden code points or group them with nearby text. The count is model-specific, so measure it using the provider’s tokenizer or returned usage rather than assuming one character equals one token.

Consider 100,000 daily requests with an average hidden overhead of 12 input tokens:

100,000 × 12 = 1,200,000 extra input tokens/day

At an illustrative input rate of $3.00 per million tokens:

1.2 × $3.00 = $3.60/day
$3.60 × 30 = $108/month

That is not automatically a serious cost, but it belongs in capacity and cache calculations. Substitute your actual model rate. AI Prime Tech can reduce Claude, GPT, and Gemini API costs through multi-model access, but lower unit pricing does not remove the need to understand what your client sends.

Should You Strip the Marker?

Usually, not by default.

Blindly deleting every Unicode format or combining character can corrupt multilingual text, source code, emoji presentation, bidirectional text, and identifiers. It can also change signed content or invalidate a provider’s expected client behavior.

If your security model requires sanitization, constrain it to machine-generated prompt sections that you own:

KNOWN_ZERO_WIDTH = {
    "\u200b",  # zero width space
    "\u200c",  # zero width non-joiner
    "\u200d",  # zero width joiner
    "\ufeff",  # zero width no-break space / BOM
}

def sanitize_generated_text(text: str) -> str:
    return "".join(ch for ch in text if ch not in KNOWN_ZERO_WIDTH)

Even this allowlist has trade-offs. U+200D, for example, participates in valid emoji sequences. Apply sanitization to a known ASCII-only generated field, not arbitrary user messages.

For cross-provider routing, I use this policy in practice:

Building a Regression Test

Add a fixture containing visible, marked, and legitimate Unicode content:

def test_prompt_normalization():
    cases = {
        "plain": "Run the tests",
        "marked": "Run\u200b the tests",
        "accented": "Review José's change",
        "emoji": "Check status: \u2705",
    }

    assert sanitize_generated_text(cases["plain"]) == cases["plain"]
    assert sanitize_generated_text(cases["marked"]) == "Run the tests"
    assert sanitize_generated_text(cases["accented"]) == cases["accented"]
    assert sanitize_generated_text(cases["emoji"]) == cases["emoji"]

Then replay representative Claude, GPT, and Gemini requests through the gateway. Verify:

Practical Takeaways

Claude Code’s request marking is a reminder that rendered prompts are not the protocol. Bytes, code points, message structure, and client-generated context all affect what reaches a model.

The operational rule is simple: if your gateway cannot show the exact string the model received, including invisible characters, it is not yet giving you a complete request trace.

MR
Marcus Reed · Senior API Engineer

Marcus has spent 9 years building LLM-backed products and integrating the Claude, GPT and Gemini APIs into production systems. He writes about API cost optimization, agent architecture, and practical model selection.

Get cheaper Claude API access

One API key for Claude Opus 4.8, Sonnet 4.6, Haiku 4.5, Fable 5, plus GPT & Gemini — up to 80% off official pricing, pay-as-you-go.

Get Your API Key →
AI Prime Tech is an independent third-party API gateway. Claude™ and Anthropic® are trademarks of Anthropic, PBC. No affiliation or endorsement is implied.