Managing Security

A local coding agent can read your files, run commands, and reach the network. This chapter is about understanding the risks and benefits of allowing that access, deciding what may happen without asking, what requires approval, and what may never happen. It also covers the critical difference between a rule the harness follows and a boundary the operating system enforces.

Why security matters

Claude Code and Codex can both run directly on your machine with access to your shell, filesystem, and network. This is what makes them powerful: they can read code, run commands, edit files, and install packages. That same access creates real risks, overlapping substantially with ordinary cybersecurity risks. Think about granting an agent access to your computer much as you would think about granting a person access to it. Either way, through malice or mistakes, harm is possible.

Information security conventionally sorts those harms into three, known as the CIA triad — no relation to the intelligence agency, just an unfortunate coincidence of initials. NIST SP 800-12 is the standard reference. All three apply here:

  • Confidentiality — information reaching someone who should not have it. An agent may be able to read anything your user account can read: API keys, credentials, SSH configs, environment variables, unpublished results, and on a shared system, other people’s work. Anything sent to the model API has left your machine.
  • Integrity — information being changed when it should not be. An agent may overwrite the wrong file, modify data in place, introduce a subtle error into an analysis, or commit something that breaks a pipeline other people depend on. The dangerous case is not the change you notice; it is the one you do not.
  • Availability — losing access to something you need. Deleted work, an exhausted storage quota, a cluster account suspended for a policy violation, a week of compute burned by a runaway job. On shared infrastructure this lands on your colleagues as much as on you.

Prompt injection cuts across all three and is worth understanding separately, because it is a mechanism rather than an outcome. Instructions hidden in a file, a web page, a dependency, or a tool’s output can redirect what an agent does—a cloned repository whose README tells it to send credentials somewhere, for instance. Anything an agent reads is potentially an instruction and not merely data, which is why untrusted code and fetched web content deserve particular care.

The permission system described below is your primary defense. It lets you decide which actions an agent can take automatically, which require approval, and which are blocked entirely. Permission rules constrain what the harness will execute; only a sandbox or other system boundary constrains what a running command can reach.

Harness-level control

Both harnesses expose controls for routine autonomy, approvals, filesystem access, and network access:

Control Claude Code Codex
Persistent configuration ~/.claude/settings.json ~/.codex/config.toml
Interactive control Permission modes and allow/ask/deny rules Sandbox and approval policies
Inspect or change during a session /permissions /permissions
Restricted planning Plan mode read-only sandbox
Normal autonomous work Accept edits or Auto workspace-write with on-request or automatic review
Remove harness restrictions bypassPermissions danger-full-access with approvals bypassed

The Coding Agents permission comparison gives the concise Codex flag and profile reference. The detailed configuration below uses Claude Code because this repository ships a Claude Code settings.json example; the security principles and system-level controls apply equally to both.

They share one limit worth holding onto: they govern the agent. A command that does run is not constrained by them, and neither is a process that command spawns. That is what System-level control is for.

Claude Code: configuring permissions with settings.json

Claude Code uses settings.json files to control what actions Claude can take. This is how you restrict dangerous commands, protect sensitive files, and tailor permissions per project or environment.

Settings file locations

There are three places you can put a settings.json:

Scope Location Purpose
User ~/.claude/settings.json Personal defaults, applied to all projects
Project (shared) .claude/settings.json Team settings, committed to git
Project (local) .claude/settings.local.json Personal project overrides, gitignored

An organization can also deploy managed settings (e.g. /etc/claude-code/managed-settings.json on Linux), which outrank everything below.

Priority order

When the same setting appears at multiple levels, higher-priority scopes win:

  1. Managed settings — highest; not even command-line flags override these
  2. Local project (.claude/settings.local.json)
  3. Shared project (.claude/settings.json)
  4. User (~/.claude/settings.json) — lowest

Permission arrays (allow, ask, deny) merge across scopes rather than replacing each other, so restrictions accumulate. If a tool is denied at any level, no other level can allow it.

One exception worth knowing: "defaultMode": "auto" in a project’s .claude/settings.json or .claude/settings.local.json has no effect. Auto mode can only be set from user settings, managed settings, or the --permission-mode flag.

The permissions object

Permissions are defined in three arrays inside settings.json:

{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": [ ... ],
    "ask": [ ... ],
    "deny": [ ... ]
  }
}
  • allow — Claude can use these tools without asking
  • ask — Claude will prompt you for confirmation each time
  • deny — Claude is blocked from these entirely

Rules are evaluated in order: deny > ask > allow. A deny rule always wins over an allow rule at the same scope.

Permission modes

A permission mode sets Claude’s baseline behavior — how often it pauses to ask before editing a file, running a command, or making a network request. You can cycle modes mid-session with Shift+Tab in the CLI (or the mode selector in VS Code, JetBrains, Desktop, and claude.ai), start in a mode with claude --permission-mode <mode>, or set a persistent defaultMode in settings.json. For the full reference, see the official permission modes documentation.

Mode What runs without asking Best for
"default" Reads only Reviewing every action yourself, sensitive work
"acceptEdits" Reads, file edits, and common filesystem commands (mkdir, touch, mv, cp, rm, sed) inside your working directory Iterating on code you’re reviewing
"plan" Reads, plus classifier-approved commands when auto mode is available Exploring a codebase before changing it
"auto" Everything, with a classifier reviewing each action Long tasks, reducing prompt fatigue
"dontAsk" Only pre-approved tools Locked-down CI and scripts
"bypassPermissions" Everything Isolated containers and VMs only

The "default" mode is labeled Manual in the CLI, the VS Code and JetBrains extensions, and the desktop app. Its config value is still default, and manual is accepted as an alias wherever you type it.

For most day-to-day work, prefer auto mode — on Pro, Max, and Team plans it is now the mode sessions start in by default. See Auto mode below for how it decides.

What no mode changes:

  • deny rules apply in every mode, bypassPermissions included. It is allow rules that stop having any effect there. A deny rule is the one control that holds no matter how the session is launched.
  • Explicit ask rules always prompt, even in auto.
  • Writes to protected paths are never auto-approved except under bypassPermissions. See Protected and critical paths.
  • rm and rmdir against a critical path are never approved by an allow rule or a hook, in any mode. Same section.

Auto mode

Auto mode lets Claude work in long uninterrupted stretches. A separate classifier model reviews each action before it runs and blocks anything that escalates beyond your request, targets unrecognized infrastructure, or appears driven by hostile content Claude read in a file or web page. You get far fewer prompts than Manual mode without surrendering the safety net that bypassPermissions removes entirely.

A few properties are worth understanding before you rely on it:

  • The classifier does not see tool results. It sees your messages, non-read-only tool calls, and your project’s standing instructions — but the contents of files and web pages are stripped out. That is what makes it resistant to the prompt injection it is meant to catch.
  • Broad allow rules are dropped on entering auto mode. A blanket Bash(*), a wildcarded interpreter like Bash(python*), or a package-manager run command stops applying, because those amount to arbitrary code execution. Narrow rules like Bash(pytest) stay in effect and are restored when you leave the mode.
  • Subagents are checked too, at spawn, on each action, and again on the results they return.
  • It costs something. The classifier adds a round-trip before shell and network commands. Reads and working-directory edits skip it.

It is not a substitute for review on sensitive operations. Use it where you trust the general direction of the work.

bypassPermissions mode

bypassPermissions mode disables permission prompts and safety checks so tool calls execute immediately. Start in it from the CLI:

claude --permission-mode bypassPermissions

(The older --dangerously-skip-permissions flag is equivalent and still works.)

bypassPermissions offers no protection against prompt injection or unintended actions — Claude will execute any command, edit any file, and access any resource without asking. Malicious content hidden in a cloned repo, a fetched web page, or a tool output can hijack the session with nothing to stop it. Only use it behind a system-level boundary — a container, a virtual machine, or a dedicated machine — where there is nothing sensitive to protect and nothing important to break. Never use it on your host machine or a shared system.

For long, mostly-unattended runs where you still want a safety net, reach for auto mode instead: it eliminates most prompts but keeps a classifier that blocks escalations and injection-driven actions. Use bypassPermissions only when isolation — not the classifier — is what protects you.

Note that the classifier is a per-action check rather than a boundary, so even in auto mode an isolation layer is worth having for unattended work. Under bypassPermissions it is not optional.

Even here, two things still hold: deny rules apply, and rm against a critical path still prompts.

On Linux and macOS, Claude Code refuses to start bypassPermissions as root or under sudo outside a recognized sandbox. The dev container configuration runs as a non-root user, so it works there.

To prevent this mode being used at all — on a shared system, say — set permissions.disableBypassPermissionsMode to "disable" in any settings file. It is most useful in managed settings, but you can also set it in your own to lock yourself out.

Protected and critical paths

Two safety checks sit outside the permission rules entirely, so it is worth knowing they exist before you write a rule that appears not to work.

Protected paths are never auto-approved for writes: .git, .claude, .vscode, .devcontainer, .cargo, and files like .bashrc, .zshrc, .envrc, .npmrc, .mcp.json, and .gitconfig. An allow rule does not pre-approve them — the check runs before allow rules are evaluated. In modes that prompt, the prompt offers to approve .claude/ writes for the rest of the session.

Critical paths are rm/rmdir targets that no allow rule and no PreToolUse hook can approve: the filesystem root and its top-level directories, your home directory, and your working directory and its parents. A glob under a shell variable (rm -rf "$DIR"/*) counts, because an empty variable turns it into a removal from /. Hiding it in $(...) does not evade the check. A matching deny rule still blocks the command outright.

Permission rule syntax

Rules follow the pattern Tool or Tool(specifier).

Bash commands

"allow": [
  "Bash(git status:*)",
  "Bash(conda activate:*)"
],
"ask": [
  "Bash(sbatch:*)",
  "Bash(git push:*)"
],
"deny": [
  "Bash(sudo:*)",
  "Bash(rm -rf /*)"
]

The * wildcard matches any sequence of characters, including spaces, so one wildcard can span several arguments.

A space before the * enforces a word boundary, and leaving it out is the most common mistake in these files. Bash(ls *) matches ls -la but not lsof; Bash(ls*) matches both. Always include the space.

The :* suffix is an equivalent way to write that trailing wildcard, so Bash(ls:*) and Bash(ls *) match the same commands. It is only recognized at the end of a pattern — in Bash(git:* push) the colon is a literal character and matches nothing.

Claude Code understands shell operators, so Bash(safe-cmd *) does not approve safe-cmd && other-cmd; every subcommand must match a rule independently.

File access

Only two tool names take a path: Read and Edit. Edit(...) covers every built-in tool that writes files, and a Read deny rule also blocks writing to the same path. Path rules written for Write, NotebookEdit, Glob, or MultiEdit are accepted but never consulted, and Claude Code warns about them at startup — write Edit(docs/**) rather than Write(docs/**).

File rules use gitignore-style glob patterns:

"allow": [
  "Read(**)"
],
"deny": [
  "Read(~/.ssh/**)",
  "Read(**/.env)",
  "Edit(**/*credentials*)"
]
  • * matches within a single directory
  • ** matches recursively across directories

The leading characters decide where a pattern is anchored, and this catches people out:

Pattern Anchored at
**/.env, ./secrets/** The current working directory
/src/** The directory of the settings file that defines it
~/.ssh/** Your home directory
//etc/** The filesystem root

A rule like Read(**/.ssh/**) in ~/.claude/settings.json does not protect ~/.ssh — it only matches a .ssh directory beneath wherever you launched Claude. For a rule in user settings that should apply everywhere, use the ~/ or // form.

Read and Edit rules apply to Claude’s own file tools and to file commands it recognizes in Bash, such as cat and sed. They do not apply to a script that opens the file itself — a Python program Claude runs can read anything your account can read. For enforcement that covers every process, use the Bash sandbox.

Other tools

"ask": [
  "WebFetch"
],
"deny": [
  "mcp__dangerous-server"
]

System-level control

Everything above depends on Claude behaving as designed. This section does not: the operating system enforces these boundaries, so they hold whether Claude is following your rules, misreading your intent, or acting on instructions injected into a file it read.

That is the distinction to keep. Permission rules decide what Claude chooses to do. Isolation decides what a running command can reach. For unattended work you want both, and the more autonomy you grant at the Claude level, the more the system level has to carry.

The options below run from lightest to heaviest. Anthropic’s sandbox environments guide compares them in more detail.

Approach What it isolates Effort
Bash sandbox Bash commands and their children Minimal on macOS, low on Linux
Dev container The whole environment, with an egress firewall Medium; needs Docker
Separate user account Your own files from the agent’s Low
Virtual machine A full operating system Medium to high
Dedicated machine Everything, physically Low once you have the hardware

The Bash sandbox

Claude Code has a built-in sandbox that confines Bash commands and everything they spawn. The operating system enforces the boundary, so it covers the gap that Read and Edit deny rules cannot: a Python script Claude runs is inside the sandbox too.

By default, sandboxed commands can write only to your working directory and the session temp directory, and the first connection to a new network domain asks for approval.

Turn it on with /sandbox, which opens a panel with three tabs:

  • Modeauto-allow runs sandboxed commands without prompting; regular permissions keeps the normal prompts even inside the sandbox.
  • Overrides — whether a command that fails under the sandbox may retry unsandboxed.
  • Config — the resolved settings.

Selecting a mode in the panel writes to that project’s .claude/settings.local.json. To turn it on everywhere, set it in your user settings:

{
  "sandbox": { "enabled": true }
}

Platform support: macOS uses the built-in Seatbelt framework, with nothing to install. Linux and WSL2 need bubblewrap and socat (sudo apt-get install bubblewrap socat). Native Windows is not supported — run Claude Code inside WSL2 there.

If the sandbox cannot start, Claude Code warns and runs your commands unsandboxed. Bubblewrap needs unprivileged user namespaces, which shared and managed systems commonly restrict, so this is a real possibility rather than an edge case. Run /sandbox and check whether a Dependencies tab appears rather than assuming you are protected. Set sandbox.failIfUnavailable to true to make an unavailable sandbox a hard error instead of a silent fallback.

If you work on an HPC cluster, see Computing at Yale—the stakes are higher there and the sandbox is less likely to be available.

A good pairing for local work: Manual mode plus sandbox auto-allow. You get few prompts, and what you get in exchange is a real kernel-enforced boundary rather than a model’s judgment.

The Bash sandbox constrains Bash and nothing else. Claude Code’s own file tools, any MCP servers you have configured, and any hooks all run as separate processes on your host, outside the boundary. This is enough to make everyday work safer; it is not enough for an unattended session. To put every tool, hook, and MCP server behind one boundary without Docker, run the whole Claude Code process through the sandbox runtime — currently a research preview whose configuration format may still change.

Dev containers

Running Claude Code inside a development container isolates the whole environment, not just Bash — file tools, hooks, and MCP servers included. Claude has full access inside the container but cannot touch your host filesystem, credentials, or network unless you explicitly mount or forward them. For most unattended work on code you trust, this is the right level.

Docker Desktop (macOS/Windows) or Docker Engine (Linux) must be installed on the host.

To get started:

  1. Install VS Code and the Dev Containers extension.
  2. Add a .devcontainer/ directory to your project — ask Claude to set up a devcontainer and the dunnlab-devcontainer skill will scaffold it.
  3. Open the project in VS Code and click “Reopen in Container” when prompted (or use the Command Palette: Dev Containers: Reopen in Container).

The simplest configuration adds the official Claude Code Dev Container Feature to any base image. For a hardened setup with an egress firewall, see the reference implementation.

Only use devcontainers with trusted repositories. While the firewall restricts network access, it does not prevent a malicious project from exfiltrating anything accessible inside the container, including Claude Code credentials.

Separate user accounts

The cheapest meaningful boundary, and the one most people skip. Create a second account on your machine, install Claude Code there, and do agent work signed in as that user. Ordinary filesystem permissions then do the isolating: the agent cannot read your real ~/.ssh, your cloud credentials, your browser profile, or the rest of your home directory, because it is not your home directory.

This costs nothing but the setup and it needs no virtualization. What it does not give you is network isolation, protection for anything you deliberately share with that account, or any defence against a command that escalates privileges. Treat it as raising the floor rather than as a container substitute.

It pairs well with a project directory that both accounts can reach, so you can review the work without switching users.

Virtual machines

A VM gives you a full operating system with its own kernel, which is the strongest separation short of separate hardware. Snapshots make it disposable in a way a container is not: take one before a long run, roll back afterwards if anything looks wrong.

Options run from a local hypervisor, through cloud instances you create and destroy per project, to microVMs. Docker Sandboxes packages a microVM with its own Docker daemon and workspace sync. Claude Code on the web is a managed version of the same idea — each session runs in an Anthropic-managed VM behind a network allowlist, with no infrastructure for you to provision.

This is the right level for genuinely untrusted code, and for any policy that requires kernel-level separation between the agent and your work.

Dedicated machines

The most secure, simplest, and most straightforward boundary is a separate computer with nothing valuable on it. Some risk remains through the network — a machine sitting inside a trusted network can still reach internal services even when it holds nothing itself — but the strategy is clean and effective, and it is my preferred approach when practical.

For long unsupervised coding sessions I use either a dedicated virtual machine in the cloud or an old computer set up with Ubuntu. Both are cheap — a spare laptop that is too slow for daily use is fine, and a small cloud instance costs little if you stop it when idle — and both mean I do not have to think carefully about what a bypassed session could reach. If it destroys itself, I reinstall.

The practical requirements are the same either way. Keep nothing on it you cannot lose, give it credentials scoped to the one project rather than your usual keys, and push work to git rather than trusting the machine to hold it.

Working Across Computers explains how to operate a dedicated machine over SSH, keep the agent alive with tmux, or leave the agent local while sending heavy computation elsewhere.


This site uses Just the Docs, a documentation theme for Jekyll.