If you want the short answer to “how should I limit an AI coding agent’s permissions?”, it’s this: give the agent its own identity instead of yours, hand it short-lived credentials scoped to the one task in front of it, make read-only the default, keep production credentials somewhere it physically can’t reach, and require a human to approve anything destructive. That’s least privilege, the same rule we’ve applied to service accounts for twenty years. We just forgot to apply it to the newest and most enthusiastic service account on the team.

The reason this matters now is that agents don’t only use the permissions you hand them. They use the permissions they find. In April 2026 an AI coding agent deleted a company’s production database in about nine seconds, and the credential it used wasn’t one anybody gave it for that job. Below is what happened, why, and the controls that would have stopped it, ending in a checklist you can copy into your team’s wiki.

Related guides: AI & LLM Security — prompt injection, jailbreaks and agent tool abuse · Authorization & Access Control — the access-control bug classes underneath all of this.

What happened: an AI agent deleted a production database

Jer Crane runs PocketOS, an automotive SaaS platform. On a Friday in late April 2026, a Cursor agent running Anthropic’s Claude Opus 4.6 was doing what Crane described as a routine task in the company’s staging environment (his original post on X). It hit a credential mismatch. Rather than stop and ask, it went looking for a credential that would work, and found a Railway API token in an unrelated file.

That token had been created to add and remove custom domains through the Railway CLI. According to The Register’s reporting, it was scoped for any operation, destructive ones included. Railway’s own post-incident write-up is blunter: the token “was provisioned with account scoped access, the maximum access possible.” The agent used it to call volumeDelete on a production volume. The volume-level backups lived in the same volume, so they went with it.

Asked afterwards what it had done, the agent was candid. The Register quotes it saying, “I guessed that deleting a staging volume via the API would be scoped to staging only,” and “I didn’t verify.” Railway says it later recovered the database, and it has since changed its API so that all deletes are soft deletes with a 48-hour window.

Why it happened: the agent had the developer’s access

It’s tempting to read this as “the model went rogue.” I don’t think that’s the useful lesson. The model made a bad guess, which models do. What turned a bad guess into a production outage was that the agent was running with everything the developer’s machine could reach, and that included a token far more powerful than its name suggested.

Crane made exactly this point when people asked why he’d given the agent production access: “Didn’t give it access, it found it” (Information Age). That’s the part that should worry you. A coding agent runs as you. It reads your files, inherits your environment variables, and uses your logged-in CLIs. Any credential sitting in a dotfile, a .env, a shell history or a config directory is effectively a credential you’ve granted the agent, whether you meant to or not.

OWASP has a name for this: LLM06:2025 Excessive Agency, with three root causes: excessive functionality, excessive permissions and excessive autonomy. This incident hit all three. The agent had a raw API instead of a narrow tool, a token with account-wide scope, and the freedom to run a destructive call without asking anyone.

Scope AI coding agent permissions to the task

Start with identity. The agent shouldn’t be you. Give it its own account or role on every system it touches, so its access can be scoped, logged and revoked independently of yours.

Then make its credentials short-lived and narrow. On AWS that means assuming a dedicated role for the session instead of letting the agent use your long-lived keys:

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/agent-staging-readonly \
  --role-session-name coding-agent \
  --duration-seconds 900

Fifteen minutes is plenty for most tasks, and it means a leaked or misused credential dies on its own. The same idea applies elsewhere: fine-grained tokens limited to one repository, database roles limited to one schema, cloud tokens limited to one project or environment. Railway’s own post makes the point: “Tokens should have scopes that match the task.”

Then go hunting for the credentials the agent can find. Grep your home directory and repos for tokens, move secrets into a manager the agent’s process can’t read, and treat any long-lived, account-scoped token on a developer machine as an incident waiting for a trigger.

Make read-only the default

Most of what a coding agent does is reading: code, schemas, logs, query results. So make that the only thing its standing access allows, and grant write access deliberately, per task, for as short a time as you can.

In Postgres, a read-only role for an agent looks like this:

CREATE ROLE agent_ro LOGIN PASSWORD 'rotate-me';
GRANT CONNECT ON DATABASE app TO agent_ro;
GRANT USAGE ON SCHEMA public TO agent_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent_ro;
ALTER ROLE agent_ro SET default_transaction_read_only = on;

The grants are the real control. The default_transaction_read_only setting is a second layer that catches mistakes. When the agent genuinely needs to write, say for a migration in dev, hand it a separate role for that task and take it back afterwards.

Keep prod and dev separate

The PocketOS agent was working in staging and destroyed production. That’s the failure mode to design against: a mistake in a low-stakes environment should not be able to reach a high-stakes one.

That means different identities for each environment, not one account with access to all of them. It means no production credentials on developer machines at all, because a developer machine is now also an agent machine. And it means separate accounts or projects for prod where your platform supports it, so a staging token physically can’t name a production resource. Replit started rolling out automatic dev/prod database separation after its own agent deleted a user’s production database in 2025, and I think that’s the right default for every platform that lets agents operate.

Require human approval for destructive commands

Some commands should never run without a person saying yes: DROP and TRUNCATE, rm -rf, terraform destroy, git push --force, and any cloud API call with “delete” in the name.

Use the agent’s own permission system as the first layer. Claude Code, Cursor and GitHub Copilot’s agent mode all let you control which commands run automatically and which need confirmation. In Claude Code that’s allow, ask and deny rules in .claude/settings.json (see the permissions docs):

{
  "permissions": {
    "ask": [
      "Bash(terraform *)",
      "Bash(psql *)",
      "Bash(curl *)"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(terraform destroy *)",
      "Bash(git push --force *)",
      "Bash(git push -f *)"
    ]
  }
}

Deny rules win over everything, so a deny on terraform destroy holds even if something broader is allowed. Don’t stop there, though. Command patterns are a guardrail, not a security boundary. There’s always another way to spell a destructive operation, and the PocketOS deletion was an authenticated API call, not a shell command anyone would have pattern-matched. Back the agent-side rules with controls the agent can’t talk its way around: branch protection that rejects force pushes, IAM policies that deny delete actions to agent roles, and soft-delete with a recovery window on the infrastructure side.

Audit logs and kill switches

Because the agent has its own identity, everything it does can be attributed to it. Turn that on. In Postgres, ALTER ROLE agent_ro SET log_statement = 'all'; logs every statement the agent role runs. In the cloud, make sure the agent’s role sessions show up in your audit trail under a recognizable name, which is what --role-session-name above is for.

Then decide, before you need it, how you stop an agent in one move. Revoking a dedicated identity is that kill switch:

ALTER ROLE agent_ro NOLOGIN;
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = 'agent_ro';

If the agent is using your credentials, there’s no kill switch that doesn’t also lock you out, which is one more reason not to let it.

The least-privilege checklist for AI agents

  1. Give every agent its own identity. Never let it run as you.
  2. Issue short-lived credentials (minutes, not months) scoped to one project and one environment.
  3. Find and remove the credentials an agent can stumble on: .env files, dotfiles, config directories, shell history.
  4. Make read-only the default; grant write access per task and revoke it afterwards.
  5. Keep production credentials off developer machines entirely.
  6. Use separate identities, and ideally separate accounts, for prod and dev.
  7. Require human approval for DROP, TRUNCATE, rm -rf, terraform destroy, force pushes and any delete API call.
  8. Back agent-side rules with server-side ones: branch protection, IAM deny policies, database grants.
  9. Turn on soft-delete and keep backups outside the blast radius of the thing they back up.
  10. Log everything the agent’s identity does, and know how you’d revoke it in under a minute.

None of this is new. It’s the same least-privilege discipline we apply to CI pipelines and service accounts. The difference is that an agent is a service account that improvises, so the credentials it can reach are your real permission model, whatever your settings file says.

Where to go next: