How one write path in a helpful bot turned into a permission problem, and the small service I built to close it.
The agent
I built an ops agent at work. You ask it what's wrong with a service and it comes back in about fifteen seconds with:
- error rate and latency
- pod restarts and Kubernetes events
- node status
- the last pipeline that touched the service, and why it failed if it did
A person clicking through dashboards takes ten minutes to get the same picture. The team was using it daily within a week.
Reading was safe. Nobody gets hurt by a bot that looks at metrics.
Then I gave it hands
"You found the problem, now fix it" was the obvious next step. In our world, fixing things means changing a YAML file in the GitOps repository.
So I added one tool: open a pull request with the change.
Not merge it. Just open it. Humans still review. Branch rules still apply. Nothing reaches production without the usual gates.
It felt conservative. It wasn't.
The problem, in one exchange
Picture Dana. She's on a product team, she has read access to the GitOps repository, and nothing more. She asks the agent:
Dana: bump the memory limit for checkout in the payments namespace
Agent: Done. Opened PR #412 in acme/gitops.
Dana cannot push to that repository. She never could.
The agent could, because the agent's token could. And the agent will open a PR for anyone who talks to it.
Why the usual fixes don't work
"Just tell the model." A line in the system prompt: only act within the requester's permissions. Useless, for a precise reason: the model has never seen your GitHub org settings, and even if it guessed right, the tool call still runs with the bot's token. A prompt is a suggestion. The API call is what happens.
"Write a policy." Copy who-can-push-where into a config. You'd be copying a permission system that already exists, is maintained by other people, and changes every week. The copy is wrong within a month, and the dangerous kind of wrong: the kind that says yes.
Ask the one who already knows
GitHub knows exactly who can push to that repository. It's not a secret; there's an API for it.
So before the agent opens a PR, it asks one question:
May this person, the one who typed the request, push to this repo?
- Yes: the agent goes ahead, with its own token, as before.
- No: it says so and stops.
- Can't tell (timeout, rate limit, token rejected): it also stops.
That last one is the part people forget. A permission check that fails open when the network hiccups is worse than no check, because everyone downstream trusts it.
The same exchange, after
Dana: bump the memory limit for checkout in the payments namespace
Agent: I checked with GitHub first: your account has read access to acme/gitops, and opening a change there needs push. I haven't opened anything. Ask someone with write access, or I can draft the change for them.
That reply is the whole feature. The agent didn't guess, didn't ask the model to judge, didn't fail quietly. It asked GitHub, GitHub said no, and Dana got a reason she can act on.
One line on the tool
I pulled the check out of the agent into a small service called hallpass, because once one write path was covered I wanted them all covered, and the agent talks to more than GitHub. Kubernetes can answer the same question for a named user. Jira has a permissions API. AWS can simulate a user's IAM policy. They all know. You just have to ask.
In the agent it looks like this:
@tool
@guarded(hp, "github-main", "repo.push", "repo:{owner}/{repo}", user=current_user)
def open_config_pr(owner: str, repo: str, patch: str) -> str:
... # runs only if GitHub says this user may push there
current_user comes from the session the request arrived in (the Slack identity, the SSO login), never from the model. If the user were a tool argument, the model could be talked into filling it with an admin's email, and I'd have built a very polite privilege escalation.
What it deliberately doesn't do
- It doesn't decide whether the agent should make a change. Reversible? 3 a.m.? Did the person ask, or did the model infer it? That's a different question, and in my setup the answer is still "open a PR and let a human decide". The check only makes sure the person proposing the change is allowed to propose it.
- It doesn't merge anything. The repo's rules and reviewers still do that.
-
It doesn't guess. When a system can't tell it, the answer is
unknown, and unknown means no.
Where it is now
hallpass is open source and talks to twenty-one systems. Two of them (Kubernetes and Argo CD) are tested against the real thing; most of the rest against the vendors' published API descriptions; a few are frankly beta and say so. It's a single Go binary, one config file, no database, and it sits next to whatever agent framework you use: Strands, LangChain, LangGraph, the Claude Agent SDK, or as an MCP server.
I'm not going to tell you every agent needs this. If yours acts with each user's own credentials, it doesn't. If it's a personal coding assistant running as you, it doesn't.
It's for the shared one. The bot in the channel that everyone talks to. The one that quietly became the most privileged account in the company.
Repo: https://github.com/roee-hersh/hallpass
I wrote about the general problem, with more code, in an earlier post: Your AI agent has more permissions than your users.
How do you handle this today? Per-user OAuth, one bot per team, or is a reviewer still the one catching it? I'd genuinely like to know.


Top comments (0)