The trace
Each search carries a small trace: hashed ids that let the platform group one task's searches, and the assistant text that led to the search, scrubbed of secrets before it goes anywhere.
What this is for is on Trajectory observability. This page is the contract.
Fields
| field | carries |
|---|---|
v | 1. |
harness | Which agent: claude-code, codex, cursor, and so on. |
session_id, turn_id, call_id | The agent's ids, hashed with SHA-256 before they leave the machine. A subagent has a session_id of its own. |
parent_session_id | On a subagent's search only: the hashed session_id of the agent that started it. Claude Code, Codex and opencode. |
window | Which context window of the session, after compactions. |
seq | A call counter. |
history | The assistant text before the search, scrubbed of secrets, last 32 KiB. Codex and Hermes send none. |
host_meta | Agent-specific metadata. |
The scrub
The scrub runs over history before it is cut and before the envelope is built. It is a filter over text a model wrote, not a guarantee; what it misses is listed at the end.
Shapes
It removes what has a credential's shape: API keys, Stripe secret keys, GitHub and AWS tokens, JWTs, a password inside a URL. The credential after an Authorization or Proxy-Authorization header's Bearer, Basic or Token scheme goes whatever its shape, in a header line, a curl -H '…' argument, or a JSON or JavaScript key; a variable such as $TOKEN or ${TOKEN} and a placeholder such as <token> stay. A PEM private key block goes whole, markers included, quoted or not; one cut short goes from its BEGIN marker through the lines that still look like a key's body (base64 and armor headers, indented, blockquoted or not). Email addresses go. So does the account name in a home path: after /home/, /Users/ or C:\Users\, also with every separator doubled as Python and JSON print a path. A Windows name of up to four words (C:\Users\Équipe Données\Documents) goes whole when the path goes on past it; at the end of a path only its first word goes.
Names
A secret is also known by its name. A name counts when one of its parts is PASSWORD, PASSWD, PASSPHRASE, SECRET, SECRETS, TOKEN, CREDENTIAL, CREDENTIALS or APIKEY in any letter case. The parts are what _, - and . separate (DATABASE_PASSWORD, client-secret, db.password) and, in camelCase, the last word (accessToken, clientSecret, .npmrc's _authToken; tokenCount and keyName hold a count and a name and stay). KEY and PASS count in a name written in capitals or of two or more parts or words (API_KEY, --api-key, apiKey, db_pass), so primaryKey= loses its value too, as primary_key= does. Also PGPASSWORD, MYSQL_PWD, and sig as a URL query parameter (an Azure SAS). A bare key=…, --key=2 or pass=2 is left.
Where a value is read
The value of a named secret goes when it is written:
- After
=, wherever the=is, so a secret chained behind another setting goes too (?user=fred&password=…,--env=DB_PASSWORD=…); with spaces or tabs around=after PowerShell's$env:NAMEand${env:NAME}, and before any quoted value (password = "…"). - After a colon, as YAML and JSON write it. A key at the start of a line, quoted or not, takes the rest of the line (
password: …,"client_secret": "…",). Elsewhere only where a member of an object, a map, a call, a statement or a code span opens (after{,,,(,[,;or a backtick), and only a quoted value that closes on its own line, and only that string ({"password":"…","user":"app"}keeps its user). A key and a colon inside a string (input("Password: ")) take nothing, and no colon rule reads past a line break. - As the word after a long flag whose last word is a secret's (
curl --api-key …,--password …,--clientSecret …), so--key-name,--secret-id,--passphrase-fileand--token-ttlkeep theirs. A last word that names the value's form,string,valueorphrase, asks the word before it instead (--secret-string …,--pass-phrase …;--valueand--default-valuekeep theirs). Not a value: lowercase letters only, the flag's own name in capitals (--token TOKEN), a redirection or pipe, a command's output ($(…)), anything after a--with-or--no-switch, or a quote that does not close on its own line. A single-dash flag such as-pis not read.
A value that is a reference or a placeholder stays: ${{ secrets.X }}, a template expression, ${VAR}, $VAR, <pad>, a type name (string, Option<String>), a size (1234 bytes), or a block that only opens ({, [, |). A reference that carries a literal is a value and goes: a default, assignment or alternate in an expansion (${VAR:-…}, ${VAR:=…}, ${VAR:+…}, and Spring's ${db.password:…}) whose word is not empty and not itself a variable, and a template expression that holds a quoted string. After a plain colon, a word that starts with a digit, a blank or ? is bash's substring (${VAR:0:5}) and stays; ${VAR:?message} stays. A variable is $ and capitals, digits and _ ($CI_JOB_TOKEN), or letters and _ with no digit ($password); a word that mixes lowercase letters and digits after $ ($ecret123) is a value.
A quoted value is read as a shell reads a word: to its closing quote, together with quoted parts and characters joined to it, so PowerShell's 'it''s …', POSIX's 'it'\''s …' and Python's """…""" go whole. Its first part may run across at most 100 line breaks only when the name starts its line (after indentation, declaration words such as export, set, const or Dockerfile's ENV, list markers such as -, 1. or 1), quote markers, and directly after $, $env: or ${env:). Even there it does not run past a later secret: a line it crosses into that assigns a secret before the quote closes (3. Then set DB_PASSWORD="…") means the quote never closed, its value is the rest of its own line, and the later secret is read as its own. Anywhere else it closes on its own line, because in print("password=", pw) the quote closes a string. A quote that does not close takes the rest of its line. An unquoted value goes to whitespace, & or ;, keeping a character a backslash escapes, and a closing bracket, quote, backtick or , that belongs to the text around it stays. cmd's set NAME=… takes the rest of the line where cmd reads it as a command, for a name with no lowercase letter.
Environment listings
Every value in a run of lines naming five or more different variables, as NAME=value, export, declare -x or typeset -x, also behind a list marker or cat -n numbering, goes. So does every value in a PowerShell Name/Value table under its rule of dashes (Get-ChildItem Env:; any hashtable PowerShell prints looks the same), including a name with no value and a line indented exactly to the Value column that continues the value before it. The first line that is none of these ends the table. A line whose value starts with = (requests==2.31.0), ends with , or holds another NAME= after a space (a logfmt record) is not part of a listing. The value of JEVLIN_TRACE_BRIDGE in a quoted command goes too, as POSIX, PowerShell and cmd write it.
This machine's names
The hostname's first label goes wherever it stands as a word. Your account name goes only where the text uses it as an account: the value of USER, USERNAME, LOGNAME or SUDO_USER, directly before @ (ssh name@host, a prompt), and a home path, including /mnt/c/Users/ and an account name with a space in it, and the same path in the dashed form Claude Code names a project's folder with (-Users-<account>-, -home-<account>-, C--Users-<account>-, every character but a letter or digit written as -, so first.last is first-last there). Anywhere else it is left, because it is often an ordinary word. The account is the one USERNAME names on Windows, USER or else LOGNAME elsewhere, or else the home directory's name, and is searched for only when it is ASCII. A generic name such as root, ubuntu, vscode or macbook-pro is left, because it identifies nobody.
What passes
A secret with no telling name and no known shape, in ordinary prose, passes. So do: password: … in the middle of a sentence; NAME = value unquoted outside PowerShell; a value inside a quote that opened before another word (echo "export PASSWORD=a b" keeps b); a lowercase cmd set name; a flag's value of lowercase letters only, or after a flag whose last word is not a secret's (--auth …); a flag's quoted value broken across lines; every line after the first of a quoted value that runs across lines in the middle of a line, though a value quoted across lines keeps its lines only up to the first one that assigns a secret inside it, where it ends; a secret of capitals and digits after $ ($ECRET123), read as a variable; a Spring default that starts with a digit; JSON escaped inside a quoted string, such as a curl -d "{\"password\": \"…\"}" body; NUL-separated env -0 output; a PowerShell table row whose name fills its column; and a Windows name with spaces at the very end of a path keeps all but its first word. In return, a line that begins Password: … loses the rest of the line, prose that writes a header with its scheme (send Authorization: Bearer token) loses the word after it, a call at a line's start (apiKey: getKey(),) loses its value, --key-value and --token-string lose their value whatever it is, and prose that quotes a PEM BEGIN marker and later its END marker loses what stands between them.
Both scrubbers take time linear in their input, so a long run of blanks or a repeated token in the assistant's text does not hold up the search it precedes.
Size, transport and identity
The text is scrubbed before it is cut, and an entry too large to scrub whole is omitted whole. A bug in preparing the trace costs the trace, not the search: since v0.4.2 a crash while scrubbing or capping the text sends the search without its trace, jevlin search says so on stderr, and the hook exits quietly so the command runs as written. Over 48 KiB, history is dropped; an envelope still too large is not sent. It travels base64url-encoded in JEVLIN_TRACE_BRIDGE, written in the syntax of the shell that runs the command, and only inside the search request, and only onto a command where that syntax actually reaches the search. A loop, a list or a pipeline with the search anywhere but first is left exactly as written; the search still runs, carrying the hashed per-shell identity instead.
With no hook, a search carries that same per-shell identity: the hostname and the parent shell's pid, keyed with a random trace.key in state_dir that setup and connect create and that is never sent, so the router cannot guess its way back to the hostname. A search that finds no key makes one if it can write state_dir. A search with no usable key that cannot make one (state_dir is read-only and holds none, or the key is a link, open to others or not 32 bytes) sends a one-off id, different on every search, rather than any id the hostname could be recovered from.
The hashed session_id is also mirrored as the request's own top-level session_id and X-Session-Id header, so the router can group quick reformulations by it, and read the trajectory from the envelope. The same identifier travels in both places, only while an envelope rides, and both are dropped on the one compatibility retry. The trace is unauthenticated metadata: nothing treats it as proof of origin.
JEVLIN_TRACE=off sends none, and the turn end with it. Searches work the same without it.