Skip to content

Claude Code Internals, Part 8 The Permission System

Section titled “Claude Code Internals, Part 8 The Permission System”

This is Part 8 of a 15-part series examining how Claude Code works under the hood. The series is based on reverse-engineering Claude Code v2.0.76.

In Part 7, we examined how responses stream from the API through Server-Sent Events. The CLI processes text deltas for real-time display and accumulates tool input JSON until blocks complete. But receiving a tool request is only the beginning. Before any tool can execute, it must pass through the permission system.

An AI assistant with unrestricted access to your filesystem and shell would be dangerous. The permission system exists to give you control over what Claude Code can do. Read-only operations like viewing files are generally safe. Deleting files or running arbitrary shell commands carry real risk.

The system works through layers. Organizations can set policies that users cannot override. Users can set personal defaults that apply across all projects. Projects can customize settings for their specific needs. And individuals can override project settings locally without affecting teammates.

Settings resolve through a strict cascade, with earlier levels taking precedence:

Policy Settings sit at the top. These are organization-level rules that cannot be bypassed. An enterprise deployment might block certain commands or require approval for all file modifications.

Flag Settings come from command-line flags and environment variables. The --dangerously-skip-permissions flag, for instance, disables most checks for automated environments.

User Settings live in ~/.claude/settings.json. These are your personal defaults, things you’ve decided should always be allowed or always require confirmation.

Project Settings live in .claude/settings.json within the project root. These are shared with the team through version control. A project might allow npm test to run without confirmation while blocking edits to .env files.

Local Settings live in .claude/settings.local.json, also in the project root. These are personal overrides that should be gitignored. You might allow more operations than the project defaults because you trust your own judgment.

When checking a permission, the system merges these layers. A deny rule at any level blocks the operation. An allow rule grants permission only if no higher-level deny rule contradicts it.

Rules come in three flavors: allow, deny, and ask.

Allow means the operation proceeds without prompting. You trust this tool with these inputs.

Deny means the operation is blocked immediately. The tool returns an error, and the model must find another approach.

Ask means the CLI prompts you before proceeding. This is the default for most write operations.

Rules can be broad or specific. "Read" applies to all file reads. "Read(src/**)" applies only to reads within the src directory. "Bash(npm *)" allows npm commands but not other shell operations.

{
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"Bash(npm test)",
"Bash(npm run build)"
],
"deny": [
"Edit(.env*)",
"Read(secrets/**)"
],
"ask": [
"Edit",
"Write",
"Bash"
]
}
}

In this configuration, reading files and searching are always allowed. Running npm test or npm run build is allowed without prompting. Editing .env files or reading anything in the secrets directory is blocked entirely. All other edits, writes, and shell commands require confirmation.

When a tool request arrives, the permission checker runs through a sequence of evaluations.

First, it checks deny rules. If any deny rule matches, the operation is rejected immediately. Deny rules are absolute, no amount of session configuration can override them.

Second, it checks allow rules. If any allow rule matches and no deny rule contradicted it, the operation proceeds.

Third, it checks the session allow list. If you previously said “always allow” for this type of operation during the current session, it proceeds.

Fourth, it checks bypass mode. If the session is running with permissions disabled (for automation), it proceeds.

Finally, if nothing matched, it prompts you. The prompt shows exactly what the tool wants to do and waits for your decision.

async checkPermission(tool, input) {
// Deny rules take priority
for (const rule of settings.permissions.deny) {
if (matchesRule(tool.name, input, rule)) {
return { allowed: false, reason: \`Denied by rule: ${rule}\` };
}
}
// Check allow rules
for (const rule of settings.permissions.allow) {
if (matchesRule(tool.name, input, rule)) {
return { allowed: true };
}
}
// Check session memory
if (sessionAllowList.has(buildKey(tool.name, input))) {
return { allowed: true };
}
// Check bypass mode
if (sessionState.sessionBypassPermissionsMode) {
return { allowed: true };
}
// Ask the user
return await promptUser(tool, input);
}

When the check falls through to prompting, the CLI displays a detailed request (layout differs):

┌──────────────────────────────────────────────────────────────────────┐
│ │
│ Claude wants to edit a file: │
│ │
│ File: /Users/dev/project/src/index.ts │
│ │
│ Change: │
│ ───────────────────────────────────────────────────────────────── │
- const version = "1.0.0"; │
+ const version = "1.1.0"; │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ [y] Allow [n] Deny [a] Always allow [Esc] Cancel │
│ │
└──────────────────────────────────────────────────────────────────────┘

You see the tool name, the specific file or command involved, and for edits, a diff of the proposed change. The options are:

Allow (y) permits this specific operation. The next similar operation will prompt again.

Deny (n) blocks this operation. The tool returns an error to the model.

Always allow (a) permits this operation and adds it to the session allow list. Similar operations won’t prompt for the rest of the session.

Cancel (Esc) is equivalent to deny.

The “always allow” option builds a session-specific allow list. If you approve editing src/index.ts, future edits to that file proceed without prompting. If you approve running npm test, future npm test commands run automatically. The session allow list resets when you exit Claude Code.

Beyond tool-level permissions, the system restricts directory access. Some paths are inherently dangerous.

System directories like /etc, /var, /usr, /bin, and /sbin are blocked by default. On macOS, /System and /Library are also blocked. On Windows, C:\Windows and C:\Program Files get the same treatment. These contain operating system files that should never be modified by an AI assistant.

The current working directory and temp directory are always accessible. This makes normal project work possible without configuration.

You can customize these restrictions in settings:

{
"allowedDirectories": [
"/Users/dev/projects",
"/tmp"
],
"blockedDirectories": [
"/Users/dev/projects/production-secrets"
]
}

When allowedDirectories is set, paths must fall within one of those directories. This creates a whitelist, anything outside is blocked. When blockedDirectories is set, those specific paths are blocked even if they would otherwise be allowed.

Shell commands receive extra scrutiny. Some commands are categorically dangerous and blocked regardless of other settings:

const blockedCommands = [
"rm -rf /",
"rm -rf ~",
":(){ :|:& };:", // Fork bomb
"dd if=/dev/zero",
"> /dev/sda"
];

Other patterns raise warnings and require confirmation even if Bash is generally allowed:

const dangerousPatterns = [
/^sudo\s/, // Elevated privileges
/^rm\s+-rf?\s/, // Recursive deletion
/>\s*\/dev\//, // Writing to devices
/\|\s*sh\s*$/, // Piping to shell
/curl.*\|\s*bash/ // Download and execute
];

These patterns catch common dangerous idioms. A curl | bash command downloads and executes arbitrary code from the internet. Piping to sh can execute unexpected commands. Recursive deletion with rm -rf can destroy filesystems if paths are wrong.

Automated environments like CI/CD pipelines need to run without interactive prompts. The bypass mode disables permission checks for these cases.

Enabling bypass requires explicit intent. You must set the CLAUDE_BYPASS_PERMISSIONS environment variable or use the --dangerously-skip-permissions flag. The “dangerously” prefix is intentional, this mode removes safety guardrails and should only be used in controlled environments.

# CI pipeline
CLAUDE_BYPASS_PERMISSIONS=true claude "run the test suite"
# Or with flag
claude --dangerously-skip-permissions "deploy to staging"

When bypass mode is active, the CLI prints a warning at startup. All tools execute without prompting, subject only to hard-coded blocks on the most dangerous commands.

The permission system fires events that hooks can intercept. A pre-tool-use hook sees every tool request before execution. A post-tool-use hook sees results after execution. A custom permission-denied hook could log blocked operations for security auditing.

runHook("permission-denied", {
tool: "Bash",
input: { command: "rm -rf /" },
reason: "Blocked command pattern"
});

This integration lets organizations build custom security workflows. A hook might send alerts when certain operations are attempted, or log all permission decisions to an audit trail.

The permission system’s design reflects a core principle: the user must always know and control what the AI does. Nothing happens silently. Dangerous operations require explicit approval. And even approved operations can be blocked at higher configuration levels.

This creates friction, but friction is sometimes valuable. A moment of confirmation before deleting files or running unfamiliar commands prevents mistakes. The “always allow” option reduces friction once you’ve established trust, while keeping the safety net in place for new operations.

Permissions control what the main agent can do, but Claude Code can also spawn sub-agents for complex tasks. The next article examines how these sub-agents work, why they exist, and how they coordinate with the main conversation.

Continue to Part 9: Sub-Agents.

Part 8 of 15 in the Claude Code Internals series.

Tech person. I write about technology, Generative AI, the cloud, design and development.

[

See more recommendations

](https://medium.com/?source=post_page---read_next_recirc—624bd7bb66b7---------------------------------------)