Claude Code settings.json: the configuration guide
Almost everything you'd want to control about Claude Code lives in one place: settings.json. What
it can run without asking, what model it uses, which hooks fire, which MCP servers it trusts. The catch is
there isn't one file, there are several, and they layer. Get the layering right and the file makes the
safe thing automatic and the dangerous thing impossible. This is the map, with the multi-tenant setup
I actually run in production.
- What settings.json is
- The files and what overrides what
- Permissions: the part that decides what runs
- Environment variables
- MCP servers: where they're configured
- How we configure permissions across 21 client accounts
- The rest of the file, as a reference
- Where to put each thing
- Frequently asked questions
What settings.json is
settings.json is the file where you configure Claude Code: what tools it can run without
asking, which environment variables every session gets, what model it defaults to, which hooks fire, and
which MCP servers are allowed. It's plain JSON, it lives in a few known locations, and Claude Code merges
all of them into one effective config at startup. Most of it reloads when you edit it, so you rarely
restart.
The thing to understand first is that there isn't one settings.json. There are several, at
different scopes, and they layer on top of each other. A rule in your project's file overrides the same rule
in your user file; a machine-local file overrides both. Once you hold that picture, the rest of the file
stops feeling arbitrary, because you can see where any given setting is meant to live.
This guide is the map. The two settings most people touch first, permissions and env, get the most space, because they're the ones that change day to day. The rest I'll cover as a reference you can scan.
The files and what overrides what
Claude Code reads settings from several places and merges them, highest precedence first: managed (deployed
by an org's IT), command-line flags, .claude/settings.local.json (machine-local, gitignored),
.claude/settings.json (project, committed to the repo), and ~/.claude/settings.json
(your user-wide defaults). When two files set the same key, the higher-precedence one wins.
In practice you'll work with three of them, and the split is the whole point:
| File | Scope | Commit it? | Use it for |
|---|---|---|---|
~/.claude/settings.json | You, every project | No, it's personal | Your defaults: model, editor mode, env you always want |
.claude/settings.json | The repo, everyone on it | Yes, into the repo | Team rules: shared permissions, project hooks, MCP servers the team uses |
.claude/settings.local.json | You, this repo only | No, it's gitignored | Your overrides here: a path only on your machine, a key you don't share |
The project file is the one that earns its keep on a team, because it ships with the repo. Whoever clones it gets the same allowed commands, the same hooks, the same MCP servers, without a setup doc nobody reads. Managed settings sit above all of this and exist so an organization can enforce rules a user can't override, which matters once you're past one developer. The full precedence order and every key is in the official Claude Code settings reference.
A quick way to see the merged result of all this is the /config command inside a session, which
shows what actually resolved rather than what any single file says.
Permissions: the part that decides what runs
The permissions key is how you tell Claude Code what it can do without stopping to ask. It
holds four things: an allow list of actions that run silently, a deny list of
actions that are always blocked, an ask list that forces a prompt, and a
defaultMode that decides what happens to everything not named. Each rule is a tool name with an
optional pattern in parentheses, like Bash(npm run test:*) or Read(./.env).
Here's a permissions block that covers the common cases:
{
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test:*)",
"Read(~/.zshrc)"
],
"deny": [
"Bash(curl:*)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
],
"ask": [
"Write(./config.json)"
],
"defaultMode": "default",
"additionalDirectories": ["/opt/shared-code"]
}
}
Read that top to bottom and you have the model. The allow list lets the lint and test commands
run without a prompt, because you've decided they're safe. The deny list blocks reading any
.env file or anything under secrets/, which deny does unconditionally: a deny rule
always wins, even over an allow. The ask list forces a confirmation on writing
config.json specifically. defaultMode of "default" means anything
you didn't mention prompts on first use, which is the sane setting for a tool that can run shell commands.
(The valid modes are default, acceptEdits, plan, auto,
dontAsk, and bypassPermissions; there's no "ask" mode, that's what the
ask list is for.)
The pattern syntax is worth getting right. Bash(npm run test:*) matches any command that starts
with npm run test, so test, test:unit, test:watch all
pass, but npm run deploy does not. For file rules, * matches within a path segment
and ** crosses segments, so Read(./secrets/**) covers the whole tree under
secrets, not just its top level. The rules are specific on purpose. A blanket
Bash(*) in your allow list hands the agent your shell, which is the one thing this system exists
to prevent.
additionalDirectories is the escape hatch for when Claude needs to read or write outside the
project root, like a shared library checked out next door. Without it, the working directory is the boundary.
If you've read our Claude Code hooks guide, this is the layer
below hooks. Permissions are declarative rules the harness checks; a PreToolUse hook is code you
write for the cases a static rule can't express, like inspecting the actual contents of a command. Most
setups want both: permissions for the broad allow/deny, a hook for the judgment calls.
Environment variables
The env key sets environment variables for every Claude Code session and every subprocess it
spawns. It's a flat map of strings, and it's where things like telemetry flags, a custom API base URL, or a
variable your hooks and MCP servers read all go.
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_METRICS_EXPORTER": "otlp",
"NODE_ENV": "development"
}
}
The reason to set a variable here rather than in your shell profile is reach. Anything in env is
present for the commands Claude runs, the hooks that fire, and the MCP servers Claude Code launches, all of
which are subprocesses that inherit it. A variable you export in .zshrc only exists if you
launched the session from that shell, which is fragile. Put it in env at the scope it belongs
to, user-wide or per-project, and it's always there.
One thing env does not cover is terminal color settings, which Claude Code reads from your shell
environment at launch, so those still go in your profile.
MCP servers: where they're configured and how settings gate them
MCP server definitions live in a separate .mcp.json file at the project root, not inside
settings.json, and the mechanics of how you
add an MCP server to Claude Code are their own topic. What
settings.json controls is which of those servers are allowed to run:
enableAllProjectMcpServers to approve all of them, or enabledMcpjsonServers and
disabledMcpjsonServers to approve or reject specific ones by name.
The split confuses people, so to be concrete: a project's .mcp.json holds the server
definitions, the command to launch each one and its arguments. settings.json holds the gate. By
default, Claude Code asks before trusting MCP servers a repo ships, which is the right default, because an
.mcp.json you cloned can launch arbitrary processes. The settings keys are how you say "yes,
trust these."
{
"enableAllProjectMcpServers": false,
"enabledMcpjsonServers": ["shopify", "github"],
"disabledMcpjsonServers": ["filesystem"]
}
We lean on this in production. The Shopify MCP server we wrote, the same read-only one in our
build an MCP server in Python walkthrough, is defined once
in the project's .mcp.json and approved by name in enabledMcpjsonServers. New
servers don't get to run just because they appeared in a repo. They get added to the allowlist deliberately,
in a file we review, which is the same default-deny posture we apply everywhere else.
There's a parallel set of keys for managed settings, allowedMcpServers and
deniedMcpServers, that an organization deploys to enforce an MCP allowlist a user can't
override. Same idea, one level up.
How we configure permissions across 21 client accounts
In a multi-tenant setup, settings.json is the per-tenant boundary, and the rule we follow is
that every client's project settings is default-deny: a tight allow list of exactly the tools that client's
work needs, an aggressive deny list for anything touching another tenant's data or a secret, and
defaultMode set to dontAsk so anything not pre-approved is denied outright. The
config is the boundary, not the prompt.
Running Claude agents for 21 client accounts, the failure we design against is one client's agent reaching
into another client's data or credentials. You don't solve that by asking the model nicely. You solve it in
the project's .claude/settings.json, which is committed per client, and which we treat as a
security file rather than a convenience. Each one denies reads on .env, credential paths, and
any directory outside that client's scope, and allows only the specific commands that client's pipeline
runs. defaultMode never sits at acceptEdits or bypassPermissions in a
tenant config. Anything we didn't explicitly permit is denied.
The pattern that makes this maintainable is keeping the dangerous stuff in deny rather than
relying on a short allow list. Allow lists drift, because someone adds a tool to get unblocked and forgets
to scope it. A deny rule on Read(./secrets/**) and the credential paths holds no matter what the
allow list grows into, because deny always wins. That single property, deny beats allow, is what lets a
permissions file stay safe as it accumulates real-world exceptions.
For the cases a static rule can't catch, the contents of a command rather than its name, that's where a
PreToolUse guard takes over, which is the enforcement layer we describe in the hooks guide.
settings.json draws the broad boundary; the hook handles the judgment. Together they're a real
boundary, in code we control, with a default of deny. The model's cooperation isn't part of the security
model.
The rest of the file, as a reference
Beyond permissions, env, hooks, and MCP, settings.json has a long tail of keys for the model,
the UI, and behavior. You won't set most of them, but these are the ones worth knowing.
modelsets the default model for the session, like"claude-sonnet-4-6". It doesn't reload live; use the/modelcommand to switch mid-session.fallbackModelnames a model to fall back to when the primary is unavailable, which keeps a session alive through a capacity blip.hooksholds your lifecycle hooks. The full schema and what each event is for is its own topic, covered in the hooks guide linked earlier.cleanupPeriodDayscontrols how long session transcripts are kept before cleanup, defaulting to 30.includeCoAuthoredByis deprecated in favor ofattribution, which customizes the trailer on commits and PRs Claude makes. If you've been setting the old key, move it.editorModeswitches key bindings between"normal"and"vim".respectGitignore, on by default, keeps gitignored files out of the@file picker.
The reload behavior is the practical thing to remember across all of these. permissions,
hooks, env, and most keys pick up edits without a restart. model and
anything baked into the system prompt do not, so after changing those, start a fresh session or you'll wonder
why nothing changed.
Where to put each thing
The recurring question with settings.json isn't what a key does, it's which file it belongs in,
so here's the short version. Personal defaults you want everywhere go in
~/.claude/settings.json. Anything the whole team should share, the allowed commands, the
project hooks, the approved MCP servers, goes in the committed .claude/settings.json, because
the value is that it ships with the repo. Anything specific to your machine or that you shouldn't commit, a
local path or a personal key, goes in .claude/settings.local.json, which is gitignored for
exactly this reason.
Get that split right and the rest follows, the same way a good subagent definition scopes a task to its own tools and context. Configuration is where you decide what the agent is allowed to be, before it does anything at all.
Frequently asked questions
Where is the settings.json file for Claude Code?
There are several. Your user-wide defaults live in ~/.claude/settings.json. A project's shared,
committed config is .claude/settings.json at the repo root. Machine-local overrides you don't
commit go in .claude/settings.local.json. Organizations can also deploy a managed settings file
that overrides all of them. Claude Code merges all of these into one effective config at startup.
How do I allow a command without being asked every time?
Add it to the allow list under permissions, as a rule like
Bash(npm run test:*). That pattern lets any command starting with npm run test run
without a prompt, while leaving everything else subject to your defaultMode. Keep allow rules
specific; a blanket Bash(*) hands the agent your whole shell.
What's the difference between settings.json and settings.local.json?
.claude/settings.json is committed to the repo and shared with the team, so it's where
team-wide rules, hooks, and approved MCP servers go. .claude/settings.local.json is gitignored
and applies only to your machine for that repo, so it's where personal overrides and anything you shouldn't
commit go. The local file takes precedence over the project file.
How do I set environment variables for Claude Code?
Use the env key, a flat map of string values. Variables set there reach every Claude Code
session and every subprocess it spawns, including the commands it runs, the hooks that fire, and the MCP
servers it launches. That reach is why env is more reliable than exporting a variable in your
shell profile.
Pavle Lazic is the founder of Scalably, where he builds and runs multi-tenant Claude agent platforms in production for real businesses. He writes about the Claude Agent SDK, MCP servers, and what it actually takes to put AI agents to work. See the platform.