Your SSO can't authorize your agent, and disabling the user won't stop it
Give every agent its own identity, mint it locally, scope it to write and never admin, and test the kill switch before you need it. That is the whole rail, and I install it before a new agent gets anywhere near a repository. The reason it looks like that — rather than "just put the bots behind SSO" — is that two of those clauses exist to route around things your identity provider genuinely cannot do for you.
I run a fleet of agents against a self-hosted forge — the generic term for a GitHub-style system where your repositories, issues, code review and CI all live in one place. The human population of that estate is exactly one, and the agents outnumber me several times over, so single sign-on is the natural first stop when a new agent needs access: it is already standing, and it already governs the only human in the building. That is where the evaluation starts, and it is also where it ends, because single sign-on cannot authorize an agent at all — for reasons that are structural rather than anything peculiar to my setup, and that will hold on whatever forge and whatever identity provider you happen to be running.
Browser login and API authorization are two different problems
Check this before you design anything. My forge's identity-provider integration is an OAuth2 browser login source — it will authenticate a human clicking a button, and it never introspects a provider-issued token on the API. There is no admin endpoint to mint a token on a user's behalf. The LDAP bridge that would have covered the gap is broken upstream.
So an agent holding a perfectly valid IdP token gets a 401 on every API call, and no amount of configuration changes that. The integration is not misconfigured. It answers a question I was not asking.
The general shape is worth carrying to whatever stack you are on: "we have SSO" is a claim about humans reaching a web UI. Whether it also authorizes a machine calling an API is a separate capability, frequently absent, and almost never stated plainly in the docs. Go read what your provider integration actually validates, on which surface, before you build an agent that assumes it.
The rail that works
Give each agent a native account on the system it acts against — a real bot user, not a shared human one. Hold its password in your secret manager, and have the agent mint its own scoped token. Three properties matter more than the mechanism:
- One identity per agent, per environment. Not per fleet. Not per team.
- The minimum scopes the code actually calls. Read what your client library invokes and grant that list, not the convenient superset.
- Write, never admin. This one deserves its own section.
Why admin is the line, and it is not about caution
My forge has no separate permission for repository secrets. The Repository admin grant includes secrets management, settings, and branch-protection editing.
Sit with that for a second. Branch protection is the setting that determines whether changes can be merged without your checks being passed. I have written about auditing it in 1,732 tests and not one of them could fail — an empty protection array is why a suite of green checks can protect nothing. Grant repo admin to an unattended loop, and you have given that loop the ability to remove the guardrail governing it.
A control the controlled party can switch off is not a control. It is a matter of preference and good manners. Withholding admin costs my agents nothing they need, and it is the difference between a boundary and an agreement.
Does disabling the account actually stop the bot?
Go test this one today. The middle of an incident is a bad time to find out.
Set the agent's user to inactive in your identity provider. Now have the agent make an API call. If it is holding a token the target system validates locally — issued by the forge, checked against the forge's own store — that call succeeds. Your IdP was never in the path. Deactivating the user there changes what a browser can do and nothing whatsoever about what the token can do.
The mental model an IdP sells you is that it is the master switch for every identity in your estate. For agents holding locally-issued credentials it is not even in the circuit. The switches that genuinely work are narrower and you should know which one you are reaching for:
- Revoke the token at the system that issued it. Immediate, and precise.
- Scale the agent to zero replicas. Blunt, immediate, and reversible — my preferred first move during an incident, for the same reason I reach for the repository-level Actions switch in the CI fork bomb.
- Deactivating the IdP user stops future browser logins. Nothing else.
Write down which of those your runbook names. If it names the third one, your incident response contains a step that does nothing, and you will find that out at the worst possible moment.
One credential in two environments is one outage in two environments
Two of my deployments — production and test — were carrying the same token. Not a copy of the same grant. The same credential, in two places, because at some point it had been convenient.
Deleting that one token took issue and pull-request functionality down in both environments simultaneously. Test could not be used to diagnose production, because test was broken in exactly the same way, for exactly the same reason. That is the property you lose: a shared credential silently couples the environment you experiment in to the one you cannot afford to break.
Mint one per environment. It costs a few minutes and it buys you a test environment that still works while production is on fire.
The layer worth your effort is the one the agent cannot disarm
Here is the finding that changed how I spend time on this, and it came from a different system entirely.
I wanted an agent to have read-only access to a records database. I did three things. I wrote the rule into the agent's standing instructions. I added around 40 deny rules covering every write and destructive tool the CLI exposed — in both spellings, because the underscore and hyphen forms of the same command both resolve. Then I scoped the API token itself to read.
Rank those by effort and the deny rules win by a mile. Rank them by strength and they come last. Those rules are string prefix matchers against a command line: a pipe, an alias, a shell invocation or a different PATH walks straight around them. They are a guardrail against an accidental write, not a security control. The read-scoped grant is the only one of the three that fails a write no matter how the call is shaped, because it fails server-side, where the agent has no reach.
The ordering inverts the effort, and that is the pattern to take with you. When you are hardening an agent, ask which layer holds if the agent is actively trying to get past it — and put your hours there. Everything above that line is ergonomics. This is the same instinct I described in reversibility gates authorization, not confidence: what matters is not how sure you are that the agent will behave, it is what remains true when it does not.
Three things to check this week
The key takeaway: your agent's identity is your guardrail, and most of what looks like an off switch is not wired to anything.
- Deactivate a bot user in your IdP, then have that bot make an API call. If the call succeeds, your documented kill switch is decorative — find out now, and write down the one that works.
- Check whether any agent holds admin on a repository. If your forge bundles branch protection into that grant, that agent can disable the checks that govern it. Drop it to write.
- Grep your environments for a shared credential. If prod and test present the same token, you have one blast radius, not two.
Think of it like a building where the badge reader on the front door is immaculate and the loading dock has been propped open for months. Nothing about the reader is wrong. It is just not the thing deciding who gets in.
If you have found a kill switch in your own stack that turned out to be inert, I would genuinely like to hear how you noticed — my sense is that most of us find out during the incident rather than before it. As always, I'm here to help!