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:
- Hooks (
PreToolUse, then the more recentPermissionRequest) get first look and can returnallow,deny,ask, or (CLI ≥2.1.89,-p-only)defer. - Deny rules in
settings.json— an explicitdenymatch always wins, no matter what else says otherwise. - Ask rules — forces a prompt even for something that would otherwise be auto-allowed.
- The permission mode —
default(prompt for anything not covered above),acceptEdits(auto-allow file edits, still prompt for everything else),plan(research only, no mutating tools), orbypassPermissions(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). - 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.