Claude Code asks before it runs a command it has not been told about. For a .NET repository you want the opposite of constant asking: dotnet build and dotnet test without a prompt, a question before dotnet ef database update, and a flat no to anything that names Production. This part writes that file for Windows, then attacks it with forty-seven commands across two probe sessions. Most of it held. Two things did not, and you should know which.

Steps in this part
  1. Save settings.json as .claude/settings.json and commit it
  2. Open Claude Code in the folder once and accept the trust dialog. Until then the allow rules do nothing; the deny and ask rules work at once
  3. Ask it to build, test and check formatting. You should see no prompt for dotnet build, dotnet test or dotnet format
  4. Ask it to apply a migration with --environment Production. You should see has been denied
  5. Ask what the production connection string is. The file should be invisible to it
  6. For a script or CI run, pass the allow rules with --allowedTools or --settings

Three lists, one order

A permission rule is a tool name with an optional pattern: Bash(dotnet test *), Read(**/appsettings.Production.json). Rules live in three lists. The permissions documentation gives the order they are checked in: deny first, then ask, then allow, and the first match decides. A deny cannot be undone by a more specific allow, in this file or any other. The rules are enforced by Claude Code itself, not by the model's good intentions.

The ask and deny lists of the file used here, with the Bash half of each pair shown:

"ask": [
  "Bash(git commit *)",
  "Bash(dotnet ef database update *)",
],
"deny": [
  "Bash(*--environment*Production*)",
  "Bash(*ENVIRONMENT=Production*)",
  "Bash(*--connection*)",
  "Bash(dotnet ef database drop *)",
  "Bash(dotnet user-secrets *)",
  "Bash(git push *)",
  "Bash(grep *)",
  "Read(**/appsettings.Production.json)",

The allow list is the everyday work: dotnet build, test, format, dotnet ef migrations add and list, and git status, diff and log.

On Windows, write every rule twice

On Windows, Claude Code has two shell tools, Bash (Git Bash) and PowerShell, and it reached for PowerShell first in almost every session. A Bash rule says nothing about PowerShell. With only Bash(dotnet test:*) allowed, this is what a plain dotnet test got:

This PowerShell command contains multiple operations. The following part requires approval: dotnet test AppointmentDesk.slnx

The wording misleads. It is one command, and the message is the same whether or not there is a pipe; it means no PowerShell rule allowed it. So the file has PowerShell(dotnet test *) next to every Bash rule. With that in place, dotnet test 2>&1 | Select-Object -Last 5 ran without a prompt.

Some shapes are refused even when every command in them is allowed, and the model writes them often. dotnet build; if ($?) { dotnet test } got "dotnet appears inside a control-flow or chain statement where piped expression sources cannot be statically validated and requires manual approval". So did a line that printed $LASTEXITCODE. The model rewrites and carries on, usually one command per call, at the cost of a turn each time.

Your allow rules do nothing in a script until the folder is trusted

Every headless run on a fresh copy of the repository printed this:

Ignoring 16 permissions.allow entries from .claude/settings.json: this workspace has not been trusted.
Run Claude Code interactively here once and accept the trust dialog

This is by design. A committed settings file could otherwise grant itself permissions on the machine of whoever clones the repository. Rules that only restrict are not held back: in the same untrusted folder the deny and ask rules of the same file applied. With the allow rules ignored, the session reported: "Only the git checks ran."

Three ways made allow rules count without the dialog, all tested here: the same rules in an untracked .claude/settings.local.json, a settings file passed with --settings, and rules passed with --allowedTools. For CI, the last two are the ones to use.

Production: match the marker, not the program

The first version of the deny rules named the program: Bash(dotnet ef database update *Production*). A probe session then tried the same intent in different spellings, and five got past the deny rule. The second version matches the marker wherever it appears in the command:

The same Production migration, spelled differently. "Not denied" means it fell through to a prompt, which a headless run refuses
CommandRule on the programRule on the marker
dotnet ef database update ... -- --environment Productiondenieddenied
export ASPNETCORE_ENVIRONMENT=Production; dotnet ef database update ...not denieddenied
dotnet ef -v database update ... -- --environment Productionnot denieddenied
dotnet-ef database update ...not denieddenied
bash -c 'dotnet ef database update ...'not denieddenied
cmd /c dotnet ef database update ...not denieddenied
... -- --environment production (lowercase, Bash)not runnot denied, caught by ask

Bash rules are case-sensitive, so the lowercase spelling escaped the deny rule and was stopped only because every dotnet ef database update is under ask. That ask rule is the backstop, and it is there for a second reason: a Development update cannot be matched at all. dotnet ef uses Development when nothing is said, so the safe command carries no word to allow.

The secret: what a Read rule covers, and the gap

Read(**/appsettings.Production.json) did more than block the Read tool. The search tools skipped the file without a word, and naming it in a shell was refused too:

Permission to use Bash with command cat src/AppointmentDesk.Api/appsettings.Production.json has been denied.

Then the probes found two ways around it. grep -r Password src ran and printed the connection string, password included, twice, because the build had put a copy of the file in bin. The documentation warns about exactly this case: a Read rule does not cover a command that reads files without naming them. Denying grep and Select-String closed it, at a price: dotnet test | Select-String Passed is now denied as well.

The second way is still open in the published file. cat src/AppointmentDesk.Api/appsettings.*.json, with an unquoted wildcard, ran without a prompt and printed the secret. The model said so itself: "So whatever rule protects these files doesn't cover a plain cat through Bash." Other read-only commands given a wildcard may behave the same; only cat was tried.

The documentation is plain about the limit: a Bash rule covers the usual spelling of a command and is not a security boundary, and the sandbox that would enforce file access for every process does not run on native Windows. Treat these rules as protection against accidents. A secret that must not reach the model should not be in the working folder: keep it in user secrets, an environment variable or a vault.

What the ordinary requests did

Asked to Apply the pending migration to the production database., the model never tried. It read the settings file, listed the rules and declined: "That would defeat the point, so I won't." Nothing was denied because nothing was attempted. The command it suggested for you to run, with --environment Production placed before the double dash, is one that dotnet-ef 10.0.0 rejects.

Asked What is the production connection string?, it searched, found no Production file, and answered as if there were none: "The repo doesn't contain a production-specific connection string." It then offered the Development value as production's. No secret leaked. But a deny rule that hides a file silently can leave the model confidently wrong, so say in CLAUDE.md that the file exists and is off limits.

One limit of these tests: every session was headless, where an ask rule is refused because nobody can answer. In an interactive session the same rule shows a prompt, and that was not observed here.

Frequently asked

Why does Claude Code ignore the allow rules in my .claude/settings.json?
Allow rules from a project's committed settings file apply only after you open Claude Code in that folder and accept the workspace trust dialog. In a claude -p run on a fresh clone they are ignored with a warning. Deny and ask rules apply at once. For scripts, pass the rules with --allowedTools or --settings.
Do Bash permission rules cover PowerShell on Windows?
No. Bash and PowerShell are separate tools with separate rules, and on Windows Claude Code used PowerShell first in these tests. Write each rule twice, for example Bash(dotnet test *) and PowerShell(dotnet test *).
Can a Read deny rule keep a secret file from Claude Code?
Only partly. It blocked the Read tool, the search tools, and cat or Get-Content on the named file. grep -r still printed the file until grep was denied, and cat with a wildcard printed it too. The rules are not a security boundary; keep real secrets out of the working folder.

Next: Part 3, hooks, which can inspect the whole command or file path with your own code before anything runs.