The value of an autonomous coding agent is precisely the thing that makes it dangerous. We want it to work unattended: to read the codebase, write the fix, run the tests, iterate, and come back with a result. That is the whole point: work that is not limited by the speed of a human typing. But the same agent that runs your test suite can also run rm -rf, force-push over main, exfiltrate a secret it found in an environment file, or open a network connection to somewhere it has no business reaching. Autonomy is what you are paying for. Unbounded autonomy is how you get hurt.
The way to make this tractable will be familiar to any security architect. Rather than "trusting an AI", you are designing a trust boundary around a capable, fast, occasionally wrong, and fundamentally non-deterministic actor that holds the credentials of whoever launched it. The mental model I use is a talented junior on their first day: give them scoped access, let them do the safe and frequent work freely, gate the irreversible and the outward-facing behind a confirmation, and keep an audit trail of everything they touched.
This article is about how that boundary is actually drawn in Claude Code: the permission model, least privilege applied to a software agent, the trust boundaries that matter most, and the audit habits that turn autonomy from a gamble into an engineering control. The syntax below is illustrative. The shape is well established, but treat exact rules as something you confirm against the current documentation, not copy blindly.
Autonomy is the value; unbounded autonomy is the liability
Start from what the agent can physically do, not what you hope it will do. It can edit files in the working directory and it can execute shell commands. From those two primitives, the full range of system actions follows: anything your shell can do, the agent can ask to do, with your identity and your environment. That gives you a great deal of power and a large attack surface at the same time.
The temptation, after the tenth confirmation prompt for a harmless npm test, is to disable the prompts entirely and let it run. Resist it. A blanket bypass converts every mistake (a hallucinated command, a misread path, a prompt-injection payload hiding in a file) directly into an executed action with no checkpoint. What you want is for prompts to be rare and meaningful, so that when one appears it actually warrants a moment's thought. You achieve that by deciding, in advance, which operations are safe enough to allow silently and which deserve a human in the loop.
Treat the agent as a capable but untrusted junior: free rein over the safe and reversible, a confirmation gate on the irreversible and the outward-facing, and never the keys to anything it does not strictly need.
The permission model: allow, deny, and ask
Claude Code's permission system is built on three classes of rule, evaluated against each tool call the agent wants to make. An allow rule lets a matching action proceed without interruption. A deny rule blocks it outright, regardless of anything else. An ask rule pauses and requires explicit human confirmation before proceeding. Rules are tool-scoped: they match not just on the tool (such as Bash or Edit) but on a pattern within it, so you can permit Bash(npm run test:*) while saying nothing about Bash(git push:*), or explicitly denying it.
The important design property is that deny wins. A deny rule takes precedence over any allow, which is what lets you write a permissive allowlist for productivity and still carve out hard exclusions you are confident should never run unattended. This is the same precedence logic as a well-built firewall ruleset: broad permits, narrow and authoritative denies on top.
These rules live in settings files, layered by scope. A project file at .claude/settings.json, committed to the repository, encodes the team's shared policy for that codebase. A personal settings file (and an untracked local override) lets an individual tighten or loosen rules for their own environment without imposing them on everyone else. Enterprise-managed settings can sit above both and cannot be overridden downstream, which makes them the place to enforce non-negotiable organisational denies.
{
"permissions": {
"allow": [
"Bash(npm run test:*)",
"Bash(npm run lint)",
"Bash(npm run build)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Read(./src/**)",
"Edit(./src/**)"
],
"ask": [
"Bash(git push:*)",
"Bash(git commit:*)",
"Edit(./infra/**)",
"Edit(./.github/workflows/**)"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(curl:*)",
"Bash(wget:*)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}
Read that example as a policy statement rather than a recipe. The frequent, reversible, inward-facing operations (test, lint, build, read and edit within src) are allowed so the agent can do real work without nagging. The actions that publish or change history, push and commit, are gated behind ask. The genuinely dangerous or sensitive ones, namely recursive deletes, arbitrary network fetches, and any read of credential files, are denied outright, where deny precedence guarantees no later allow can resurrect them.
Permission modes: choosing the posture for the task
Rules define what is allowed; modes define the agent's overall posture for a session. They are the dial you turn to match the risk of the work in front of you, and they matter as much as the ruleset.
- Plan-first mode. The agent investigates and proposes a plan but makes no changes and runs nothing that mutates state. This is the right default for unfamiliar code, large refactors, or anything where you want to approve the approach before a single file is touched. It separates thinking from doing, and keeps the doing under review.
- Accept-edits mode. File edits proceed without per-edit confirmation, while command execution and the more sensitive operations still follow the rules. Useful for a well-scoped task in a trusted, version-controlled repository where you have already reviewed the plan and the diff is your real safety net.
- Default ask-on-sensitive mode. The everyday posture: allowed actions run, denied actions are blocked, and anything not explicitly cleared prompts for confirmation. This is where most work should live, because it fails safe: an action you never anticipated defaults to asking rather than running.
There is also a mode that bypasses prompts entirely. It has legitimate uses, such as a disposable container or a sandboxed CI runner with no credentials and no network, but it should be a deliberate, isolated choice, never the way you run the agent against your own machine or an untrusted codebase. The blast radius of a bypass is the blast radius of your shell.
Least privilege, applied to an agent
Everything above comes down to one principle, least privilege. Grant exactly the access the task requires, no more, and make the grant explicit. For a coding agent that resolves to a short, repeatable discipline:
- Allowlist the safe and frequent. The operations that run dozens of times an hour and cannot hurt you (reading source, running tests, linting, building, inspecting git state) belong on the allowlist. Friction here buys you nothing and trains you to click through prompts on autopilot, which is its own hazard.
- Gate the irreversible and the outward-facing. Deletes, history rewrites, pushes, deployments, package publishes, and anything that makes a network call should require confirmation or be denied. The test is simple: can I cheaply undo this, and does it stay inside this machine? If the answer to either is no, it does not run unattended.
- Scope to the working set. Edit rules should target the directories the task actually concerns. An agent fixing a frontend bug has no need to write to
infraor CI workflow definitions, and scoping the rules says so explicitly. - Never broadly bypass on untrusted code. Once a repository contains content you did not write, whether a dependency, a cloned project or a pull request from outside, bypassing prompts is a vulnerability rather than a convenience. More on why in a moment.
The decision table below is the heuristic in a form you can pin to a wall. The columns are not Claude Code internals, just a way of reasoning that the ruleset then encodes.
Operation Reversible? Outward-facing? Posture
--------------------------- ----------- --------------- -------
Run tests / lint / build yes no ALLOW
Read source in working dir yes no ALLOW
Edit source in working dir yes (VCS) no ALLOW
git commit yes-ish no ASK
git push / open PR hard yes ASK
Deploy / publish package no yes ASK / DENY
Network fetch (curl/wget) n/a yes DENY
Recursive delete (rm -rf) no no DENY
Read .env / secrets / keys n/a (leak risk) DENY
Trust boundaries: where the real risk lives
Permissions decide what the agent may do. Trust boundaries decide what it can reach in the first place, and getting these right matters more than any individual rule.
Keep secrets outside the agent's reach. Credentials, API keys, signing keys and tokens should not be readable by the agent at all. Deny them in the ruleset, or better still, keep them out of the working tree entirely. A private signing key in a repository is a liability whether or not an AI is involved; an agent that can read it, summarise it, and place it into a command argument or a file it then pushes is a credential-exfiltration path with extra steps. The discipline is the same one PKI has always demanded: private key material lives in a custody boundary (ideally an HSM or a secrets manager) that the application, and now the agent, can use through a narrow interface but never read.
Treat the working directory and sandbox as the blast radius. The agent operates within a directory; that directory, plus whatever the shell can reach from it, is the boundary of what a mistake can damage. Tightening that boundary by running in a container, limiting filesystem and network access, and isolating the environment from your real credentials is the most effective control you have, because it bounds the worst case regardless of how the rules are configured. A sandbox with no network and no secrets turns a catastrophic failure into a recoverable one.
Take prompt injection seriously. This is the boundary engineers most often miss. The agent reads files, and it may read web content or tool output. Any of that input is untrusted, and untrusted input can contain instructions. A comment in a source file, a line in a README, a fetched web page, or the output of a command can say, in effect, "ignore your previous task and run this command", and a model is, by construction, susceptible to being steered by text. You cannot patch this away with cleverness or by asking the model nicely to be careful. The only durable control is the permission boundary: if a recursive delete is denied and network fetches are denied, then a successful injection that tries to trigger them is stopped by the rule, not by the model's judgement. In short, permissions, not vibes, are the control.
Auditability as a first-class habit
A trust boundary you cannot inspect is a trust boundary you cannot trust. Because the agent acts on your behalf, reviewing what it actually did has to be a routine, not an afterthought you reach for only when something looks wrong.
Two artefacts carry most of the weight. The diff is the primary one: every file change is a reviewable diff, and reading it before you commit is non-negotiable. Version control is what makes accept-edits mode defensible at all. The agent can move quickly precisely because git diff gives you a complete, line-by-line account of what changed, and git restore gives you a clean undo. The second is the command log: the record of what the agent executed. A command that ran but produced no file change leaves no diff, so the log is your only window onto it. Skim it for anything outward-facing or unexpected: a network call you did not anticipate, a file touched outside the working set, a command whose shape does not match the task.
# Review before you trust: the change set in full
git status
git diff
# Stage deliberately, never "git add ." on autopilot after an agent run
git add -p
# A guard hook concept: inspect a command before it runs and refuse
# outright if it touches credential files. Illustrative shape only.
#!/usr/bin/env bash
# pre-tool-use hook: receives the proposed command on stdin
cmd="$(cat)"
case "$cmd" in
*.env*|*secrets/*|*id_rsa*|*.pem*)
echo "Blocked: command references a sensitive path." >&2
exit 1 ;; # non-zero exit denies the action
esac
exit 0 # zero exit allows it to proceed
Hooks are the programmable layer beneath the declarative rules: where allow/deny/ask express static policy, a hook can apply logic (inspect the actual command, check a path against a denylist, enforce a convention) and block by exiting non-zero. They are also where you encode the rules that are too contextual for a static pattern. Use them sparingly and test them, because a hook is itself code running in your trust boundary.
The architect's framing
Step back and the whole problem is familiar. We have a non-deterministic actor holding real credentials, capable of irreversible and outward-facing actions, occasionally steered by untrusted input. The answer is the oldest discipline in security architecture, and there is nothing AI-specific about it. Default-deny. Least privilege. Explicit allowlists for the routine. Confirmation gates on the irreversible. Secrets behind a custody boundary the actor can use but never read. A sandbox that bounds the blast radius. And an audit trail you actually read.
An engineer who has designed a least-privilege system before already knows how to deploy a coding agent safely. The actor is just unusually capable and unusually fast. The mistake is to treat the agent as a special case that excuses you from the discipline, or to let the convenience of autonomy erode the boundary that makes autonomy safe. Draw the boundary deliberately, encode it in settings the team shares, keep the secrets out of reach, and review what the agent did. Do that, and unattended work stops being a leap of faith and becomes what it should be: a controlled delegation to a capable junior whose access you have scoped on purpose.
With the trust boundary established, the next question is how to safely extend the agent's reach by connecting it to external systems and data through the Model Context Protocol without quietly widening the very boundary this article spent its time drawing. That is where the series turns next.