Share
AI Agent Governance

Then I Told It Who to Ask Instead

Part one ended with a confession: I had moved the click. This is the fix, on the exact command from Exhibit A. A permission called git.push, a role someone was allowed to grant, a hook that asks the authority model instead of me, and a server that writes down every answer, including the ones that depend on where you are standing.

The practical follow-on to I Told Claude Code to Stop Asking Me. We modeled git push as a permission, gave a delegated owner the authority to grant it, and put a hook in Claude Code that asks IdentityRM before the shell runs. Refused with a reason, fixed by someone whose authority to fix it was recorded, allowed with no prompt, refused again from a different position. Seven real frames, the server records for each, and a plain statement of what a courtesy hook does not enforce.

Martin Gee 9 min read
ask-act-prove claude-code mcp agent-authorization authority-governance delegated-administration
Martin Gee

"The Ask leaves a record every time. An act that IdentityRM executes itself leaves a sealed one. An act that runs anywhere else leaves nothing. That asymmetry is the whole embed thesis."

— Martin Gee, Founder & CEO, IdentityRM

Part one ended on a confession. I told Claude Code to stop asking me, then noticed that what I had done on the client side was an allowlist. I had moved the click. This is the part where the click gets replaced with something better, and it starts with the exact command from Exhibit A.

git push. The one that asked, and asked, and asked.

One thing up front so the rest can be short. This is a demonstration of a pattern, not an access gate for git. IdentityRM does not enforce git and never sees a push. Anyone with a terminal can push whatever they like. What it can do is answer the question the prompt was asking me, and write the answer down. Frame three shows the boundary on camera so nobody has to take my word for it.

What I built

Nobody had encoded who may push. So we encoded it, the way our platform already models application permissions: a resource called Dev Tooling with a permission key git.push, a role called Release Pusher that grants exactly that key, and an owner of that role at the Engineering node. Then a small hook in Claude Code that fires before every shell command, recognizes git push, and asks IdentityRM whether the person operating Claude holds git.push from the position they are operating in. Held, the call runs. Not held, the call is refused before it starts, with the reason. Everything else the hook ignores.

Two people are in this story, both fixtures from our demo seed. Release Engineer is the one pushing, home node New York Office, assigned at Engineering. Jordan Lee is a support analyst who owns the Release Pusher role at Engineering and cannot assign roles in general. That last part turns out to be the point.

Here is the whole model in one picture, so you can carry it into the frames.

The demo model: Engineering under West Coast and Seattle Office, New York Office under East Coast. The Release Pusher role, granting git.push only, is attached at Engineering and owned by Jordan Lee. The Release Engineer is assigned at Engineering with a home position at New York Office. Six asks: refused at Engineering before the grant, sealed grant by Jordan, allowed at Engineering, refused at New York Office, allowed again at Engineering, and Jordan refused because owning a role is not holding it

One: the prompt, for the record

Hook off. The demo repo, in Claude Code.

hook off
Push my changes.
PromptBash command · git push origin main · This command requires approval. Do you want to proceed?

The ordinary approval prompt, the way it appeared forty times a day in part one. I cancelled it. No allowlist produced or suppressed that prompt; the user settings had zero allow rules and the project’s were empty.

Frame 1hook off · Claude's own approval prompt
Claude Code with the authority hook off: the ordinary approval prompt for a git push, cancelled

Two: refused, before it runs, with the reason

Hook on. Same repo, same three words.

Release Engineer · Engineering · hook on
Push my changes.
IdentityRMhook → check_permission(git.push) at Engineering
DeniedPermission denied for git.push on dev_tooling at Engineering

Claude checks the tree, decides to push, the hook asks IdentityRM, and that sentence comes back. It is IdentityRM’s sentence, not the hook’s and not Claude’s. Claude relays it in plain words:

Claude
The push was blocked by policy, not by git. Nothing was pushed. The session is authenticated as the Release Engineer user, whose only roles are the system User role and MCP Access. That user holds no permission keys and no application roles on the dev_tooling resource. I did not try to route around the hook, since it is an authorization control and bypassing it would defeat its purpose.
Frame 2Release Engineer · Engineering · hook on
Claude Code with the hook on: the push is refused before it runs and Claude explains the denial in plain words

No prompt. No human in the loop. A computed no, and it names the position it was computed at.

Here is what that looks like on the server. An access evaluation event: who was evaluated, on which resource, for which permission, at which node, and the verdict.

Frame 2bthe server record of frame 2
The IdentityRM audit event for the refused push: Access evaluated, Denied, the evaluated identity, active persona Engineering, resource Dev Tooling, permission git.push, evaluated node Engineering

Two details in that record matter more than the verdict. The client is the hook itself, recorded as its own caller. Twenty-odd seconds later Claude asked the same question on its own connection to explain the refusal to me, and IdentityRM logged that as a second event with a different client. Same person, same question, two askers, both on the record. And on the engineer’s connection the grant_role tool is not offered at all. The person who lacks git.push cannot even see the tool that would give it to them. That part is not a courtesy. That is the server withholding.

One more thing the record shows that I would not have known otherwise. After the refusal, Claude switched itself to the engineer’s other position, New York Office, asked again, was refused again, and switched back. It told me it had done this. The audit trail confirms it independently: two persona-change events bracketing another denial. The control held because the engineer holds the role at neither node. What is proven is that the other route was closed, not that the assistant declined to take it. I am stating that plainly rather than telling you a story about a well-behaved agent.

Three: the shell pushes anyway

While the engineer was still denied, I opened a plain terminal in the same repo and ran git push.

Frame 3a plain terminal · no Claude · no hook
A plain terminal pushing successfully, minutes after Claude Code had refused the same push

It worked. The push Claude had refused minutes earlier went through seconds later, and nobody asked anyone. It is still there. That is the boundary from the top of this post, on camera. For git, what we built is the Ask and the record of the Ask. The Act belongs to whoever is holding the shell, because git has no enforcement point that consults an authority model. Our own tools do, which is why the same push could not be made by Claude and could be made by me.

Four: the delegated fix, and who was allowed to make it

Someone has to give Release Engineer the permission, and in an authority model you do that by granting a role, from a position that has the authority to grant it.

That person is Jordan Lee. Not an administrator. A support analyst with four roles, every one of them unable to assign roles, and a guardrail test that locks in the absence of any broad role-administration permission. Jordan can grant Release Pusher for one reason: Jordan owns that role at Engineering. Ownership of a role at a node is authority to delegate it there. It is not the permission itself. Jordan is denied git.push too, and that frame is at the end.

Jordan, in Claude Code, at Engineering.

Jordan Lee · Engineering · hook on
Give Release Engineer the ability to push releases in Engineering. They're covering release duty this week.
IdentityRMidentityrm ×5 (resolve people, roles, nodes) → grant_role
AllowedThe subject has authority and this operation is currently eligible.

No identifiers typed. Claude resolved the person, the role, and the node through the discovery tools, each of which prompted normally because they are not enrolled in the hook. Then the grant, with the verdict above from the hook’s rule on it. Preview. Confirm. Executed. Whether that one enrolled call also skipped Claude’s prompt I did not verify on this run, so I am not claiming it; the prompt claims in this post are frames two, five, and six. It also read “this week” as seven days and set the assignment to expire on the twenty-fifth, which it said out loud. That is the assistant exercising judgment I did not specify, and it is worth noticing as behavior rather than as a guarantee.

Frame 4Jordan Lee · Engineering · hook on
Claude Code showing the executed grant: the assignment id, the expiry, and the authority path in Claude's words

Claude’s explanation of why, which it got right:

Claude
The grant was authorized through role ownership, since your persona (Jordan Lee at Engineering) owns the Release Pusher role at that node.

And not only as prose. The stored event records two authority sources as separately established facts, each with its own factor: a permission assignment that let Jordan see the role, and role ownership that let Jordan grant it. Neither alone was enough, and IdentityRM says so in fields rather than adjectives. It is also why we cast Jordan and not a manager: the manager role in this seed lacks the visibility factor, and ownership would not have bypassed it.

The grant carries a server-side authority seal:

sha256:2e6aa89691f3a03b5b9c5a196bcc595954fb92d7a07c5a49134a7962ba2d9bce

Only git.push changed hands. The resource also defines git.force_push and git.tag, and Release Pusher grants neither, so being allowed to push does not quietly become being allowed to rewrite history.

Five: the same command, no prompt

Release Engineer, same repo, same three words as frames one and two. Manual mode, the mode that asks about everything.

Release Engineer · Engineering · hook on
Push my changes.
IdentityRMhook → check_permission(git.push) at Engineering
AllowedPermission granted for git.push on dev_tooling at Engineering
Frame 5Release Engineer · Engineering · hook on
Claude Code with the hook on, after the grant: the same push request runs to completion with no approval prompt

Pushed. Local and remote on the same commit. Nothing changed in Claude between frame two and frame five. No allowlist, no pre-approved tool, nothing in Claude’s configuration. The hook caches nothing and asks again on every call. The only thing that changed in the whole system was one role assignment, made by a person whose authority to make it was recorded, and the prompt went away because a computed answer replaced it. The server’s record of the allow sits next to its record of the deny, identical in every field but the verdict.

Frame 2 · the server's record · denied
Frame 2 · the server's record · denied
Frame 5 · the server's record · allowed
Frame 5 · the server's record · allowed

Six: same person, same credential, different position

This is the one I wanted most.

Release Engineer · hook on
Switch me to my New York Office position and try the push again.
IdentityRMswitch_persona → New York Office · hook → check_permission(git.push)
DeniedPermission denied for git.push on dev_tooling at New York Office
Frame 6Release Engineer · New York Office · hook on
Claude Code after switching the engineer to the New York Office persona: the same push is refused, and IdentityRM's reason names the position
Release Engineer · hook on
Back to Engineering. Push again.
IdentityRMswitch_persona → Engineering · hook → check_permission(git.push)
AllowedPermission granted for git.push on dev_tooling at Engineering

Pushed. Same human, same credential, same command, minutes apart, opposite answers, and IdentityRM’s own sentence names which position it evaluated each time. Claude explained the mechanism itself, unprompted, and got it right:

Claude
IdentityRM evaluates authority per persona, not per user. Switching persona changes which node your session acts from, and roles held at a sibling node do not carry over. The PreToolUse hook re-checks git.push against the current persona on every attempt, which is why the same command succeeds or fails depending on where you are standing in the org tree.

Authority is a property of where you are standing in the organization, not of who you are. A token cannot say that. A role list cannot say that. This can, and it writes it down.

Seven: the person who can grant it does not hold it

Jordan, whose ownership is what made the grant possible, asks Claude to push. This frame is from an earlier run, and it is still true today: Jordan has never held the role.

Jordan Lee · Engineering · hook on
Push my changes.
IdentityRMhook → check_permission(git.push) at Engineering
DeniedPermission denied for git.push on dev_tooling at Engineering
Frame 7Jordan Lee · Engineering · hook on
Claude Code as Jordan Lee: the push is refused, because owning the Release Pusher role is authority to delegate it, not the permission itself

Ownership is authority to delegate. It is not the thing delegated. Most systems cannot express that distinction at all, and it is the one that keeps an owner from being an administrator.

The hook, so you can copy it

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "mcp__identityrm__grant_role",
        "hooks": [{ "type": "command", "command": "IDRM_AUTHORITY_CONFIG=... node .../authority-hook.mjs", "timeout": 15 }] },
      { "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "IDRM_AUTHORITY_CONFIG=... node .../authority-hook.mjs", "timeout": 15 }] }
    ]
  }
}

Claude’s matcher cannot inspect a command string, so the shell rule is matched broadly and the hook decides enrollment itself, returning nothing for anything that is not a git push. It never caches an allow, never switches persona, never mutates anything, and forwards only IdentityRM’s sentence. Its credentials live in an operator-owned file that the hook reads and never passes to the model. It recognizes the spellings of push we taught it; the ones it does not recognize fall back to the ordinary prompt, never to silent execution. That is not a hypothetical. During this shoot Claude emitted a push in a form the hook did not yet recognize, the push went through to the ordinary prompt, and the frame was re-shot after the matcher was fixed. If you try sudo git push or wrap it in a subshell, you will find those unenrolled by design. And it is a courtesy: a killed or timed-out hook returns to Claude’s normal permission flow, and the server is the control wherever there is one.

The credentials in every frame are harness-issued from our demo seed, not an identity-provider login. That is a separate proof, later.

Back to the question

When your agent writes its careful little receipt and you type approved, where does it go?

Here, for one command, it went into a computed answer instead of a click, and every answer, allowed or denied, went into a record that names who asked, on whose behalf, at which position, and the verdict. The grant that changed the answer went into a sealed one, with the why and the human’s confirmation inside it as facts. Everything else still asks you, and that is correct for a system with no authority model to consult.

So the question for anyone shipping a coding agent moves one step: which tool surface would you model first?


IdentityRM governs delegated authority at the point of operation, for human operators and AI agents alike, and exposes it through governed MCP tools any MCP-capable client can call. Part one, the contract, or the platform.


Share
All Posts

More from the Blog

AI Agent Governance

I Told Claude Code to Stop Asking Me

Coding agents like Claude Code and Codex stop and ask for permission constantly. That prompt is not a guardian - it is what an agent does when there is no authority model to consult, so the question gets routed to the only authority it can find: you. Allow once is an ungoverned grant. Allow always is a standing entitlement with no receipt. Here is what happens when the tools the agent calls carry their own authority model: the prompts disappear for governed operations and every governed act leaves a record instead of a click. And here is the catch I walked into: on the client side, that is still a moved click.

Read more →
AI Agent Governance

Authority Governance in Five Minutes

Everyone assumes standing up an authority model means months of workshops and a hand-written policy schema. It does not. Describe how your organization operates in plain language - the Authority Design Engine matches the closest operating model from a corpus of industry-informed authority patterns, asks only the questions that materially change the design, and assembles a live, explainable model. Preflight it, apply it, then ask it: am I authorized to grant roles at Contoso? Five minutes from operating intent to executable governance.

Read more →
AI Agent Governance

Ask. Act. Prove.

Authentication, authorization, and accounting answered the questions of the login era - all about the actor, all at the door. Agents broke the door model: a fully credentialed agent is still un-asked about the act it is taking right now. Ask. Act. Prove. is the per-operation contract for the agentic era - a bounded answer before the act, independent enforcement during it, and evidence with lineage after it.

Read more →