What --dangerously-skip-permissions actually disables

And five ways to run Claude Code unattended without reaching for it.

On this page

What the flag actually does

--dangerously-skip-permissions (equivalently, permissions.defaultMode: "bypassPermissions" in settings.json) sets Claude Code's permission mode to one that never asks. Every tool call — every Bash, every Edit, every Write, every MCP tool — is approved automatically, with no allow-rule check, no hook, no interactive prompt. It is the mode that lets a headless claude -p run finish a multi-step task at 3am without stalling on a TTY that doesn't exist.

That's also exactly the problem: it doesn't skip some prompts, it skips the entire permission system. A prompt that would have caught rm -rf ~/.ssh or an Edit to a CI config that ships credentials to a webhook gets the same free pass as a harmless ls. There's no per-tool carve-out with this flag — it's all-or-nothing, which is why the CLI prints a scary confirmation the first time you use it and the flag name says "dangerously" out loud.

The permission decision flow it skips

Understanding what's being bypassed means knowing what runs when it isn't. Roughly, in order, for each tool call Claude Code is about to make:

  1. Hooks (PreToolUse, then the more recent PermissionRequest) get first look and can return allow, deny, ask, or (CLI ≥2.1.89, -p-only) defer.
  2. Deny rules in settings.json — an explicit deny match always wins, no matter what else says otherwise.
  3. Ask rules — forces a prompt even for something that would otherwise be auto-allowed.
  4. The permission modedefault (prompt for anything not covered above), acceptEdits (auto-allow file edits, still prompt for everything else), plan (research only, no mutating tools), or bypassPermissions (this flag — skip everything below this line, and everything above it that would have led to a prompt still resolves through hooks/deny/ask, but nothing is left to actually show a dialog for).
  5. Allow rules — the last thing checked before an interactive prompt is what would otherwise appear.

Precedence for hook decisions specifically is deny > defer > ask > allow — a deny from any hook always wins even if another hook said allow. bypassPermissions mode doesn't touch hooks or deny rules at all; it just removes the "nothing else matched, so ask" step at the bottom, which in practice means most people running it also haven't bothered writing the rules above it, since the whole point was to stop being asked.

Alternative 1: allow rules

The narrowest fix: describe exactly which tool calls are safe to auto-approve, and leave everything else prompting (or denying, for a job with no human to prompt). In settings.json:

{
  "permissions": {
    "allow": [
      "Bash(git diff:*)",
      "Bash(git status:*)",
      "Bash(npm test:*)",
      "Read(./src/**)"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Read(./.env)",
      "Read(./.ssh/**)"
    ]
  }
}

This gets a cron job through its routine steps without a prompt while leaving the dangerous generic case — anything not on the allow list — still gated. It doesn't help with a job whose whole point is to run an unpredictable set of commands (a general-purpose coding agent, say); for that, allow rules alone will always either under- or over-match.

Alternative 2: hooks

A PreToolUse hook is a program you control that gets the tool call before Claude Code decides anything, and can allow, deny, ask, or (for -p) defer it based on whatever logic you want — not just a static pattern list:

#!/usr/bin/env bash
# .claude/hooks/gate.sh — read stdin JSON, decide, write stdout JSON
input=$(cat)
tool=$(echo "$input" | jq -r .tool_name)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

if [[ "$tool" == "Bash" && "$cmd" == *"terraform apply"* ]]; then
  echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask"}}'
else
  echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
fi
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/gate.sh" }] }
    ]
  }
}

Exit code 2 from a hook blocks unconditionally regardless of what it printed. The default hook timeout is 600 seconds, and a hook that times out doesn't block — the call proceeds through the normal permission flow as if the hook hadn't fired, so a slow or hung hook script fails open, not closed. Build that into your gate script's own timeout budget rather than assuming Claude Code will wait forever.

Alternative 3: --permission-prompt-tool

Hooks are great for logic you can express as code ahead of time. For a decision that genuinely needs a human — "is this the right database to migrate" — --permission-prompt-tool points every prompt that would otherwise need a TTY at an MCP tool instead, which can relay the question anywhere: email, Slack, Telegram, a phone push. See the undocumented contract for the exact payload shape and a minimal do-it-yourself server; a hosted option (pendnt's permission_prompt tool) exists if you'd rather not run that server yourself, but the contract works with anything that speaks MCP, hosted or not.

Alternative 4: sandboxing

Every option above still trusts that "allow" means "safe to actually run on this machine." The complementary control is making the blast radius smaller regardless of what gets approved: run the agent in a container or VM with a throwaway filesystem, no access to your real credentials, and restricted network egress (an allowlist of the few hosts it actually needs). A wrong "allow" then costs you a container, not your laptop or your production account. This is not mutually exclusive with anything above — it's the backstop for when your allow rules or your hook logic have a bug, or when you decide the risk of a fully-unattended run is only acceptable inside a sandbox in the first place. Cloud CI runners (GitHub Actions, a disposable Docker container, a scratch VM) already give you most of this for free if the job runs there instead of on a workstation.

Combining them for a cron job

In practice these compose rather than compete: run the job in a sandboxed container (alternative 4), scope routine commands with allow rules (alternative 1), gate the handful of genuinely dangerous ones with a hook (alternative 2), and route the ones that need a real human decision through --permission-prompt-tool (alternative 3). That combination gets you most of the unattended convenience --dangerously-skip-permissions was reached for, without its actual failure mode — a single bad tool call with nothing left to catch it. See Running claude -p on cron for the rest of the unattended-job checklist: locking, timeouts, logging, and exit codes.