Skip to the content
Software Made Clear Diagrams that show the mechanism About

Scoped credentials and the kill switch

ANSWER

Anything an agent must never do should be something it cannot do. That means credentials issued per run rather than per fleet, a permission matrix written per workflow rather than per system, and a stop the platform enforces rather than a label somebody could put back.

IN PLAIN TERMS

A contractor gets a key card at the front desk: one door, one floor, and it stops working at six. An agent should hold credentials like that — issued for one run, not for the whole fleet. And the alarm needs more than one setting: the whole site, one trade, one wing, one worker sent home.

You have written down what the agents must not do. It is in the instruction files, it is in the prompt, and every run reads it. Then one run does it anyway — because a language model treats an instruction as strong advice, and a factory runs enough times a day that strong advice is not a control. The question is what remains true when the text is ignored.

A prompt is a request, a permission is an answer#

Anything an agent must never do should be something it cannot do. That single rule converts most of what teams write as guidance into something the platform decides before the model sees a token. A prompt asks. A permission answers, and it answers the same way whether the run is behaving well, confused, or being steered by somebody else.

The first place it applies is the credential. Agents are usually given a service account with the breadth of a helpful colleague — every repository, every project, indefinitely valid — because that account was created once and reused. The alternative is per run: a short-lived token scoped to exactly what this run needs, one repository and one project, minted from a secrets store when the run starts and worthless shortly after it ends. It is never committed, never baked into an image, and never covers production by default. If a run genuinely needs production, that is a named exception with an expiry on it, not a property of the account.

Next comes the matrix, and the word that carries it is per workflow. Permissions written per system produce one answer for the tracker and one for the repository, which is another way of saying every agent gets the union of what any agent ever needed. Written per workflow, each row states separately what that kind of run may read and what it may write across the tracker, the repository, branches, review, deployment and production. Most of the interest is in the cells you leave empty.

Situation Take Because
Planning agent turning a filed request into a work item Comment only It reads the ticket and adds a comment. It holds no write access to the description field, so nothing it reads there can rewrite what the requester typed.
Review agent on an open change No branch write Findings are its only output. A comment cannot become a commit, and the second read stays independent of the repair.
Builder on an accepted work item One branch Write access to its own branch in one repository, plus a status transition on one item. No merge, no deployment, no second repository.
Incident analyser reading production logs No repository Log lines carry text an outsider may have written, so the run that reads them holds no code access for that text to reach.
Verifier checking a change before release Never production Test and staging only. Production cannot be granted to it by mistake, because it is never issued a production credential at all.

Dwell on the first row for a moment, because it looks like an inconvenience and is not. A planning agent that may comment on a ticket but has no write access to its description field cannot damage the requester’s own words — not through a bug in its instructions, and not through an instruction hidden inside the very ticket it is reading. The requester’s text stays the record of what was asked, and every later step is checked against it. A convention would have achieved that on most days. The empty cell achieves it on all of them.

Access is only half of it, though, and the half that gets attention. What a run can reach outwards decides whether anything it learns can leave, so each agent also gets an outbound network allowlist: the tracker, the forge, the model endpoint, nothing else. An agent that reads log lines an outsider may have written, holds credentials to real systems, and can also reach the open internet has the full set that makes exfiltration possible: private data, untrusted text, and a way out. Take the third away and the other two are harmless together. Restricting egress is unglamorous, and it is enforced by infrastructure rather than judgement — which is what makes it the leg that stays removed.

Four levels of stop, not one#

Now the other half of the same idea. Permissions decide what a healthy system allows; a stop decides what happens when the system is not healthy, and it needs at least four settings. A global stop that disables every agent run there is. A workflow stop that disables one kind of agent while the others keep going — turn the builder off, leave the advisory reviewer running. A service stop that disables agents on one part of the system only. And run cancellation, which terminates one active run and revokes what it was holding.

ONE CELL PER WORKFLOW, PER SERVICEInvoicingExportReportsBuilderReviewerVerifierWORKFLOW STOPSERVICE STOPRUN CANCELLEDGLOBAL STOP — THE WHOLE BOX
A team that owns only the outer box has one honest answer to every incident, which is to turn every agent off.

In practice the second and third settings are the ones incidents call for, and they are the two that get built last. A team with only the master switch faces a bad choice every time something looks wrong: stop everything and lose the day, or leave it running while somebody works out how serious this is. The predictable outcome is that nobody pulls the switch, and the argument about severity happens while the thing continues.

Two properties decide whether a stop is real. The first is where it is enforced. A label on a work item, an assignee change or a paused schedule can each be quietly undone by the next automated sweep — and a factory is full of automated sweeps, since that is what you built it to be. The stop has to live in the platform that decides whether a run starts at all, checked at dispatch, with no path around it.

The second is the credential. Stopping the dispatcher prevents new runs; it does nothing about the twelve already executing, each holding a valid token in a container that is still running. Every credential issued to an active run therefore has to be revocable on demand, or short-lived enough that it expires within minutes regardless. A stop that leaves working credentials in flight has stopped the future and left the present alone, and the present is the part causing the incident.

Then there is the part no diagram covers. A kill switch nobody is named for is not a control. Somebody has to be able to use it at three in the morning without asking permission, from a phone, without a deployment — and choosing that person during the incident costs the minutes the switch existed to save. Two or three names, written where the on-call rota is written, and exercised the way a change to the shared instruction set is: by actually running it.

The permissions nobody withdrew#

Almost every real failure here is a slow one. A run kept failing on something nobody could reproduce, somebody widened the token for an afternoon to see what was happening, and it worked. The agent has held that token for seven months. Nobody granted it permanently; nobody withdrew it either, because withdrawal was never anybody’s task and the system gives no signal that anything is wrong. The fix is structural rather than cultural: every widening carries an expiry at the moment it is granted, so the default direction is back to narrow and a second person has to decide the reason still holds.

The stop rots differently. It was tested once at rollout, it worked, and it has not been exercised since — while the orchestrator was rewritten, the queue moved, credentials went from static to minted, and a new workflow was added by a team that did not know the flag existed. The next time anybody presses it will be the first time in a year, during an incident, with no idea what it actually covers. Pull it on a quiet Tuesday, on a schedule, and watch what keeps running. Anything still going is a gap you would otherwise have found at the worst possible moment.

The third failure is the one that erodes the whole design, and it never announces itself. A narrow token made a run fail on a Friday afternoon. Somebody replaced it with a broader one to unblock the release, meaning to tighten it on Monday. Repeat that a dozen times over a year and the matrix on the wiki no longer describes the system — every agent holds roughly what every other agent holds, and the empty cells that were the entire point have quietly filled in. The counter-pressure has to be as ordinary as the pressure: a permission change is a reviewed change, the matrix is generated from the configuration rather than maintained beside it, and a run that fails on a narrow token is a bug in the scope, not an argument for widening it.

None of this is expensive relative to the rest of a factory. Short-lived tokens, an egress allowlist, a stop with four settings and two named people who may pull it come to about a week of work, and they decide whether the worst case is bounded by what a run could reach or by whether every run behaved. The same instinct gives each run its own disposable environment and its own data.

IF YOU REMEMBER ONE THING

A stop that only turns everything off will not be pulled, and a permission granted for an afternoon will still be there next year. Both are decided before the incident or not at all.

Bounds tell you the worst case. They say nothing about whether the thing still works inside them, and that is a question you can only answer by running the same inputs again and comparing — a golden set.

Questions people also ask

4 QUESTIONS
What should an AI coding agent's credentials be scoped to?

To one run, not to the fleet. A short-lived token covering exactly what this run needs — one repository, one project, one environment — injected from a secrets store when the run starts and expiring shortly after it ends. Nothing is committed to the repository, and production access is never part of the default grant. Invariant Labs reached the same two mitigations from the attack side: one repository per session, and least-privilege tokens.

Does a kill switch need more than a single off switch?

Yes, and this is the common design mistake. Most incidents want something narrower than everything: one workflow disabled while the others keep running, or every agent stopped on one service while the rest of the system carries on. If the only control is global, the honest response to any problem is to turn the whole factory off, so people hesitate to use it and instead argue about whether the problem is bad enough.

Who should be allowed to stop the agents?

A named person, and more than one of them, with the authority to act at three in the morning without asking anybody first. A control that nobody is named for is not a control; it is a feature. Deciding who may pull it, and confirming they can reach it from a phone, belongs to the design of the system rather than to the conversation during the incident, when the answer is needed in seconds.

How do you stop agent permissions from widening over time?

By giving every widening an expiry at the moment it is granted. Broad access almost always arrives for a good reason — a debugging session on a Friday, a run that failed on a narrow token — and it stays because withdrawing it is nobody's task. An expiry makes the default direction narrow again, and forces a second decision from someone who can see whether the reason still holds.