What —dangerously-skip-permissions actually skips, and what still stops you (Chapter 11)

Before writing this chapter I went and looked at how people phrase the question. I pulled Google’s autocomplete for twenty-five seed phrases about Claude Code, extending the eighteen shortest of them by a single letter a to z — 493 queries in all, collected 2026-09-14 — which returned 3,252 distinct completions. Twenty contain both dangerously and permission — how to, mid session, vs auto mode, settings json, root, while running, during session, vscode, sandbox. Eighteen more are the same question spelled bypass permissions, one of which is also in the twenty, and those include not working and greyed out. Underneath all of them is one worry:

If I run with that flag, have I turned off everything I set up?

I run a session under that flag around the clock, so this is a question I am exposed to rather than one I only read about.

The short answer

The reference already answers half of it. The permission-mode table defines the mode this flag selects in one line:

bypassPermissions — Skips permission prompts, except for the actions no mode auto-approves

So the flag is about prompts. What it does to the other two layers — hooks and permission rules — is not something to take on trust from a flag with dangerously in its name, so I measured it on Claude Code 2.1.259, Linux (WSL2), on 2026-09-14:

  • PreToolUse hooks still fire, and a hook that exits 2 still refuses the call.
  • A permissions.deny rule still applies — measured headless only, see below.
  • I did not measure what the flag removes. My own setup allows Bash outright, so no prompt was going to appear either way. That gap is printed here rather than hidden.

How I checked

Two empty directories, and nothing changed in ~/.claude. That is not the same as a clean machine, and the difference matters enough to print.

The user-level settings that were live throughout have 25 PreToolUse hook registrations, 17 of them matching Bash; a permissions.allow list of 14 entries that includes a bare Bash; 8 permissions.deny rules (Bash(rm -rf:*), Bash(sudo rm:*), two force-push spellings, Bash(git reset --hard:*), Bash(git clean -fd:*), and two package-release spellings); and defaultMode: auto. None of them names touch, which is the command every run below uses, and that is why the runs are readable at all.

The project-level .claude/settings.json in the first directory registers one hook:

1 {
2   "hooks": {
3     "PreToolUse": [
4       { "matcher": "Bash",
5         "hooks": [ { "type": "command", "command": "<dir>/.claude/hooks/marker-guard.sh" } ] }
6     ]
7   }
8 }

and the hook is this:

 1 #!/bin/bash
 2 IN=$(cat)
 3 LOG="${CLAUDE_PROJECT_DIR:-.}/hook-fired.log"
 4 CMD=$(printf '%s' "$IN" | jq -r '.tool_input.command // empty' 2>/dev/null)
 5 printf '%s fired cmd=%s\n' "$(date +%H:%M:%S)" "${CMD:-<none>}" >> "$LOG"
 6 if printf '%s' "$CMD" | grep -q 'CANARY'; then
 7   printf '%s BLOCKED\n' "$(date +%H:%M:%S)" >> "$LOG"
 8   echo "blocked by marker-guard: the canary command is not allowed" >&2
 9   exit 2
10 fi
11 exit 0

It writes a line every time it is invoked, before it decides anything. That matters. If it only logged refusals, “the hook stayed quiet” and “the hook was never called” would look identical, and Chapter 9 is about how often the second one is what actually happened.

Then three runs, each asking for exactly one shell command. The first two are controls, so you can watch the instrument produce both answers before you are asked to believe the third.

Run Flag Command asked for Hook invoked? Outcome File created?
A none (--allowedTools Bash) touch made-a.txt yes allowed (exit 0) yes
B none (--allowedTools Bash) touch made-b-CANARY.txt yes BLOCKED no
C --dangerously-skip-permissions touch made-c-CANARY.txt yes BLOCKED no

Run A is the one that makes the other two mean something: the hook is not simply refusing everything. It saw a harmless command, said nothing, and the file appeared.

The first line the model returned in run C, and the line my hook printed on stderr, further down the same reply:

1 コマンドは実行できませんでした。フックにブロックされています。
2    ...
3 blocked by marker-guard: the canary command is not allowed

It is in Japanese because that is the language my session runs in. The first line reads “the command could not be run; a hook blocked it”.

The other layer: permissions.deny

Hooks are one fence. The deny list is a different one, and the reference is explicit that the two are independent:

Hook decisions don’t bypass permission rules. Claude Code evaluates deny and ask rules regardless of what a PreToolUse hook returns.

A second empty directory, no project-level hook at all, and only:

1 { "permissions": { "deny": ["Bash(touch:*)"] } }
Run Flag Command asked for Outcome File created?
D none (--allowedTools Bash) touch made-d.txt denied no
E --dangerously-skip-permissions touch made-e.txt denied no
F --dangerously-skip-permissions printf x > kept-f.txt allowed yes

F is again the control: a command the rule does not match runs normally under the flag, so the “denied” in E is the rule working and not the harness refusing everything.

These three runs are headless. The hook result above has a fourth data point behind it; this one does not. Weigh them accordingly.

The interactive observation, and what it is worth

The runs above are headless (claude -p), which has no interactive prompt to skip in the first place. A sceptical reader should ask whether the flag was doing anything at all in them. Fair — and the honest answer is that they measure only whether the two configured layers survive the flag, not what the flag removes.

Separately: the session I wrote this chapter in is interactive and was started as claude --dangerously-skip-permissions (checked against its own process arguments). While writing, one of my user-level hooks stopped a tool call that session had issued, printing:

1 BLOCKED: rm -rf detected.

It was a false positive, and the false positive is the interesting part. The call was a read-only Python script that counts search phrases. One of its lines was a list of keywords to grep for, and 'rm -rf' was a string inside that list. The hook matches on the text of the command, so a script that merely mentions a destructive command reads the same to it as one that runs it. Chapters 8 and 9 are about exactly that: matching happens on strings, and a guard cannot tell a command from a description of one.

What it shows is narrow and real: under the flag, a PreToolUse hook fired and refused a call in an interactive session. What it does not show is that the flag removed my prompt — my own permissions.allow contains a bare Bash, so no prompt was coming either way.

(The message that hook prints also recommends git clean, which is itself destructive. That is bad advice sitting in my own hook, and a separate problem from this chapter.)

What this does not prove

I would rather you find the edges here than find them later — and this list is not a promise that there are no others.

  • One version. 2.1.259, measured 2026-09-14. A flag whose name contains the word dangerously is exactly the kind of thing whose behaviour can change.

  • One operating system, Linux under WSL2. One tool, Bash. Project-level settings only, not user-level rules and not enterprise policy.

  • The startup flag only. Several of the search phrasings ask about mid session, during session and settings json. Switching modes inside a session, and setting defaultMode in a settings file, are different paths and I tested neither.

  • permissions.ask is untested, and I would not assume it survives. The reference defines this mode as skipping permission prompts, and an ask rule is a prompt. deny held in my runs; do not read that as “my rules held”.

  • The exact shape only. Bash(touch:*) was tested against touch made-e.txt, the spelling the rule names. Whether it also catches /usr/bin/touch or sh -c 'touch x' is a different question, and Chapter 9’s lesson about string matching says do not assume.

  • A hook with no refusal path is unaffected by any of this, because there was never anything to bypass. In this book’s own collection, 610 of the 916 example scripts are in that category. That is Chapter 8, and it is the bigger problem.

  • The mode skips the prompt that stood in front of your settings. The reference warns:

    In bypassPermissions mode, Claude Code skips permission prompts, including for writes to protected paths such as .git and .claude.

    Your hooks live in .claude. The flag does not disable them, and it does remove the confirmation between an agent and the file that defines them. I did not test Edit or Write, so I cannot tell you how that plays out in practice.

Check yours in about a minute

Start the session twice: once normally, once as claude --dangerously-skip-permissions. The point of the exercise is that the two logs should look the same. Running it once without the flag reproduces run B and tells you nothing about the question.

Copy the hook above into an empty directory as .claude/hooks/marker-guard.sh, make it executable, register it with the JSON above, and ask for a command containing CANARY. Then, in this order:

  1. Is there a hook-fired.log at all? If not, check CLAUDE_PROJECT_DIR before concluding anything — the hook writes to wherever that points, which may not be where you are looking. If the variable is right and the file is still missing, the hook never ran, which is silent and is the failure mode of Chapter 9.
  2. Does the log say BLOCKED, with the file absent? Both, or you have a witness and not a guard (Chapter 8).

Run jq --version first. Without jq, this hook logs every call as cmd=<none>, matches nothing and exits 0 — so every command sails through while the log looks healthy, and you would conclude the flag beat your hook when in fact your hook did. That is Chapter 9’s lesson arriving in the middle of the test for Chapter 11.

The part that should still worry you

Nothing above says the flag is safe. It says the flag does not remove the two layers you configured. It removes the layer that was you — the pause where a human looked at a command and thought about it — and, by the reference’s own warning, the pause that stood in front of .claude itself.

That is the whole trade. If your hooks cover three shapes of destructive command and the model reaches for a fourth, the prompt was the thing that would have caught it, and you turned the prompt off. The measurements in this chapter are a reason to keep your hooks; they are not a reason to relax about the flag.

There is now a flag that refuses the mode outright — and I missed it

Added 2026-09-19. --restricted shipped in 2.1.248 — eleven version numbers before the version this chapter measured, of which six have changelog entries (2.1.249 and 2.1.253–256 do not; I am not claiming those were never published, only that the changelog does not list them). It was already there while I was writing the chapter, and I did not mention it. If you came here looking for something stronger than “keep your hooks,” this is the closest thing — but read to the end of the section before you switch to it, because it takes your hooks away while it does the rest.

What it does, in the CLI’s own words:

Restricted mode: removes the built-in tools that run commands or code (Bash, PowerShell, REPL and the other code-running tools) and WebFetch unless --tools names them, and ignores user, project and local settings files (managed settings and --settings still apply; add --strict-mcp-config to skip MCP servers too). Also confines the file tools to the working directories (--add-dir included), refuses bypassPermissions, and lets only a person or the configured permission handler approve writes to settings, git and tool-configuration files.

Read the second half of that quote, not just the first. This flag is not only taking things away from you: it confines the file tools to your working directories, refuses bypassPermissions, and keeps a human in front of writes to settings, git and tool configuration. The part that costs you is in the middle — ignores user, project and local settings files — and that is the part I went and measured.

Measured on 2.1.278, 2026-09-19, Linux under WSL2.

The first measurement is the one that matters for this chapter:

1 claude --restricted --permission-mode bypassPermissions -p 'Say OK'
2 Error: bypassPermissions not supported in restricted mode

It does not warn. It refuses. The two cannot be combined.

The second is a control, because “the tool is gone” and “the tool was refused” look the same from outside. Same prompt, same explicit allow-rule, one flag different:

Run made-*.txt created?
claude -p 'Run the shell command: touch made-b.txt' --allowedTools 'Bash(touch:*)' yes — Bash ran
same, plus --restricted no — Bash was gone; the model reached for Write instead and asked for permission

The allow-rule naming Bash did not bring Bash back. I cannot see the internals, so I will say only what I observed: an explicit allow rule for a removed tool did not restore it.

Related, and easy to miss: in 2.1.257 the changelog records that defaultMode: "bypassPermissions" in .claude/settings.json or .claude/settings.local.json is now ignored, like "auto" — you must set it in user or managed settings, or pass --permission-mode. That closes one of the paths this chapter explicitly says it did not test (“setting defaultMode in a settings file”). I have not measured that one; I am reporting the changelog, and the distinction between those two is the whole point of this book.

The trade — and this part I did measure

The help text says --restricted ignores user, project and local settings files. Your hooks live in those files. I first wrote that sentence down as the chapter’s conclusion while only having read it — which is the exact thing this book exists to warn you about. So I went and measured it.

Measured on 2.1.278, 2026-09-19, Linux under WSL2. An isolated HOME and a scratch working directory, with a hook in each scope that appends a different marker to a log. The event is UserPromptSubmit, deliberately: --restricted removes Bash, so a PreToolUse hook on Bash cannot tell you “the hook was ignored” apart from “nothing pulled the trigger.”

Run markers that arrived
control: no flags user, project, local
--restricted none
control: --settings f.json user, project, local, external
--restricted --settings f.json external only
--safe-mode none
--safe-mode --settings f.json none

The control fires all three scopes, so the instrument can tell the difference — and hooks from the three scopes add up rather than overriding each other.

So: the help text was right. --restricted drops all three scopes, and --settings is a real way back in.

But the last two rows are the ones to write down. --safe-mode also removes your hooks — and with --safe-mode the --settings escape hatch does not work. Same missing hooks, opposite answers to “can I get them back?”

And --safe-mode is worse than that, because of what it keeps. It is not new, either — it shipped in 2.1.169, long before --restricted, and this chapter has never mentioned it. Read its help text rather than its name:

Start with all customizations (CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands and agents, output styles, workflows, custom themes, keybindings, and more) disabled — useful for troubleshooting a broken configuration. Admin-managed (policy) settings still apply. Auth, model selection, built-in tools, and permissions work normally.

It is a diagnostic flag, not a hardened mode. It turns off the layer you added and — by its own help text — leaves the built-in tools and permissions working normally. Measured, with an ordinary PreToolUse guard on Bash installed the normal way in the project:

Run guard consulted? touch made-by-bash.txt
control: no flags yes — refused, exit 2 not created
--dangerously-skip-permissions yes — refused, exit 2 not created
--safe-mode no — hook gone the file was created

Same guard, same project, same prompt, and Bash explicitly allowed in every row, so the only thing that differs is whether the hook was there. The first two rows prove the guard refuses — including under the flag with “dangerously” in its name. Under --safe-mode it was never asked, and the shell command ran.

Of the three flags in this chapter, the one whose name sounds safest is the only one that removes your protection while leaving the tools that need protecting. If you use it to debug a broken configuration — which is what it is for — do that, and do not leave a session running in it.

Still unmeasured: managed settings. The help says managed settings survive both. There is no /etc/claude-code/ on the machine I measure on and I cannot create one there, so for managed settings I am still only reporting the documentation — same as the defaultMode note above.

Both of them also have an environment variable, and that is the worse door

CLAUDE_CODE_RESTRICTED=1 and CLAUDE_CODE_SAFE_MODE=1 do the same thing as the flags. Same guard, no flags, variable set:

Set guard consulted? shell command
CLAUDE_CODE_SAFE_MODE=1 no it ran
CLAUDE_CODE_RESTRICTED=1 no did not run — Bash was gone

The behaviour matching the flags is not the point. The visibility is. A flag is retyped every time you start; a line in your shell profile is read once and never seen again. A “let me just rule out my config” line from six months ago leaves every session since then running with your guards silent.

--restricted at least leaves a tell. Asked to run a shell command under it, the model said in substance that the shell tool was absent and that the absence looked deliberate. A missing tool is visible to the session. --safe-mode leaves no such tell in what comes back: every tool is present, the command runs, the answer reads normally, and only the guard is quiet.

Be precise about how far that goes. I measured non-interactive runs (claude -p), so what I can say is that the reply gives you nothing to notice. Interactively you would have a good chance of noticing, because --safe-mode also drops CLAUDE.md, skills, plugins, MCP servers and custom commands — a session missing all of that feels wrong quickly. The quiet case is the automated one: a script, a cron job, a loop, where nobody reads the session and the only thing that changed is that your guards stopped being asked.

That makes four doors out of your hooks: two flags and two environment variables — and for the two variables, the person who opened the door gets no reminder that it is open.

Getting your hooks back is not the same as being guarded

--restricted --settings reloads your hooks. It does not follow that they protect you, because the tool they watch may no longer exist. I have never counted the hook collections in the wild, so I will not claim anything about them. I can count mine. Counting PreToolUse registrations that cc-safe-setup ships — the six in hooks/hooks.json plus the eighteen across its four plugins — 24 registrations, of which 16 are matched on Bash. (That is the count of default-registered guards, not the number of scripts in the repository, and it is counted in my working copy rather than against the published package.) --restricted deletes Bash while leaving the file tools, confined to the working directories. I checked both of the ones that matter rather than trusting “file tools”: under --restricted, Write overwrote a file while the guard was loaded and never consulted, and Edit changed a line inside one in a run where no hook was loaded at all.

Same setup, a Bash-only guard passed in correctly with --settings — the arrival marker confirms it loaded in every row — asked to overwrite a three-line file:

Run guard consulted? the file
control: no flags, Bash and Write allowed no overwritten
--restricted --settings, Write allowed no overwritten
control: same guard, only Bash allowed, told to use a shell command yes — refused, exit 2 untouched

That third row exists because “the guard did not fire” and “the guard is broken” look identical from outside. It fired, it refused, and the model said so and declined to route around it. The guard works. It was simply never asked.

Read the first two rows honestly: --restricted did not open this hole. Even with Bash available, the model went straight to Write. A Bash-only guard never protected that file. What --restricted changes is that Write stops being one option among several and becomes the only road — so the gap goes from likely to certain.

The conclusion is not “avoid --restricted.” It is: if you run --restricted, your guards need to be on Write and Edit, because that is all that is left.

So, knowing which one you are getting:

  • --dangerously-skip-permissions — your hooks still run; the human pause is gone. Measured again here on 2.1.278 with the same instrument: the guard arrived and refused the command.
  • --restricted — the dangerous tools are gone and bypassPermissions is refused, so the human pause stays (the run above went to Write and asked). What is gone is your hooks, unless they are managed or passed with --settings. Passed back in, they still only cover the tools that survive, and those tools are confined to the working directories.
  • --safe-mode — your hooks are gone, --settings does not bring them back, and the dangerous tools and normal permissions are all still there. It is for debugging a broken configuration, not for running one.

None of the three is a safe mode. They remove different layers, and Chapter 8’s question still decides the outcome: can the guards you have actually refuse anything?

Chapter 7 is about which event you can put a refusal on. Chapter 8 is about finding out whether the hooks you already have can refuse at all. If you read one of them after this, read Chapter 8 — in the one collection I can count, my own, the names promise more than the matchers deliver.