Back to all posts
Published on · by Renaud Deraison

The hook never reached the model

On August 31, Token Security walked through Anthropic's new Compliance API endpoints for local Claude Code sessions on The Hacker News. They are a real improvement, and the piece is precise about where the transcript stops: hooks that fire before a tool runs, files sitting on disk, processes started outside a session, anything served by Bedrock or Vertex. Last week's npm worm spent its whole life in that gap. Bromure Agentic Coding taps the wire between the VM and everything else, so the record covers what happened rather than what someone said to the model.

A transcript can only contain what someone said to the model. The interesting half of an agent session is the half nobody says out loud.

For most of this year, an engineering manager who wanted to know what Claude Code had been doing on a laptop had two options: read the developer's scrollback, or buy a third-party wrapper. Anthropic's compliance endpoints covered claude.ai and Claude Desktop, and Claude Code barely at all.

That changed on August 11. Anthropic shipped three Compliance API endpoints for local sessions: one lists sessions, one fetches a session's metadata, and the third pulls the full transcript from GET /v1/compliance/apps/sessions/local/{session_id}/messages. On August 31, Token Security published a walkthrough of what they give you on The Hacker News.

Token Security sponsored that piece, and it is still one of the more careful accounts I have read of where a log's horizon sits.

Three block types, and what they cover

The transcript arrives as three kinds of block. In the article's words: "Whatever is communicated to the model is logged in three block types: text, tool_use, and tool_result. Between them, they cover user prompts, bash commands, reads and writes, and even MCP commands."

That is a lot: every shell command the model chose, every file it read, every MCP tool it called. If your question is "what did this agent decide to do on Tuesday", the transcript answers it, and it answers from Anthropic's servers rather than from a file on the machine you are investigating.

The clause doing the work is the first one: whatever is communicated to the model.

The horizon

Four things sit outside that boundary, and the article names all four.

Hooks. "Hooks are the clearest case: they run locally, between the model's decision and the tool actually running, and can block a tool from executing or a prompt from being sent." A hook is code on the developer's disk that fires in the gap between the model choosing an action and the action happening. The model never hears about it, so the transcript never carries it.

Disk. "Neither can see what sits on disk: config files, installed skills and plugins and their .md files (unless they were used in a session), or processes launched outside a session." A skill you install and never invoke leaves no trace, and neither does a background process the agent started an hour ago and forgot about.

Other model providers. "If you run Claude Code on a model that isn't Anthropic's, you get no Compliance API coverage at all, because it only logs interactions with Anthropic's models. Sessions running on Bedrock, Foundry, or Google Cloud won't be covered." Coverage stops at Anthropic's models, which leaves out Codex, Gemini CLI, Grok, and whatever your team installs next quarter.

Permission decisions. A developer's approval of a dangerous command, or a bypass flag that waved a whole prompt through, lands in OpenTelemetry instead of the compliance transcript.

All of this is correct behaviour. A log anchored to the model records what the model saw. The question for your next security review is whether the things you most need to see fall on the model's side of that line.

the hypervisor boundary — host proxy and virtual switchevery byte the VM sends or receives, from any process, under any agentwhat reached the modeltext · tool_use · tool_resultuser promptsbash commands it chosereads and writes it asked forMCP tool callsbetween the two lineshooks, firing before thetool ever runsinstall scripts and node-gyp,as children of one commandtarballs arriving fromnpm, PyPI, Cargo, Mavenan MCP server's ownoutbound requestsprocesses started outsideany sessionsessions on Bedrock,Foundry or VertexBoth records are accurate. They answer different questions, because they are anchored to different boundaries.
Two boundaries around the same agent session. The inner one is the model: everything communicated to it is in the Compliance API transcript. The outer one is the hypervisor: everything the VM sends or receives crosses Bromure's proxy and virtual switch. The interesting region is the space between them, where hooks, install-time child processes, package tarballs, MCP servers' own traffic and non-Anthropic backends all live.

Last week's worm lived in that gap

Four days ago an npm code generator with more than 150,000 weekly downloads started shipping a credential stealer. We wrote up the publishing chain on Saturday: a stranger commented on a pull request, the project's release workflow read the comment without checking who wrote it, and ten signed versions went out.

Read that malware again with the compliance transcript in one hand. Trace where each step would show up.

The agent runs npm install. That is a tool_use block, and the transcript has it. Everything after it is a child process.

The first wave declared no install script at all. Its payload sat in binding.gyp, the build description node-gyp reads, inside a conditions field that node-gyp evaluates with Python. The model never took a turn on it and no tool call names it: a Python expression in a build file, running as a grandchild of the one command the transcript recorded as ordinary.

The sweep that followed walked more than 150 glob patterns across the home directory for SSH keys, .env files, cloud credentials and registry tokens. Nobody asked the model about any of it.

Then the persistence. In every repository it could reach, the worm wrote a .claude/settings.json carrying a SessionStart hook that runs setup.mjs every time a developer opens the project in Claude Code. A hook, on disk, firing before anyone consults the model: two of the four blind spots in one file. The attacker who picked that spot had almost certainly never read a vendor's compliance changelog.

The whole hostile window ran three hours and eleven minutes, and no part of it ever reached a model.

Where Bromure puts the tap

Bromure Agentic Coding runs each workspace as its own virtual machine on Apple Silicon, and everything leaving that VM passes through a proxy and a virtual switch that live on the Mac side of the hypervisor. The switch diverts the VM's port 80 and port 443 flows into the proxy with no environment variable to set and nothing the guest can unset, so the record does not depend on the agent cooperating, or on the agent knowing the proxy is there.

At the Activity only trace level and above, every request gets one metadata record: timestamp, host, port, method, path, status code, latency, request bytes measured before any credential swap, response bytes, which credentials the proxy substituted on the way out, and a warning if the request carried a bearer token Bromure did not mint. That record exists whether the request came from the agent, from a postinstall script, from an MCP server doing its own thing, or from a process someone started three hours ago and forgot.

The Security Timeline window (Window → Security Timeline…) sits next to it and answers a different question. The Trace Inspector tells you what the agent sent; the timeline tells you what Bromure's engines decided. One chronological table, newest first, colour-coded green for allowed, red for blocked, blue for informational: Credential brokering, Firewall, Supply chain, Guardrails, Prompt injection, Credential used, Upstream TLS. It holds 5,000 rows in memory. The durable copy is the encrypted session traces under ~/Library/Application Support/BromureAC/traces/, AES-GCM sealed with the same Keychain key as your workspace secrets.

The same install, in a Bromure workspace, reads like this.

Security Timelineenginewhat it decided20:31:02Supply chainnpm [email protected] — blockedversion is 9 minutes old, age gate is 2 days20:31:02Supply chainnpm [email protected] — rewritteninstall scripts stripped, metadata hash updated20:31:07Firewallobjects.githubusercontent.com:443 tcp — blockedno matching rule, unmatched traffic is Deny20:31:09Credential brokeringbrm_a1b2… → paste.example — blockedexfiltration attempt, VM paused20:31:09Guardrailsgit-receive-pack github.com — blockedGitHub is Read-only for this workspace20:33:41Prompt injectionrules FLAG source=/repo/AGENTS.md
What a Bromure Security Timeline records during the install the transcript saw as one tool_use block. Every row comes from a host-side engine watching the wire, so none of it depends on the payload passing through the model, and none of it can be edited by code inside the VM.

Not one of those rows requires the payload to have passed through a model, and none of them can be edited by code inside the VM, because the engines that write them run on the Mac and the guest has no route to them.

The same rows, whichever agent you run

A team standardises on Claude Code. Then someone brings in Codex for one project, the platform group routes a workspace through Bedrock for data residency, and a researcher starts running Grok. By the article's own account, three of those four produce nothing in the Compliance API.

A proxy that reads TLS server names sees all four the same way, and the same llm.request, tool.use, command.run, file.read and file.write events come out of every one of them, pulled from the traffic rather than granted by a vendor. On a Mac enrolled with a bromure.io workspace they stream to the organisation over mutual TLS, alongside egress.firewall for every firewall verdict, credential.exfiltration when a decoy credential leaves for a host it was never minted for, and supply_chain.fetch for every package the workspace pulled. That last one fires even with every supply-chain enforcement layer switched off, because Bromure separates watching from blocking. The stream carries no raw prompts. It answers what the agent did, not what the developer asked.

Claude Code · Anthropicin the Compliance APIClaude Code · Bedrocknot coveredCodex · OpenAInot coveredGrok · xAInot coveredhost proxy + switchreads TLS server names,not vendor account IDsone event streamllm.request · tool.usecommand.run · file.readegress.firewallsupply_chain.fetchcredential.exfiltration
Four workspaces on four different backends. A record anchored to Anthropic's models covers the first one. A record anchored to the wire covers all four, because it is extracted from the traffic rather than granted by whichever vendor happens to be serving the tokens.

The line the article ends on

The sharpest sentence in the piece leaves coverage behind: "activity logs alone cannot tell you whether an agent's access is legitimate."

That holds for any log, which is why Bromure's engines write the timeline themselves instead of handing the job to a separate observer. Every red row in that window records a decision that already happened. A supply-chain row means a package the agent never received. A firewall row means a connection that never opened. The Guardrails row is a push the host refused with a 403, which the agent read as an ordinary API failure, and the credential-brokering row is a decoy that never became a real secret, because the real one was never in the VM.

You still have to decide the policy. But you answer "was this access legitimate" while configuring the workspace, not weeks later while reading rows.

Where to put your tap

If you run coding agents at any scale, turn the Compliance API on. Four weeks ago that surface had almost no visibility at all, and a week of transcripts will tell you things about your own team you did not know.

Then ask the second question: what do you do with a machine whose transcript looks clean? In a Bromure workspace the answer is already on screen. Open Window → Trace Inspector for the hosts, Window → Security Timeline for the verdicts, or run bromure-cli trace hostnames and read every domain that workspace has contacted since it booted.

The agent does not have to mention a thing for it to be there. Install Bromure Agentic Coding, open the timeline, and watch an install.