Lesson 9 of 9 · Claude Code

Settings and secrets

CLAUDE.md is advice the model reads. Settings are rules the program enforces. Five places hold them, one order decides who wins, and a few habits keep your API keys out of a transcript that lives on disk.

Everything before this lesson taught Claude what to do. This one is about what Claude Code, the program around the model, will and will not let it do. The distinction matters because the official docs say it plainly: permission rules are enforced by Claude Code, not by the model. A line in CLAUDE.md that says "never read .env" shapes what Claude tries. A deny rule in a settings file stops the read whether Claude tries or not. Everything here is as of September 2026 and checked against the official Claude Code docs.

Five places a setting can live

A settings file is a JSON file named settings.json that Claude Code reads at startup and watches while it runs. Installing Claude Code creates none of them. Each one has a scope: the set of people and projects it reaches. Tap each layer below. The examples are for a fictional window tint shop in St. George whose website lives in one git repo.

Settings layers, lowest precedence first

Shared with

Just you, in every project on this machine.

What goes here

Personal preferences: theme, default model, and the permission rules you want everywhere. The /config menu writes most of its options here.

{
  "model": "claude-sonnet-5",
  "theme": "light-daltonized",
  "permissions": {
    "allow": ["Bash(git diff *)", "Bash(git log *)"],
    "deny": ["Read(~/.ssh/**)"]
  }
}

Higher layers win for the same key. List keys like permissions.allow and permissions.deny merge across every file instead of replacing each other, and a deny in any file cannot be undone by an allow in any other file.

Illustrative contents for a tint shop's website repo. Paths and key names match the Claude Code docs as of September 2026; the shop and its rules are made up.

  • User is ~/.claude/settings.json. It follows you into every project on this machine and nowhere else. Choices you make in the /config menu mostly land here.
  • Shared project is .claude/settings.json inside the repo. Commit it and every clone gets it, including a teammate's laptop and a cloud session started on the repo. Until you commit it, it is a file on your disk like any other.
  • Project local is .claude/settings.local.json. It is yours, for this one repo. The first time Claude Code writes it, it adds the file to your global git excludes so it never ends up in a commit. If you create it by hand, add it to .gitignore yourself.
  • Command line is claude --settings with inline JSON or a file path. One session, nothing written.
  • Managed is a file an organization deploys, or a policy pushed from the claude.ai console. If nobody manages your machine, you will never see one.

Who wins

When the same key appears in more than one file, Claude Code uses the value from the highest layer that sets it. Highest first: managed, then the command line, then project local, then shared project, then user. So if the team's committed file says "model": "claude-sonnet-5" and your local file says "model": "claude-opus-4-8", your sessions use Opus and nobody else's change.

Lists are the exception, and it is the exception that makes the whole system safe. A list key is one whose value is an array, like permissions.allow or permissions.deny. Claude Code merges list keys across every file it loads instead of picking one. Each file can add rules and no file can remove another file's. That is why a deny rule in your user file still blocks a read the project file allows, and why the reverse is also true. Managed settings sit above everything: a key you pass with --settings does not override the same managed key, and a managed deny cannot be overridden by any flag.

The keys that matter first

The settings reference lists dozens of keys. Five do most of the work.

permissions holds three lists. allow lets a tool use through without a prompt. ask forces a prompt every time. deny blocks it. Rules are checked in that order, deny, then ask, then allow, and the first match decides. Specificity does not matter: a broad Bash(aws *) deny blocks a narrow Bash(aws s3 ls) allow. Each rule is a tool name, optionally with a specifier in parentheses:

  • Bash(git *) matches every git command. Bash(git log *) matches only git log commands, including bare git log. Put the star after the subcommand, because everything before the first star is matched as written and is what limits the rule.
  • Read(.env*) uses gitignore syntax, so a bare filename matches at any depth under the current folder and the star matches within a name. It covers .env, .env.local, and .env.production. The docs' own paste-ready pair is Read(./.env) and Read(./.env.*). A Read deny also blocks Edit and Write on the same path.
  • WebFetch(domain:shopify.dev) matches fetches to one domain. mcp__github__* matches every tool from an MCP server named github. A bare tool name like Bash with no parentheses matches all uses, and as a deny it removes the tool from Claude's view entirely.

env sets environment variables for every session and the commands it runs. An environment variable is a named value programs read from their surroundings, like a timeout or a feature toggle. Values here are plain text in a file you may commit, and most env values in a committed project file wait until each teammate trusts the folder. Use it for non-secret configuration only. The secrets section says what to do with the rest.

hooks runs your own shell command at fixed moments, like after every file edit. The shape is an event name, a matcher, and a list of commands, exactly as you saw in Subagents and hooks. A hook in the shared project file runs on every teammate's machine, so it is a team decision.

enabledPlugins turns individual plugins on or off per scope, keyed as "name@marketplace": true. A committed entry means every clone gets the plugin, though a plugin from an external source still needs each person to install it once. model picks what new sessions start on. The docs' examples use claude-sonnet-5 and claude-opus-4-8, and /model saves your pick to the user file.

Where "don't ask again" goes, and trust

Every time a permission prompt offers "Yes, and don't ask again," picking it writes an allow rule. In the terminal that rule normally goes to .claude/settings.local.json at the root of the git repo, and it applies to future sessions anywhere in that repo. That is how most people's local file gets created: they never open it, it fills itself up. Two limits. A Bash command, a WebFetch domain, or a WebSearch approval persists to the file, while a file edit approval lasts only until the session ends. And a local allow rule does not outrank an ask rule from the project file, so a committed Bash(git push *) under ask still prompts you no matter how many times you said don't ask.

Workspace trust is the dialog Claude Code shows the first time you open a folder, asking whether you trust it. It exists because a committed .claude/settings.json grants capability: its allow rules and additionalDirectories entries take effect only after you accept that dialog for the repo, and the dialog lists them so you can read what you are granting. deny and ask rules apply right away, trusted or not, because they only restrict. Your untracked local file skips the trust step, since it is yours and not the repository's. Clone a stranger's repo and this is the line between their rules restricting Claude on your machine and their rules pre-approving commands on it. Read the second kind first.

Check what actually loaded

Four commands, each answering one question. Settings files reload when you save them, so you rarely need a restart to see a change.

  • /status answers "which files did it read?" Its Setting sources line names each settings file loaded for this session. It does not say which file supplied which key.
  • /permissions answers "what rules are in force, and from where?" It lists every allow, ask, and deny rule with the settings file each one came from, and you can add or remove rules in the dialog while Claude is working.
  • /config answers "how do I change a personal option?" It is a short menu of things like theme, editor mode, and verbose output, not a view of your whole settings file. It writes to the user file, with a few options going to the local file.
  • /doctor, or claude doctor from the shell, answers "why is this setting being ignored?" It runs a setup checkup and lists entries Claude Code rejected, like a malformed rule or a value the schema refused. A broken settings file, a stray comment or trailing comma, shows as a Settings Error at the start of the next session.

/context answers a different question: what is in the context window right now, meaning memory files, agents, and tools. It does not list settings files.

Secrets

Five habits, in the order they pay off.

  • Deny the read. Put Read(.env*) and Read(./secrets/**) under deny in the shared project file so every clone gets it. Read deny rules apply to Claude's file tools and to file commands Claude Code recognizes in Bash, like cat, head, tail, sed, and tee, plus redirections. They do not catch a command that reads files without naming them, like grep -r over the folder, or a script that opens files itself. The sandbox setting closes that gap by adding your Read deny paths to what every sandboxed command cannot read.
  • Never print a secret to see it. Not cat .env, not echo $TOKEN, not a debug line that logs request headers. To confirm a key is set, ask for its length.
  • Pull secrets through a command, inside the command. A password manager or vault CLI that prints one value on demand lets you write SHOPIFY_TOKEN="$(my-vault get SHOPIFY_ADMIN_TOKEN)" bun run sync. The transcript stores the command as written and whatever it printed. The substitution happens inside the shell, so the value never appears as long as the output does not echo it back. Whatever your vault is called, the name goes in the transcript and the value does not.
  • Keep .env out of git and out of settings. .env belongs in .gitignore. The env key in a settings file is for things like NEXT_TELEMETRY_DISABLED, never for a token, because a committed settings file is a plain text file in a repo.
  • MCP servers get keys from the environment. The add command accepts --env NAME=value, and the docs show it with a placeholder. Put a shell variable or a $(...) lookup in that slot, not the pasted key, and for a committed .mcp.json write ${SHOPIFY_TOKEN} so the file expands the variable at runtime and holds no secret. How the three MCP scopes work is back in MCP servers.

When a key does slip through, claude project purge <path> deletes that project's transcripts, auto memory, and prompt history lines. Rotate the key too. Deleting the copy on your laptop does not un-send anything that already left it.

Try this yourself

Do this on any repo that has, or could have, a .env file. Never test with a real key. First, create a throwaway file by hand, because once the deny rule is in place Claude cannot create one for you:

echo "SHOPIFY_ADMIN_TOKEN=fake-value-for-testing" > .env

Then create or open .claude/settings.json in the repo and add the rule. Strict JSON, no comments, no trailing commas:

{
  "permissions": {
    "deny": ["Read(.env*)"]
  }
}

Start Claude Code in the repo, or keep the session you have open, since the file reloads on save. Ask it plainly:

Read the .env file in this project and tell me what variables it defines.

Claude should report that the read was denied by a permission rule and stop. Try a second angle: "run cat .env." Same result, because the deny reaches the file commands Claude Code recognizes in Bash. Now run /permissions. Your rule is listed under Deny with .claude/settings.json named as its source. Commit the settings file, delete the throwaway .env, and every clone of that repo refuses the read on day one, trusted folder or not, because deny rules never wait. That is the whole module in one rule: instructions tell Claude what you want, settings make sure it cannot do what you never want.

That's Claude Code. Next moduleCoding Agents and CLIs

Last updated September 17, 2026