Least-privilege scopes for AI coding agentsThe minimum each credential can run on.
Your coding agent does not need your credentials. It needs the handful of actions sitting behind them, and almost nothing else. This is the minimum scope for each credential a coding agent commonly touches, with the permissions to grant and the ones to refuse.
-
01
Deny
Default off
Start from zero. The agent gets a permission only once a task proves it needs it.
deny by default -
02
Grant
Per action
Add the few scopes the task needs, on the one resource that satisfies it.
least privilege -
03
Expire
Short lease
Put a clock on every grant. A token scoped to minutes is near worthless if it leaks.
blast radius
TL;DR· the answer, in twenty seconds
What this is: A per-credential reference for the minimum scope an AI coding agent needs across GitHub, AWS, GCP, databases, and provider API keys. The permissions to grant, and the ones to leave off.
The minimum fix: Replace broad, long-lived credentials with the narrowest scope the task needs, pointed at one resource, bounded by a short lifetime.
The lesson: Scope is the only control that still works after a leak. Decide it once, up front, so the broad credential never reaches the agent's environment.
Your coding agent does not need your credentials. It needs the handful of actions sitting behind them, and this page is the smallest scope each common credential can run on.
The default setup hands an agent the same keys you carry: a personal access token with full repository and admin rights, an AWS access key bonded to an over-broad role, the database URL with the application's write user baked in. Each of those was sized for a person who occasionally needs everything. An agent runs one task at a time and should be able to do close to nothing outside it. Closing that gap takes three moves. Grant the minimum permission, point it at the one resource the task touches, and put a clock on it.
Why scope is the control that survives a leak
Start from the assumption that the credential leaks, because the data says it will. GitGuardian's State of Secrets Sprawl 2026 counted 29 million new hardcoded secrets pushed to public GitHub in 2025, a 34% jump in a single year. Commits co-authored by AI coding agents leaked secrets at 3.2%, against a 1.5% baseline for human commits. The agent in your editor is, on those numbers, about twice as likely to commit a key as you are.
You cannot drive the leak rate to zero. Pre-commit hooks miss things, and scanners only catch a secret after it is already pushed. The one variable you control is what a leaked credential can do once it is out. That is scope, and it is the difference between a key you rotate at leisure and a billing incident that starts before you finish reading the alert.
The gap is concrete. A leaked classic GitHub token with the repo scope exposes every private repository you can reach, not the one the agent was working in. A leaked AWS access key on an admin user can spin up GPU instances in every region at once. The same agent, scoped down, leaks a token that reads one bucket for one hour, or a fine-grained PAT that touches one repository and expires next week. Same leak, two outcomes, and the only thing that changed was the size of the grant you handed over in the first place.
Every serious framework lands here. OWASP's Top 10 for Agentic Applications puts excessive agency and excessive permissions near the top of the list. SANS makes the case for treating the agent as a confused deputy that should hold no standing authority of its own. Anthropic's zero-trust guidance at its Trust Center frames least privilege as least agency: the minimum permission, scoped to the moment, expired on its own. A token good for five minutes and one operation is worth almost nothing to whoever picks it up.
GitHub: a fine-grained PAT, never a classic token
Most agents touch GitHub, and most get handed a classic token, which is the wrong instrument. A classic personal access token scopes by capability, not by repository. Grant it repo and the agent can read and write every private repository you can, along with their issues, deployments, and webhooks. There is no per-repository limit and no real expiry story.
A fine-grained PAT is the replacement GitHub built for this. It binds to a chosen set of repositories, expires on a date you set, and splits access into roughly 60 individual permissions across repository, organization, and account categories. You grant the few the task needs and leave the rest off.
For an agent that reads code and opens pull requests, that set is small:
# Fine-grained PAT for a coding agent that reads code and opens PRs.
# Repository access: ONLY the repos it works in. Never "All repositories".
# Expiration: 7 days (shortest the task tolerates), then regenerate.
Repository permissions
Contents .............. Read and write # clone, read, push a branch
Pull requests ......... Read and write # open and update PRs
Metadata .............. Read # mandatory, auto-selected
Issues ................ Read # only if it triages issues
Leave OFF unless a task proves it needs them:
Administration, Secrets, Environments, Webhooks,
Workflows, Actions, Deployments, Packages, Pages
The honest way to find the minimum set is to start below it and let the API tell you. Grant Contents and Metadata read, run the task, and when GitHub returns a 403 naming a permission, add that one and nothing more. GitHub's permissions reference for fine-grained tokens lists the exact scope each REST endpoint requires, so you look it up instead of granting wide and hoping.
Two more controls tighten this. In an organization, require approval for fine-grained PATs so no one can self-issue a broad token against company repositories, and review what the agent's token requested. If the agent only needs to read one repository, a read-only SSH deploy key scoped to that single repo is narrower still than any PAT, because it cannot reach a second repository even by accident.
In CI, skip the PAT. A GitHub Actions job authenticates to your cloud with OpenID Connect and a short-lived token, so there is no durable secret for the agent or the runner to leak in the first place.
AWS and GCP: a scoped role and a short lease, not an access key
An AWS access key handed to an agent is the worst case: long-lived, often attached to a user with more rights than anyone audited, and valid until someone notices. Replace it with a role the agent assumes for a bounded session.
Two controls do the work. A session policy passed at assume-role time narrows the permissions down to the task. A permissions boundary on the role caps the ceiling, so even a misconfigured policy cannot grant more than the boundary allows.
# Assume a task-scoped role for one hour, with an inline session policy
# that further narrows what the temporary credentials can do.
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-read-s3 \
--role-session-name coding-agent \
--duration-seconds 3600 \
--policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::app-build-artifacts",
"arn:aws:s3:::app-build-artifacts/*"
]
}]
}'
The role itself carries a permissions boundary, so the effective permissions are the intersection of the boundary and the session policy. Set the duration to the shortest the work tolerates. A one-hour credential that leaks is a problem you have until the top of the next hour, not forever.
GCP follows the same shape through service account impersonation. The agent runs as a low-privilege principal and mints a short-lived token for a task-specific service account, which can be downscoped with a credential access boundary that restricts it to named buckets:
# Short-lived token for a task-specific service account, GCP.
gcloud auth print-access-token \
--impersonate-service-account=agent-read@project.iam.gserviceaccount.com \
--lifetime=900
A session policy can only narrow, never widen. It intersects with the role's own policy and the boundary, so a typo grants too little rather than too much, which is the failure direction you want. Keep the static credential that bootstraps the assume-role call out of the agent's reach. On an EC2 host or a container, the instance or task role supplies temporary credentials through the metadata service, so nothing durable lands in the environment for the agent to read or commit.
Both clouds give you the same three knobs: which principal, which actions on which resources, and for how long. Use all three. Deny by default, then add back only what a 403 forces you to.
Databases: a read-mostly role, not the application user
The database URL in your .env almost always points at the application's user, which can write, delete, and often alter schema. An agent inspecting data or drafting a migration needs far less. Give it a dedicated role.
-- A read-mostly role for an agent. Postgres.
CREATE ROLE coding_agent LOGIN PASSWORD 'rotate-me';
-- No inherited rights from PUBLIC.
REVOKE ALL ON DATABASE app FROM coding_agent;
GRANT CONNECT ON DATABASE app TO coding_agent;
-- Read access to one schema, nothing else.
GRANT USAGE ON SCHEMA public TO coding_agent;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO coding_agent;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO coding_agent;
-- Cap any single query so a bad one cannot pin the box.
ALTER ROLE coding_agent SET statement_timeout = '5s';
Point the role at a read replica when you have one, so the agent cannot reach the primary at all. If a task needs to write, grant INSERT and UPDATE on the named tables it touches and withhold DELETE and DROP. Give each agent its own role rather than sharing one, so the database logs attribute every query to the principal that ran it and you can revoke one agent without touching the others. The walkthrough in give your agent database access without the password covers brokering this without putting a standing credential in the agent's environment.
Provider API keys: restricted keys and project scoping
SaaS API keys are where the worst blast radii hide, because one key often reaches the whole account. Most providers now ship a narrower option, and the agent should always get that one.
Stripe issues restricted keys with per-resource permissions. A reporting agent that only reads payment data gets a key with read-only access to the few resources it reports on and no write access to anything else. It can never issue a refund or change a payout, because the key was never granted the permission.
The model providers split keys the same way. OpenAI and Anthropic both scope keys to a project rather than the whole organization, attach them to service accounts, and let you set a spend cap. Give the agent a project-scoped key with a hard monthly limit, so a runaway loop or a leaked key bills a bounded amount instead of the company card. Package registries do the same: npm granular access tokens limit which packages and scopes a token can publish and carry an expiry, so an agent that builds against the registry never holds a token that can push to everything you own. The same logic covers every other credential the agent touches: read-only where the task only reads, with a spend or rate limit wherever the provider offers one. Keeping the provider key out of the model's context in the first place is the companion move.
The scope reference, on one page
Keep this next to wherever you wire credentials into an agent. The defaults on the left are what most setups ship. The scope on the right is the version that survives a leak.
LEAST-PRIVILEGE SCOPE REFERENCE (AI coding agent)
==================================================
CREDENTIAL DEFAULT (too broad) SCOPE IT TO
------------------ ----------------------- -----------------------------
GitHub Classic PAT, repo scope, Fine-grained PAT, named repos,
all repos, no expiry Contents+PR+Metadata, 7-day exp
AWS IAM access key on a sts:AssumeRole, session policy,
broad user permissions boundary, <=1h
GCP SA key JSON file Impersonation token, downscoped,
minutes-long lifetime
Database App user (write + DDL) Dedicated role, SELECT on one
schema, read replica, stmt timeout
Stripe Secret key (full) Restricted key, read-only,
named resources
LLM / SaaS API Org-wide key Project-scoped key, service
account, hard spend cap
RULES
1. Deny by default. Grant a permission only when a 403 proves it is needed.
2. Name the resource. One bucket, one repo, one schema, not the wildcard.
3. Put a clock on it. Shortest lifetime the task tolerates.
4. Never a static key where a short-lived token will do.
None of this is exotic. Every control here ships in the provider you already pay for. The work is deciding, once, that the agent gets the narrow version by default, and wiring it so the broad credential never makes it into the agent's environment.
Scope is necessary, not sufficient. A read-only key still reads data you might not want leaving the building, so pair these grants with an egress allowlist that limits where the agent can send what it reads, and a tamper-evident audit log so you can reconstruct what a given credential did. Scope sets the ceiling on the damage. The other two tell you whether the ceiling held.
What this means for your stack
Pick one credential your agent holds today and scope it down before you read further. The fastest win is GitHub: swap the classic token for a fine-grained PAT bound to the repositories the agent works in, with a seven-day expiry. It takes five minutes and shrinks the blast radius of that token more than any scanner will.
The pattern under all of this is that the agent should hold no durable, broad credential at all. It holds a short-lived, narrow reference that a broker issues per task and expires on its own, against an identity that is the agent's and not yours. Static keys make every leak a full-scope incident. Brokered, scoped, expiring credentials make the same leak a non-event, because the value is near worthless by the time anyone tries to use it.
hasp is one working implementation of that pattern. It brokers scoped, short-lived references to your coding agent instead of exporting raw keys, and writes every access to an append-only log you can audit later. Source-available (FCL-1.0), local-first, macOS and Linux, no account.
Whatever you run, the test is the same. For every credential the agent can reach, ask what it could do if it leaked in the next commit. If the answer is anything close to "everything I can do," the scope is wrong, and the fix is on this page.
Sources· cited above, in one place
- GitGuardian State of Secrets Sprawl report
- Anthropic security Vulnerability disclosure and Trust Center
- OWASP GenAI Security Project Top 10 for Agentic Applications 2026 (ASI01-ASI10)
- GitHub OIDC Configuring OpenID Connect in cloud providers
- SANS Institute Your AI Agent Is an Easily Confused Deputy: Why Cloud Security Needs a Credential Broker
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.