HookStack
All guides

Claude Code settings.json: The Complete Reference

8 min read · Reviewed 2026-06-12

The `settings.json` file is where you configure Claude Code per project and per user — which hooks fire on agent events, which tools run without prompting, and a handful of other keys. If you have ever wondered where Claude Code reads its configuration from, why a hook silently does nothing, or how `settings.local.json` differs from `settings.json`, this is the file you need to understand.

This guide walks through what goes in `settings.json`, the three locations it can live in and how they merge, the structure of the `hooks` and `permissions` blocks, how to set environment variables, the mistakes that quietly break a config, and how to keep the file under control across a team.

What is the .claude/settings.json file and what goes in it?

settings.json is Claude Code’s configuration file, written as plain JSON. Each top-level key controls one area of behaviour. The keys you will use most often are:

  • hooks — scripts that run automatically on lifecycle events such as PreToolUse, PostToolUse, SessionStart, and Stop.
  • permissionsallow, deny, and ask lists that decide which tool calls run without a prompt, which are blocked, and which require confirmation.
  • env — environment variables exported into the session and into the processes your hooks spawn.
  • model — an override for which model the session uses, when you want to pin it rather than rely on the default.

A minimal file is just an object with the keys you care about. Everything you omit falls back to Claude Code’s defaults, so it is normal for a project to ship a settings.json that only defines hooks and permissions.

Where is Claude Code settings.json located?

There are three locations, each with a different scope. Claude Code reads all of them and merges the result:

  • Project — .claude/settings.json at the repository root. Committed to git and shared with the whole team. This is where your project’s hooks and shared permissions belong.
  • Local — .claude/settings.local.json, also at the repository root but gitignored. Personal overrides that should never reach your teammates, such as machine-specific paths or experimental permissions.
  • User — ~/.claude/settings.json in your home directory. Global defaults that apply to every project you open on this machine.

So the short answer to "where is Claude Code settings" is: per-project in .claude/, per-user in ~/.claude/, plus a gitignored .claude/settings.local.json for personal tweaks.

How do settings.local.json and settings.json merge?

The three files are merged, not replaced. The more specific location wins on conflicting keys: local overrides project, and project overrides user. In other words settings.local.json has the final say, then the committed project settings.json, then your global ~/.claude/settings.json.

This is exactly the difference between settings.local.json and settings.json: the project file is the shared, committed baseline for the team, while the local file is your personal, gitignored layer on top. Put anything the team depends on in settings.json, and anything that is just for you — or that contains a secret — in settings.local.json.

How is the hooks block structured?

The hooks block is keyed by event name. Each event holds an array of matcher groups; each group has a matcher (the tool name pattern it applies to) and a hooks array of commands to run. The nesting is event → matcher group → command:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.mjs"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/format-on-write.mjs"
          }
        ]
      }
    ]
  }
}

The matcher is a pattern against the tool name — "Bash" for one tool, "Write|Edit" for several, "*" for every tool. The referenced script is a Node .mjs file: a pure export function run(input, deps) holding the logic, plus an entry guard if (process.argv[1] === fileURLToPath(import.meta.url)) { ... } that reads the event JSON from stdin and runs it. Block decisions go to stdout; logs and diagnostics go to stderr.

How does the permissions block work?

The permissions block decides, ahead of time, which tool calls need your confirmation. It holds three lists:

  • allow — tool calls that run without prompting. Use it for the commands you already trust, like Bash(git status) or read-only operations.
  • deny — tool calls that are blocked outright, regardless of anything else.
  • ask — tool calls that always require an explicit confirmation, even if they would otherwise be allowed.
{
  "permissions": {
    "allow": ["Bash(git status)", "Bash(pnpm test)", "Read"],
    "deny": ["Bash(rm -rf:*)"],
    "ask": ["Bash(git push:*)"]
  }
}

A well-tuned allow list is the single biggest reducer of approval friction: the operations you run constantly stop interrupting you, while deny and ask keep a guard on the dangerous ones. Keep team-wide rules in the project file and personal additions in settings.local.json.

How do you set environment variables and the model?

The env key takes an object of string key/value pairs. Those variables are exported into the Claude Code session and are visible to the processes your hooks spawn, which makes env the right place for non-secret configuration your hooks read:

{
  "env": {
    "NODE_ENV": "development",
    "HOOKSTACK_LOG_LEVEL": "info"
  },
  "model": "claude-opus-4-8"
}

The model key pins the session to a specific model instead of the default. Treat both keys as optional: only add them when you have a concrete reason, and prefer settings.local.json for anything machine-specific. Do not put secrets such as API tokens directly in a committed settings.json — keep them in the local file or in your real shell environment.

What are the common settings.json mistakes?

Most "my hook isn’t running" reports trace back to one of these:

  • Invalid JSON. A trailing comma or a missing brace makes the whole file unparseable, and the config is silently ignored — hooks and permissions both stop applying with no obvious error.
  • Wrong file location. The file must be .claude/settings.json at the repo root, not in a subdirectory or your home folder when you meant it to be project-scoped.
  • Relative command paths. A bare node .claude/hooks/x.mjs breaks when the working directory differs. Always anchor with $CLAUDE_PROJECT_DIR, as in node $CLAUDE_PROJECT_DIR/.claude/hooks/x.mjs.
  • Committed secrets. Tokens in a project settings.json end up in git history. Keep them in settings.local.json (gitignored) or in your shell environment via env indirection.

Validate the JSON before you trust it — this prints the parsed object or a precise parse error:

node -e "JSON.parse(require('fs').readFileSync('.claude/settings.json','utf8')); console.log('valid')"

How do you keep settings.json under control on a team?

The split between the two files is what keeps a shared config sane:

  • Commit .claude/settings.json so every contributor gets the same hooks and the same baseline permissions the moment they clone the repo.
  • Add .claude/settings.local.json to .gitignore so personal overrides and secrets never get pushed.
  • Audit changes to the committed file in review — a new allow entry or a new hook command is a meaningful change in what runs automatically, and deserves the same scrutiny as code.

If you would rather not hand-edit the JSON, HookStack’s CLI does it for you: npx hookstack-cli@latest install patches your settings.json with the hooks you selected and writes the matching scripts into .claude/hooks/, so the config and the code stay consistent.

Frequently asked questions

Where is the Claude Code settings.json file?
Project settings live in `.claude/settings.json` at the repository root, personal gitignored overrides in `.claude/settings.local.json`, and global defaults in `~/.claude/settings.json` in your home directory.
What is the difference between settings.local.json and settings.json?
`settings.json` is the committed, team-shared config; `settings.local.json` is a gitignored personal layer that overrides it. Local wins over project, and project wins over the user-level file.
Why are my Claude Code hooks defined in settings.json not running?
The most common cause is invalid JSON — a trailing comma or missing brace makes the file unparseable and silently ignored. Validate it with a JSON parser, and make sure command paths use `$CLAUDE_PROJECT_DIR`.
Should I commit settings.json to git?
Yes, commit `.claude/settings.json` so the team shares the same hooks and permissions. Keep secrets and personal tweaks in `.claude/settings.local.json`, which should be gitignored.

Related hooks

Sources

Read next