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:
- Either side is
nullif 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_keyfield) 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_cotis 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_cotgate that bodies get:user_agentand thecorrelationbag 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 preferjq -rover 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.