Why a line in the prompt is not a guardrail
In February 2026, during an internal red-team exercise, an Anthropic researcher phished an employee into starting Claude Code with a malicious prompt. It came in a "can you run this for me?" email and read like routine task instructions. One step asked Claude to read ~/.aws/credentials, encode the contents and POST them to an outside server. Across 25 retries, Claude sent them 24 times.
The model had no reason to refuse. The instruction came through the user, and Anthropic's model-layer defenses anchor on what the user intends, so there was nothing for a classifier to flag. In its post on how it contains Claude, Anthropic says only the environment holds in a case like this. Egress controls block the POST whatever the intent, and filesystem boundaries keep ~/.aws out of reach.
Asking a person first does not fix it. Claude Code used to ask for permission at each turn, and Anthropic's telemetry showed users approved about 93% of those prompts. The post gives the reason: the more approvals a user sees, the less attention they pay to each.
A guardrail that holds is a limit set outside the model. It decides which files the agent can read and write, which hosts it can reach, which tool calls run without a person and which checks run in code first. Anthropic sums up its own approach in one line. "Design for containment at the environment layer first, then steer behavior at the model layer."
Before you start
This is the last lesson of the harness engineering course. It picks up where long-running agents stopped, at a restart loop with --dangerously-skip-permissions, the flag that switches every prompt off. Everything here is about what goes around that flag.
Claude Code's allow, ask and deny rules have their own lesson in Claude Code settings, and hooks in Claude Code covers shell hooks in a settings file. This lesson puts the same ideas inside your own code, through the Claude Agent SDK in Python and the OpenAI Agents SDK.
Three parts to defend, and the one that holds when the model is fooled
Anthropic defends three parts of an agent. The environment is the sandbox, virtual machine, filesystem boundary and egress control around it. The model holds system prompts, classifiers, probes and training. The third is the outside content the agent can reach through MCP servers, plugins and web search. There, narrower tool permissions limit the damage: an agent with read-only database access can be deployed far more widely than one that writes to production.
Only the environment sets a hard boundary. Because models are probabilistic, Anthropic writes, measures at the model layer shape only what the agent tends to do. Its best numbers are still above zero. An attacker who adapts also does far better than one who tries once.
Permission rules and hooks sit in between, in the agent harness. A check written in code holds whatever the model believes. A person clicking yes for the hundredth time is a weaker check.
93%
of permission prompts that Claude Code users approved
Source: Anthropic: How we contain Claude across products (read November 6, 2026)
84%
fewer permission prompts once Bash ran in Claude Code's sandbox, in Anthropic's own use
Source: Anthropic: Making Claude Code safer with sandboxing (read November 6, 2026)
83%
of overeager actions that auto mode catches before they run
Source: Anthropic: How we contain Claude across products (read November 6, 2026)
5 to 6%
prompt-injection success against Claude Opus 4.7 after 100 adaptive attempts on Gray Swan's benchmark, against 0.1% on one try
Source: Anthropic: How we contain Claude across products (read November 6, 2026)
| What Anthropic defends | Its defenses | Holds when the model is fooled? |
|---|---|---|
| The environment | Process sandbox, virtual machine, filesystem boundary, egress control | Yes, within what it covers |
| The model | System prompt, classifiers, probes, training | Not reliably |
| The outside content it reaches | Narrower tool permissions, such as read-only database access | It limits the damage |
Lock down a coding agent: sandbox, scoped rules, an allowlist
Anthropic's autonomous-coding quickstart, the worked example of the last lesson, writes its guardrails into a settings file before the first session starts. The block below is copied from its client.py with the comments left in. PUPPETEER_TOOLS is defined above it in that file as a list of seven Puppeteer MCP tools. Three defenses sit in it, and a fourth sits in a hook.
security_settings = {
"sandbox": {"enabled": True, "autoAllowBashIfSandboxed": True},
"permissions": {
"defaultMode": "acceptEdits", # Auto-approve edits within allowed directories
"allow": [
# Allow all file operations within the project directory
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Glob(./**)",
"Grep(./**)",
# Bash permission granted here, but actual commands are validated
# by the bash_security_hook (see security.py for allowed commands)
"Bash(*)",
# Allow Puppeteer MCP tools for browser automation
*PUPPETEER_TOOLS,
],
},
}- Turn the sandbox on. Bash commands then run inside an OS-level sandbox, bubblewrap on Linux and Seatbelt on macOS, and
autoAllowBashIfSandboxedlets them run without a prompt. Anthropic publishes the runtime as sandbox-runtime. - Scope the file tools to paths under
./, so Read, Write, Edit, Glob and Grep stay inside the project directory. - Set
defaultModetoacceptEdits. Edits inside the allowed directories then go through without asking. - Allow
Bash(*)only because a hook checks every command against a list of 18 entries, fromlsandgittonpmandinit.sh. - Add checks for the risky entries. In security.py,
pkillmay stop only node, npm, npx, vite or next,chmodmay only add+x, andinit.shmust be run by a path such as./init.sh, not by name alone.
The sandbox covers Bash and what it starts. The SDK's type definitions say the file tools and WebFetch follow permission rules instead, and they list two defaults to know. The sandbox is off until you enable it, on macOS and Linux only. allowUnsandboxedCommands is true, so a command can ask to run outside the sandbox unless you set it to false.
Check every tool call with a hook
The Claude Agent SDK's README defines a hook as a Python function that the Claude Code application, not Claude, calls at set points of the agent loop. The model never decides whether a hook runs, and no prompt can talk its way past one. A PreToolUse hook sees the tool name and its input before the call runs, and it can deny the call with a reason the model reads.
The example below is copied from that README, without the two test queries that follow it. It denies any Bash command that contains foo.sh.
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
async def check_bash_command(input_data, tool_use_id, context):
tool_name = input_data["tool_name"]
tool_input = input_data["tool_input"]
if tool_name != "Bash":
return {}
command = tool_input.get("command", "")
block_patterns = ["foo.sh"]
for pattern in block_patterns:
if pattern in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Command contains invalid pattern: {pattern}",
}
}
return {}
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
],
}
)- A list of what to block is the weaker design in our view, because one command can be written many ways. The quickstart's hook works the other way round: it pulls every program out of a command line and blocks the call if any one is missing from its list.
- A command it cannot parse is blocked too, with the reason "Could not parse command for security validation". When a check cannot decide, it says no.
allowed_toolstrips people up. The README calls it a permission allowlist: listed tools run without asking, and the rest fall through topermission_modeandcan_use_tool. It removes nothing from Claude's toolset. To take a tool away, list it indisallowed_tools.
Tool guardrails in the OpenAI Agents SDK
The OpenAI Agents SDK lesson covers input and output guardrails and needs_approval. For an agent that acts, the piece that matters most is the tool guardrail. It wraps one function tool and runs on every call to it. An input check can skip the call or raise a tripwire, and an output check can replace the result.
The example is copied from the SDK's guardrails guide on GitHub. One check refuses arguments that contain sk-, and the other refuses output that does.
import json
from agents import (
Agent,
Runner,
ToolGuardrailFunctionOutput,
)
from agents.decorators import tool, tool_input_guardrail, tool_output_guardrail
@tool_input_guardrail
def block_secrets(data):
args = json.loads(data.context.tool_arguments or "{}")
if "sk-" in json.dumps(args):
return ToolGuardrailFunctionOutput.reject_content(
"Remove secrets before calling this tool."
)
return ToolGuardrailFunctionOutput.allow()
@tool_output_guardrail
def redact_output(data):
text = str(data.output or "")
if "sk-" in text:
return ToolGuardrailFunctionOutput.reject_content("Output contained sensitive data.")
return ToolGuardrailFunctionOutput.allow()
@tool(
tool_input_guardrails=[block_secrets],
tool_output_guardrails=[redact_output],
)
def classify_text(text: str) -> str:
"""Classify text for internal routing."""
return f"length:{len(text)}"
agent = Agent(name="Classifier", tools=[classify_text])
result = Runner.run_sync(agent, "hello world")
print(result.final_output)Scope is the catch. Agent-level input guardrails run only for the first agent in a chain, which is why the guide points to tool guardrails for checks that must hold after a handoff. Hosted tools such as WebSearchTool and HostedMCPTool skip the pipeline, and so do ShellTool, ApplyPatchTool, ComputerTool and LocalShellTool. Guard those with approvals or with where they run. By the human-in-the-loop guide, approval for a local shell or file patch is opt-in, with needs_approval false by default.
Prompt injection and the lethal trifecta
OpenAI's guide to safety in building agents defines prompt injection as untrusted text or data entering an AI system, with malicious contents that try to override its instructions. Untrusted means anything you did not write yourself. A web page counts, and so do an email, an issue comment and a tool result.
Simon Willison named the dangerous mix in June 2025: the lethal trifecta. An agent with access to private data, exposure to untrusted content and a way to send data out can be tricked into leaking that data. Take away any one of the three and this attack has no path. We would start with the way out, since few agents need to reach every host.
The way out is wider than an allowlist suggests. Claude Cowork's egress allowlist let traffic to api.anthropic.com through, because the product needs Anthropic's API. A file planted in a user's workspace carried hidden instructions and an attacker's API key. Claude read other files and uploaded them through Anthropic's Files API, into the attacker's account. Anthropic's conclusion is that every function reachable through any domain on an allowlist is now an attack surface.
Content also arrives through tools you trust. A GitHub connector can load a poisoned README straight into the model's context even after it passes malware checks. OpenAI wrote its guide for Agent Builder, which it shuts down on 30 November 2026, and the advice fits any agent:
- Pass untrusted input in user messages, never in developer messages, which take precedence.
- Put structured outputs between steps, such as enums and fixed schemas, so no free text is left to smuggle instructions through.
- Keep approvals on for MCP tools, for reads as well as writes.
- Pull only the fields you need out of outside input, so untrusted data never directly drives what the agent does.
The guide ends on its limit. Structured outputs and isolation greatly reduce this risk, and they do not fully remove it.
What never to let an agent do alone
This is our list of actions to keep behind a hard limit or a person, each tied to a failure or a setup in the sources above. One question decides each row. If the model were fooled right now, what could this action do?
| Action | Guardrail | From |
|---|---|---|
| Read cloud credentials or SSH keys | Keep them outside the sandbox, so there is nothing to read | Anthropic's red-team exercise |
| Send data to a host it has not used | An egress allowlist and a proxy that asks about each new domain | Claude Code sandboxing |
| Push to a git remote | A proxy that checks a scoped credential and the target branch, then adds the real token | Claude Code on the web |
| Run any shell command | An allowlist hook that blocks what it cannot parse | The autonomous-coding quickstart |
| Cancel an order or send an email | needs_approval, with a person who approves or rejects the call | OpenAI Agents SDK |
| Run with every prompt off | A container with nothing worth stealing in it | The C compiler post |
What Simon Willison and Boris Cherny posted
Simon Willison, co-creator of Django and creator of Datasette, posted the lethal trifecta on X on 16 June 2025, quoted below. It speaks to anyone who uses AI agents, which the post defines as LLMs that call tools, so the warning covers a chat you wire tools into as much as an agent you build.
Boris Cherny, who created Claude Code, posted about auto mode on 16 April 2026, the day Opus 4.7 launched and Anthropic extended auto mode to Max users. His post ties it to Opus 4.7. Anthropic's post on how it built auto mode is dated 25 March 2026. In Anthropic's post on containing Claude, permission prompts go to a model-based classifier, and a command it judges safe is approved without asking. Read it next to the chart above. The classifier cuts the prompts a person sees, and the sandbox is what holds when it misses.
Any time you combine access to private data with exposure to untrusted content and the ability to externally communicate an attacker can trick the system into stealing your data
Screenshots of X's own embeds of the posts, taken on 6 November 2026, with the time in UTC. Select one to open the post on X. X cuts both posts short with Show more.
Common mistakes
- Writing "never read .env" in the system prompt and calling it a guardrail.
- Allowing
Bash(*)with no hook behind it. - Listing a tool in
allowed_toolsand expecting the others to disappear. - Leaving API keys or cloud credentials where the agent can read them.
- Adding a domain to the allowlist without asking what else that domain lets the agent do.
- Pasting web pages, emails or tool output into the developer message.
- Showing so many approval prompts that nobody reads them.
- Running
--dangerously-skip-permissionson your own machine.
Try it yourself
- Run
/sandboxin Claude Code, then ask for a command that reaches the network and see what happens. - Copy the README hook, add
rm -rftoblock_patternsand ask the agent to clear a scratch folder. - Put a line in a test repository's README that tells the agent to print an environment variable. Ask a sandboxed agent to summarize the repository and check whether it obeyed.
- Attach
block_secretsto a tool, call it with an argument that containssk-testand read what the model gets back.
Next course: AI security
That closes the harness engineering course. The next course, AI security, opens with prompt injection: instructions hidden in a web page, an email or a file that turn your agent against you, and the designs that limit the damage. The AI evals course after it measures what your guardrails catch.
Guardrails need the same measuring. Count the safe commands your hook blocks and the risky actions it lets through. Anthropic reports the same two rates for auto mode: benign commands blocked, and overeager actions let through. Count again each time you change a rule.
Check yourself
In Anthropic's February 2026 red-team exercise, what would have stopped the stolen credentials from leaving?
Show answer
B. Egress controls, and a filesystem boundary that keeps ~/.aws out of reach The instruction came through the user, so the model layer had nothing to flag. Only limits in the environment hold in that case.
What does allowed_tools do in the Claude Agent SDK?
Show answer
D. Lets listed tools run without asking, while the rest fall through to the permission mode It is a permission allowlist and removes nothing from the toolset. A tool is taken away with disallowed_tools.
Which three things make Simon Willison's lethal trifecta?
Show answer
A. Private data, untrusted content and a way to send data out An agent that has all three can be tricked into leaking the data. Remove one and that attack has no path.
What does the quickstart's Bash hook do with a command it cannot parse?
Show answer
C. Blocks it It fails closed, with the reason "Could not parse command for security validation".
Frequently asked questions
What are AI agent guardrails?
Anything that limits or checks an agent's actions, from a sandbox and an egress allowlist to a hook, a tool guardrail and a person who approves a call. Sort them by one test: does the limit still hold if the model is fooled?
Is --dangerously-skip-permissions safe to use?
Only somewhere you can throw away. Nicholas Carlini ran his compiler agents with it and told readers to use a container, not their own machine. Keep credentials out of that container and limit where it can connect.
Can a classifier stop prompt injection?
It lowers the rate. Anthropic writes that any probabilistic defense has a non-zero miss rate. Both incidents it learned most from were data leaving through a permitted path, where the model saw nothing unusual.
What is the difference between a permission rule and a sandbox?
The agent harness checks a permission rule against the tool call as written, before the tool runs. The operating system enforces a sandbox on Bash and every process it starts, so a script that opens a file on its own still hits the wall.
Is Claude Code auto mode safe without a sandbox?
Anthropic does not present it that way. Its classifier blocks roughly 0.4% of benign commands and lets about 17% of overeager actions through, so the post calls it one layer of defense inside a sandbox, not a substitute for one.
Sources
- Anthropic: How we contain Claude across products https://www.anthropic.com/research/how-we-contain-claude
- Anthropic: Making Claude Code safer with sandboxing https://www.anthropic.com/research/claude-code-sandboxing
- GitHub: anthropics/sandbox-runtime https://github.com/anthropics/sandbox-runtime
- Anthropic Engineering: Building a C compiler with a team of parallel Claudes https://www.anthropic.com/research/building-c-compiler
- GitHub: anthropics/claude-quickstarts, autonomous-coding README https://github.com/anthropics/claude-quickstarts/tree/main/autonomous-coding
- GitHub: autonomous-coding client.py security settings https://github.com/anthropics/claude-quickstarts/blob/main/autonomous-coding/client.py
- GitHub: autonomous-coding security.py command allowlist https://github.com/anthropics/claude-quickstarts/blob/main/autonomous-coding/security.py
- GitHub: anthropics/claude-agent-sdk-python README https://github.com/anthropics/claude-agent-sdk-python
- GitHub: claude-agent-sdk-python types.py https://github.com/anthropics/claude-agent-sdk-python/blob/main/src/claude_agent_sdk/types.py
- GitHub: openai/openai-agents-python, guardrails guide https://github.com/openai/openai-agents-python/blob/main/docs/guardrails.md
- GitHub: openai/openai-agents-python, human-in-the-loop guide https://github.com/openai/openai-agents-python/blob/main/docs/human_in_the_loop.md
- OpenAI Developers: Safety in building agents https://developers.openai.com/api/docs/guides/agent-builder-safety
- Anthropic: How we built Claude Code auto mode https://www.anthropic.com/research/claude-code-auto-mode
- Anthropic: Introducing Claude Opus 4.7 https://www.anthropic.com/news/claude-opus-4-7
- X: Simon Willison on the lethal trifecta https://x.com/simonw/status/1934602159984984235
- X: Boris Cherny on Claude Code auto mode https://x.com/bcherny/status/2044847849662505288


