Unwait

Claude Code auto mode: what the classifier actually blocks

· 7 min read claude code auto mode permissions security classifier workflow

Auto mode replaces you with a second model that reviews every action. What it blocks by default, why it flags routine internal work, the order deny and ask rules run in, how to fix a denial properly, and the settings footgun that silently deletes the built-in exfiltration rule.

Auto mode is the starting permission mode on Pro, Max, and Team plans, which means most people are using it without having read a word about it. It replaces the permission prompt with a second model, the classifier, that reviews each action before it runs and blocks anything that escalates beyond what you asked for, targets infrastructure it does not recognize, or looks driven by content Claude read rather than by you.

That last clause is the point. Auto mode is not the bypass flag with a nicer name. Bypass removes the check; auto mode moves it off your keyboard and onto a model. Which means it has opinions, and you will meet them.

The order things run in

Almost every confusing denial makes sense once you know the sequence:

  1. permissions.deny blocks first, before the classifier is consulted. Nothing overrides it, not the classifier and not your stated intent. This is the layer for things that must never run.
  2. permissions.ask forces a prompt, even in auto mode. A content-scoped ask rule is your stated intent to be asked, so the classifier cannot auto-approve past it.
  3. Narrow allow rules resolve before the classifier runs. Bash(npm test) still works. Broad ones do not: rules that grant arbitrary execution, like Bash(*) or wildcarded interpreters, are suspended while auto mode is on, along with every rule naming Monitor.
  4. The classifier judges whatever is left.

So the recipe for a human checkpoint on the operations you care about is an ask rule, not a mode change:

{
  "permissions": {
    "ask": [
      "Bash(git push *)",
      "Bash(gh pr create *)"
    ]
  }
}

What it blocks by default

Inside the classifier there are four tiers. hard_deny blocks unconditionally, and the built-in list here is the data exfiltration rule. soft_deny blocks next, and the built-ins include force pushing, deleting remote branches or tags, rewriting remote history, curl | bash, production deploys, and attempts to bypass auto mode itself. allow rules act as exceptions to soft blocks. Then explicit user intent clears the remaining soft blocks.

That last tier is worth stating precisely, because it is the difference between the classifier being useful and being noise: general requests do not count as intent. Asking Claude to "clean up the repo" does not authorize a force push. Asking it to "force-push this branch" does. The classifier is looking for you to have described the specific action, not to have vaguely blessed the neighborhood.

Pushes are more permissive than they used to be. Pushes to any branch of the repository you are working in, including the default branch, plus pull request creation, are allowed by default. The exceptions are branches whose names mark them as deploy targets, like production, release, or gh-pages, which get judged on their own terms, and content-based blocks that still apply: a force push, a secret entering the commit, or a change that would leak secrets when CI runs it.

Why it flags your ordinary work

Here is the thing nobody tells you: by default the classifier trusts only your working directory and the current repo's configured remotes. Everything else is a potential exfiltration target until you say otherwise. Your company's GitHub org, your team's S3 bucket, your internal package registry, your CI host: none of it is trusted out of the box.

This is a defensible default and it produces a specific failure pattern. Routine internal operations get blocked, you assume the classifier is broken, and you turn auto mode off. The actual fix is to describe your environment to it in prose:

{
  "autoMode": {
    "environment": [
      "$defaults",
      "Source control: github.example.com/acme-corp and all repos under it",
      "Trusted cloud buckets: s3://acme-build-artifacts",
      "Key internal services: Jenkins at ci.example.com"
    ]
  }
}

Entries are natural language, not patterns. Write them the way you would describe your infrastructure to a new engineer. /auto-mode-setup will draft the list from your project and recent sessions, and it is worth noting what it reads: your CLAUDE.md, README.md, config files, git remotes, your existing settings, and the hosts, buckets, and command names from commands Claude ran. Never your messages.

The classifier also reads the same CLAUDE.md Claude does. An instruction like "never force push" in your project memory steers both at once, which is the cheapest place to put a project convention.

What happened to us

On a deploy day, auto mode blocked this:

cd server && fly deploy

Reason given: Blocked by classifier. A production deploy is a built-in soft block, so the classifier was doing exactly its job on a target it had never been told about. What we found next is the useful part: running cd server on its own, then fly deploy on its own, went through.

That tells you the unit of judgment is the command as written, not its eventual effect. It is not a fix, and it should not be treated as one. If the classifier was right, splitting the command means you defeated a safety check by accident and did not notice. The correct responses to that denial were to add our deploy target to autoMode.environment so routine deploys stop being surprises, or to state the intent plainly in the next message and let Claude retry. We were splitting a command to get unblocked, which worked and taught us nothing until we read why.

Fixing a denial properly

The on-screen notice is deliberately terse: bash denied by auto mode · Blocked by classifier · /permissions. It names the tool and the reason and omits the command. To see what was actually blocked, find the tool call in the conversation, and press Ctrl+O for the transcript viewer if it got folded into a summary line like Ran 3 shell commands.

Then pick the fix by what the call was reaching for:

/permissions has a Recently denied tab where you can press r on a denial to mark it for retry, and an Auto mode tab for editing the rule lists without opening a settings file. For anything programmatic, a PermissionDenied hook receives the exact tool_input, which is the only place the full command is available to a script.

Two commands make the configuration legible: claude auto-mode defaults prints the built-in rules, and claude auto-mode config prints what your session actually uses with your settings applied. Run the second one after every change.

Three things that will bite you

Omitting $defaults deletes the built-ins. Setting allow, soft_deny, or hard_deny without the literal string "$defaults" in the array replaces the entire default list for that section. Drop it from hard_deny and you have silently removed the data exfiltration rule. Include "$defaults" unless you have deliberately decided to own the whole list, in which case print the defaults first and review them one by one.

Project settings are ignored on purpose. The classifier does not read autoMode from .claude/settings.json or .claude/settings.local.json. Both live in the repo, so a checked-in file or a build step could otherwise inject its own allow rules. Put autoMode in ~/.claude/settings.json or managed settings. This is the same threat model we walked through in the security post: the repository is input, not authority.

A boundary you only said in chat can evaporate. Telling Claude "don't push until I review" does work, the classifier honors it, but that instruction lives in the conversation, and compaction can remove the message that stated it. If the boundary matters, it belongs in an ask or deny rule where it cannot be summarized away.

Tightening or loosening it

To tighten: autoMode.classifyAllShell: true suspends every Bash and PowerShell allow rule while auto mode is active, so the classifier sees every shell command. This closes the gap where a narrow rule like Bash(npm run *) lets an argument through that its prefix never anticipated. It costs latency and a classifier call per command.

To loosen: add to allow when a routine pattern keeps getting flagged, and to environment when the problem is a destination rather than an action. claude auto-mode critique will review your custom rules and flag ones likely to cause false positives.

If Claude Code reports auto mode as unavailable, the session falls back to Manual rather than failing. Check the model requirements, whether a settings file sets disableAutoMode, and whether your organization turned it off. A session that gets refused server-side keeps auto mode off until it ends, so the fix is a new session rather than a retry.

The summary that matters: auto mode is a real check, it defaults to a very small trust boundary, and most of the friction people blame on it is the gap between that boundary and their actual infrastructure. Spend twenty minutes on autoMode.environment and it stops being in your way, which is a much better outcome than the thing most people do instead, which is turning it off and reaching for a flag with "dangerously" in the name.

Unwait does this for you

A macOS menu bar app that watches your Claude Code and Codex sessions, shows a short card while they work, and puts a strip on screen the moment one finishes. Free for two weeks, no card and no sign up.

Try for free
← All posts