Skip to content

Your own Middleman traffic

Every request made through Middleman with your token is recorded, and you can read it back through the Hawk API, exactly as Middleman saw it. That covers:

  • Direct traffic: a coding agent set up with hawk proxy setup-client claude-code, and scripts or notebooks that call the proxy themselves.
  • Your evals and scans: the model calls of the runs you launched, tagged with the run (job_id) and, for calls made inside a sample, the sample (sample_uuid).

Only you can see your requests. A request is yours when it was made with your token (its sub); a run's model calls are made with the token of the person who launched it. Others who can read a run's results in the viewer can't see its traffic here. There is no admin or team view, and nothing here can change or delete a record.

There is no CLI command or viewer page for this yet. Call the API with your Hawk token:

TOKEN="$(hawk auth access-token)"
API="<your Hawk API URL>"   # e.g. from `hawk config`
curl -sH "Authorization: Bearer $TOKEN" "$API/traffic?limit=20"

Listing requests

GET /traffic returns your requests newest first:

Parameter Meaning
start, end Unix seconds. Default: the last 24 hours.
scope direct (outside evals and scans), jobs (their model calls) or all (default).
job_id Only this eval set or scan run.
sample_uuid Only this sample (the UUID the viewer shows).
model Only this model (the name you called).
status Only this HTTP status code.
errors_only Only failed requests (status 400 or above, or an error recorded).
limit Page size, 1–200 (default 50).
cursor The previous page's next_cursor. It keeps the first page's time range and filters: leave start and end out (or repeat the first page's), and pass the same filters; anything else is a 400.

Each item has the request's id, timestamps, latency, job_id, job_type and sample_uuid (null for direct traffic), model, endpoint, status, token counts, error, user agent, body sizes, the correlation headers you sent (correlation), and body_state. Correlation headers are the x-metr-*, x-hawk-*, x-inspect-* and x-scout-* headers, plus the session headers coding agents send: Claude Code's x-claude-code-session-id (shared by its subagents) and Codex CLI's session-id (shared by its sub-agents), thread-id (one per agent) and x-codex-parent-thread-id (a sub-agent's parent's thread-id), opencode's, Kilo CLI's and Crush's x-session-id (opencode also sends the same value as x-opencode-session-id), and goose's agent-session-id. Use them to group direct traffic by agent session. They're capped at 8 KB per request, with the x-metr-* / x-hawk-* / x-inspect-* / x-scout-* headers kept first; an agent session header longer than 256 characters isn't kept.

GET /traffic/{request_id} returns one request. Every Middleman response carries an x-metr-request-id header, so you can go straight from an error message or a client log to the request that caused it.

Reading a request and its response

GET /traffic/{request_id}/body returns exactly what Middleman stored for the request, byte for byte:

{"request": {"headers": {...}, "body": ...}, "response": {"headers": {...}, "body": ...}}
curl -sH "Authorization: Bearer $TOKEN" "$API/traffic/$ID/body" | jq .response.body
  • Either side is null if Middleman didn't capture it.
  • A JSON body comes back as the JSON you sent or received.
  • A streamed response's body is the raw server-sent-events text.
  • A body that wasn't JSON is its text.
  • Known auth headers (and a top-level api_key field) were redacted when Middleman stored them. Anything else you sent, including secrets in a prompt, is there as you sent it.

The body is streamed, so a large one starts arriving straight away. A client that sends Accept-Encoding: zstd (curl --compressed built with zstd, Python's httpx or requests with zstandard installed) gets the stored zstd bytes as they are, with Content-Encoding: zstd, and decompresses them itself; anyone else gets plain JSON. Every check happens before the first byte. If something fails after that, the connection is cut, so your client reports an error rather than a short body that looks complete; retry. A download is cut off after 5 minutes.

Whether a body can be read is in body_state:

body_state Meaning
available Viewable.
truncated Viewable, but one side was larger than Middleman's capture cap (25 MiB for requests, 10 MiB for responses) and was cut short.
unavailable_age The request is more than 180 days old. Its record stays; its body can no longer be read.
not_captured No body was stored: the deployment logs traffic at summary level, or the upload failed.
withheld_raw_cot Stored, but not served, because the model may return raw chain-of-thought (see below).
body_access_disabled Stored, but this deployment doesn't serve bodies at all.
too_large Only from the body endpoint: larger than the API will decompress (256 MiB).

When a body can't be read the body endpoint answers 409 with the body_state. A request id that isn't yours, or doesn't exist, is a 404 either way.

Bodies need the deployment to log traffic at full level and to enable body access (hawk:middlemanTrafficApiBodyAccess); without it records say body_access_disabled while the listing still works. The body endpoint answers 503 if the server is already streaming many bodies; retry shortly.

Bodies of models that return raw chain-of-thought

Some models' responses include their raw, unsummarized chain-of-thought (reasoning). Middleman records this per model as raw_cot: true, false, or unknown. Each record shows the model's current value as raw_cot.

A body, request or response, is served only when its model is currently recorded as raw_cot: false. Models discovered from public provider catalogs, which have no recorded value, are assumed not to return raw chain-of-thought and show raw_cot: false, unless an admin has recorded otherwise: the withholding is for reasoning that labs don't publish, and public catalog models, open-weight reasoning models included, publish theirs. Otherwise its body_state is withheld_raw_cot and the body endpoint answers 409. That includes models recorded as true, models whose status is unknown, models no longer registered, and requests that named no model. Request bodies are withheld too, because a multi-turn request resends the earlier assistant turns, reasoning included. The value is checked when you read, not when the request was made, so a corrected flag applies to past requests within about a minute. The record's metadata is always listed.

The reason is where a body goes next. Whatever reads it, often an agent driving another lab's model, takes it into that model's context. The API can't tell which model is reading, so it doesn't hand out raw reasoning at all.

Using this API from an agent

If you point an agent at these endpoints:

  • Have it filter with jq (or similar) rather than read whole bodies: a body holds the entire conversation so far, which can be far more than it needs, e.g. curl -sH "Authorization: Bearer $TOKEN" "$API/traffic/$ID/body" | jq -r '.response.body'.

  • Don't have it fetch bodies of models whose raw chain-of-thought it isn't allowed to see. The API withholds the ones it knows about, but raw_cot is an admin-recorded label, and you know your own rules best.

  • Stored bodies include past tool outputs and fetched web content, so an agent reading them may re-ingest prompt injections they contained.
  • The same goes for metadata, which is listed without the raw_cot gate that bodies get: user_agent and the correlation bag are request headers, copied verbatim from whoever sent them. A listing is attacker-influenced text, so don't have an agent act on it, and prefer jq -r over feeding whole records into a prompt.
  • Anything the agent reads goes into its context, and so to that agent's model provider. Pasting another model's prompts or outputs into an agent sends them to that agent's provider, whoever made the original request.

What's recorded, and for how long

Records go back to when the deployment started recording them, and up to 90 days before that if an operator ran the backfill. Records are kept indefinitely; bodies for 180 days. Requests sent without a token aren't recorded.

If your identity provider changes your sub (for example in an identity provider migration), requests made under the old one stop being visible to you. They don't become visible to anyone else.