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:
PreToolUsehooks still fire, and a hook that exits 2 still refuses the call.- A
permissions.denyrule still applies — measured headless only, see below. - I did not measure what the flag removes. My own setup allows
Bashoutright, 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
PreToolUsehook 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
defaultModein a settings file, are different paths and I tested neither.permissions.askis untested, and I would not assume it survives. The reference defines this mode as skipping permission prompts, and anaskrule is a prompt.denyheld in my runs; do not read that as “my rules held”.The exact shape only.
Bash(touch:*)was tested againsttouch made-e.txt, the spelling the rule names. Whether it also catches/usr/bin/touchorsh -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
bypassPermissionsmode, Claude Code skips permission prompts, including for writes to protected paths such as.gitand.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 testEditorWrite, 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:
- Is there a
hook-fired.logat all? If not, checkCLAUDE_PROJECT_DIRbefore 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. - 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
--toolsnames them, and ignores user, project and local settings files (managed settings and--settingsstill apply; add--strict-mcp-configto skip MCP servers too). Also confines the file tools to the working directories (--add-dirincluded), refusesbypassPermissions, 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 andbypassPermissionsis refused, so the human pause stays (the run above went toWriteand 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,--settingsdoes 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.