AnyLearn
All lessons
AIadvanced

Permissions, Sandboxes and Hooks: Containing an AI Agent

An agent with a shell can make destructive mistakes or follow injected instructions. Learn the four layers of control, from instruction files to OS sandboxes, why command-pattern rules are not a boundary, how hooks add deterministic checks, and how to design out the lethal trifecta.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 11

The threat model of an agent with a shell

A coding agent runs commands it wrote a moment ago, on a machine that holds your source code, credentials and network access. Two things can go wrong. The model can simply make a destructive mistake. Or text it reads, in an issue, a web page or a dependency's README, can steer it: prompt injection, covered in its own lesson in How Agentic AI systems work.

Simon Willison named the dangerous combination the lethal trifecta. An agent is exposed to data theft when it has all three of:

  1. access to private data
  2. exposure to untrusted content
  3. the ability to communicate externally

Any one or two are manageable. All three together let injected instructions read your secrets and send them somewhere. Willison's advice is to avoid the combination entirely rather than trust a filter to catch attacks, noting that vendors claim to stop "95% of attacks" while "in web application security 95% is very much a failing grade."

Key idea: Harness security is mostly about removing a leg of the trifecta by construction, then making the remaining permissions narrow enough that mistakes stay small.

Full lesson text

All 11 steps on one page, for reading, reference, and search.

Show

1. The threat model of an agent with a shell

A coding agent runs commands it wrote a moment ago, on a machine that holds your source code, credentials and network access. Two things can go wrong. The model can simply make a destructive mistake. Or text it reads, in an issue, a web page or a dependency's README, can steer it: prompt injection, covered in its own lesson in How Agentic AI systems work.

Simon Willison named the dangerous combination the lethal trifecta. An agent is exposed to data theft when it has all three of:

  1. access to private data
  2. exposure to untrusted content
  3. the ability to communicate externally

Any one or two are manageable. All three together let injected instructions read your secrets and send them somewhere. Willison's advice is to avoid the combination entirely rather than trust a filter to catch attacks, noting that vendors claim to stop "95% of attacks" while "in web application security 95% is very much a failing grade."

Key idea: Harness security is mostly about removing a leg of the trifecta by construction, then making the remaining permissions narrow enough that mistakes stay small.

2. Four layers, four enforcers

Controls on an agent differ in who enforces them, and that decides what they can be trusted with.

LayerEnforced byExampleHolds against a confused or injected model?
InstructionsThe model's own compliance"Never push to main" in CLAUDE.mdNo
Permission rulesThe agent programdeny: Bash(git push *)Partly: matches the command text
HooksYour script, run by the agent programA pre-tool check that blocks writes to migrationsPartly: as good as the script's logic
OS sandboxThe operating system kernelFilesystem and network isolationYes, for everything launched inside it

Claude Code's documentation states the first line of this table directly: permission rules are enforced by Claude Code, not by the model, and instructions in a prompt or CLAUDE.md shape what Claude tries to do but do not change what it is allowed to do.

In practice: Use instructions for preferences, permission rules and hooks for policy, and the sandbox for anything whose failure would be a security incident.

3. Permission modes: how much to ask

Agents expose a dial between asking about everything and asking about nothing. Claude Code's documented modes make the spectrum concrete:

ModeBehaviour
planReads and explores; does not edit source files
defaultPrompts on first use of each tool that needs approval
acceptEditsAuto-accepts file edits and common filesystem commands inside the working directories
autoNo routine prompts; a background classifier checks actions such as shell commands and network requests first
dontAskAuto-denies anything that would prompt, except pre-approved tools
bypassPermissionsSkips prompts; documented as only for isolated environments such as containers or VMs

OpenAI's Codex CLI splits the same dial into two settings: a sandbox mode (read-only, workspace-write, danger-full-access) that decides what the process can do, and an approval policy (such as untrusted, on-request, never) that decides when it asks.

Gotcha: dontAsk and bypassPermissions sound alike and are opposites. One refuses whatever is not pre-approved; the other approves almost everything.

4. Rules: deny wins, and order is fixed

Permission rules are patterns over tool calls, kept in settings files you can commit to the repository so the whole team shares them:

{
  "permissions": {
    "allow": ["Bash(npm run *)", "Bash(git commit *)", "WebFetch(domain:docs.python.org)"],
    "ask":   ["Bash(git push *)"],
    "deny":  ["Read(./.env)", "Read(./secrets/**)", "Bash(curl *)"]
  }
}

Claude Code evaluates rules in a fixed order: deny, then ask, then allow, and the first match wins regardless of which rule is more specific. So a broad deny cannot be carved open by a narrow allow. Managed settings deployed by an administrator sit above user and project settings, and no other level, including command-line flags, can override a managed rule.

Two practical consequences follow. Deny lists are the right tool for "never", since nothing downstream can undo them. And allow rules should be as narrow as the work permits: Bash(npm run *) approves every npm script, including ones added to package.json later.

5. Predict: does the curl rule stop exfiltration?

The settings above deny Bash(curl *). Suppose injected text convinces the agent to send the contents of a file to an outside server.

Predict first

Is the agent now unable to reach the network from the shell?

6. The OS sandbox: two boundaries, both required

An operating-system sandbox restricts the agent's processes directly, so it applies to every command and every child process, however it was spelled. Anthropic's post on Claude Code sandboxing describes two boundaries:

  • Filesystem isolation: processes can read and write only approved directories, so an injected agent cannot modify sensitive system files.
  • Network isolation: outbound traffic goes through a proxy that allows only approved domains and asks the user about new ones.

These are enforced with OS primitives, bubblewrap on Linux and Seatbelt on macOS, and Anthropic published the runtime as the open-source sandbox-runtime package. The post is emphatic that you need both: without network isolation a compromised agent can exfiltrate files like SSH keys; without filesystem isolation it could escape the sandbox and gain network access.

Key idea: Network isolation removes the third leg of the lethal trifecta for everything inside the box. That is why it is worth more than any number of command-pattern rules.

7. Fewer prompts can mean more safety

It is tempting to treat every approval prompt as a safety feature. Anthropic's sandboxing post argues the reverse: constantly clicking approve leads to approval fatigue, "where users might not pay close attention to what they're approving, and in turn making development less safe."

A sandbox changes the question from "is this command safe?" to "is this command safe inside these boundaries?", which is nearly always yes. Anthropic reports that in its internal usage, sandboxing reduced permission prompts by 84%. The prompts that remain are the ones that deserve attention: a request to reach a new domain, or to write outside the project.

DesignPromptsAttention per promptProtection if the human clicks yes anyway
Prompt for everything, no sandboxManyLowNone
Sandbox plus prompts only at its edgesFewHighBoundaries still hold

In practice: Measure how often your team approves prompts without reading them. If the answer is "most of the time", the prompts have stopped being a control.

8. Hooks: your own deterministic checkpoint

A hook is a script the agent program runs at a fixed point in its lifecycle. In Claude Code a PreToolUse hook receives the pending tool call as JSON on stdin; exit code 2 blocks the call and whatever the script writes to stderr is shown to the model. This one protects paths that no agent should edit:

#!/usr/bin/env python3
# PreToolUse hook for Edit|Write: refuse writes to protected paths.
import json, sys
from fnmatch import fnmatch

PROTECTED = [".env*", "*/.env*", "*/migrations/*", ".github/workflows/*"]

event = json.load(sys.stdin)
path = event.get("tool_input", {}).get("file_path", "")
rel = path.removeprefix(event.get("cwd", "") + "/")
if any(fnmatch(rel, pat) for pat in PROTECTED):
    print(f"Blocked: {rel} is protected. Propose the change in your reply instead.",
          file=sys.stderr)
    sys.exit(2)   # block; stderr goes back to the model
sys.exit(0)       # no decision; normal permission rules apply

Registered under hooks.PreToolUse with matcher "Edit|Write", it turns a write to db/migrations/0042_drop_users.sql into a refusal with a reason. The documentation specifies the interaction with rules: a blocking hook stops the call even if an allow rule matches, but a hook returning "allow" cannot override a deny rule.

9. One tool call through the layers

Each layer can stop the call; only the last one is enforced by the operating system.

sequenceDiagram
  participant M as Model
  participant H as PreToolUse hook
  participant P as Permission rules
  participant S as OS sandbox
  M->>H: Bash "python upload.py"
  H->>P: exit 0, no objection
  P->>S: allowed by Bash(python *)
  S-->>M: connect to unknown host blocked by network proxy

10. Keep credentials out of reach

The most reliable secret is one the agent's processes cannot read at all. Anthropic's post describes how Claude Code on the web applies this: git credentials and signing keys are never inside the sandbox. A proxy outside the sandbox handles git operations, and the sandbox authenticates to that proxy with a scoped credential, so a compromised session cannot copy the real key.

The same principle scales down to a laptop:

  • Deny reads of secret files (Read(./.env), Read(./secrets/**)) and keep them outside the sandbox's readable paths.
  • Give agents scoped, short-lived tokens: read-only where possible, limited to one repository, expiring after the task.
  • Prefer a proxy or tool that performs a privileged action over handing the agent the credential that performs it.
  • Keep deploy and billing access out of agent sessions entirely; let a human or a separate pipeline hold them.

Gotcha: An environment variable is readable by every process the agent starts. Exporting a production key "just for this session" puts it one env command away from any injected instruction.

11. Designing out the trifecta

Put together, the layers let you decide which leg of the trifecta each agent gives up, instead of hoping a filter catches attacks.

Agent setupPrivate dataUntrusted contentExternal communicationLeg removed
Refactoring agent, sandbox with network offYes (source)LittleNoCommunication
Research agent browsing the web, no repo or secrets mountedNoYesYesPrivate data
Triage agent reading public issues, can comment only via a narrow toolYesYesOnly through one audited toolCommunication narrowed to one channel
Unsandboxed agent with a shell, browser and .envYesYesYesNone: all three legs present

The last row is the default state of many local setups. The other three are ordinary configurations of the tools covered in this lesson.

Try it: For each agent your team runs, fill in this table. Any row with three "Yes" entries needs a sandbox with network isolation, a narrower tool, or a split into two agents that never share context.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Which three capabilities make up Simon Willison's 'lethal trifecta'?
    • Shell access, root privileges and GPU access
    • Private data access, exposure to untrusted content, and external communication
    • Long context, tool use and memory
    • Web browsing, code execution and file writes
  2. A settings file allows Bash(git *) and denies Bash(git push *). What happens when the agent runs 'git push origin main'?
    • It runs, because the allow rule was listed first
    • It runs, because allow rules are more permissive
    • It asks the user, because the rules conflict
    • It is denied, because deny rules are evaluated first and the first match wins
  3. Why does denying Bash(curl *) not prevent data exfiltration?
    • Rules match command text, so other programs or forms like sh -c or python -c still reach the network
    • Deny rules only apply in plan mode
    • curl is always pre-approved
    • The model ignores deny rules it disagrees with
  4. According to Anthropic's sandboxing post, why is effective sandboxing said to need both filesystem and network isolation?
    • Because bubblewrap cannot run without both
    • To reduce prompts by exactly 50% each
    • Without network isolation files can be exfiltrated; without filesystem isolation the agent can escape the sandbox
    • Because macOS requires both for Seatbelt
  5. In Claude Code, what does a PreToolUse hook exiting with code 2 do?
    • Approves the call without a prompt
    • Blocks the tool call and shows the hook's stderr to the model
    • Restarts the session
    • Logs a warning but lets the call run

Related lessons