CLAUDE.md and settings.json
September 13, 2026
I’ve been using Claude Code on a full-stack side project: a React dashboard, a GraphQL API, and Python looking into Postgres. Before letting it loose on the repo, I wanted AI to know the house rules, and it should have a few doors it simply can’t open.
Claude Code gives you two main files for that, and they do very different jobs:
- CLAUDE.md is guidance. It’s instructions the model reads and should follow.
- settings.json is enforcement. It’s rules the tool applies whether the model likes them or not.
If I write “don’t push” in CLAUDE.md, that’s a polite request. If I put git push on the deny list in settings, that’s a locked door. Polite requests work most of the time. So does “please don’t commit directly to main.”
CLAUDE.md
CLAUDE.md sits in the root of the repo and is loaded at the start of every session. Mine covers:
- Architecture and stack
- Constraints, such as budget, database roles, and no secrets in the repo
- Git workflow: issue branches, commit message format, PR review
My repo is public, and some of what I want Claude to know is personal: how I like to work, what I’m relearning, and so on. So I split it in two:
CLAUDE.mdis committed and holds the project rules.CLAUDE.local.mdis excluded from git and holds the personal stuff.
The public file pulls in the private one with an import line at the bottom:
@CLAUDE.local.md
The rules stay public (they’re worth showing), and my notes to self stay private.
One small gotcha: I excluded the local file with .git/info/exclude, but Prettier doesn’t read that file. It went into .prettierignore too.
settings.json
Permissions live in .claude/settings.json. Each rule has the form Tool(pattern), and goes in one of three lists:
allow: runs without askingask: always promptsdeny: always refused
Bash(npm run lint:*) matches any shell command that starts with npm run lint.
Here’s what I set up:
{
"permissions": {
"allow": [
"Bash(npm run lint:*)",
"Bash(npm run typecheck:*)",
"Bash(npm run test:*)",
"Bash(npm run build:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(gh issue view:*)",
"Bash(gh pr checks:*)"
],
"deny": [
"Bash(git push:*)",
"Bash(gh pr merge:*)",
"Bash(gh release:*)",
"Read(./**/.env)"
]
}
}
The allow list is read-only and check commands. Claude can lint, test, and build its own work without me clicking “yes” forty times an hour.
The deny list is anything that publishes (push, merge, release) plus reading secrets. My workflow is “Claude plans and suggests, helps me write, and I review and commit.” The tool shouldn’t be able to skip my part of that, even if I ask it to by mistake at 11pm.
Testing the deny list
How do you test that git push is blocked? Not by running git push and hoping.
Claude tested it with gh release list, a harmless read-only command that the gh release deny rule covers. It was refused without a prompt. Test your guardrails with something that wouldn’t hurt if the guardrail failed.
The three settings files
There’s more than one settings file. Claude Code reads every one that exists and merges them:
| File | Committed? | Use it for |
|---|---|---|
~/.claude/settings.json | No | Your preferences for every project |
.claude/settings.json | Yes | Rules everyone on the repo gets, like the deny list |
.claude/settings.local.json | No | Your personal conveniences on this repo |
How they combine:
- Single values like
model: the most specific file wins (local > project > user). - Permission lists are combined, not replaced. A local
allowadds to the project’s list. denyalways wins, no matter which file it’s in.
That last rule is the important one. My personal settings can make prompts less annoying, but they can’t undo the project’s git push deny.
Where does settings.local.json come from? Mostly from Claude Code itself. Every time you pick “Yes, and don’t ask again” on a permission prompt, the rule is saved there. Those approvals can be pretty broad (Bash(curl:*)), which is why the file is gitignored. It’s a record of my choices, not project policy.
The gap
Here’s the catch. Read(./**/.env) stops Claude’s Read tool from opening .env. It does not stop this:
cat .env
Permission rules apply per tool, and cat goes through the Bash tool. So I locked the front door and left the side door open.
Closing that gap is what hooks are for, and that’s the next article.
Summary
- Put the house rules in CLAUDE.md. Split out personal notes into
CLAUDE.local.mdif the repo is public. - Put the hard limits in
.claude/settings.jsonand commit it. Allow the boring check commands, deny anything that publishes or reads secrets. - Remember that deny always wins, and that permissions are per tool.
A committed .claude/settings.json also tells anyone reading the repo that you use AI tools with some deliberate guardrails, not on vibes alone.
