Appearance
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.

What you'll learn
- The
--permission-modeflag 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), andbypassPermissions(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, andBash(npm run lint:*)scopes to linting scripts. MCP tools usemcp__<server>__<tool>syntax - Set the
Agenttool todenyto 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=1disables 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
| Mode | Behavior | When to use it |
|---|---|---|
default | Ask 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-approved | Interactive development, you want visibility into what Claude is doing but don't want to approve every file read |
acceptEdits | Auto-approve file edits (Write, Edit). Ask for everything else: shell commands, git operations, network calls | Pair programming, you trust Claude to write code but want to approve dangerous operations |
plan | Read-only + allowed to write to .claude/plans/ only. No file edits, no shell commands that modify state, no git writes | Research and planning phases, Claude explores and documents, you review before granting write access |
auto | Approve every tool call automatically. No prompts. Claude runs at full speed | Scripted or non-interactive sessions: CI/CD pipelines, one-shot commands with -p, background agents you've scoped with tool restrictions |
dontAsk | Never prompt for permission. Claude proceeds without asking, but tool calls are still logged | Similar to auto but with slightly different internal behavior, use when you want zero interaction and trust the session's scope |
bypassPermissions | Skip every permission check, including the ones auto and dontAsk still enforce. No guardrails at all | Debugging 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 autoTool 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
| Tool | Pattern | What it gates |
|---|---|---|
Read | Exact name | Reading files |
Write | Exact name | Writing files |
Edit | Exact name | Targeted edits |
Glob | Exact name | File pattern matching |
Grep | Exact name | Content search |
Bash(...) | Glob inside parens | Shell commands. Bash(*) , any command. Bash(git *) , any git subcommand. Bash(npm run lint:*) , npm scripts with lint: prefix |
Task | Exact name | Subagent delegation (spawning subagents) |
Agent | Exact name, or deny to block all delegation | Same as Task, controls whether an agent can spawn other agents |
mcp__<server>__<tool> | Exact match with server and tool names | MCP 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: denyThis 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
| Variable | Effect | When to set it |
|---|---|---|
CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS=1 | Prevents Claude from spawning Explore or Plan built-in agents | CI/CD pipelines where research-only agents waste credits; production environments where you want explicit control over every action |
CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 | Prevents Claude from updating the terminal title | tmux 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 planGive 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 autoThen:
Run: git push origin mainWith 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
| Mistake | How you notice it | The fix |
|---|---|---|
bypassPermissions in a production session | Claude runs an unintended destructive command with no prompt, files deleted, secrets exposed | Never 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 function | Every tool call fails with "tool not allowed," session is unusable | Start 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 orchestrator | A subagent that needs to delegate can't, and the parent's batch delegation grinds to a halt | Deny 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 unintentionally | Claude makes changes you didn't approve, and you realize the mode changed | The 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 subagent | A 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.