Most people give an AI agent access to their business the way they give a builder the house keys: one master key, handed over at the start, never taken back. It works until the agent misreads an instruction, or reads a malicious one hidden in a log file, and does something you would never have signed off on. The other common option is to make the agent ask before every single action, which is safe but so noisy that you start clicking "yes" without reading.
Vercel's engineering team published a third option on 23 October 2026. In How Vercel Agent accesses production safely, Tom Dale describes how their debugging agent works on customers' live projects. The agent looks around with read-only access, writes a plan of what it wants to change and which permissions each step needs, and only when you approve that plan does it receive a key that covers exactly those permissions. I call it plan-scoped credentials: approve the plan, mint the token.
The pattern
A credential is anything that proves the agent is allowed to do something: an API key, a password, a login token. A scope is one named permission inside that credential, such as "read deployments" or "write deployments". The idea is simple. Permissions follow the plan. The agent never holds more access than the plan you approved, and the access stops when the plan stops.
For a solo builder, this matters because your agent probably touches the things that hurt most when they break: your Stripe account, your GitHub repos, your customer email list. Plan-scoped access means one decision per job instead of twenty, and a key that is useless to anyone who steals it after the job ends. It also gives you a written record of what you approved and when, which is the first thing you want after something goes wrong.
When to use it and when not
Use it when an agent touches production, money, customer data or third-party accounts, and when approving each call one by one would be so frequent that you stop paying attention. A support agent that issues refunds, a coding agent that opens pull requests, or an ops agent that redeploys your site are all good fits. Earlier we covered risk-tiered code review, where the size of a change decides how many humans look at it; plan-scoped credentials apply the same thinking to access.
Skip it for agents that only read public data, agents that run in a throwaway sandbox, and tiny scripts where you press every button yourself. Per-call approval gates, which make the agent stop and ask before each email, payment or post, still suit low-volume, high-stakes actions such as a single large payout. The two combine well: a plan grants the access, and a per-call gate guards the one step inside it that could cost real money.
Vercel is candid about the limits. It says "even a valid scope can grant broader access than necessary", giving the example that deployment write access covers more than one redeploy. It also warns that read-only work is not harmless, because reads can expose sensitive information. Plans sit alongside sandboxing, network rules, secret handling, rate limits and audit logs. They do not replace them.
How it works (input → step → output)
Input: a request for an outcome, such as "my latest deploy is broken, fix it".
Investigate under a read-only baseline. According to Vercel, the baseline includes every registered scope that starts with
read:. Short lookups run without a plan at all.Draft a plan. The plan links the intent, the tasks, the scopes each task needs, and a plain-language reason for each. Vercel's example: an investigation task asks for
read:deployment; a redeploy task asks forread-write:deployment.Validate the scopes. Each requested scope is checked against a registry of known scopes. Unknown or malformed names are rejected before the plan is stored or shown to you, and the agent gets the list of valid scopes back so it can correct itself rather than reaching for something broader.
Approve once. You approve the whole plan in one decision. If any task needs more than read access, the whole plan waits, including its read-only tasks.
Mint a short-lived token. On approval the scopes are checked again, and Vercel's OAuth service (the part that hands out login tokens) issues "shorter-lived tokens with specific scopes": the baseline plus the approved scopes, nothing else. The post does not state an exact lifetime.
Re-check before every use. Before resuming, the runtime reloads the plan and confirms it is still active. A stopped plan fails that check even if its token has time left. Vercel's APIs also check the token's scopes on every request.
Output: findings, a reviewable plan, changes made only under that plan, and a stored record of the tasks, state, approver and approval time. One ceiling sits above all of this: the agent acts on your behalf, so approving a plan cannot grant access you do not already have.
Components
Baseline credential: a read-only key the agent always holds.
Scope registry: a hard-coded list of every permission name your system knows, with a one-line description of each.
Plan store: a database table with plan id, tasks, scopes, reasons, status (draft, active, stopped, done), approver and timestamps.
Approval surface: a Slack message, email or page showing the plan with Approve and Stop buttons.
Credential broker: a small server-side function that hands out the right key only for an active plan, and only for its approved scopes. The agent never sees your master keys.
Runtime guard: a check that runs before every privileged call and refuses if the plan is no longer active.
Minimal implementation
Here is the shape in pseudo-code. It works the same in a Python script, a Node server or an n8n workflow with a database node and a Slack approval step.
ALLOWED = {
"github:contents:read", "github:pull_requests:write",
"stripe:refunds:write", "stripe:customers:read"
}
plan = llm("Investigate with read-only tools. If you need more, return JSON "
"{intent, tasks:[{task, why, scopes:[...]}]}")
bad = [s for t in plan.tasks for s in t.scopes if s not in ALLOWED]
if bad:
return llm_retry(plan, error=f"Unknown scopes {bad}. Valid: {ALLOWED}")
plan_id = db.insert(plan, status="draft")
send_for_approval(plan_id) # one Approve / Stop message
on_approve(plan_id):
db.update(plan_id, status="active", approver=me, approved_at=now())
def privileged_call(plan_id, scope, action):
p = db.get(plan_id)
assert p.status == "active" and scope in p.approved_scopes
key = broker.issue(scope, ttl_minutes=60)
return action(key)The honest part is broker.issue, because what "minting" means depends on the service. On GitHub you can truly mint: a GitHub App's installation access token expires after 1 hour, and the request can narrow it to named repositories and named permissions. If you prefer not to build an app, GitHub's fine-grained personal access tokens let you choose an expiry, specific repositories and specific permissions, though you create them in GitHub's settings page by hand.
Stripe works differently. Stripe restricted keys let you set None, Read or Write for each resource, and Stripe recommends them for keys given to AI agents. The docs describe creating them in the Dashboard with two-factor verification, not minting them on demand, and the creation steps do not include setting an expiry. So for Stripe, create a few restricted keys ahead of time, one per plan type (say, "read customers" and "write refunds"), keep them in your secrets store, and let the broker release one only while a matching plan is active. When you stop a plan, the guard blocks the next call; if you suspect a leak, expire the key in the Dashboard. Stripe also lets you tag a restricted key as an agent key, which applies its approval rules to high-risk actions such as payouts and refunds.
Failure modes and evals
Too little access. The agent under-asks and the task fails halfway. Test: give it ten past jobs and count how many plans needed a second approval. Above two in ten, improve the scope descriptions in your registry.
Too much access. The agent asks for write when read would do. Test: for each approved plan, compare requested scopes with scopes actually used in your logs, the same way Stripe suggests auditing a key's request logs. Any unused scope is a prompt fix.
Invented scopes. The model makes up a permission name. Test: feed it a task that tempts a made-up scope and confirm the validator rejects it and the retry succeeds.
Stale approval. You press Stop but work continues. Test: approve a plan, stop it mid-run, and confirm the next privileged call fails even though the key has not expired. Vercel also handles approval and stop arriving at the same moment: the stop wins if it landed first.
Misread denials. A refusal can mean a missing scope, the wrong resource, or an action that should stay blocked. Vercel says the agent must tell these apart before asking for more. Log the reason with every denial.
Poisoned input. Vercel notes the agent may meet malicious instructions in content it reads, such as build logs. Scoped keys limit the blast radius; they do not stop a bad action that is within scope. Pair this with a read-back loop before any write.
Build it this weekend
Pick one agent that currently holds a full-access key. Swap that key for a read-only one and see what breaks.
Write the registry as a short file: four to eight scope names, each with a one-line description the model can read.
Add one prompt rule: "If you need anything beyond read access, stop and output a JSON plan with task, why and scopes." Keep the enforcement in code, as in order in code, judgment in prompt.
Create a plans table with a status column, and a Slack or email message with Approve and Stop.
For GitHub, create a GitHub App and mint one-hour tokens per plan. For Stripe, create two restricted keys in a sandbox, one read and one write, and gate them behind the broker.
Run the stale-approval test above. If Stop does not block the next call, you have built a fancier master key; fix the guard before going live.
Done well, you make one decision per job, you can read exactly what you agreed to, and a leaked key is worth very little to anyone else.

