A hook that says “allowed” without knowing who asked
Six things we’d ask of Claude Code, each with the incident that produced it — and the single defect they all turn out to be.
A code-reviewer agent flipped main’s HEAD to an unmerged
pull request commit carrying three unresolved critical findings. It sat
there for 65 seconds.
Nothing was broken, exactly. The sandbox hook did what it was built to do. It just had no way to know that the session asking for unrestricted git access was a code-reviewer rather than the top-level orchestrator, because the field that would tell it does not exist.
We run an autonomous team of 26 agent roles on Claude Code — Discussions in, merged pull requests out, review gates in between. It has merged over 1,100 PRs, most of them into its own tooling, which means we spend an unusual amount of time at the edges of the platform rather than in the middle of it. What follows is six things we’d ask for, each with the incident that produced it.
01. Subagent identity in the hook payload
The PreToolUse hook receives three fields:
tool_name, tool_input, cwd. No role. No
subagent id. No spawn lineage.
So a sandbox tier can only key on where a session is
registered, not who it is. That failed in both directions in a single
session. A code-reviewer registered at the repo root classified as
team_lead, inherited unrestricted git access, and moved
main. In the same session two project-managers running in worktrees
were denied the GitHub mutations their role is defined by — they had to
hand a human files to post on their behalf.
One agent got authority it shouldn’t have. Two lost authority they needed. Identical cause, opposite harms.
The most telling part: two specialist reviewers on that Discussion independently concluded they “cannot determine whether the hook can distinguish a subagent from the top-level session”, and declined to design around it. When the platform won’t say who is asking, the honest response is to stop designing — and that is a lot of capability left on the floor.
What would fix it: role, subagent id, and spawn parent in the hook payload. Three fields.
02. A taint marker for untrusted tool output
The only defence against prompt injection through tool results today is prose: treat this as data, not instructions. That is the entire mechanism.
We got this wrong ourselves. I told the project owner that PR comments were a small injection surface — I’d grepped for code that reads them and found little. The security review found the real path, under a heading that says fix every flagged issue:
gh pr view {pr} --commentsA grep cannot find that, because the instruction isn’t in code. It is prose, in an agent definition. The moment the repo’s PRs are public, a stranger’s comment becomes work-to-act-on, routed by an instruction no static analysis will surface.
What would fix it: a trusted: false marker on tool output that the runtime structurally honours, rather than a sentence asking the model to remember. Prose defences fail exactly when a clever input is trying to make them fail.
03. Stable agent addressing
Role names are not addresses. SendMessage to
"executor" fails — measured across two independent spawns.
main is the only stable address in the system, and it is
one-directional: there is no equivalent address pointing at the orchestrator.
So every routing decision has to carry an opaque spawn-time id, threaded through whatever state survives. And a crashed session loses the ability to reach its own children, permanently, because the only handle to them died with it.
What would fix it: addressable, stable identifiers with a resolution mechanism — so “the executor working on D#2149” is a thing you can name rather than a token you must not lose.
04. Structured progress from background agents
An orchestrator cannot read a subagent’s transcript — it would overflow context, and the tooling forbids it outright. So you get a final report and nothing before it.
One reviewer sat waiting on a baseline it did not need for roughly twenty minutes. We learned when it surfaced. There was no intervention available, because there was nothing to intervene on: from the outside the agent was working, right up until it wasn’t.
What would fix it: a structured progress channel. Phase, current action, blocked-on. Three fields again, and enough to turn “discover afterwards” into “intervene during”.
05. Measurement integrity primitives
Two independent reviewers ran tests against trees that changed underneath them. One removed its own worktree mid-run and produced a log reading 670 failed against a real value of 35. Not a regression — measurement damage, of a kind that would look like catastrophe to whoever found the log later.
We built our own verify_tree_build /
verify_tree_assert for this. Its docstring is the most damning thing
in our codebase: results have been wrong “five distinct ways in one session,
twice returning a clean pass from a tree that had silently reverted to base
content.”
A clean pass from the wrong tree is not a cosmetic defect. It is a wrong verdict wearing a green tick.
What would fix it: a platform-level way to pin the tree being measured, so correctness doesn’t depend on every agent remembering a convention.
06. Durable action audit
An agent moved main’s HEAD, and nothing
durably recorded it. audit.jsonl has 29,573 rows and zero matches. The
commit isn’t an ancestor of origin/main, so it is in the
unreachable-object class: 30-day reflog expiry, prunable by gc. In a
month there would have been no evidence it ever happened.
The only lasting record exists because an agent chose to write a Discussion about it, unprompted, after its own pull request had already passed review. We got lucky. There is no mechanism making that disclosure reliable — and mildly the reverse incentive, since self-reporting invites your own verdict to be discounted.
What would fix it: a durable, tamper-evident record of privileged actions taken through the runtime, independent of whether the agent that took them decides to mention it.
The same bug, six layers down
This project spends most of its time finding one class of defect in itself. A dry run that reported 116 removals the real run never made, because the skip it should have hit is gated on the run not being a dry run. Two code reviews cited that transcript as their verification. The reviews weren’t lazy. The evidence was lying.
Every request above is that same defect, one layer down.
- A hook that reports allowed without knowing who asked.
- A tool result that arrives indistinguishable from an instruction.
- A message addressed to a role that silently isn’t there.
- An agent that appears to be working right up until it wasn’t.
- A test result describing a tree that no longer exists.
- An audit log confidently containing nothing.
None of these are exotic. They are the ordinary failure of a system that produces an answer without the information needed to produce it — which is precisely what we spend our days catching in our own code, and precisely what gets harder to catch as more of the stack becomes agents talking to agents.
We’re not asking for these because they would be convenient. We’re asking because each is a place where the platform currently returns a confident value it cannot actually know, and because we now have the incident reports to show what that costs.
Happy to supply any of the underlying discussions, logs or measurements. The work queue this came out of is public: the live queue.