Home / Blog / Claude Code

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

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:

FileScopeCommit it?Use it for
~/.claude/settings.jsonYou, every projectNo, it's personalYour defaults: model, editor mode, env you always want
.claude/settings.jsonThe repo, everyone on itYes, into the repoTeam rules: shared permissions, project hooks, MCP servers the team uses
.claude/settings.local.jsonYou, this repo onlyNo, it's gitignoredYour 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.

  • model sets the default model for the session, like "claude-sonnet-4-6". It doesn't reload live; use the /model command to switch mid-session.
  • fallbackModel names a model to fall back to when the primary is unavailable, which keeps a session alive through a capacity blip.
  • hooks holds your lifecycle hooks. The full schema and what each event is for is its own topic, covered in the hooks guide linked earlier.
  • cleanupPeriodDays controls how long session transcripts are kept before cleanup, defaulting to 30.
  • includeCoAuthoredBy is deprecated in favor of attribution, which customizes the trailer on commits and PRs Claude makes. If you've been setting the old key, move it.
  • editorMode switches 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.

P

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.