Client identity

Warning

Client identity comes from request headers, which any caller can set.

curl -H 'User-Agent: claude-cli/2.1.0' is indistinguishable from Claude Code here, and always will be. Nothing about this is a security boundary, and no rule built on it should be load-bearing in a threat model.

It is genuinely useful for the thing it is: knowing which of your tools is doing what, and noticing an agent nobody procured. Use it for that.

What is read

Nothing in the request path read these before; the gateway forwarded them upstream untouched and still does. The rules are ordered most specific first, and the first match wins.

SignalExampleYields
User-Agentclaude-cli/2.1.0 (external, cli)claude-code, version 2.1.0
originatorcodex_cli_rscodex-cli
User-Agentcodex_cli_rs/0.47.0codex-cli
User-AgentCursor/1.7.3cursor
x-appcliclaude-code, when a proxy has rewritten the agent string
anthropic-betaclaude-code-20250219claude-code
x-stainless-lang + x-api-keypythonanthropic-sdk, family python
x-stainless-lang alonejsopenai-sdk, family js
X-OpenAI-Client-User-Agent{"bindings_version":…,"lang":"python"}openai-sdk
editor-versionvscode/1.9xcopilot
nothing recognisedunknown

Canonical names are lowercase and stable, because a rename would silently change what an existing policy means: claude-code, claude-desktop, codex-cli, cursor, kiro, copilot, continue, aider, anthropic-sdk, openai-sdk, langchain, curl, unknown.

Why the order matters

Claude Code sends the Anthropic SDK’s Stainless headers as well as its own agent string. A rule table that checked the SDK first would report every Claude Code session as anthropic-sdk, and an organisation would never see the agent it actually wanted to govern. Specific first, generic last.

Stainless generates both vendors’ SDKs, so its headers say which language but not which vendor. The vendor is inferred from which authentication header the SDK chose: only the Anthropic SDK sends x-api-key by default.

Rules

# Record anything that is not one of our two supported agents.
vulnetix ai-firewall policy guardrail supported-agents \
  --rule-type client_allow --action flag --pattern 'claude-code' \
  --priority 110 --enable
vulnetix ai-firewall policy guardrail supported-agents-codex \
  --rule-type client_allow --action flag --pattern 'codex-cli' \
  --priority 110 --enable

unknown is a name rather than an absence, which is what makes “only our supported agents” expressible at all — an unrecognised client fails an allowlist that does not name it.

client_deny and client_allow accept block and flag only. strip is rejected: there is nothing in the request to remove.

Note

An allowlist here is easy to get wrong. Your organisation almost certainly runs more clients than it thinks: a CI script using the OpenAI SDK, a notebook, a curl in a runbook. Every one of them is refused the moment you enable a client_allow rule at block.

The shipped blueprint for this ships disabled for exactly that reason. Enable it at flag, read a fortnight of inventory, then decide.

What this is good for

Attribution. “Which agent triggered these forty blocked requests” is a question you cannot answer without it, and it turns an incident from a hunt into a lookup.

Finding unmanaged agents. A client name your organisation does not recognise appearing in the inventory is worth a conversation, whether or not you ever write a rule about it.

Rollout sequencing. Rolling a strict tool policy out to claude-code first and everything else later is a reasonable thing to want, and this is how you express it.

What it is not good for

Stopping a determined party. Setting a header is one flag. If your requirement is “only approved software may reach the provider”, the mechanism for that is the Vulnetix API key and who holds it, not this.