Part of my job is running access reviews. Human accounts are the easy part: there is an HR system, a manager, a leaver process. The hard part has always been the service accounts, and AI agents have made that part bigger.
An agent does not act with its own authority. It acts with whatever identity it was given. In practice that is an Entra app registration or service principal, a managed identity, an AWS IAM role or an access key, a GitHub App installation or a deploy key. These are the service accounts we already struggle to govern, and agents add more of them, created faster, often by a team running a pilot rather than by the people who run identity.
The pattern I keep seeing has three parts. Nobody owns the list. Nobody recertifies it. And when something goes wrong, nobody can stop one of these identities quickly and prove it is stopped.
The OWASP Top 10 for Agentic Applications for 2026 lists ASI03, Identity & Privilege Abuse, as its own category. The OWASP announcement describes leaked credentials letting agents operate "far beyond their intended scope". Whatever an agent is tricked into doing, the blast radius is set by its identity's grants.
Microsoft says much the same in its governance guidance for agent identities: agents have historically reached Microsoft services through tools that use service principals. Microsoft Entra Agent ID is its answer, a dedicated identity type for agents built from agent identity blueprints, with Conditional Access, Identity Protection, governance and audit logs extended to them. It is a good direction. It does not cover the AWS role your coding agent assumes or the GitHub App it pushes with, and many agents in Entra still run as ordinary app registrations.
These are the patterns I look for first in a review.
Mail.ReadWrite works for every mailbox in the tenant with no signed-in user. If nobody owns the app registration, nobody will notice when it is misused, and nobody will answer the question "do we still need this?".Action: "*" that a CI agent assumes through OIDC.* on AWSRevokeOlderSessions policy keyed on aws:TokenIssueTime. If your off switch is "edit the trust policy", it is half a switch.
None of these controls is new. They are the service account controls, applied to a population that grew while nobody was counting.
I built agent-off-switch (aos) for the reviews I run. It reads saved exports from Entra, AWS and GitHub, plus an optional agents.csv register, and works offline with no credentials of its own. Everything below is real output from the synthetic demo tenant that ships in the repository: the ids are fake and the AWS account is the documentation account 111122223333.
Inventory, owner, least privilege and credential hygiene come from aos review, which ranks findings by blast radius (0 to 100) and gives each a fix:
$ aos review examples/demo-tenant --as-of 2026-10-09
As of 2026-10-09: 13 non-human identities, 11 look like agents. 15 findings (8 error), ranked by blast radius (0 to 100, see docs/scoring.md).
## 4 agent identities nobody owns
- **coding-agent-runner** `aws-role:coding-agent-runner` (blast radius 100): Action * on Resource *
- switch off: `aos off examples/demo-tenant aws-role:coding-agent-runner`
- **repo-autopilot** `github-app:repo-autopilot` (blast radius 95): administration: write on every repository (settings, branch protection, deletion)
- switch off: `aos off examples/demo-tenant github-app:repo-autopilot`
- **mail-triage-agent** `entra:aaaaaaaa-0000-4000-8000-000000000001` (blast radius 90): Mail.ReadWrite as an application permission changes data for every user in the tenant
- switch off: `aos off examples/demo-tenant entra:aaaaaaaa-0000-4000-8000-000000000001`
...
The findings table that follows covers the rest of the failure modes above, including a client secret 434 days old and a deploy key that can push, each with its fix.
The same review can be written as SARIF for GitHub code scanning. aos inventory gives the full table of identities with owners, credentials, grants and last use, as Markdown, CSV or JSON.
The off switch is aos off. It prints an ordered runbook and runs nothing. For the coding agent's role, the first two steps are the point I made above: stop new sessions, then revoke the ones already issued.
$ aos off examples/demo-tenant aws-role:coding-agent-runner --as-of 2026-10-09
Identity `aws-role:coding-agent-runner` (aws-iam-role on aws). Owners: none recorded. Blast radius 100. Exports as of 2026-10-09.
aos printed these commands and ran none of them. Run each change in order, then its verification, and keep the verification output for the incident record. Values in <angle brackets> are for you to fill in.
## 1 disable
### Step 1: Stop new sessions: replace the trust policy (now trusts Federated: arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com) with a deny-all one; save the current one first
...
## 2 revoke sessions and grants
### Step 2: Revoke every session issued before the disable time (the console's Revoke active sessions)
...
## 5 verify with the audit log
### Step 5: Confirm in CloudTrail that nothing succeeded after the disable time
...
## Coverage
Every credential and grant in the exports is named by a step.
Each step in the full output has a change command, a verification command and a line to record the time and the person. The coverage line matters: if a grant in the exports is not named by any step, aos says so and exits 1.
Drift in CI is aos drift, which compares a saved inventory with today's exports:
$ aos drift examples/drift/baseline.json examples/demo-tenant --as-of 2026-10-09 --accept examples/drift/accepted.txt
From 2026-09-01 (11 identities) to 2026-10-09 (13 identities): 4 changes, 3 not reviewed.
| Change | Identity | Detail | Reviewed |
|---|---|---|---|
| new-identity | `github-app:repo-autopilot` | repo-autopilot (github-app, no owner, blast radius 95) | **no** |
| new-identity | `github-app:docs-preview-bot` | docs-preview-bot (github-app, web-team@example.com, blast radius 55) | yes |
| new-grant | `aws-role:coding-agent-runner` | agent-all (score 100) | **no** |
| owner-lost | `entra:aaaaaaaa-0000-4000-8000-000000000001` | was owned by messaging-team@example.com; now no owner | **no** |
...
It exits 1 while anything is unreviewed, so it can gate a pull request. Accepting a change means adding its key to the accept file, so the decision sits in version control next to the inventory.
I would rather say this plainly.
aos gives you the data a recertification needs: owners, grants, credential ages, last use, and the unused and no-owner findings. It does not send attestations, collect decisions or remove access when nobody answers. That is still a process you run, or a governance product's job.off prints commands for a person to run.drift flags an owner that disappears between two inventories. It does not check whether a named owner's account is still active.
If you run access reviews, add the agent identities to the next one. Take the read-only exports, list what exists, and put a name against every row. The ones nobody claims are your first off-switch drill.
The repository is at github.com/basitalisandhu/agent-off-switch, with the demo tenant, the threat model and the scoring rules. Issues and pull requests are welcome, especially new export formats and corrections to the scoring.