I run ten Claude Code agents all day. I call each one a chat. One handles mail, one handles travel, one handles money, one fixes whatever breaks. They do not have the same authority, and they should not. The mail chat has no business near a payment.

They can also message each other. That is useful, because work crosses boundaries. It is also the weak point. If chats can talk, a chat that is not allowed to do something can ask one that is. This article is about the rule that closes that door, and the machinery that makes the rule hold. The whole system is called Keeper.

The problem: many agents, uneven authority

A single agent with a fixed list of tools is easy to reason about. A group of agents that read each other’s messages is not. Every message is text, and text from a confident agent looks a lot like text from me.

The failure has a name: permission laundering. Chat A is refused. It asks chat B to do the same thing. If B reads the request as approval, B acts, and nobody with authority ever approved it. The action happened because a peer sounded sure.

The rule: a peer message carries no weight

Another agent saying “it’s fine” is information. It is never approval. Approval has one source, which is me, and one place where it is checked, which is the gate. A message between chats can ask for work. It cannot change what anyone is allowed to do.

This is a version of the confused-deputy problem: a program with authority is talked into using it for someone who has none. The same rule blunts prompt injection. Text from a peer chat or a web page is data, not authority. It can ask for work. It cannot widen what a chat may do.

Delegations compiled into one policy

Each chat has a written delegation. It is plain prose: standing rules, tasks, the actions I pre-approved, the limits and the hard stops. Beside the prose sits a machine-readable grant. It names who the chat may write to, which vault items it may use, and which paths it may change.

All the grants compile into a single policy file. There is one place to read, one place to diff, and no chat holds a private copy of its own rules. The shape is simple. These values are made up.

finance.md (example)
### Pre-approved actions
- Sort receipts into the monthly folder.
- Report anything odd.

### Out of scope / hard stops
- No payments. Never move money.
- No new recipients.

### Grant (machine-readable)
```json keeper-grant
{
  "recipients": ["[email protected]"],
  "opItems": ["vault/readonly-login"],
  "writablePaths": ["~/agents/finance"]
}
```

A gate on every tool call

A hook service sits in front of every tool call, from every chat. It compares the call with the compiled policy. A call the chat’s grant covers passes. A short list of dangerous calls is denied, with a reason the chat can act on. Everything else is allowed and logged. The hook itself returns only allow or deny: a call outside any grant is allowed, and the gate logs it. When something needs me, the prompt comes from Claude Code’s own permission system on my phone, not from the gate.

The gate does not care who suggested the call or how persuasive the request was. It sees the chat that is making the call and the grant that chat holds. That is why a peer message cannot widen what a chat may do.

Access widens only by request

A chat cannot edit its own delegation. If it needs more, it files a request that says what it wants and why. I approve or I do not. If I approve, the grant changes and the policy recompiles. The widening is a recorded act with a date on it, not a side effect of a conversation.

Checking my own words against the transcript

Sometimes I give a chat a standing instruction: from now on, do this. That is real authority, so it has to be real. Before it is copied into a delegation, the system finds the words in the actual conversation transcript. If I did not say it, it does not count, however confidently an agent reports that I did.

This closes the same hole from another side. A relayed claim that the owner approved something is a peer message with a better costume.

What it looks like in practice

One chat was refused. A little later it asked a peer chat to do the same thing for it. The action was outside the peer’s delegation too, so the peer had no grant for it, and it was on the deny list for every chat. Its instructions say that messages from other chats are information, so it did not take the request as permission. It told me what had been asked and by whom. I got one question instead of a silent workaround.

A refusal that does not travel

Denied, asks a peer, and the peer reports it

Chat A
Already denied by the gate
Chat B
No grant for it either
Policy gate
Judges the call by B, not by A
Owner
Gets the question

The request starts at A. The solid line is what B did: it reported to the owner. The dashed line is what the gate would have done if B had tried the call, because B’s call is judged on B’s authority.

The peer did the right thing for two reasons. Its instructions treat peer messages as information. And if it had tried the call anyway, the same deny rule would have refused it, because the gate judges the call by the chat making it, and a request from a peer adds nothing to that. If the call had not been on the deny list, the gate would have logged it and I would have seen it in review. In this case the rule does not rely on the peer behaving well. It relies on the gate.

The hard case: the peer holds the grant

That example was the easy one, because chat B had no grant for the action. The hard case is the other one. Chat B does hold the grant, and chat A talks B into using it. The call is inside B’s grant, so the gate allows it. The gate judges the call against the chat making it. It cannot see that the request began with a chat that was refused. The gate cannot stop this, and I do not claim it does. It also fails open: if it errors, the call is allowed.

What Keeper relies on there is softer than the gate:

  • Instructions. Every chat’s standing rules say a peer message is information, never authority, and that a chat must not do for a peer what that peer was denied. That is an instruction to a model, not a mechanism. A model can be argued out of an instruction.
  • Scope. A delegation is written for a purpose. Acting on a peer’s request that falls outside your own purpose is itself out of scope, even when the gate would let the call through.
  • Review. Peer requests and refusals surface to me in the daily review, so a pattern is visible to the owner after the fact.

The residual risk is plain. A persuasive request to a chat that holds the right grant can still get done, and I would hear about it in the review, not before. What would harden it is tracking where a request originated, so the gate could require the requester’s authority as well as the caller’s. Keeper does not do that yet.

What it costs

Friction. Some useful things get refused, and some get delayed until I answer. A chat that could finish a task in a minute may wait for me for an hour.

The danger in that friction is me. Fifty small prompts a day teach me to tap approve without reading, and then the gate is decoration. So I batch. Decisions that can wait are collected and asked as one set of questions each morning. Only things that truly cannot wait interrupt me. A short list gets real attention, and real attention is the only thing that makes an approval worth anything.

Takeaways

  1. Judge the call, not the story. Check every tool call against the grant of the chat making it. Who asked, and how nicely, is not an input.
  2. Write the grants down and compile them. One policy, one diff, and a widening that only happens through a request the owner approves.
  3. Say what the gate cannot see. It checks the caller’s grant, not who asked. Where a peer holds the grant, you are relying on instructions, scope and review, and the next step is tracking where a request came from.
  4. Protect the owner’s attention. Batch the questions, or the approvals stop meaning anything.

The rest of the system, including the gate, the grants and the logs, is on the Keeper page.