Skip to content

Permissions & Security

Every tool call Claude Code makes, reading a file, running a shell command, pushing to git, passes through a permissions gate. By default, Claude asks for approval on the first use of each tool and remembers your answer for the session. But once you add subagents, agent teams, and background sessions, the attack surface multiplies: a subagent with unrestricted Bash access can push to production, a custom agent with overly broad tool patterns can modify files it shouldn't touch, and a background agent running unattended can accumulate changes you don't discover until next week. This lesson covers the full permissions system so you can give agents exactly the access they need and nothing more.

Gnome mascot

What you'll learn

  • The --permission-mode flag controls the approval workflow across six levels: default (ask first time, remember), acceptEdits (auto-approve file edits, ask for everything else), plan (read-only + plan file writes only), auto (approve everything automatically), dontAsk (never ask, just do it), and bypassPermissions (skip all permission checks entirely)
  • Shift+Tab cycles through permission modes during a session, and Alt+P switches the model, both are quick-access shortcuts for changing the security posture mid-work
  • Tool allow/deny lists use glob patterns: Bash(git *) allows all git commands, Bash(git commit *) allows commits specifically, and Bash(npm run lint:*) scopes to linting scripts. MCP tools use mcp__<server>__<tool> syntax
  • Set the Agent tool to deny to prevent any subagent from spawning other subagents, critical for leaf workers and CI/CD pipelines where unbounded delegation wastes credits
  • CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS=1 disables the Explore and Plan built-in agents globally, use in CI where research-only agents are wasteful and in production where you want explicit control over every tool call

The problem

You give Claude Code a task. It spawns a General-purpose subagent. That subagent, trying to be helpful, decides the best way to test the new endpoint is to deploy to staging first. It has your shell access, your git credentials, and your cloud provider CLI config, because General-purpose inherits the parent's tool access. It runs git push and terraform apply. The deploy succeeds. You didn't want a deploy.

This isn't hypothetical. Every tool a subagent can access is a tool it might use. The permissions system exists so you can say, with precision: "This agent can lint and test, but it can never commit. That agent can read database schemas, but it can never write to the database. This background agent can run for 10 minutes, but if it tries to push to git, block it and ask me."

Permission modes

The --permission-mode flag sets the baseline security posture for a Claude Code session. It applies to the session and all subagents spawned within it (unless overridden per-agent).

Mode reference

ModeBehaviorWhen to use it
defaultAsk for approval on first use of each tool category. Remember the answer for the session. Subsequent uses of the same tool in the same category are auto-approvedInteractive development, you want visibility into what Claude is doing but don't want to approve every file read
acceptEditsAuto-approve file edits (Write, Edit). Ask for everything else: shell commands, git operations, network callsPair programming, you trust Claude to write code but want to approve dangerous operations
planRead-only + allowed to write to .claude/plans/ only. No file edits, no shell commands that modify state, no git writesResearch and planning phases, Claude explores and documents, you review before granting write access
autoApprove every tool call automatically. No prompts. Claude runs at full speedScripted or non-interactive sessions: CI/CD pipelines, one-shot commands with -p, background agents you've scoped with tool restrictions
dontAskNever prompt for permission. Claude proceeds without asking, but tool calls are still loggedSimilar to auto but with slightly different internal behavior, use when you want zero interaction and trust the session's scope
bypassPermissionsSkip every permission check, including the ones auto and dontAsk still enforce. No guardrails at allDebugging permissions issues, or when you've externally sandboxed the environment (Docker container, VM) and want no overhead

Switching modes mid-session

Shift+Tab cycles through permission modes while a session is running. You don't need to restart. If you're in default mode reviewing a plan, and you decide to trust Claude with the execution, hit Shift+Tab until you reach acceptEdits or auto. The change takes effect immediately.

Alt+P switches the model mid-session. Combined with Shift+Tab, you have one-hand access to both the security posture and the reasoning power: switch to a cheaper model for simple edits, permission-up for a dangerous operation, then switch back.

Setting permission mode per session

Control the initial security baseline of your session by providing the permission mode flag directly at launch.

bash
# Start with plan mode, research only
claude --permission-mode plan

# Start with acceptEdits, trusted pair programming
claude --permission-mode acceptEdits

# Scripted session, full auto
claude -p "Run the test suite" --permission-mode auto

# Background agent with auto approval
claude --agent-view "Monitor logs for errors" --permission-mode auto

Tool allow/deny lists

Beyond the permission mode, you can define precise tool-level rules. These go in .claude/settings.json under permissions.allow and permissions.deny.

Allowlist syntax

Define explicit allowlist patterns in your project configuration file to restrict precisely which tools Claude can execute.

json
{
  "permissions": {
    "allow": [
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep",
      "Bash(git diff *)",
      "Bash(git log *)",
      "Bash(git status)",
      "Bash(npm run lint:*)",
      "Bash(npm test)",
      "mcp__postgres__query_readonly"
    ],
    "deny": [
      "Bash(git push *)",
      "Bash(git commit *)",
      "Bash(rm *)",
      "Bash(sudo *)",
      "mcp__postgres__query_write",
      "mcp__aws__*"
    ]
  }
}

Rules are evaluated in order: deny rules take priority over allow rules. If a tool matches both allow: Bash(git *) and deny: Bash(git push *), the push is denied. This means you can write broad allows and narrow denies, the safest pattern.

Tool naming reference

ToolPatternWhat it gates
ReadExact nameReading files
WriteExact nameWriting files
EditExact nameTargeted edits
GlobExact nameFile pattern matching
GrepExact nameContent search
Bash(...)Glob inside parensShell commands. Bash(*) , any command. Bash(git *) , any git subcommand. Bash(npm run lint:*) , npm scripts with lint: prefix
TaskExact nameSubagent delegation (spawning subagents)
AgentExact name, or deny to block all delegationSame as Task, controls whether an agent can spawn other agents
mcp__<server>__<tool>Exact match with server and tool namesMCP server tools. mcp__postgres__* gates all tools on the postgres server. mcp__*__* gates all MCP tools

Denying subagent delegation

To prevent an agent from spawning subagents (making it a pure leaf worker), deny the Agent tool:

json
{
  "permissions": {
    "deny": [
      "Agent"
    ]
  }
}

Or in a custom subagent's frontmatter:

yaml
tools:
  - Read
  - Write
  - Edit
  - Agent: deny

This is the single most important security rule for CI/CD pipelines and background agents. A background agent running in auto mode with unrestricted delegation can spawn subagents that spawn subagents, a tree of parallel Claude instances all burning credits on a task that should have been bounded. Deny Agent on every agent that doesn't explicitly need to delegate.

Environment variables that gate agent behavior

VariableEffectWhen to set it
CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS=1Prevents Claude from spawning Explore or Plan built-in agentsCI/CD pipelines where research-only agents waste credits; production environments where you want explicit control over every action
CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1Prevents Claude from updating the terminal titletmux sessions where title updates interfere with pane labels

Build it

Step 1: Start with plan mode for research

Kick off your research session with plan mode to guarantee Claude cannot make any destructive changes to your files.

bash
claude --permission-mode plan

Give Claude a research task:

Research how our authentication system handles token refresh.
Find every file involved, trace the flow from request to response,
and write a plan to .claude/plans/auth-refresh-audit.md.

Claude reads files, searches the codebase, but cannot modify anything (except writing the plan file). You review the plan before granting write access. Hit Shift+Tab to switch to acceptEdits when you're ready.

Step 2: Configure tool deny rules

In .claude/settings.json:

json
{
  "permissions": {
    "deny": [
      "Bash(git push *)",
      "Bash(git commit --amend *)",
      "Bash(rm -rf *)",
      "Bash(sudo *)",
      "Bash(curl *)",
      "Bash(wget *)"
    ]
  }
}

Now even in acceptEdits or auto mode, Claude cannot push to git, amend commits, recursively delete, run sudo, or make network requests via curl/wget. It can still use git diff, git add, git commit (non-amend), and other safe commands.

Step 3: Create a fully restricted subagent

Combine a custom subagent with permission gates for maximum safety:

.claude/agents/safe-linter.md:

markdown
---
name: safe-linter
description: Runs project linters. Read-only operation. Use me for linting tasks.
tools:
  - Read
  - Glob
  - Grep
  - Bash(npm run lint)
  - Bash(npx eslint *)
  - Bash(npx prettier --check *)
  - Agent: deny
model: claude-haiku-4-5-20251001
---

And in .claude/settings.json, lock the Bash patterns to prevent escape:

json
{
  "permissions": {
    "deny": [
      "Bash(npm run lint:*)",
      "Bash(npx eslint --fix *)",
      "Bash(npx prettier --write *)"
    ]
  }
}

This agent can run linting and check formatting, but it can never auto-fix, never modify files, and never spawn other agents. It's a pure leaf worker.

Step 4: Disable Explore and Plan in CI

In your CI pipeline script:

bash
#!/bin/bash
export CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS=1

claude -p "Review the PR diff for security issues" \
  --permission-mode auto \
  --agents .claude/agents/

Explore and Plan are disabled because in CI you want Claude to take action, not research. --permission-mode auto lets it run without interactive approval. --agents points to your project's custom agents (which should have Agent: deny to prevent delegation trees).

Step 5: Verify the rules are enforced

Try to trigger a denied operation:

bash
claude --permission-mode auto

Then:

Run: git push origin main

With Bash(git push *) in the deny list, Claude should refuse, either blocking the call or asking for explicit override. The exact behavior depends on whether the deny is absolute or prompt-level, but the operation will not proceed without your intervention.

What goes wrong

MistakeHow you notice itThe fix
bypassPermissions in a production sessionClaude runs an unintended destructive command with no prompt, files deleted, secrets exposedNever use bypassPermissions outside of debugging or fully sandboxed environments (Docker, VMs). It removes every guardrail. Start with plan or default and work up
Allowlist is too narrow and Claude can't functionEvery tool call fails with "tool not allowed," session is unusableStart with broad allows and narrow denies. deny: [Bash(git push *)] is safer than allow: [Bash(git diff *), Bash(git log *)] , if you forget to allow git status, the agent breaks
Agent: deny on an orchestratorA subagent that needs to delegate can't, and the parent's batch delegation grinds to a haltDeny Agent only on leaf workers, agents that do the work and return. Orchestrators (agents that coordinate other agents) need Agent allowed. If in doubt, start with Agent: deny and remove it only when you prove delegation is necessary
Shift+Tab cycled to auto unintentionallyClaude makes changes you didn't approve, and you realize the mode changedThe current permission mode is shown in the status line. Check it if Claude's behavior seems too aggressive. Shift+Tab cycles, so hitting it one more time brings you back around
Permission rules in settings.json don't apply to a custom subagentA custom subagent with tools: [Bash(git *)] pushes to remote even though settings.json denies Bash(git push *)Custom subagent tools frontmatter is the agent's allowlist, not the session's deny list. The session's deny rules still apply. If the subagent pushes anyway, check that the deny rule uses the exact same syntax (Bash(git push *), not Bash("git push"))

Confirm it worked

Run these commands to validate that your tool restrictions successfully block unauthorized operations while permitting standard tasks.

bash
# 1. Verify plan mode
claude --permission-mode plan
# "Write a new file /tmp/plan-test.txt with the word 'hello'."
# Verify: Claude refuses or explains it's in plan mode. No file is created.

# 2. Verify tool deny
claude --permission-mode auto
# "Run: rm -rf /tmp/nonexistent-test-dir"
# Verify: if rm is in your deny list: Claude refuses.

# 3. Verify Shift+Tab cycle
# Start a session, note the permission mode in the status line.
# Press Shift+Tab three times.
# Verify: the mode changed each time and is reflected in the status line.

# 4. Verify CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS
CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS=1 claude
# "Search for all TODO comments in this project."
# Verify: Claude searches directly, no Explore agent spawns.

# If all four checks pass, the permissions system is configured correctly.

Next: Day 4 , MCP, Plugins & Integration , connecting Claude Code to external systems.