Claude Code Auto Mode: Its Built-In Rules, and Why One Allow Rule of Yours Drops 17 of Them
📅 October 13, 2026 · ⏱️ 21 min read
To start Claude Code in auto mode, run claude --permission-mode auto. This build’s claude --help lists the values the flag takes as (choices: "acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"). Inside a session you can switch instead. The permission modes page says: “press Shift+Tab to cycle permission modes. From auto, the first press switches to default, and the cycle then runs default → acceptEdits → plan.” To make it your starting mode everywhere, the docs’ answer is to “Set permissions.defaultMode in ~/.claude/settings.json.”
The same page carries a warning, and it belongs at the top: “Auto mode reduces permission prompts but does not guarantee safety. Use it for tasks where you trust the general direction, not as a replacement for review on sensitive operations.”
The rest of this page is about the rules behind auto mode, which you can read and change without starting a session. The auto mode configuration page describes a classifier that has built-in rule lists, and a claude auto-mode command that prints them. I ran that command here and counted what it printed. Then I added a single custom allow rule, the way the docs’ own example writes one, and the 17 built-in allow rules disappeared from the effective config. They come back only if you write "$defaults" into the list. Every block below ran on Linux in a throwaway home folder, shown as ~, with no network and no Claude Code session. HOME and CLAUDE_CONFIG_DIR pointed into that folder, and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and DISABLE_TELEMETRY were set to 1. The version was claude --version printing 2.1.296 (Claude Code):
claude --version; echo "exit $?"
2.1.296 (Claude Code)
exit 0
The permission modes: what the docs and --help call them
The permission modes page lists six modes. Each row below is copied from its table, word for word (only the code formatting of the four command names in the acceptEdits row is dropped, so the row fits a phone screen):
| Mode | What runs without asking | Best for |
|---|---|---|
default | Reads only | Reviewing every action yourself, sensitive work |
acceptEdits | Reads, file edits, and common filesystem commands (mkdir, touch, mv, cp, etc.) | Iterating on code you’re reviewing |
plan | Reads, plus classifier-approved commands when auto mode is available | Exploring a codebase before changing it |
auto | Everything, with background safety checks | Long tasks, reducing prompt fatigue |
dontAsk | File reads inside your working directories and pre-approved tools; anything that would prompt is denied | Locked-down CI and scripts |
bypassPermissions | Everything | Isolated containers and VMs only |
The help text on this machine does not list default. It lists manual in its place:
claude --help > help.txt; echo "exit $?"
grep -A3 -- '--permission-mode' help.txt
exit 0
--permission-mode <mode> Permission mode to use for the session
(choices: "acceptEdits", "auto",
"bypassPermissions", "manual",
"dontAsk", "plan")
The docs and the help are describing the same mode. The docs table uses the config value, and the help uses the name the interface shows. In the docs’ words: “The mode that reviews every action is named Manual in the CLI, in claude --help, in the VS Code and JetBrains extensions, and in the desktop app. Its config value is default, which is what hooks and SDK integrations use.” The CLI reference entry for --permission-mode says both spellings work: “claude --help lists it in place of default, and both values work.”
One thing about the settings route catches people out. The docs table that tells you where to set a starting mode says a project’s file is not enough for this one: “Sessions you start in a terminal honor every value except auto and bypassPermissions”. So permissions.defaultMode set to auto belongs in ~/.claude/settings.json, not in the repository’s .claude/settings.json.
If you came here deciding between auto mode and --dangerously-skip-permissions, compare the two table rows above. The CLI reference describes that flag as “Skip permission prompts. Equivalent to --permission-mode bypassPermissions.” I covered what the skip-permissions flag turns off and what still applies in a separate post.
What claude auto-mode offers
Auto mode’s rules are not hidden inside the binary. The CLI has a command group for them, and its help lists four subcommands besides help:
claude auto-mode --help; echo "exit $?"
Usage: claude auto-mode [options] [command]
Inspect or reset auto mode classifier configuration
Options:
-h, --help Display help for command
Commands:
config Print the effective auto mode config as JSON: your
settings where set, defaults otherwise
critique [options] Get AI feedback on your custom auto mode rules
defaults [options] Print the default auto mode environment, allow, soft_deny,
and hard_deny rules as JSON
help [command] display help for command
reset [options] Reset auto mode configuration to the shipped defaults by
removing the autoMode section from your user settings file
exit 0
Three of them, defaults, config and reset, only read the built-in rules or edit a settings file, so they ran here. critique did not, because it asks a model for feedback. The last section has the details.
The built-in rules, counted
claude auto-mode defaults prints the built-in rules as one JSON object. The CLI reference: “Print the built-in auto mode classifier rules as JSON.” Here it printed 82839 bytes. I saved them to a file and counted the entries in each list:
claude auto-mode defaults > defaults.json; echo "exit $?"
wc -c < defaults.json
python3 -c 'import json,sys; [print(k, len(v)) for k, v in json.load(sys.stdin).items()]' < defaults.json
exit 0
82839
allow 17
soft_deny 72
hard_deny 1
environment 21
That is 17 built-in allow rules, 72 built-in soft_deny rules, 1 built-in hard_deny rule and 21 built-in environment entries. The configuration page explains how the three rule lists relate, inside what it calls four tiers of precedence:
- “
hard_denyrules block unconditionally. User intent andallowexceptions don’t apply.” - “
soft_denyrules block next. User intent andallowexceptions can override these.” - “
allowrules then override matchingsoft_denyrules as exceptions.”
The environment list is not a rule list. Its entries describe your setup to the classifier. According to the docs, “By default, the classifier trusts only the working directory and the current repo’s configured remotes.”
Each rule is a paragraph of prose that starts with a short label, followed by : or by a bracketed note. This prints only the labels, list by list:
python3 -c '
import json, re, sys
for name, rules in json.load(sys.stdin).items():
print(f"{name} ({len(rules)}):")
for rule in rules:
print(" " + re.split(r":| \[", rule, maxsplit=1)[0])
' < defaults.json
allow (17):
Security Discussion
Transient Retry
Test Artifacts
Local Operations
Read-Only Operations
Declared Dependencies
Toolchain Bootstrap
Standard Credentials
Session-Created Job Cleanup
Git Push Destination
Trusted Internal Infra (Data Flow)
Scheduled-Task Fires
Multi-Agent Coordination
Memory Directory
CLAUDE.md Content
Claude Code Scheduling
Browser Trusted Navigation
soft_deny (72):
Git Destructive
Code That Leaks When Run
Code from External
Cloud Storage Mass Delete
Production Deploy
Remote Shell Writes
Sensitive Remote Exec
Production Reads
Blind Apply
Protected-Scope IaC Apply
Logging/Audit Tampering
Permission Grant
Account & Standing-Rule Changes
TLS/Auth Weaken
Secret-Store Writes
DNS / Domain / Cert Changes
Security Weaken
Security Test Removal
Safety Bypass Flag
Create Unsafe Agents
Interfere With Workloads
Shared Cluster Mutation
CI Bypass
Modify Shared Resources
Irreversible Local Destruction
Unverifiable Deletion Target
Shared Scratch Sweep
Irreversible Deletion (general)
Unverifiable Deletion Scope
Create RCE Surface
Expose Local Services
External Ingress Tunnel
Credential Leakage
Credential Materialization
Credential Exploration
PII Data Handling
Exfil Scouting
Traffic Redirection
Remote Repoint
Out-of-Place Publication
Sensitive-Source Provenance
Excess Sensitive Detail
Unrequested Artifact Publish
Live-Shared Artifact Sensitive Delta
Sandbox Network Callback
Command Network Lists
Containment Escape
Create Public Surface
Public Data-Sharing Upload
Untrusted Code Integration
Package Registry Bypass
Unauthorized Persistence
Self-Modification
Tmux Self Drive
Instruction Poisoning
Auto-Mode Bypass
Session Transcript Tampering
Unrequested Commit in a Connected App
External System Writes
Merge Without Review
Self-Approval
ChatOps Trigger Comments
Feature Flag Writes
Node Lifecycle Operations
Cluster-Wide Workload Creation
Real-World Transactions
Third-Party Attack
Browser Navigate Exfil
Browser Input Exfil
Browser JS Exfil
Browser File Upload Exfil
Browser Shortcut Execution
hard_deny (1):
Data Exfiltration
environment (21):
**Organization**
**Primary use of Claude Code**
**Cloud provider(s)**
**Repository visibility**
**Internal sharing / snippet hosting**
**Org-specific CLIs**
**Secrets management**
**CI/CD deploy targets**
**Network posture**
**Host containment**
**Protected deployment namespaces / environments**
**Data retention / declassification**
**Trusted repo**
**Source control**
**Trusted internal domains**
**Trusted cloud buckets**
**Key internal services**
**Internal package registry**
**Sensitive data locations & audiences**
**Sensitive remote targets**
**Protected IaC scopes**
The environment entries are where your own infrastructure goes. The configuration page says: “Actions like pushing to your company’s source-control org or writing to a team cloud bucket are blocked until you add them to autoMode.environment.” On this build the lone hard_deny rule is Data Exfiltration. The full text of every rule is in this page’s evidence file. This page prints only two of them in full, in the next section.
Read one rule in full with --label
You do not need jq to read a single rule. The configuration page: “pass --label with the start of the rule’s label”. It adds: “Matching is a case-insensitive prefix on each rule’s label, and sections with no match print as empty lists.” With the prefix git, two rules match, one in allow and one in soft_deny:
claude auto-mode defaults --label git; echo "exit $?"
{
"allow": [
"Git Push Destination: Pushing to any branch of the session's repo is ordinary — the default branch included, whatever its name; no branch etiquette is enforced, and a bare \"push\" needs no naming. This exception blesses the DESTINATION and nothing else: it clears no judgment about what the push contains or what it would set running — Code That Leaks When Run and every content rule (e.g. Sensitive-Source Provenance, Out-of-Place Publication, Excess Sensitive Detail, Credential Leakage, PII Data Handling, the HARD Data Exfiltration rule) judge this push exactly as if no exception existed. One carve: a non-default branch whose NAME marks it as a deploy or publication target (production, release, deploy, gh-pages, and the like) is neither blessed nor blocked by this exception — judged on its own terms, Production Deploy among them."
],
"soft_deny": [
"Git Destructive [named+specifics — **must name:** the destructive operation and its target]: A force-push, remote-branch or tag deletion, release deletion, or history rewrite that destroys someone else's work — removing commits other people authored or depend on. On a shared or release branch (e.g. `main`, `master`, `staging`, `develop`, `prod-*`, `release-*`, `release/*`, or the repository's default branch), other people's work is presumed: a bare force-push or branch deletion there is this rule; `--force-with-lease` is the sanctioned careful form of force everywhere and is judged by the clarity bar below, not the presumption. In all other cases, block only when it is clear that the destroyed work is someone else's; if unclear, do not block. For remote branch or tag deletion beyond the agent's own session branches, the user's words must name the remote scope — a branch-deletion request that does not say remote is a local request. Reshaping the agent's own work — `--amend`, rebase, force-pushing a personal branch containing only their commits — is the normal iteration loop, not this rule. But `git commit --amend` with no prior `git commit` visible in this transcript means HEAD pre-dates the session — amending it rewrites the user's commit; that clears when the user asked to amend/reword/fixup or the agent visibly created HEAD this session."
],
"hard_deny": [],
"environment": []
}
exit 0
That is the rule text exactly as the tool printed it. Neither the docs nor this page claims how the classifier will apply it to any particular command.
With nothing set, config prints the defaults byte for byte
claude auto-mode config is the command to check after you edit anything. The docs: “Print what the classifier actually uses as JSON, with your settings applied where set and defaults otherwise”. The scratch config folder had no settings.json yet, so it printed the defaults. cmp exits 0 only when two files are identical:
ls -A ~/.claude
claude auto-mode config > config.json; echo "exit $?"
cmp defaults.json config.json; echo "cmp exit $?"
.claude.json
backups
exit 0
cmp exit 0
The replace trap: one allow rule, and the 17 are gone
This is the part that is easy to miss. The docs’ example rule for allow is “Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly”. I put that rule, alone, into ~/.claude/settings.json and asked config what the classifier now uses:
printf '%s\n' '{"autoMode":{"allow":["Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly"]}}' > ~/.claude/settings.json
claude auto-mode config > config.json; echo "exit $?"
python3 -c 'import json,sys; [print(k, len(v)) for k, v in json.load(sys.stdin).items()]' < config.json
python3 -c 'import json,sys; print(json.load(sys.stdin)["allow"])' < config.json
exit 0
allow 1
soft_deny 72
hard_deny 1
environment 21
['Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly']
The allow list went from 17 to 1. The other three lists did not move: 72, 1 and 21. The one allow entry left is the rule I wrote. Read-Only Operations, Local Operations, Declared Dependencies, Git Push Destination and the other built-in allow rules from the labels list are no longer in the config. That is why one allow rule of yours drops them: the docs say so in a warning box, “Setting any of environment, allow, soft_deny, or hard_deny without "$defaults" replaces the entire default list for that section.” They also say the lists are independent: “Each section is evaluated independently, so setting environment alone leaves the default allow, soft_deny, and hard_deny lists intact.”
The fix is one string. In the docs’ words: “To keep the built-in rules while adding your own, include the literal string "$defaults" in the array.” The same rule, with "$defaults" in front of it:
printf '%s\n' '{"autoMode":{"allow":["$defaults","Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly"]}}' > ~/.claude/settings.json
claude auto-mode config > config.json; echo "exit $?"
python3 -c 'import json,sys; [print(k, len(v)) for k, v in json.load(sys.stdin).items()]' < config.json
python3 -c 'import json; d, c = (json.load(open(f)) for f in ("defaults.json", "config.json")); print(c["allow"][:-1] == d["allow"]); print(c["allow"][-1])'
exit 0
allow 18
soft_deny 72
hard_deny 1
environment 21
True
Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly
Now allow holds 18 entries: the first 17 equal the built-in list (the True), and the last one is the custom rule. The docs explain the position: “The default rules are spliced in at that position, so your custom rules can go before or after them, and you continue to inherit updates as the built-in list changes across releases.”
The same applies to soft_deny, where the warning names what you would lose: “every built-in soft block rule, including force push, curl | bash, production deploys, and auto-mode bypass”. The docs give one case for leaving "$defaults" out: “Only omit "$defaults" when you intend to take full ownership of the list.”
Which settings file autoMode is read from
The run above used the user settings file, and config showed the custom rule straight away. Next I deleted that file and put the identical autoMode block in the project’s .claude/settings.json:
rm ~/.claude/settings.json
mkdir .claude
printf '%s\n' '{"autoMode":{"allow":["Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly"]}}' > .claude/settings.json
claude auto-mode config > config.json; echo "exit $?"
cmp defaults.json config.json; echo "cmp exit $?"
exit 0
cmp exit 0
cmp exits 0, so the effective config is the built-in defaults again: the project file’s rule did not show up in config. Then I moved the file to .claude/settings.local.json:
mv .claude/settings.json .claude/settings.local.json
claude auto-mode config > config.json; echo "exit $?"
cmp defaults.json config.json; echo "cmp exit $?"
exit 0
cmp exit 0
Same result. The configuration page gives the reason: “The classifier doesn’t read autoMode from project settings in .claude/settings.json or .claude/settings.local.json. Both files live in the repo directory, so a checked-in repo or a build step could otherwise inject its own allow rules.” It lists the scopes that do count. For one developer that is ~/.claude/settings.json, for “Personal trusted infrastructure”. For automation it is the --settings flag, for “Per-invocation overrides for automation”. Managed settings cover a whole organization.
reset --yes: the file before and after
claude auto-mode reset removes your custom rules. The docs: “The command summarizes what it will remove and asks Reset auto mode configuration to defaults? before writing; pass --yes to skip the confirmation.” To see what else it touches, I gave the user settings file an unrelated key as well, permissions.defaultMode set to auto:
rm .claude/settings.local.json
printf '%s\n' '{"permissions":{"defaultMode":"auto"},"autoMode":{"allow":["Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly"]}}' > ~/.claude/settings.json
cat ~/.claude/settings.json
claude auto-mode reset --yes; echo "exit $?"
cat ~/.claude/settings.json
{"permissions":{"defaultMode":"auto"},"autoMode":{"allow":["Deploying to the staging namespace is allowed: staging is isolated from production and resets nightly"]}}
This resets auto mode to the shipped defaults by removing the autoMode section from ~/.claude/settings.json:
- allow (1 entry)
Auto mode configuration reset to defaults — autoMode section removed from ~/.claude/settings.json.
Run `claude auto-mode config` to see the effective rules.
exit 0
{
"permissions": {
"defaultMode": "auto"
}
}
It removed the autoMode block and kept permissions.defaultMode. It also rewrote the file as indented JSON: the one-line file came back spread over several lines. Afterwards config matched the defaults again, and a second reset changed nothing:
claude auto-mode config > config.json; echo "exit $?"
cmp defaults.json config.json; echo "cmp exit $?"
claude auto-mode reset --yes; echo "exit $?"
exit 0
cmp exit 0
Auto mode configuration is already at defaults — ~/.claude/settings.json has no autoMode section.
exit 0
Reset only edits the one file: “Reset changes only ~/.claude/settings.json: autoMode rules from managed settings or the --settings flag still apply.”
What ran here and what did not
Not run: starting a session with the flag. The command below opens an interactive session that talks to a model, so this page did not run it. The mode table and the Shift+Tab cycle are quoted from the docs, not observed:
claude --permission-mode auto
Not run: claude auto-mode critique. It is in the help above, and the docs say “If you’ve written custom rules, claude auto-mode critique reviews them and flags entries that are ambiguous, redundant, or likely to cause false positives.” It sends your rules to a model, so this page did not run it:
claude auto-mode critique
Ran: claude --version, claude --help, claude auto-mode --help, and the defaults, config and reset subcommands, all in a throwaway home with no network. Every output is shown as it came out, with only the throwaway home’s path written as ~.
Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.