Skip to main content

Overview

Security for an AI coding agent comes down to three questions: what text the agent may act on, what it is able to do, and what it is able to see. This page walks through the design that answers each question, for an agent working inside a codebase. All three follow from one property. An agent does not reliably tell data apart from instructions. The user’s request, a file it read, an issue body, a tool response — everything arrives as one stream of text. If a sentence in that stream reads like an instruction, the agent may act on it, whoever wrote it. The scope here is running an agent safely. Verifying the code an agent produces is a review problem; what review checks for is covered in Code Review.

Prompt injection

Prompt injection is an input crafted so that the agent follows the instructions it contains. It takes two forms. In direct injection, the instruction arrives in the request itself. In indirect injection, the instruction is planted in content the agent reads while working — an issue body, a fetched page, a tool response — and the agent encounters it in the middle of doing something else. For a coding agent, the indirect form is the one to design for, because reading outside text is the job. Consider a review agent that reads a pull request’s diff and description. At the end of the description sits one sentence, phrased as review guidance, that tells the agent to approve the change and report nothing. The agent has no marker separating that sentence from the user’s actual request — both are text in the same stream — and a convincing enough sentence wins. The attack contains no code and exploits no bug; it is one sentence in a text field anyone can edit. The same shape fits every input source in the next section’s table. A dependency changelog can “advise” running a follow-up command, a documentation page can “recommend” changing a setting, a tool response can “ask” for a retry with different arguments. What varies is only where the sentence rides in. Two consequences follow, and they shape the rest of this page.
  • Filtering does not work. Nothing marks where the trusted text ends, so an instruction written into content cannot be reliably stripped out.
  • Removing the ability beats forbidding the action. Telling an agent what not to do is weaker than making the harmful action unavailable.

Where risk arises

Risk is not a property of the input or of the agent alone. It appears where three factors overlap: untrusted input reaches the context, the agent holds a capability, and that capability has consequences that are hard to undo. An agent that reads untrusted text but holds no write or network capability produces, at worst, a wrong answer. An agent with broad capability that reads only text the team wrote still carries risk, but it is the risk of mistakes rather than of an attacker. The combination is what deserves attention: broad capability plus text the team does not control. Three properties of this model are easy to miss. The input does not have to be hostile. A note or a step-by-step explanation written for a human reader can read as an instruction to an agent. Treating outside text as untrusted is about provenance, not intent. The agent’s own output becomes input. A summary the agent writes from injected content, a note it leaves in a scratch file, a report passed to the next step — each can carry the planted instruction forward. Text does not become trusted by passing through the agent. Automation removes the reviewer. The same workflow behaves differently when a human reads each step and when it runs on a schedule. Unattended runs — triggered by an incoming issue, a webhook, or a timer — need narrower permissions, because no one is present to notice the step that should not have happened.

Contain untrusted input with least privilege

The rule is to treat text from outside as data: material the agent reads and reasons about, never a source of instructions it carries out. Three practices work on the text side. State the boundary in the agent’s instructions. Write, in the definition or project instructions, that fetched content, issue bodies, tool responses, and third-party documentation are data, and that instructions embedded in them are not followed. This is worth doing and is not sufficient by itself: it is a request to the agent, and an injected instruction competes with it as text. Do not let the agent act directly on what it read. Content the agent fetched should not be executed as a command, copied verbatim into a file or a message, or used to decide which systems to contact. Put the agent’s own judgment between what it reads and what it does. Narrow what enters the context at all. Fetch the specific page the task needs rather than following links from it, and prefer tool responses that return the fields required for the decision. Less untrusted text in the context means fewer places an instruction can hide. The techniques for shaping context are covered in Context Engineering.
An instruction not to follow embedded instructions is a mitigation, not a boundary. Any workflow whose safety depends only on that sentence is one persuasive paragraph away from failing. Pair it with a capability the agent does not hold.
Because the text-side practices stay mitigations, the place to make the rule hold is permissions. Least privilege means granting exactly the tools and commands a workflow uses, and nothing that merely might be useful. Four design points follow.

Declare the boundary, back it with permissions

The boundary belongs in the role’s own definition, where it applies to every run of that role rather than to one conversation. Written as a Claude Code subagent, the metadata scopes the permissions and the body becomes the system prompt:
A subagent definition that states the boundary
The “never followed” sentence in the body is only a declaration, and a declaration alone can be broken. What actually makes this definition safe is the tools line: with nothing beyond Read, Grep, and Glob, this subagent has no way to change a file even if it does accept an injected instruction. How to constrain a role this way is covered in Subagents.

Enumerate what is allowed, not what is forbidden

Design permissions as an allowlist. A denylist fails because the set of harmful commands is open-ended: every entry describes one way to cause damage, and the next one is not on the list. An allowlist describes the workflow, which is finite and known. A shell tool is different in kind: allowing it once means allowing every command, rm and curl included. So permit the shell not as a whole tool but in units of command patterns — which commands may run. Patterns, though, can match more than they appear to. A pattern meant to allow nothing but viewing diffs can, depending on how it is written, also match a command that launches arbitrary programs — a rule that looks read-only becomes a path to execution. Subagents covers how to write patterns so the match stops where it looks like it stops.

Separate roles by what they may change

Split work so that the roles which read, judge, and report hold no write permissions, and the roles which change files hold the narrowest write scope that completes the task. This keeps the surface legible: the question “what could this step have done?” has an answer you can read off the tool list. Written as a declaration, the difference between the two roles is one line. For a skill, Claude Code takes it as allowed-tools in the frontmatter of SKILL.md:
Neither list contains a tool the role does not use. The reads-and-judges role has no Edit, so it cannot change a file. The role that makes the change does hold Edit, but no push or delete commands: when it gets something wrong, the damage stays in the local diff and is undone with a normal operation.

Bound the reach, not just the tool list

A tool list decides which operations are allowed, and stops there. Allowing Edit says nothing about which files may be edited. The reach of each operation has to be bounded separately, in two dimensions — paths and hosts — and both are the routes an injected instruction would try to use. Which paths. Limit writes to the working directory and its subdirectories. Without that boundary, a single edit can land in a parent directory, another checkout, or a configuration file in the home directory. Reads need the same boundary: an agent that can read anywhere can load a credential file into its context without changing a single file. The one exception worth opening is a temporary directory for scratch data — it keeps intermediate files out of the diff, so what the role actually produced stays easy to read. Which hosts. An agent that can reach the network can send what it has read somewhere. So decide not just whether it may go out, but which hosts it may go out to. Keep web-fetching commands outside the auto-approved set by default, and where the runtime supports it, list the allowed destinations in advance. With Claude Code as the example, both ranges can be written as permission rules: paths go on Edit and Read rules, hosts on WebFetch domain rules.
.claude/settings.json
The allow side lists the paths that may be edited and the hosts that may be fetched. The deny side blocks reads of files that hold secrets, and closes general-purpose network commands like curl and wget so that traffic goes through WebFetch and its allowlisted domains. Deny rules are not a substitute for the allowlist — use them as a backstop that reliably seals known dangerous routes, like secret files and general-purpose network commands. For both dimensions, having the runtime environment enforce the boundary beats describing it in a rule file: even when a pattern is written too loosely, the environment’s boundary still stands.

Designing the approval gates

Approval decides which of the permitted actions a human sees before they run. Where permissions bound what the agent can do at all, approval picks the points where a person confirms it.

Choose approval gates by reversibility

Ask for confirmation on everything and the person doing the approving wears out. Put approval only on actions that are hard to undo, not on the ones that are frequent. Local, reversible edits are the natural candidates for auto-approval: a wrong one is visible in the diff and undone with a normal operation. The three kinds of rule are written to different standards, and the difference is deliberate.
  • Auto-approve rules must be exact or end at a word boundary. A pattern that matches more than it appears to grants what nobody reviewed.
  • Confirmation rules can err wide. Matching too much costs one extra prompt.
  • Refusal rules cannot err wide. A refusal blocks without prompting, and a narrower allow rule does not carve out an exception, so an over-broad refusal stops legitimate work until someone edits the rule itself.

Know what widening auto-approval costs

Broadening the auto-approved set is a legitimate trade — it is how a long task runs without stalling. What it spends is the moment a human sees the concrete action before it happens. After widening, an action that was risky-and-visible becomes risky-and-silent: what remains is the tool list, whatever the run leaves behind in logs and diffs, and whatever automated check the tool substitutes for the human — some agents screen auto-approved actions with a separate classifier. Two conditions make that trade reasonable: the run is isolated, so a bad action’s blast radius stops at a container or a dedicated working directory, and the result is inspected as a diff before it reaches anywhere shared. A dedicated worktree gives both at once — a separate directory to work in and a branch whose result arrives as an ordinary diff:
Run wide-open, but bounded
The worktree mechanics are covered in Agentic Workflow; what matters here is that widening auto-approval and isolating the run are the same decision, made together.

Vet what you connect

Connecting an MCP server grants capability too. The tool list shows names; what those tools do is decided by the server. An unvetted server has two properties the tool list does not reveal.
  • Its startup command runs first. It executes before any tool is called, so an entry in a configuration file is itself an execution path.
  • It runs with the client’s privileges. Whatever files, network, and credentials the user can reach, the server can reach too.
The MCP specification treats both as considerations for local servers, and recommends showing the exact command before it runs and starting servers with restricted file system and network access. What you verified at connection time also drifts out of date. Tool sets and descriptions change on update, and a remote server can change its behavior with nothing changing on your side — yet nothing ever asks for approval a second time. Three practices keep this in check.
  • Prefer first-party servers and ones the team controls.
  • Read what the install command actually fetches and runs.
  • Give each server the narrowest credential that works.
Permissions on the agent’s side decide which capabilities it may use. Permissions on the other side — what a connected server’s credentials allow in the backing system — are a separate control, covered in MCP.

Handling secrets

A secret that enters the agent’s context has been exposed, and no later action takes it back. The practices below are about keeping values out, not about cleaning up after. Never print the value. Not into the conversation, not into a log, not into a file or a commit. Partial output is not a safe middle ground: a masked prefix still narrows the value, and the same habit eventually prints the whole thing. Check existence without revealing content. Verifying that a credential is configured is a different operation from displaying it, and only the first one is needed to decide whether a step can run.
Checking a credential is set
Pass references, not values. The agent needs the name of an environment variable or a secret-manager entry, not what it contains. A command that reads the variable at execution time keeps the value out of the text the agent processes. A local .env file holding real values is the same exposure in slower form. The file sits in the working tree, any role with file-read access can open it, and a single read puts the value into the context. Keep the references in the file and the values in a secret manager. The example uses the 1Password CLI. The env file holds op://-style references in place of the values; settings that are not secrets stay literal:
.env
A reference names the vault, the item, and the field, with an optional section between the item and the field — op://Development/aws/Access Keys/access_key_id reads the access_key_id field of the Access Keys section. Quotes are stripped before the value is evaluated, so wrapping a reference that contains a space keeps it unambiguous. Then start the agent through op run. It resolves every op:// reference in the env file and makes the results available to the subprocess for its lifetime as environment variables — the values exist only inside that process:
Injecting values when the agent starts
Nothing changes on the agent’s side — it still reads the same environment variables — but the values are never written to disk, and a file that holds only references has no secret to leak when it is read, committed, or shared. op run additionally masks secrets that reach stdout or stderr by default, which is a backstop rather than a reason to relax the rules above about printing values. Assume anything in context persists. Session history, transcripts, summaries, and any log of the run may retain what passed through. A value pasted once is present for the remainder of the session and in whatever that session writes down. Know where data crosses a boundary. Connected external tools, web fetches, CI logs, and comments posted to a repository all move content out of the local environment. Decide deliberately what is allowed to cross each one, rather than discovering the answer from an incident.
If a secret does reach the context, treat it as disclosed and rotate it. Deleting the message, clearing the session, or truncating the log does not undo the exposure, because copies exist wherever that text was carried.
Automated scanning is the backstop, not the control: a pre-commit hook or a CI check that looks for credential-shaped strings catches what slipped through, and catching it late is still better than shipping it. A pattern-based check has both false positives and false negatives, so treat a hit as a prompt to look rather than a verdict, and never rely on a clean run as proof that no secret is present.

Operational practices

Individual care does not survive a team. Four practices turn the decisions above into something a group maintains. Keep permission settings in the repository. Check the agent’s permission configuration into the repository, so every member and every automated run starts from the same boundary, and changes to it arrive as a reviewable diff. The split that makes this work is between the settings everyone shares and the overrides that stay on one machine:
Permission settings under version control
Committing the shared file means a widened permission is a diff someone approves, not a change one person makes on their own machine and nobody else sees. Keep a record of what ran. Logs of the commands, tools, and external calls a run made are what makes an incident reconstructable. Without them, the question “what did it touch?” has no answer, and the response has to assume the worst. Contain the work so rollback is ordinary. Run the agent on a branch or a dedicated working directory, review its result as a diff, and let reverting be a normal repository operation. A workflow whose output lands directly somewhere shared has no cheap undo.
Undoing an agent's work
Re-review when capability changes. An agent’s reach grows when a server is connected, a plugin is installed, or a permission rule is widened — each of those moments is a review point, not just the initial setup. When something does go wrong, the response has three parts: rotate any credential that was exposed, revert the change through the repository, and narrow the rule that allowed the action. Skipping the third leaves the same path open.
These controls sit inside a larger delegation design — which work goes to an agent, what a human verifies, and where the boundary of responsibility falls. That model is covered in Agentic Workflow.

MCP

Connecting external systems is where untrusted responses and credentials enter an agent’s context.

Subagents

The concrete way to constrain a role by capability — tool scopes, read-only roles, and safe command patterns.

Plugins

How permission settings and agent extensions are distributed to a team, and what to review before installing them.