# API Scopes for Autonomous Coding Agents That Contain Risk

An autonomous coding agent should receive a credential for a defined job, not a credential that happens to make errors disappear. The hard part is not finding a scope label called `write`; it is proving that the token can complete one task while failing at nearby actions that do not belong to it.

I have watched teams start with a personal token because the agent needed to "just get going." Weeks later, that token can read every repository the developer ever touched, edit deployment settings, and issue destructive requests the original task never called for. The agent did not create that risk. The lazy permission boundary did.

## A task is narrower than a role

An agent task describes an outcome and a bounded set of state changes. A role describes a person or service in broad terms. If you grant access from the role, you almost always grant more than the task requires.

Take a request such as: "Update a dependency in the payments service, run the test suite, and open a pull request." The agent may need to read one repository, create a branch, push commits to that branch, and create a pull request. It might need read access to build logs if the test service exposes them through an API. It does not need to administer the organization, edit protected branch rules, rotate deployment credentials, or merge its own work.

Write task contracts as verbs against named resources. Do not write "repository write." Write what the agent may actually do:

- Read source, issues, and existing pull requests in `payments-service`.
- Create and update branches whose names begin with `agent/`.
- Create one pull request from that branch to the designated base branch.
- Read the status and logs for the workflow started by that pull request.
- Post a comment containing the test result.

This is not bureaucracy. The list exposes missing decisions. Can the agent close an issue? Can it edit someone else's pull request? May it re-run a costly workflow? Does it need to fetch a package from a private registry? Each verb either earns a permission or gets removed.

A task contract also distinguishes a necessary side effect from a convenient one. An agent may want to update an issue label after it opens a pull request. That may be useful, but it does not make the dependency update possible. Keep it out of the first permission set. Add it later only after someone accepts the effect and tests the boundary.

Treat recurring jobs as separate tasks even when one agent process performs them. A nightly dependency check, a release promotion, and a production rollback have different consequences. One identity with a pile of accumulated permissions makes all three harder to review and impossible to revoke cleanly.

## Scope names are not permission boundaries

A scope string is an input to authorization, not proof that an API call is safe. Providers use the word "scope" for several different mechanisms: OAuth strings, repository permissions, project roles, installation grants, and tokens constrained to a resource list. They are not interchangeable.

OAuth 2.0 RFC 6749 defines scope as a space separated set of strings that limits an access token's access. It deliberately leaves the meaning of each string to the authorization server. That flexibility is useful for providers, but it means `repo:write`, `projects.write`, and `api` tell you almost nothing until you inspect the provider's endpoint documentation and test the token.

RFC 8707 adds resource indicators. A client can request a token for a specific protected resource, rather than treating every endpoint behind an authorization server as one target. This helps when an issuer supports it. It does not repair a provider that maps a single broad scope to every project or every destructive endpoint within that resource.

Keep these three layers separate in your design notes:

| Layer | Question it answers | Failure if confused |
|---|---|---|
| Token scope | Which permission labels did the issuer place in this token? | You assume a friendly label means a narrow action. |
| Resource grant | Which repositories, projects, accounts, or environments can this identity reach? | The token can act on a neighbor resource. |
| Endpoint rule | Which method and path will the API accept for this request? | A write grant permits deletion or administration. |

The common bad recommendation is "use read only plus write." It is popular because it fits in a setup guide and often works on the first try. It is wrong because write often covers several unrelated verbs. Creating a pull request, deleting a repository, changing a webhook, and modifying access control may all sit behind the same broad grant.

When a provider offers only a broad scope, do not pretend you solved least privilege by naming it carefully. Constrain the resource layer instead. Create a dedicated repository, project, environment, or service account with access only to the target. If the agent needs one production action, give it a separate identity for that action and require an explicit approval. The provider's coarse model remains coarse, but the credential can reach less.

## Build an endpoint ledger before issuing a token

An endpoint ledger turns a vague request into a reviewable permission design. It records every call the agent is allowed to make, why it needs the call, the resource it may touch, and the exact permission that enables it.

Start from the action sequence, not from the provider's permissions page. An agent that opens a pull request often needs more calls than people expect: it reads the base revision, creates a ref, creates or updates files, obtains workflow status, and submits a pull request. A permission page rarely tells you which one is essential for your chosen workflow.

Use a ledger like this. Replace the illustrative paths with the paths your provider actually documents.

```yaml
task: update dependency and open pull request
resource: org/payments-service
calls:
  - method: GET
    path: /repos/org/payments-service/contents/package-lock.json
    purpose: read current dependency lockfile
    permission: contents:read

  - method: POST
    path: /repos/org/payments-service/git/refs
    constraint: "ref starts with refs/heads/agent/"
    purpose: create working branch
    permission: contents:write

  - method: PUT
    path: /repos/org/payments-service/contents/package-lock.json
    constraint: "branch starts with agent/"
    purpose: commit updated lockfile
    permission: contents:write

  - method: POST
    path: /repos/org/payments-service/pulls
    constraint: "base is main; head starts with agent/"
    purpose: request review
    permission: pull_requests:write

forbidden_calls:
  - DELETE /repos/org/payments-service
  - PATCH /repos/org/payments-service/branches/main/protection
  - POST /repos/org/organization-hooks
  - GET /repos/org/another-service/contents/secrets.yml
```

The `constraint` field matters because endpoint permissions frequently stop short of task permissions. An API may allow branch creation but offer no native restriction to the `agent/` prefix. Record that gap. You may need an intermediary action service, a separate repository, or a review gate because a scope cannot enforce the branch rule you want.

Do not rely on an agent prompt to keep the constraints intact. A prompt can describe the intended branch prefix, but it cannot reject a request sent to `main`. The enforcement point must be the API provider, the target resource's settings, or an action gateway that checks the request before it sends it.

The ledger should include read calls as seriously as write calls. Reading a deployment secret, customer export, security advisory, or a second repository can expose more than a bad commit. Most permission reviews spend all their attention on writes because writes are visible. The agent's context window makes broad reads dangerous too.

## Separate discovery access from mutation access

Discovery access and mutation access should usually use different credentials because an agent needs broad context more often than it needs broad authority to change state.

A planning agent may need to search code, inspect issues, examine build output, and compare versions across several repositories. A patching agent may only need to write to one branch in one repository. If both jobs share a token, the patching agent inherits the planner's wide read surface, and the planner inherits write capability it never needs.

Split the work into stages when the provider lets you do so. The discovery stage emits a bounded plan or patch proposal. A second process receives that artifact and a narrower credential to make the requested mutation. A human can review the handoff when the change affects a protected area.

This separation catches a practical failure that prompts cannot cure. Suppose a planner searches an organization for references to a package and finds an old internal repository containing deployment notes. If the same token can push to every result it read, a later mistaken tool call can modify the wrong repository. The model may understand the task perfectly and still select the wrong identifier. Restricting the writer to the intended repository converts that mistake into a denied request.

Do not split tokens merely to create more tokens. Split them when the permissible resource set or verbs differ. A single read token can support a coherent investigation. A single write token can support tightly related edits inside one target. The point is to make each token's answer to "what can this process do?" short enough that an engineer can verify it without guessing.

For source control, separate branch write authority from merge authority whenever the provider permits it. A branch is a proposed change. A merge changes the shared baseline and often triggers deployments, releases, or downstream automation. The agent can open a useful pull request without receiving permission to merge it.

## Constrain the resource before you polish the scope

A narrow scope attached to an organization wide credential is often worse than a broad scope attached to a disposable, isolated target. Scope controls verbs. Resource boundaries control where those verbs land. You need both, but resource boundaries usually make mistakes survivable.

Give autonomous agents service identities rather than personal tokens. Personal access tokens tend to inherit a human's old memberships, temporary administrator grants, and access to projects nobody remembered during setup. Revoking one later can also interrupt unrelated work, which causes teams to postpone revocation. That is how temporary exceptions become permanent access.

A dedicated identity should begin with no access and receive only the resource grants listed in the endpoint ledger. If an agent works on a repository, grant it that repository rather than an entire organization. If it updates a staging deployment, grant it the staging environment rather than all environments. If it writes records for one customer account, grant it that account rather than a global API credential.

Use separate nonproduction targets for testing permissions. Testing a token by performing actual writes in production teaches you whether the token works, but it does not establish that it is appropriately constrained. A test repository or project lets you exercise creation, update, failure, revocation, and audit behavior without leaving cleanup work in a live system.

Resource isolation also compensates for APIs with unfortunate scope models. Some services issue a token that has one `api` scope and no endpoint granularity. You can still create a dedicated project containing only the resources the agent may operate, deny it organization administration, and use a distinct identity for each environment. That is less elegant than a fine grained API, but it is far better than handing an all purpose token to a process that constructs requests dynamically.

Do not give an agent access to production just because the code it changes eventually reaches production. The release system should own that transition through an approved, separately authorized path. If the task genuinely includes a production operation, write a separate contract for that operation. It should name the target environment, allowed method, allowable parameters, rollback behavior, and the person who approves it.

## Test success and denial as one contract

A permission set is incomplete until you demonstrate two things: the agent can finish its assigned work, and nearby unassigned actions fail. Testing only the happy path proves convenience. It says nothing about containment.

Use a clean test identity for each permission change. Existing credentials often have cached grants, inherited roles, or a second authentication path that makes a test look successful for the wrong reason. Record the token's subject, intended resources, issued scopes, and expiry before the run.

A practical test sequence looks like this:

1. Create a disposable target resource and a credential with the proposed grants.
2. Run the agent or a deterministic request fixture through every allowed call in the ledger.
3. Verify the expected state, such as a branch, pull request, comment, or updated record.
4. Send each forbidden call using the same credential and expect a denial.
5. Remove the credential or revoke the session, then rerun one previously allowed call and expect denial.

Use direct requests alongside an agent run. Direct requests remove the uncertainty of tool selection and reveal whether the provider itself enforces the boundary. This shell fixture illustrates the shape of the test. It assumes an API that returns JSON and uses `403` for an authorized identity that lacks permission.

```sh
base="https://api.example.internal"
auth="Authorization: Bearer $AGENT_TOKEN"

curl -sS -o allowed.json -w "%{http_code}\n" \
  -H "$auth" \
  -X POST "$base/repos/acme/payments-service/pulls" \
  -H "Content-Type: application/json" \
  -d '{"head":"agent/dependency-bump","base":"main","title":"Update parser"}'
# Expected output: 201

curl -sS -o denied.json -w "%{http_code}\n" \
  -H "$auth" \
  -X DELETE "$base/repos/acme/payments-service"
# Expected output: 403

cat denied.json
# Expected shape: {"message":"Resource not accessible by integration"}
```

Do not assert only the status code. Inspect the resulting state for allowed operations. Some APIs accept a request and process it asynchronously, or return success while ignoring a field the agent relied on. For denials, distinguish `401` from `403`. A `401` may mean the test credential was malformed or expired. A `403` after successful authentication better demonstrates that authorization blocked the call. Providers differ, so document their semantics in your fixture.

Keep a negative test for every dangerous permission boundary. If an agent may create a deployment, test that it cannot promote one. If it may comment on an issue, test that it cannot edit labels or assignees. If it may write a staging secret value, test that it cannot read it back if the API supports write without read. These tests prevent a later scope edit from quietly widening access.

## A failed 403 should change the task or the grant

A `403 Forbidden` response is evidence about the contract. It should trigger a decision, not a reflexive request for the provider's broadest permission.

I have seen this failure pattern repeatedly. An agent creates a branch and commits a fix, then gets denied when it tries to open a pull request. Someone discovers that the provider's pull request permission also permits review dismissal or broader discussion edits. They grant it because the agent needs to finish. A few days later the same agent begins "cleaning up" stale pull requests and edits work outside its assignment.

The first denial contained a design question: does opening a pull request require that broader capability, and can the team accept the side effects? There are several honest answers.

- Grant the permission after testing its full endpoint surface and recording the accepted risk.
- Change the task so the agent prepares a branch and a human opens the pull request.
- Use a different provider identity or resource where the broad grant reaches only the intended repository.
- Put a narrow action service in front of the provider API that accepts only a pull request creation request with fixed resource and branch constraints.

The wrong answer is to add every scope that turns red responses into green ones. That converts authorization errors into delayed incidents.

Request bodies deserve scrutiny too. Many API permission models authorize an endpoint but do not distinguish safe values from harmful ones. `POST /deployments` may accept `staging` and `production` under the same permission. `PATCH /projects/{id}` may allow a harmless description update and a harmful visibility change. If the provider cannot split those operations, your boundary must sit above the endpoint. Require a human approval, use a dedicated target, or expose a purpose built operation rather than raw API access.

Capture the denied call in the ledger with the reason you either added or rejected it. Six months later, that record explains why the agent can create a branch but cannot rename a repository. Without it, someone will call the boundary arbitrary and widen it during a hurried fix.

## Approval gates catch the remaining high consequence calls

Narrow provider permissions reduce what an agent can attempt. Approval gates help with the permitted actions that still deserve a human decision, such as an external payment operation, an SSH command on an important host, or a write against a production service.

Do not use approval as an excuse to give the agent broad credentials. A one click confirmation may stop an obvious bad request, but people approve repetitive cards quickly, especially when an agent needs several routine calls to finish a job. The permission boundary must reject whole categories of actions before an approval card ever appears.

Use approval where human context changes the decision. A deployment may be technically authorized but inappropriate during an incident. A request to delete a branch may be allowed but wrong if another engineer is using it. An approval prompt can present the actual target and requested action at the moment the person can judge it.

Sallyport keeps API and SSH credentials in its encrypted macOS vault and executes the requested action without exposing the secret to the agent. Its session authorization and per credential approval controls can put a person in the path of calls that deserve it, but provider side scope design still decides what an approved credential can reach.

Keep audit records that answer two separate questions: which agent process received permission to act, and which individual API calls it made. Those are different records. A process approval proves a human allowed that run to use a credential; it does not explain whether the run created a pull request, changed an environment variable, or made a failed deletion attempt. Review both after a permission change and after an incident.

## Scope reviews need a trigger, not a calendar promise

Permission reviews work when an engineering event triggers them. A vague quarterly reminder tends to find old tokens after nobody remembers their purpose. Tie review to task changes, new endpoints, resource expansion, provider permission changes, and agent workflow edits.

Keep the endpoint ledger with the code that invokes the agent. When a pull request changes an agent's tool instructions or adds an API call, require an update to the ledger and its positive and negative tests. This puts the permission decision beside the behavior that needs it.

Review revocation before you need it. Remove a test credential and confirm that a previously allowed request fails. Disable the service identity and confirm that a running agent cannot continue through a cached session. Check whether the provider has issued refresh tokens or duplicate credentials that keep the same access alive. Teams often discover these paths during an incident, when the answer is least useful.

Watch for permission creep in small changes. A request to read workflow logs can turn into permission to re-run jobs. A request to update one issue can turn into organization wide issue administration. A request to access one environment can turn into a production fallback "just in case." Every added call should survive the same question: can the agent finish its stated task without it?

If the answer is no, add the smallest grant that enables the call and add a denial test around its nearest harmful neighbor. If the answer is yes, leave it out. That discipline makes agent failures more visible in the short term. It also keeps a malformed instruction, a confused model, or a compromised process from inheriting authority nobody meant to give it.
