# agentgateway v1.7.0: How Agent Session Detection Works

> agentgateway v1.7.0 detects a coding agent's session from headers like x-claude-code-session-id, with an 8-character minimum that fails silently, no error.

- Canonical URL: https://webofmike.com/agentgateway-v17-agent-sessions/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-11-01
- Last modified: 2026-11-01
- Tags: AI Gateways, AI Agents, Security, Generative AI, Tutorials
- Cite as: Mike Moore, "agentgateway v1.7.0: How Agent Session Detection Works", Web of Mike (webofmike.com), 2026-11-01. https://webofmike.com/agentgateway-v17-agent-sessions/


agentgateway v1.7.0 ([GA, 2026-10-27](https://github.com/agentgateway/agentgateway/releases/tag/v1.7.0)) can tell which coding-agent session sent a request without any client-side change: it reads the session header the agent is already sending. [themsquared/agw-17-hands-on](https://github.com/themsquared/agw-17-hands-on) validates the detection rule against the real v1.7.0 binary and a mock LLM backend, no API key, no network calls, five curl requests that show exactly when it fires and when it doesn't.

## Why this matters before the mechanism

Without session detection, agentgateway can rate limit or log by API key or IP address, but it has no way to group the many requests one agent run makes into a single unit. Coding agents like Claude Code, Cursor, and Codex already send a session or conversation id in their own headers so their own backends can correlate turns. v1.7.0 reads that same header. Once agentgateway knows which requests belong together, that grouping is available to any CEL-driven policy: a rate limit keyed per session instead of per key, an audit trail grouped by session, routing a whole session to the same backend, or flagging a session whose request pattern looks unusual. None of this needs the client to adopt a new header or send extra metadata. It needs the client to keep doing what it already does.

## The headers it reads, in priority order

agentgateway's detection logic (`crates/agentgateway/src/http/session.rs` in the v1.7.0 tag) checks, in order:

1. Any header shaped `x-<vendor>-session-id`. Claude Code sends `x-claude-code-session-id`; this is a generic pattern, not a Claude-specific special case, so Cursor's `x-cursor-session-id` or any other vendor's equivalent is detected the same way.
2. Codex's unprefixed `session-id`, `session_id`, `thread-id`, or `conversation_id` headers, but only when the request's `User-Agent` starts with `codex`. This gate exists because those header names are generic enough that treating them as agent-session signals from any client would produce false positives.
3. A vendor-less `x-session-id` header, as a last resort.

Whichever header matches, the detected value becomes available to CEL as `request.agent.session`.

## The rule that silently drops a session id

The validation is stricter than it looks, and it fails quietly:

```rust
fn valid(value: &HeaderValue) -> Option<&str> {
    value.to_str().ok().filter(|v| {
        v.len() >= 8
            && v.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
    })
}
```

A session id under 8 characters, or one containing a character outside `[A-Za-z0-9_-]`, doesn't get rejected with an error. It gets treated exactly like a missing header: `request.agent.session` is simply never set, and nothing in the response or the logs calls that out. If a session grouping you expect to see isn't showing up, this rule is the first thing to check, not a deeper config problem.

## Proving it with a mock backend

[agw-17-hands-on](https://github.com/themsquared/agw-17-hands-on)'s config points agentgateway's standalone LLM route at a 40-line Python HTTP server instead of a real provider, using v1.7.0's `custom` provider with the `completions` wire format:

```yaml
# config/config.yaml
config:
  session:
    key: disabled   # demo only: v1.7 standalone always provisions a session key on first run
frontendPolicies:
  accessLog:
    add:
      agent.session: request.agent.session
llm:
  port: 4000
  models:
  - name: mock
    provider:
      custom:
        formats:
        - type: completions
    params:
      baseUrl: http://127.0.0.1:8091/v1
      model: mock-model
```

`request.agent.session` only depends on the incoming request, not on what the backend does with it, so a mock response proves the same behavior a real Anthropic or OpenAI call would. That also means this demo costs nothing to run and needs no API key.

Five requests, one gateway:

```
$ curl -s -X POST http://localhost:4000/v1/chat/completions \
    -H "x-claude-code-session-id: 79291636abc123" -d '{...}'          # request 1
$ curl -s -X POST http://localhost:4000/v1/chat/completions \
    -H "x-claude-code-session-id: 79291636abc123" -d '{...}'          # request 2, same session
$ curl -s -X POST http://localhost:4000/v1/chat/completions \
    -H "x-cursor-session-id: cursorsession999" -d '{...}'             # request 3, different vendor header
$ curl -s -X POST http://localhost:4000/v1/chat/completions -d '{...}'  # request 4, no session header
$ curl -s -X POST http://localhost:4000/v1/chat/completions \
    -H "x-claude-code-session-id: short1" -d '{...}'                  # request 5, 6-character id
```

All five return `HTTP 200`. The access log, with `agent.session: request.agent.session` wired into `accessLog.add`, is where the real answer is:

```
http.path=/v1/chat/completions agent.session="79291636abc123"
http.path=/v1/chat/completions agent.session="79291636abc123"
http.path=/v1/chat/completions agent.session="cursorsession999"
http.path=/v1/chat/completions
http.path=/v1/chat/completions
```

Requests 1 and 2 share a session, as expected from a repeated header. Request 3's completely different vendor header still gets picked up, which is the generic-pattern claim proven rather than assumed. Requests 4 and 5 both have no `agent.session` field: one sent no header at all, the other's `short1` is 6 characters, two short of the minimum. Same outcome, two different causes, neither one visible unless you already know to look for it.

This is the same `accessLog.add` mechanism the [v1.6.0 cost-tracking post](/agentgateway-v16-cost-tracking/) used to surface `llm.cost.total` into the request log: any CEL field, `request.agent.session` included, becomes a logged attribute with one line of config.

## Overriding detection

`config.standardAttributes.session` takes a CEL expression that runs before the automatic header scan. Returning a string from it sets the session explicitly, which is useful if an internal proxy or SDK already generates its own session identifier under a header name agentgateway wouldn't otherwise recognize. Returning `null` marks the request as having no session even when a technically-valid vendor header is present. If the expression itself errors, or returns something other than a string, agentgateway falls back to the header-based detection described above rather than failing the request. This repo doesn't set the override, so everything in the demo runs through the default header scan.

## What this doesn't cover

This demo is scoped to proving the detection rule, not building something on top of it. It doesn't exercise a real LLM provider (the mock backend never sees or uses the session value), Codex's `User-Agent`-gated header set, or any policy that actually *acts* on `request.agent.session`, like a per-session rate limit. v1.7.0 shipped a lot more than this one field: [MCP tool-call guardrails](/tool-call-guardrails/) (written against a pre-release build, now generally available), model-catalog metadata over `/v1/models`, service-tier-aware cost pricing, and a new `agctl standalone analytics` CLI. This post is one field, validated.

Repo: [themsquared/agw-17-hands-on](https://github.com/themsquared/agw-17-hands-on). Release notes: [agentgateway v1.7.0](https://github.com/agentgateway/agentgateway/releases/tag/v1.7.0).

