Skip to content
← All posts
Privacy · 5 min read

Why we hash arguments instead of storing them

Twelve hex characters of a SHA-256 over the arguments, keys sorted. Enough to tell whether two calls were the same, and not enough for anything else.

9c1b

The single most useful piece of retry detection is knowing whether two calls used the same arguments. Different arguments inside 30 seconds means the model reworded and tried again. Identical arguments means pagination or polling, which is not a retry at all.

You could get that by storing the arguments. You must not.

What runs through an MCP tool call

The arguments to a tool call are whatever the conversation contained. In practice that has meant, in servers we have seen: customer email addresses, order numbers, internal account IDs, search queries typed by real people, file paths, calendar entries, and free-text fields that users treat as private because they are.

An analytics vendor holding a copy of that is a data breach waiting to be written up. It is also the answer to the first question on every security review your customers will run on you: what leaves the process, and what do you keep?

There is exactly one answer that survives that conversation, and it is “we cannot reveal it because we never had it”.

The hash

args_hash = sha256(JSON.stringify(args, sortedKeys)).slice(0, 12)

Twelve hex characters. It answers one question — were these two calls the same? — and nothing else.

Keys are sorted first. Without that, {a:1,b:2} and {b:2,a:1} produce different hashes, identical calls look distinct, and every repeat is misread as a rewording. Sorting is not a detail; it is the difference between the metric working and not.

Twelve characters is deliberate. It is enough that collisions within a session are not a practical concern, and short enough that it is plainly an identifier rather than an artefact anyone would try to reverse.

It is not reversible, but that is not the argument. Hashing a low-entropy value is not privacy on its own — someone with a candidate list can hash the list and compare. The real protection is that we never have the candidate list, and the hash alone conveys nothing about the shape, length, field names, or types of what was hashed.

Everything else is a size

The rest of the payload is deliberately dimensional:

{
  "v": 1,
  "type": "call",
  "session_id": "s_7f2a91",
  "tool_name": "search_orders",
  "client_name": "claude-desktop",
  "started_at": "2026-08-09T14:22:31Z",
  "duration_ms": 240,
  "outcome": "ok",
  "response_bytes": 1420,
  "is_empty": false,
  "args_hash": "9c1b4e2f0a11"
}

response_bytes is JSON.stringify(result).length, not the result. The size is what drives the cost metrics, and the size is all they need. is_empty is a boolean derived at the moment of the call and then the result is discarded.

Tool names are stored, because a metric attached to no tool is not actionable. Session IDs are generated by the SDK and mean nothing outside your own data.

No setting turns this off

There is no captureArguments: true. Not as a default-off flag, not as an enterprise option.

A guarantee with a switch is not a guarantee — it is a default, and defaults get changed by whoever is debugging at 2am. The moment the capability exists in the code, the honest sentence becomes “we do not store arguments unless configured to”, and that sentence does not pass a security review.

It also removes an entire class of incident. There is no code path that could be misconfigured into logging arguments, because there is no code path that has them in a form worth logging.

What it costs

Being precise about the trade: you cannot use MCPulse to debug a specific failing call. There is no “show me the arguments that crashed search_orders at 14:22”. That is your own logging’s job, inside your own infrastructure, where the data already is and where it belongs.

What you get instead is the population view — which tool, how often, how it ended, whether the model had to try again — which is the part you could not get any other way, and the part that does not require anyone to trust us with the contents.