Security How-to

Teach Claude Code's auto mode about your infrastructure, and don't drop the $defaults

Auto mode trusts your repo and nothing else. One settings block fixes the false blocks, and one missing string quietly deletes 71 of the 72 safety rules.

A tall open iron gate in a vast concrete wall, pale blue light spilling onto the floor

Since v2.1.283 auto mode is the built-in starting permission mode for interactive terminal and VS Code sessions, and by default it trusts exactly two things: the directory you launched in and that repo’s configured remotes. Everything else counts as external. So the first time Claude pushes a fix to a sibling repo in your company’s GitHub org, or writes a build artefact to your team’s bucket, the classifier blocks it, and it’s right to by its own lights. It has no idea those destinations are yours.

The fix is one block in ~/.claude/settings.json. The block also has a trap in it. Leave out one literal string and the safety rules you thought you were extending are gone. Both are easy to see with a command that ships in the CLI.

What auto mode trusts out of the box

Auto mode sends each tool call to a second model, the classifier, which blocks anything that looks irreversible, destructive or aimed outside your environment. Anthropic’s permission modes page lists the default blocks: curl | bash, production deploys, force pushes, mass deletion in cloud storage, IAM grants, disabling CI checks and similar. Pushes to any branch of the session’s own repo are fine, default branch included, though a push’s contents are still judged.

The part that bites is the trust boundary. Per the configuration docs, pushing to your company’s source-control org or writing to a team cloud bucket stays blocked until you add it to autoMode.environment. A repeated block is also more than an annoyance. After 3 blocks in a row or 20 in total, auto mode pauses and Claude Code goes back to prompting you. In a non-interactive claude -p run with no --permission-prompt-tool, there’s nobody to prompt, so the action simply doesn’t run and Claude carries on without it.

So an unattended script that needs to push to a second repo doesn’t fail loudly. It skips the step and the agent works around it. That’s the case worth fixing before it happens at 3am.

See what the classifier is working from

Print the built-in rules first:

claude auto-mode defaults

On Claude Code 2.1.302 that prints one JSON object with four lists. A count by list looks like this:

allow: 17
soft_deny: 72
hard_deny: 1
environment: 21

Each rule is a prose string, and the soft_deny entries carry a label such as Git Destructive or CI Bypass. The label is the bracketed name you see in a denial message, for example [Data Exfiltration]. To read one rule’s full wording without paging through JSON:

claude auto-mode defaults --label 'CI Bypass'

That prints the single matching rule (matching is a case-insensitive prefix on the label, and it needs v2.1.208 or later). The CI Bypass rule, for instance, covers things like gh workflow disable, marking a required check as not-required and editing branch protection, while noting that retrying failed jobs is fine.

The environment list is the interesting one. Many of its 21 entries are slots that read None configured: Organization, Cloud provider(s), Secrets management, CI/CD deploy targets, Trusted internal domains, Trusted cloud buckets, Key internal services, Internal package registry and so on. Each unfilled slot is something the classifier is guessing about.

Fill in the environment

For most teams autoMode.environment is the only field worth touching. Entries are prose, not patterns, and the docs say to write them the way you’d brief a new engineer. Put this in ~/.claude/settings.json, with your own org and domains:

{
  "autoMode": {
    "environment": [
      "$defaults",
      "Source control: github.com/acme-corp and all repos under it",
      "Trusted internal domains: *.corp.example.com",
      "Trusted cloud buckets: s3://acme-build-artifacts",
      "Internal package registry: artifacts.corp.example.com (npm and NuGet installs go through it)"
    ]
  }
}

The "$defaults" string splices the 21 built-in entries in at that position, so yours extend the list instead of replacing it. Then check what the classifier will actually use:

claude auto-mode config

With a two-entry environment block (source control plus one internal domain) added after "$defaults", the count for environment goes from 21 to 23 and the two new lines sit at the end of the list. Nothing else changes.

A sensible order, taken from the docs’ own rollout advice: start with your source-control org and CI, which clears the most common false blocks, then add trusted domains and buckets, then fill the rest as blocks come up. The Internal package registry slot is worth the time for a security reason. Once it’s set, an install that bypasses your registry for a public one gets blocked.

Don’t want to write it by hand? Run /auto-mode-setup (v2.1.228 or later, Pro, Max or Team plan, not available in cloud sessions). It drafts entries from your CLAUDE.md, README, git remotes and the hosts and command names in your recent sessions in the project, then shows the draft to accept or discard as a whole. Read it before accepting. It’s inferring your infrastructure from what Claude has already touched, and a draft can trust something you’d rather it didn’t.

The trap: forgetting $defaults

Now add a rule of your own. Say production Terraform should be off limits. The natural edit is:

{
  "autoMode": {
    "soft_deny": [
      "Never modify files under infra/terraform/prod/"
    ]
  }
}

Run claude auto-mode config against that and the counts are:

allow: 17
soft_deny: 1
hard_deny: 1
environment: 21

soft_deny went from 72 rules to your one. Force push, curl | bash, production deploys, the CI bypass rule and the auto-mode bypass rule are all gone from that list, because an array without "$defaults" replaces the built-in list for that section. The docs flag this in a danger box, and it’s easy to miss when the edit looks like an addition. Omit "$defaults" from hard_deny and you lose the built-in data exfiltration rule the same way.

The corrected block keeps the defaults in front of your rule:

{
  "autoMode": {
    "soft_deny": [
      "$defaults",
      "Never modify files under infra/terraform/prod/"
    ]
  }
}

Now claude auto-mode config reports 73 soft_deny entries, the 72 built-ins plus yours, and your rule is the last one. Each section is evaluated independently, so setting environment alone leaves the other three lists intact. The risk is per list.

Make claude auto-mode config part of the habit. After any edit to autoMode, compare the counts to defaults. A section that dropped from dozens to one is the signature of a missing "$defaults". If you do want to own a list outright, the docs’ advice is to print the defaults, copy them into your settings and review each rule.

claude auto-mode critique is the other check. It asks a model to review your custom entries and flag ones that are ambiguous, redundant or likely to cause false positives. It needs a live model call, so it wasn’t run for this piece, and what it says about your entries will depend on what you wrote.

Which list for which job

The four lists have a strict order inside the classifier. hard_deny blocks unconditionally. soft_deny blocks next, but an allow exception or explicit user intent can clear it. allow entries are exceptions to soft blocks. And if your message directly and specifically describes the exact action about to run, the classifier lets it through even when a soft rule matches.

That last tier has a limit. “Clean up the repo” doesn’t authorise a force push, while “force-push this branch” does. Use it as designed: when you name the action, you’ve made the decision.

A working split:

  • environment for facts: which orgs, buckets, domains and registries are yours.
  • allow for a routine pattern the defaults keep blocking, with the reason in the sentence (the docs’ example is staging deploys, because staging is isolated and resets nightly).
  • soft_deny for risks specific to you that the defaults miss, where a deliberate request should still get through.
  • hard_deny for lines that must never be crossed.

Note the scope of these as a policy tool. Entries from each scope combine, so a developer can add an allow that overrides an organisation’s soft_deny. The docs say outright that this is additive, not a hard policy boundary. If a rule must hold whatever anyone configures, it belongs in permissions.deny in managed settings, which blocks before the classifier is consulted.

Where the config is read from, and what it ignores

The classifier reads autoMode from your user settings (~/.claude/settings.json), from managed settings and from the --settings flag or Agent SDK inline JSON. It does not read it from .claude/settings.json or .claude/settings.local.json in the repo. That’s deliberate: a checked-in repo or a build step could otherwise ship its own allow rules to everyone who opens it. A team that wants shared trusted infrastructure has two routes, managed settings or a documented snippet each developer pastes into their user file.

It does read your CLAUDE.md, the same content Claude loads. A line like “never force push” in the project’s CLAUDE.md steers both Claude and the classifier. The docs suggest starting there for project conventions, and keeping autoMode for rules that span projects.

Two further settings are worth knowing. Narrow allow rules such as Bash(npm test) still apply in auto mode and resolve before the classifier runs, so a script path or flag the rule’s prefix didn’t anticipate can slip through unreviewed. Set "classifyAllShell": true inside autoMode and every Bash and PowerShell allow rule is suspended while auto mode is on, so the classifier sees every shell command. The price is latency, since each shell command becomes a classifier call.

For pushes and pull requests you want a person on, don’t rely on the classifier at all. Add permissions.ask rules such as Bash(git push *) and Bash(gh pr create *). Ask rules are evaluated before the classifier and always prompt. They match commands that start that way, so git -C <dir> push isn’t caught. For that, use a PreToolUse hook that reads the full command text.

What goes wrong

Prose rules are judged by a model, so they can be misread. An entry that says “staging is fine” with no namespace or cluster name leaves room for the classifier to guess. Name the thing.

An allow entry that’s too broad is a quiet hole, and it has no test the way code does. Review allow and soft_deny edits like you’d review a firewall rule.

A denial without a verdict is a different animal. If the row reads Not run ยท auto mode's check had no usable answer, the classifier didn’t judge the action. That’s a safety check or parse failure, not a rule to tune, and the errors page covers it.

The first 15 minutes

Run claude auto-mode defaults and skim the 21 environment slots. Add your source-control org and your CI host to ~/.claude/settings.json with "$defaults" first, then run claude auto-mode config and confirm the count rose by the number of lines you added. If a later denial names a rule in brackets, claude auto-mode defaults --label shows its exact wording, and /permissions has a Recently denied tab to retry it from.