Skip to content
← All posts

Safety Nets for Claude Code with --dangerously-skip-permissions

The flag skips permission prompts, not all safety controls. Hooks can block tool calls before execution. Deny rules still apply. Sandboxing still restricts filesystem and network. Here is how to layer them.

· 7 min read · Sylvester Damgaard
Safety Nets for Claude Code with --dangerously-skip-permissions

TLDR: copy this into your Laravel project

If you just want Claude Code to stop nuking your database and pushing broken code to production, paste this into .claude/settings.json and move on. The rest of the post explains why.

.claude/settings.json
json
{
  "permissions": {
    "deny": [
      "Bash(php artisan db:wipe *)",
      "Bash(php artisan db:wipe)",
      "Bash(php artisan migrate:fresh *)",
      "Bash(php artisan migrate:fresh)",
      "Bash(php artisan migrate:reset *)",
      "Bash(php artisan migrate:reset)",
      "Bash(php artisan migrate:rollback *)",
      "Bash(php artisan migrate:rollback)",
      "Bash(php artisan key:generate *)",
      "Bash(php artisan key:generate)",
      "Bash(php artisan down *)",
      "Bash(php artisan down)",
      "Bash(rm -rf *)",
      "Bash(rm -r *)",
      "Bash(curl *)",
      "Bash(wget *)",
      "Bash(git push *)",
      "Bash(git push)",
      "Bash(git reset --hard *)",
      "Bash(git checkout -- *)",
      "Bash(git clean *)",
      "Bash(docker *)",
      "Bash(sudo *)",
      "Bash(chmod 777 *)",
      "Bash(kill *)",
      "Bash(pkill *)",
      "Bash(npm publish *)",
      "Bash(composer global *)",
      "Read(.env*)",
      "Read(./secrets/**)",
      "Read(//**/.env)",
      "Edit(.env*)",
      "Edit(./secrets/**)",
      "WebFetch"
    ],
    "allow": [
      "Read",
      "Glob",
      "Grep",
      "Bash(php artisan *)",
      "Bash(php artisan test *)",
      "Bash(php artisan make:* *)",
      "Bash(php artisan route:list *)",
      "Bash(php artisan tinker --execute *)",
      "Bash(php artisan migrate --no-interaction *)",
      "Bash(php artisan migrate)",
      "Bash(composer require *)",
      "Bash(composer update *)",
      "Bash(composer install *)",
      "Bash(npm run *)",
      "Bash(npm install *)",
      "Bash(npx *)",
      "Bash(git status)",
      "Bash(git diff *)",
      "Bash(git log *)",
      "Bash(git add *)",
      "Bash(git commit *)",
      "Bash(git branch *)",
      "Bash(git checkout -b *)",
      "Bash(git stash *)",
      "Bash(vendor/bin/pint *)",
      "Bash(vendor/bin/phpstan *)",
      "Bash(vendor/bin/pest *)",
      "Bash(cat *)",
      "Bash(ls *)",
      "Bash(find *)",
      "Bash(grep *)",
      "Bash(head *)",
      "Bash(tail *)",
      "Bash(wc *)",
      "Bash(mkdir *)"
    ]
  }
}

For permission matching, deny rules are evaluated first and override ask and allow, also when --dangerously-skip-permissions. The agent can run php artisan migrate but not migrate:fresh. It can git commit but not git push. It can read your code but not your .env.


Claude Code is an agent. It reads files, edits code, runs shell commands, and makes HTTP requests. The permission system asks you before each action. --dangerously-skip-permissions turns those prompts off.

Some people use it anyway. CI pipelines, automated workflows, or just impatience. This post isn't about whether you should. It's about what you can do if you do.

The flag doesn't disable everything. Deny rules still apply. Hooks still run. Sandboxing still works. You can layer these controls so that even with bypass mode active, the agent can't touch secrets, push to production, or reach the internet unsupervised.

What the flag actually skips

It skips interactive permission prompts. That's it. Everything else still runs:

  • deny rules still block matched commands (ask rules become effectively allow since there are no prompts)

  • All hooks execute (PreToolUse, PostToolUse, etc.)

  • Some protected paths and actions may still trigger prompts depending on runtime and Claude Code version

  • Sandbox restrictions remain active

Note: hooks didn't always run in bypass mode. Anthropic fixed this as a bug. If you tested this before that fix, your experience is outdated.

Layer 1: Deny rules

The simplest and fastest control. Deny rules are evaluated before everything else, even in bypass mode. First match wins. The TLDR config above covers a solid Laravel setup. Put it in .claude/settings.json (shared with team) or .claude/settings.local.json (personal).

Evaluation order: deny, then ask, then allow. If a command matches a deny rule, it's blocked. No override, no prompt, no bypass.

Watch the wildcard syntax. Bash(rm *) (space before *) enforces a word boundary, so it matches rm -rf / but not rmdir. Bash(rm*) (no space) would match both.

Layer 2: PreToolUse hooks

Hooks run code before or after a tool call. PreToolUse is the important one. It runs before the tool executes and can block it by returning a deny decision.

The hook receives JSON on stdin with the tool name and input. To block with a structured permission decision, print JSON to stdout and exit 0. To block with a plain error message instead, write to stderr and exit 2.

.claude/hooks/guard.sh
bash
#!/bin/bash
# Block destructive bash commands and network access
# Receives JSON on stdin: { "tool_name": "Bash", "tool_input": { "command": "..." } }

INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')

if [ "$TOOL" = "Bash" ]; then
  CMD=$(echo "$INPUT" | jq -r '.tool_input.command')

  # Block patterns that deny rules might miss
  if echo "$CMD" | grep -qEi 'rm -rf /|mkfs|dd if=|:()\{|fork bomb|\| base64|> /dev/sd|shutdown|reboot|passwd'; then
    echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Destructive or dangerous command blocked by hook"}}'
    exit 0
  fi

  # Block exfiltration patterns
  if echo "$CMD" | grep -qEi 'curl.*-d|wget.*--post|nc -|ncat|socat|ssh.*@|scp '; then
    echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Data exfiltration pattern blocked by hook"}}'
    exit 0
  fi
fi

# Allow everything else
exit 0

Make it executable and wire it up in settings:

bash
chmod +x .claude/hooks/guard.sh
.claude/settings.json (hooks section)
json
{
  "hooks": {
    "PreToolUse": [
      {
        "type": "command",
        "command": ".claude/hooks/guard.sh"
      }
    ]
  }
}

You can scope hooks to specific tools with matchers:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "type": "command",
        "command": ".claude/hooks/guard.sh",
        "matchers": [
          { "tool": "Bash" },
          { "tool": "WebFetch" }
        ]
      }
    ]
  }
}

Hooks and deny rules protect against different things. Deny rules match exact tool names and patterns. Hooks can inspect the full command string, check environment variables, call external APIs, or apply arbitrary logic. Use both.

Layer 3: Sandboxing

Sandboxing restricts what bash commands can access at the OS level. On macOS it uses Seatbelt and works out of the box. On Linux you need bubblewrap and socat installed. Configure what the sandbox allows:

.claude/settings.json (sandbox section)
json
{
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "allowWrite": ["/tmp/build"],
      "denyRead": ["~/.aws/credentials", "~/.ssh"],
      "allowRead": ["."]
    },
    "network": {
      "allowedDomains": ["github.com", "*.npmjs.org", "packagist.org"]
    }
  }
}

The important setting is allowUnsandboxedCommands: false. Without it, commands may be allowed to run outside the sandbox. Set it to false to prevent unsandboxed execution except where you have explicitly excluded commands.

Layer 4: Managed settings (teams)

If you run a team, you can disable bypass mode entirely so nobody can use the flag. Managed settings are deployed to a system-level path that individual users can't override.

macOS: /Library/Application Support/ClaudeCode/managed-settings.json
json
{
  "disableBypassPermissionsMode": "disable",
  "allowManagedPermissionRulesOnly": true,
  "allowManagedHooksOnly": true,
  "permissions": {
    "deny": [
      "Bash(curl *)",
      "Bash(wget *)",
      "Read(.env*)",
      "Read(./secrets/**)",
      "WebFetch"
    ],
    "allow": [
      "Read",
      "Glob",
      "Grep",
      "Bash(npm run *)",
      "Bash(git status)",
      "Bash(git diff *)"
    ]
  }
}

File locations by platform:

  • macOS: /Library/Application Support/ClaudeCode/managed-settings.json

  • Linux: /etc/claude-code/managed-settings.json

  • Windows: C:\Program Files\ClaudeCode\managed-settings.json

Deploy via MDM (Jamf, Intune, Kandji) or just drop the file. allowManagedPermissionRulesOnly means users can't add their own allow rules. allowManagedHooksOnly means they can't register their own hooks. The managed config is the only config.

Layer 5: SDK-level controls

If you embed Claude Code via the Agent SDK, you can enforce tool restrictions in code. disallowedTools wins over everything, including bypass mode:

typescript
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Help me refactor this codebase",
  options: {
    disallowedTools: ["Bash", "WebFetch"],
    allowedTools: ["Read", "Glob", "Grep", "Edit", "Write"],
    permissionMode: "dontAsk"
  }
})) {
  if ("result" in message) {
    console.log(message.result);
  }
}

With permissionMode: "dontAsk" and an explicit allowedTools list, the agent can only use the tools you whitelist. Anything else is silently denied. No prompt, no bypass.

What this doesn't fix

None of this makes bypass mode safe. It reduces risk. The agent can still write to any file in your project, still create new files, still run any command that isn't explicitly denied. If the deny list misses something, it goes through.

Subagents inherit the parent's permission mode. If you run with bypass, every subagent spawned during the session also runs with bypass. Your deny rules and hooks still apply to them, but they get the same level of autonomy as the parent. One more reason to keep the deny list tight — it's the shared boundary for every agent in the tree.

The point is defense in depth. Deny rules catch the obvious patterns. Hooks catch the non-obvious ones. Sandboxing limits the blast radius at the OS level. Managed settings prevent bypass mode entirely when you need that guarantee.

Security isn't about whether the agent is smart. It's about which actions are technically impossible.