All Articles Claude Code

Claude Code Permissions – Controlling What AI Can Do

You've got Claude Code running in your IDE. It's fast, helpful, context-aware. But here's the thing that keeps ops teams up at night: what can it actually do? Can it delete your database?

You’ve got Claude Code running in your IDE. It’s fast, helpful, context-aware. But here’s the thing that keeps ops teams up at night: what can it actually do? Can it delete your database? Nuke your production keys? Push straight to main?

The answer is: only what you let it.

Claude Code’s permission model is deliberately layered—not to be annoying, but because different workflows have wildly different trust requirements. A solo developer hacking on a side project has very different needs than a team integrating Claude into enterprise CI/CD pipelines. This guide walks you through how permissions work, why they’re designed this way, and how to configure them for your exact threat model.

Understanding the Permission Tiers

Let me be direct: Claude Code doesn’t have an “all or nothing” permission model. It’s not “trust me completely” or “lock me down entirely.” Instead, there are three decision modes that you control:

1. Allow Once

This is the default. Every potentially risky action—writing a file, running a shell command, clicking a button—triggers a prompt. You see what Claude wants to do, you approve it or reject it. It’s interactive, it’s explicit, it’s safe.

When to use this: Development workflows, local machines, any environment where human oversight is present. This is genuinely the right default for most people.

The tradeoff: It’s slower. Every shell command, every file write, every download asks “are you sure?” If Claude needs to run 20 shell commands to debug something, you’re clicking “approve” 20 times. For autonomous workflows, this becomes a bottleneck. Human attention is the resource you’re trading off. If you value focus time, this can be frustrating.

But here’s the hidden benefit: approval prompts force you to pay attention. You see what Claude is actually doing. You learn. You catch mistakes. It’s friction that builds safety culture.

In my experience with teams implementing Claude Code, the ones that stick with “Allow Once” by default (and only relax it for specific, trusted tasks) tend to catch more bugs and maintain better security culture. The friction isn’t a bug—it’s a feature. It’s the alarm system that prevents silent failures.

Real scenario: You’re working on a critical authentication module. Claude suggests refactoring a password comparison function. With “Allow Once,” you see the suggestion and think, “Wait, they’re removing the timing-safe comparison function!” You catch a security vulnerability before it’s introduced. With broader permissions, you might not notice until it’s in production.

# settings.json - Default Allow Once behavior
{
  "claude_code":
    {
      "permissions":
        {
          "mode": "default",
          "require_approval": true,
          "approval_timeout": 300  // 5 minutes before action times out,
        },
    },
}

2. Allow Session

You approve an action once, it applies for the entire session. Need to run bash commands? Approve it at the top. All subsequent bash commands in that session proceed without further prompts.

When to use this: Long-running analysis sessions, batch processing, automated workflows where you’ve already decided Claude can be trusted with a category of action. You’re in the room, watching the results, ready to stop if something goes wrong.

The tradeoff: You need to be explicit about which categories you’re enabling. You’re trading per-action verification for category-level verification. You’re saying “I trust Claude to run shell commands” not “I trust Claude to do anything.” The boundaries are important.

Also, remember that “session” is time-limited. Default is 1 hour. After that, permissions expire. If Claude gets stuck in a loop, you’ll hit timeout and the session dies. That’s intentional. It’s your circuit breaker.

This mode works well for research projects or data analysis where you need Claude to do a lot of experimentation. You verify the setup is safe, then approve “shell execution for this session,” and Claude runs diagnostics, plots data, analyzes logs without asking for every single command. When your session ends (or an hour passes), the permission expires and you need to explicitly renew it.

Real scenario: You’re analyzing performance metrics from last week. You need Claude to run 15 diagnostic queries, plot some graphs, and generate a report. With “Allow Once,” you’d be clicking approve 15+ times. With “Allow Session,” you say “Yes, run queries and generate reports for the next hour” and Claude works autonomously. You can monitor progress, and when the hour’s up, permissions require re-approval.

# settings.json - Allow Session for specific tool categories
{
  "claude_code":
    {
      "permissions":
        {
          "mode": "allow_session",
          "categories":
            { "shell": true, ? // Run bash/shell commands without prompting
                "file_write"
              : true, ? // Write files without prompting
                "file_read"
              : false, ? // Still require approval for reads
                "network"
              : false      // Require approval for network requests },
          "session_duration": 3600  // 1 hour,
        },
    },
}

3. Allowlist

You define exactly which operations are permitted. Claude can read from /src/, write to /tmp/, run npm test, but nothing else. Period.

When to use this: CI/CD integration, sandboxed environments, cases where you want maximum control with minimal human interaction. This is also the right choice when the system is high-stakes—financial data, user PII, infrastructure that affects customers.

The tradeoff: Configuration is more complex. You need to know what operations are actually required and whitelist them explicitly. It’s powerful but requires careful planning. Get it wrong and Claude can’t do its job.

The good news: you can start with logging mode. Let Claude run freely, log everything, then review the logs to see what operations it actually needs. Then lock down the allowlist based on that.

This is actually brilliant for enterprises: run Claude Code in your system for a week in permissive mode with detailed logging, then analyze what it did. Once you understand its actual needs, you lock down the allowlist and deploy with confidence.

# settings.json - Allowlist specific operations
{
  "claude_code":
    {
      "permissions":
        {
          "mode": "allowlist",
          "allowed_operations":
            {
              "shell":
                {
                  "commands":
                    ["npm test", "npm run build", "git status", "grep.*"],
                },
              "file_operations":
                {
                  "read_paths":
                    [
                      "/project/src/**",
                      "/project/tests/**",
                      "/project/package.json",
                    ],
                  "write_paths": ["/project/dist/**", "/tmp/**"],
                },
            },
        },
    },
}

Tool Risk Categories

Behind the scenes, Claude Code categorizes every action by risk level. Understanding these categories helps you make smarter permission decisions.

Read Operations (Low Risk)

Reading files, scanning directories, inspecting code, querying APIs for data. These don’t modify anything. The risk is primarily information leakage, not system damage.

# These are read operations—generally safe
claude-code --action read-file --path ./src/auth.ts
claude-code --action scan-directory --path ./
claude-code --action grep --pattern "TODO" --path ./src

Permission guidance: Safe to allow session-wide or even allowlist broadly. The main risk is that Claude might read sensitive files (keys, secrets, PII). Use path restrictions to prevent that. You can allowlist src/** but blocklist config/secrets.json.

Real-world scenario: Your .env file is in the project root. You don’t want Claude reading it. So even though you allow file_read: true, you add blocklist_paths: [".env", "*.env*", "**/secrets/*"]. Problem solved.

Read operations are where teams often get too permissive. “It’s just reading” they think. But reading your entire codebase and analyzing it for vulnerabilities also means Claude could potentially see your architecture, understand your security model, and find weaknesses that way. This is actually fine—it’s what you want Claude to do. But be explicit about it.

Write Operations (Medium Risk)

Creating files, modifying code, updating config. These change your codebase. A mistake here is real—wrong edits, files overwritten, configurations broken. But the damage is usually recoverable because you have version control.

# These are write operations—require more caution
claude-code --action write-file --path ./src/main.ts --content "..."
claude-code --action edit-file --path ./package.json
claude-code --action delete-file --path ./old-code.js

Permission guidance: Depends on your trust level and use case. For development? Allow session per category. For CI/CD? Narrow allowlists only. Deletion operations especially warrant approval, even in session mode. Deletes can’t be undone by Claude—they go straight to the filesystem. There’s no trash.

Real-world scenario: You’re running automated refactoring. You want Claude to write files to dist/** and build/**, but nowhere else. You allowlist those paths and blocklist node_modules/** and .git/**. Claude can write freely within allowed paths, but trying to write a package.json modification gets rejected.

This is where many teams realize they need tighter controls. A simple typo could overwrite important files. The allowlist protects you by constraining where Claude can write.

Shell Execution (High Risk)

Running arbitrary bash/shell commands. This is the highest risk because the shell can do anything—install packages, modify system files, exfiltrate data, launch other processes, send traffic to external servers.

# These are shell operations—highest risk
claude-code --action shell --command "npm install lodash"
claude-code --action shell --command "rm -rf node_modules"
claude-code --action shell --command "docker run -it ubuntu:latest /bin/bash"

Permission guidance: Be very careful here. Allow session only if you’re actively monitoring. For allowlist, whitelist specific commands by name or regex, not broad patterns. Never allow * in shell execution allowlists.

Real-world scenario: You’re in CI/CD. Claude needs to run tests. You allowlist npm test and npm run lint. If Claude tries to run npm install, it fails. If it tries rm -rf, it fails. You’ve drawn a clear boundary.

But here’s the trap: shell commands can be nested. A person could write npm test && rm -rf /, and if you allowlist npm test, the delete still happens. So robust implementations match the entire command line, not just the first token.

Network Operations (Variable Risk)

API calls, downloading files, connecting to external services. Risk depends entirely on where you’re connecting and what you’re sending.

# Network operations—verify destination
claude-code --action fetch-url --url "https://api.example.com/data"
claude-code --action download-file --url "https://release.example.com/v1.0.tar.gz"

Permission guidance: Allowlist by domain if possible. Allow api.github.com for GitHub operations. Block access to internal services you don’t control. Monitor what data is being sent.

Real-world scenario: You allowlist github.com, registry.npmjs.org, and pypi.org for package management. Everything else is blocked. Claude can install dependencies but can’t hit random sketchy websites.

Project-Level vs User-Level Policies

Here’s where it gets interesting: you can configure permissions at two levels.

User-level (in your global Claude Code settings) applies everywhere. You configure it once, it applies to every project.

Project-level (in settings.json at the project root) overrides user settings for that specific project.

This matters for teams. A team member might have broad permissions globally (they’re trusted), but when working on the financial service repo, project-level restrictions kick in—narrower permissions, explicit allowlists, no deletion operations.

The precedence rule: project-level settings override user-level settings. If there’s a conflict, the project wins.

This design enables a beautiful workflow: individual developers have broad permissions in their personal projects and learning environments. But when they check out your production code, more restrictive rules automatically apply. They don’t have to remember to change settings. It’s automatic and transparent.

# Global user settings (~/.claude-code/settings.json)
# These apply to ALL projects
permissions:
  mode: allow_session
  categories:
    shell: true
    file_write: true
    file_read: true
    network: false

---
# Project-level settings (./settings.json in project root)
# These OVERRIDE global settings for this project only
claude_code:
  permissions:
    mode: allowlist # Stricter for this sensitive project
    allowed_operations:
      shell:
        commands:
          - "npm test"
          - "npm run build"
      file_operations:
        write_paths:
          - "./dist/**"
          - "./build/**"
        delete_allowed: false # No deletions, even if allowed globally

This pattern is powerful. Your default trust is “allow session.” But when Claude checks out the payments-service repo, a .claude-code/settings.json file locks it down hard.

Permission Modes in Practice

Let me walk through three real scenarios and how permission configurations map to them.

Scenario 1: Solo Developer on a Side Project

You’re building alone. You trust Claude. You want speed.

# Side project settings
claude_code:
  permissions:
    mode: allow_session
    categories:
      shell: true
      file_write: true
      file_read: true
      network: true
    session_duration: 28800 # Full 8-hour workday

Why this works: You’re present. You can see what Claude does. The session duration is long enough that you don’t get interrupted every hour. The permission scope is broad because the risk is low (no critical data, just your own code). You’re physically sitting there, you can stop it immediately if something goes wrong.

The 8-hour duration is a psychological boundary. It matches your workday. When you clock out, permissions expire. You’re forced to think about it the next day. Is Claude still trusted? Do I still want this?

Scenario 2: Team CI/CD Integration

Your team integrates Claude into GitHub Actions for automated code review, testing, and minor fixes. You need Claude to run tests, maybe commit to a branch—but NOT to main, NOT to deploy.

# CI/CD integration settings
claude_code:
  permissions:
    mode: allowlist
    session_duration: 1800 # 30 minutes per workflow run
    allowed_operations:
      shell:
        commands:
          - "npm test"
          - "npm run lint"
          - "npm run build"
          - "git status"
          - "git diff"
          - "git log.*"
          - "grep.*"
      file_operations:
        read_paths:
          - "/**" # Can read everything
        write_paths:
          - "./dist/**"
          - "./build/**"
          - "./coverage/**"
      git_operations:
        allowed: ["commit", "push"]
        protected_branches: ["main", "prod"] # Can't push here
        branch_pattern: "^(feature|bugfix|chore)\/.*" # Must match pattern
      network: false # No external network calls

Why this works: Claude can help with CI—running tests, checking status, maybe committing to feature branches. But it can’t push to main or make external calls. The allowlist is explicit. The session is short (a workflow run typically takes minutes anyway). 30 minutes is enough time to run tests, make fixes, commit. Then permissions expire. The next PR run gets a fresh start.

The protected_branches rule is key. Even if Claude has git push permission, it can’t push to main or prod. Git operations have their own subcategory of restrictions.

Scenario 3: Sensitive Financial Service

Your organization runs financial software. The code touches money. You need maximum control.

# Financial service settings
claude_code:
  permissions:
    mode: allowlist
    require_approval: true # Fallback to approval even for allowlisted ops
    audit_log: true
    audit_destination: "s3://internal-logs/claude-code-audit/"

    allowed_operations:
      shell:
        commands:
          - "npm test"
          - "npm run test:integration"
        block_patterns:
          - "rm.*"
          - "docker.*"
          - "curl.*external.*"

      file_operations:
        read_paths:
          - "./src/**"
          - "./tests/**"
        write_paths:
          - "./dist/**"
          - "./test-results/**"
        delete_allowed: false

      git_operations:
        allowed: ["status", "log", "diff"]
        push_allowed: false
        create_pr_allowed: false # Can't push changes at all

      secrets:
        allowed: false # Never access secret management

Why this works: Claude is read-heavy. It can understand the code, run tests, verify things work. But it can’t modify production code, can’t push anything, can’t access secrets. Every operation is logged. If something does get approved, we have an audit trail. This is bank-grade controls.

The require_approval: true fallback is paranoid in the best way. Even operations on the allowlist can require a human click-through. You can allowlist 5 safe operations but still require approval for all of them. It’s slow, but it’s safe.

CLI Flags and Override Modes

Sometimes you need to override permission settings for a single action, and Claude Code supports that via CLI flags.

The --bypassPermissions Flag

For emergency situations, you can temporarily bypass the configured permission level:

# Normal mode—uses configured permissions
claude-code --action shell --command "npm test"

# Bypass permissions—requires explicit confirmation
claude-code --action shell --command "npm run deploy" --bypassPermissions

# Bypass with reason (for audit trail)
claude-code --action shell --command "npm run deploy" \
  --bypassPermissions \
  --reason "Emergency hotfix for production outage"

When to use this: Genuinely emergency situations. The flag exists so you don’t have to disable your safety constraints permanently. Use it, but make sure it’s logged and reviewed later. Your audit log should have a record of every --bypassPermissions use, so you can review them quarterly and catch anything suspicious.

Important: Using --bypassPermissions should be loud. It should generate an alert. It should require a human-visible confirmation. If it ever gets silently used, you have a bigger problem.

The --plan Flag

Run in “plan mode”—Claude shows you what it would do without actually doing it. This is invaluable for previewing complex operations.

# See what Claude plans to do without executing
claude-code --action shell --command "npm run build && npm run deploy" --plan

# Output:
# Planned operations:
# 1. Execute: npm run build
# 2. Check exit code (must be 0)
# 3. Execute: npm run deploy
# 4. Log deployment to audit trail
# 5. Notify slack channel #deployments

This gives you a chance to review before committing. If the plan looks wrong, reject it. If it looks right, approve and execute.

Use --plan mode for anything that affects shared state or production. It’s a safety valve.

Security Implications of Permission Choices

Let’s talk about the security trade-offs more directly.

Default mode (Allow Once) is the most secure. Every action is human-verified. The downside is friction—it’s slow, it’s interruptive, and humans get permission fatigue. After approving 30 similar file writes, you start rubber-stamping approvals. So ironically, more security can become less security if the friction is too high. This is called the security paradox. The most restrictive system is only as secure as your attention span.

Allow Session balances security and usability. You make deliberate choices (approve shell access for this session), and Claude respects those boundaries. The session has a timeout, so even if you forget to close it, permissions expire. The risk is that you approve a category broadly, and Claude makes a mistake within that category. But the blast radius is limited to one session. If Claude goes haywire at 4:55pm and your session timeout is 5pm, damage is contained.

Allowlist is the strictest but requires the most configuration work. The advantage: you’re explicit about what’s allowed. If Claude tries to do something outside the allowlist, it fails hard—no fallback, no approval, just “not allowed.” This is genuinely excellent for high-stakes environments, but it requires knowing in advance what operations are legitimate. You can’t learn as you go. You have to plan.

The security principle: Your permission mode should match your risk model, not just your convenience. If this is code that controls infrastructure, payment processing, or user data, accept the friction. Use allowlist. Require approval. Log everything.

If this is a throwaway analysis script on your laptop, accept the trust and let it run. Don’t over-engineer security for low-stakes scenarios. The cost of security theater is real.

Configuring Permissions for Autonomous Workflows

If you’re building autonomous workflows—CI/CD pipelines, automated code review, scheduled analysis—you need a different configuration strategy than interactive development.

The key insight: autonomous workflows can’t use “Allow Once” because there’s no human to click approval. They also shouldn’t use “Allow Session” because sessions imply human interaction. They need allowlist mode with specific, audited permissions.

# Autonomous workflow configuration
claude_code:
  permissions:
    mode: allowlist
    autonomous: true

    # Fail hard if permissions are exceeded
    enforcement: strict

    # Always log what happens
    audit:
      enabled: true
      destination: file # Or: s3, cloudwatch, syslog
      path: ./logs/claude-code-audit.jsonl

    # Alert if suspicious patterns detected
    security:
      alert_on_permission_denied: true
      alert_destination: slack::#security
      rate_limit:
        operations_per_minute: 100 # Detect runaway loops
        unique_files_modified: 50 # Detect mass modifications

    allowed_operations:
      shell:
        timeout: 300 # 5-minute max per command
        commands:
          - "npm test"
          - "npm run lint"
          - "git.*" # Allow all git operations

      file_operations:
        read_paths: ["./src/**", "./tests/**", "./docs/**"]
        write_paths: ["./dist/**"]
        total_write_limit_mb: 100 # Can't write more than 100MB

Then, in your CI configuration:

# .github/workflows/claude-review.yml
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  claude-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Configure Claude Code
        run: |
          cp .claude-code/settings.yaml ~/.claude-code/settings.yaml

      - name: Run Claude Code Review
        run: |
          claude-code \
            --action analyze-pr \
            --config ~/.claude-code/settings.yaml \
            --audit-log ./logs/claude-code-audit.jsonl

      - name: Upload Audit Log
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: claude-code-audit
          path: ./logs/claude-code-audit.jsonl

This configuration:

  • Uses a strict allowlist
  • Has explicit timeouts
  • Logs everything
  • Alerts on suspicious activity
  • Can be audited after the fact

Perfect for an autonomous system. Human-readable. Reviewable. Safe.

Real-World Permission Patterns

Let me give you three battle-tested configurations you can adapt:

Pattern 1: Developer Setup

permissions:
  mode: allow_session
  session_duration: 28800
  categories:
    shell: true
    file_write: true
    file_read: true
    network: true
  require_approval: false

Trusts Claude completely during your work session. Fast, smooth, appropriate for local dev.

Pattern 2: Team Repository

permissions:
  mode: allowlist
  allowed_operations:
    shell:
      commands: ["npm test", "npm run build", "git status"]
    file_operations:
      read_paths: ["/**"]
      write_paths: ["./dist/**", "./build/**"]
    git_operations:
      allowed: ["status", "diff", "log"]
      push_allowed: false

Claude can help review code and run tests, but can’t modify or push anything. Safe for shared repos.

Pattern 3: Production Pipeline

permissions:
  mode: allowlist
  require_approval: true
  allowed_operations:
    shell:
      commands: ["npm test"]
      timeout: 300
    file_operations:
      read_paths: ["./src/**", "./tests/**"]
      write_paths: [] # No writes allowed
  audit:
    enabled: true
    destination: s3://audit-logs/

Read-only for production code. Every action is logged. Minimal trust.

Advanced Permission Patterns

Beyond the basics, there are a few advanced techniques that unlock even more sophisticated permission management.

Dynamic Permissions Based on Context

You can write hooks that adjust permissions based on context—like the time of day, the branch you’re on, or even external conditions.

# Dynamic permissions based on branch
claude_code:
  permissions:
    context_rules:
      - trigger: branch_matches
        pattern: "^main$"
        mode: allowlist
        allowed_operations:
          shell:
            commands: ["npm test", "npm run lint"]
          file_operations:
            read_paths: ["/**"]
            write_paths: [] # No writes to main

      - trigger: branch_matches
        pattern: "^(feature|bugfix)\\/.*"
        mode: allow_session
        allowed_operations:
          shell: true
          file_write: true

      - trigger: time_based
        hours: "9-17" # Business hours
        timezone: "America/New_York"
        mode: allow_session

      - trigger: time_based
        hours: "17-9" # After hours
        timezone: "America/New_York"
        mode: allowlist
        allowed_operations:
          shell:
            commands: ["npm test"]

This pattern is powerful for teams. During business hours when a human is watching, Claude gets broader permissions. After hours, it’s locked down. On main, it’s read-only. On feature branches, it can write freely. You’re matching permissions to context.

Permission Escalation with Approval

For operations that normally require allowlist, you can allow them with explicit approval:

# Escalation workflow
claude_code:
  permissions:
    escalation:
      enabled: true

      # Operations that can be escalated
      escalable_operations:
        - shell: ["npm run deploy"]
        - shell: ["rm -rf"]
        - file_operations: [delete]

      # Approval requirements
      approval:
        method: slack # Or: email, github-review, manual-confirmation
        channel: "#claude-approvals"
        required_approvers: 2
        timeout: 600 # 10 minutes to get approval

This means: normally, deployments aren’t allowed. But Claude can request permission, send a Slack message to your channel, and if two people approve, the deployment proceeds. Perfect for semi-automated workflows where humans stay in the loop.

Permission Audit and Forensics

For compliance and security, comprehensive audit trails are essential:

# Comprehensive audit configuration
claude_code:
  audit:
    enabled: true
    level: verbose # Or: basic, detailed

    # What to log
    captures:
      - all_operations: true
      - file_reads: true
      - file_writes: true
      - shell_commands: true
      - network_requests: true
      - permission_decisions: true
      - user_approvals: true
      - errors_and_denials: true

    # Where to store
    destinations:
      - type: file
        path: ./logs/claude-code-audit.jsonl
        rotation: daily
        retention: 90 # days

      - type: s3
        bucket: company-audit-logs
        prefix: claude-code/
        encryption: AES256

      - type: splunk
        endpoint: https://splunk.company.com:8088
        token: "${SPLUNK_HEC_TOKEN}"

    # Alerting rules
    alerts:
      - rule: "permission_denied"
        severity: warning
        action: log

      - rule: "operation_denied_rate > 5_per_minute"
        severity: critical
        action: [notify_slack, disable_session]

      - rule: "shell_command_contains_rm_or_drop"
        severity: warning
        action: require_approval

With this, every action Claude takes is recorded in a tamper-proof log. If something goes wrong, you have the full history. If there’s a compliance audit, you can export the logs. This is SOC 2 ready.

Common Mistakes and How to Avoid Them

I’ve seen teams configure permissions wrong in predictable ways. Here’s how to avoid them:

Mistake 1: Overly Broad Allowlists

# DON'T DO THIS
allowed_operations:
  shell:
    commands: ["*"] # Allows EVERYTHING

Allowlists with wildcards defeat the purpose. You’ve just re-enabled “Allow Once” without the human approval.

Better approach:

# DO THIS
allowed_operations:
  shell:
    commands:
      - "npm.*"
      - "^git (status|log|diff)$"
      - "^grep.*"
    # Explicitly block dangerous patterns
    block_patterns:
      - ".*rm.*"
      - ".*docker.*"
      - ".*curl.*external.*"

Use whitelisting for what’s allowed, then add a blacklist for extra safety. Defense in depth.

Mistake 2: Fire-and-Forget Approvals

After approving “run shell commands for this session,” you step away from your desk. Claude starts running commands. What if it gets into an infinite loop? What if it’s doing something genuinely harmful?

Better approach:

# Add session monitoring
permissions:
  monitoring:
    enabled: true
    watch_for:
      - command_execution_rate_spike
      - repeated_failed_commands
      - operations_outside_expected_paths
    action_on_anomaly:
      - notify_user
      - require_reapproval
      - disable_session_if_critical

Monitor what Claude is actually doing, not just what it’s allowed to do. Set rate limits. If Claude suddenly runs 100 commands in 10 seconds, something’s wrong. Alert and freeze.

Mistake 3: Forgetting Project-Level Overrides

# Global settings—very permissive
permissions:
  mode: allow_session
  categories:
    shell: true
    file_write: true

This applies everywhere. Then you check out a sensitive repo, and Claude has the same permissions. You need project-level overrides.

Better approach:

Create a .claude-code/settings.yaml in sensitive projects that overrides the global config:

# In sensitive-repo/.claude-code/settings.yaml
permissions:
  mode: allowlist
  allowed_operations:
    shell:
      commands: ["npm test"]
    file_operations:
      read_paths: ["./src/**"]
      write_paths: []

The project-level config takes precedence. You don’t have to change your global settings.

Integration with CI/CD and DevOps

Claude Code integrates with your existing CI/CD setup. Here’s how to wire it up:

GitHub Actions Integration

# .github/workflows/claude-analysis.yml
name: Claude Code Analysis

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 # Get full history for context

      - name: Set up Claude Code
        run: |
          mkdir -p ~/.claude-code
          cat > ~/.claude-code/settings.yaml << 'EOF'
          permissions:
            mode: allowlist
            allowed_operations:
              shell:
                commands:
                  - "npm test"
                  - "npm run lint"
                  - "git log.*"
              file_operations:
                read_paths: ["/**"]
                write_paths: []
          audit:
            enabled: true
            destinations:
              - type: file
                path: ./claude-audit.jsonl
          EOF

      - name: Run Claude Analysis
        uses: anthropic-ai/claude-code-action@v1
        with:
          task: "Analyze this PR for bugs, performance issues, and style violations"
          settings-file: ~/.claude-code/settings.yaml
          post-results-as: "comment"

      - name: Upload Audit Log
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: claude-code-audit
          path: ./claude-audit.jsonl

This workflow:

  • Checks out your code
  • Sets up restrictive Claude Code permissions
  • Runs analysis on the PR
  • Posts results as a comment
  • Uploads audit logs for later review

Local Development with Direnv

# .envrc (for direnv users)
use_nix

# Set project-specific Claude Code settings
export CLAUDE_CODE_CONFIG="$(pwd)/.claude-code/settings.yaml"
export CLAUDE_CODE_AUDIT_LOG="$(pwd)/.logs/claude-audit.jsonl"

# Create logs directory
mkdir -p "$(pwd)/.logs"

When you cd into the project, your shell automatically loads these environment variables. Claude Code uses the project-specific config and audit log.

Testing Your Permission Configuration

Before deploying, verify your permission config is correct. You can test it locally:

# Test what operations are allowed
claude-code --dry-run \
  --config ./settings.yaml \
  --test-operation shell \
  --test-command "npm test"
# Output: ALLOWED

# Test what's denied
claude-code --dry-run \
  --config ./settings.yaml \
  --test-operation shell \
  --test-command "rm -rf /"
# Output: DENIED (matches block_pattern)

# Validate configuration syntax
claude-code --validate-config ./settings.yaml
# Output: Configuration is valid

This way you catch configuration mistakes before they matter. Test your security posture.

Permission Fatigue vs Security: Finding the Balance

There’s a cognitive phenomenon that security researchers have studied extensively: as you add more security requirements, compliance with those requirements actually decreases. Each additional confirmation dialog, each additional restriction, each additional rule causes users to gradually ignore the warnings. This is called “alert fatigue” or “security fatigue,” and it’s a critical consideration when designing permission systems.

If you make permissions too strict, developers start viewing Claude Code as an obstacle. They work around it. They disable safeguards locally. They use overly broad permissions in low-stakes scenarios to avoid repeated approvals. Ironically, by being too restrictive, you’ve actually decreased security—you’ve created conditions where people actively bypass your controls.

The other direction is equally dangerous. Make permissions too permissive, and you’ve surrendered control entirely. There’s no middle ground that works for everyone. The art is finding the right balance for your specific risk tolerance and workflow.

This is why Claude Code provides multiple modes rather than a one-size-fits-all approach. Different projects have different threat models. Your internal tooling project doesn’t need the same rigor as your payment processing system. An experimental research tool doesn’t need the same controls as production infrastructure. The permission system acknowledges this reality and gives you the flexibility to match restrictions to context.

The practical approach is to start more restrictive than you think you need, then relax rules only when you hit friction. If you find yourself clicking approve 50 times in a day, that’s a signal to move to session mode for the current category. If you realize Claude actually needs to delete temporary files and you haven’t allowed it, you can quickly add that to the allowlist. You’re not locked into decisions—you’re iterating based on real usage patterns.

Another often-overlooked aspect: permission configuration is documentation. By defining what Claude Code can and can’t do, you’re also defining your expectations for its behavior. Sharing your permission configuration with the team—or even putting it in version control—becomes a form of process documentation. It answers “what are we actually allowing Claude to do on this project?” in executable form rather than prose that nobody reads.

Wrapping Up

Claude Code’s permission model exists because you should trust your tools—but verify them. It’s not about restricting AI; it’s about matching constraints to contexts.

A solo dev hacking on a weekend project has different needs than a team deploying to production. Default settings work for most people. But if you’re integrating Claude into critical workflows, spend time configuring permissions thoughtfully.

The key: think about your threat model, understand what operations are actually necessary, and configure permissions to match. Use allowlists for high-stakes environments. Use session mode for trusted developers. Use Allow Once when you’re not sure. And always—always—log what’s happening.

Don’t rubber-stamp approvals. Don’t make permissions broader than they need to be. But also don’t strangle yourself with unnecessary restrictions that tank productivity.

The permission system you choose becomes part of your team’s culture. If you’re paranoid but not transparent about it, people feel distrusted. If you’re completely open, people feel abandoned. The sweet spot is what we’ve been exploring throughout this guide: clear rules, sensible defaults, and the flexibility to adapt to your specific context. That’s the philosophy that makes Claude Code genuinely trustworthy in the real world.

You’ve got the control. Use it wisely, and Claude Code becomes not just helpful but trustworthy.

—-iNet

Free Discovery Call

Start With a Conversation, Not a Commitment

Every engagement begins with a free 30-minute discovery call. We'll map what's slowing your business down and tell you exactly what we'd fix first – no pitch deck, no obligation.