[FEATURE] Settings.json parent directory traversal for monorepos

Open 💬 19 comments Opened Dec 3, 2025 by skorfmann

Problem

When working in a monorepo subdirectory (e.g., /repo/packages/app/), Claude Code only looks for .claude/settings.json in the current working directory. It does not traverse parent directories to find shared settings at the repo root.

This means you either need to:

  • Duplicate settings in every subdirectory
  • Always launch Claude Code from the repo root
  • Use global user settings (~/.claude/settings.json) which apply to all projects

Proposed Solution

Claude Code should traverse parent directories (up to the git root or filesystem root) looking for .claude/settings.json files, similar to how many tools handle config files (e.g., .gitignore, tsconfig.json, .eslintrc).

Settings could be merged with deeper directories taking precedence, or the first found settings file could be used.

Use Case

In a monorepo with multiple packages/services, you want shared settings (allowed tools, MCP servers, permissions) defined once at the repo root, while still being able to work from within subdirectories.

Related Issues

  • #2365 - Better Monorepo Support (MCP and commands)
  • #374 - MCP repo scope
  • #705 - Nested CLAUDE.md context
  • #3146 - Configure additional directories via settings files

This issue focuses specifically on settings.json lookup behavior, complementing the related requests for MCP, commands, and CLAUDE.md parent directory support.

View original on GitHub ↗

19 Comments

skorfmann · 7 months ago

Workaround: Using Mise for per-project environment variables

While waiting for native parent directory traversal, you can use Mise to manage per-project environment variables that Claude Code will pick up.

Setup

  1. Install mise and activate it in your shell:

```bash
# Install (macOS)
brew install mise

# Add to ~/.zshrc or ~/.bashrc
eval "$(mise activate zsh)"
```

  1. Add a .mise.toml at your repo root:

```toml
[env]
_.file = ".env" # optional: load secrets from .env

# Claude Code OTEL config (example)
CLAUDE_CODE_ENABLE_TELEMETRY = "1"
OTEL_METRICS_EXPORTER = "otlp"
OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"
```

  1. When you cd into any subdirectory, mise automatically injects env vars from parent .mise.toml files.

Why this works

Mise traverses parent directories and merges configs - exactly what we're requesting for settings.json. This covers environment variable needs but not Claude Code-specific settings like allowed tools or MCP servers.

github-actions[bot] · 6 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

madhermit · 6 months ago

I'm still interested in this addition

roccolangeweg · 6 months ago

Adding another use case beyond monorepos:

Multi-Client Consultancy Setup

As a consultant working with multiple clients, my repository structure is:

~/Repositories/
├── client-a/
│   ├── .claude/
│   │   ├── settings.json      # Client A's permissions, hooks
│   │   ├── .mcp.json          # Client A's Slack workspace, APIs
│   │   └── CLAUDE.md          # Client A's coding standards
│   ├── repo-1/
│   ├── repo-2/
│   └── repo-3/
│
├── client-b/
│   ├── .claude/
│   │   ├── settings.json      # Client B's different config
│   │   ├── .mcp.json          # Client B's Slack workspace, APIs
│   │   └── CLAUDE.md
│   └── ...

Current limitation: When I cd ~/Repositories/client-a/repo-1 and run claude, only CLAUDE.md is picked up from the parent client directory. The settings.json and .mcp.json at client level are ignored.

Desired behavior: Parent directory traversal for settings.json and .mcp.json (similar to how CLAUDE.md already works), with repo-level configs taking precedence for overrides.

This would allow:

  • Client-specific MCP servers (different Slack workspaces, Jira instances, etc.)
  • Client-specific permissions and hooks
  • Repo-specific overrides when needed

Currently the only workarounds are symlinks (which break repo-level overrides) or duplicating config in every repo.

PaulRBerg · 5 months ago

For organizations, it would be very helpful if Claude discovered settings.json in parent directories.

In our organization, we all check out our code in the ~/sablier directory on our machines. It would be great if Claude could be made to always look up the .claude/settings.json file under ~/sablier/.claude/settings.json whenever a project under ~/sablier/** is opened.

VitorAndTxr · 5 months ago

+1 on this. I'd like to propose expanding the scope beyond settings.json to cover the entire .claude directory: skills, agents, commands, and CLAUDE.md files.

The directory walk-up should apply to all configuration types, not just settings. Here's the full picture:

Current behavior — console opened at company/backend/service-a:

| Priority | Path |
|----------|------|
| 2 (highest) | company/backend/service-a/.claude |
| 1 (lowest) | ~/.claude |

Proposed behavior:

| Priority | Path |
|----------|------|
| 4 (highest) | company/backend/service-a/.claude |
| 3 | company/backend/.claude |
| 2 | company/.claude |
| 1 (lowest) | ~/.claude |

This means settings, skills, agents, commands, and CLAUDE.md at each ancestor level are loaded and merged, with deeper directories taking precedence.

Why this matters for multi-repo workspaces:

company/
  .claude/           → Company-wide skills, coding standards, shared agents
  backend/
    .claude/         → Backend conventions (API patterns, DB skills)
    service-a/
      .claude/       → Service-specific config
    service-b/
      .claude/
  frontend/
    .claude/         → Frontend skills (React patterns, design system)
    app-web/
      .claude/

Opening Claude Code in company/backend/service-a would automatically inherit company-wide standards + backend conventions + service-specific config — no duplication needed.

This is consistent with how .gitignore, .editorconfig, .eslintrc, and .npmrc handle hierarchical config. It's also fully backward compatible (users with only ~/.claude + project/.claude see no change).

Nice to have: A root: true flag (like ESLint) to stop the upward traversal at a specific level.

Related: #11626, #12259, #4275, #4442

nikhilsitaram · 5 months ago

We need this feature. Permissions are so annoying. I like to break up my work into folders and creating a new folder in a project means I have to approve everything again or manually copy the settings.

petrovi4 · 5 months ago

My use case: VS Code multi-root workspaces

I have a monorepo with multiple projects (server/, frontend/, shared/) added as separate folders in a .code-workspace file. When I use the Claude Code VS Code extension, it always picks the first folder in the workspace as its working directory — regardless of which project I'm actually working on.

This causes several problems:

Settings pollution: Permission changes and .claude/settings.local.json updates go into server/.claude/ even when I'm working on frontend tasks.
Third-party tool confusion: Other extensions (e.g. Entire.io) that rely on the same root directory also write their data into the wrong project folder.
No isolation: The agent has unrestricted access to all workspace folders when I often want it scoped to just one project.
What I'd like: A workingDirectory setting in the VS Code extension configuration (or per-folder workspace settings) that lets me explicitly control where Claude Code operates. Ideally, it should be configurable per workspace folder in .code-workspace:

{
  "folders": [
    { "path": "server" },
    { "path": "frontend" }
  ],
  "settings": {
    "claude.workingDirectory": "${workspaceFolder:frontend}"
  }
}

Or even better — Claude Code should automatically detect the active editor's workspace folder and switch context accordingly, similar to how VS Code's built-in terminal does it with terminal.integrated.cwd.

Parent directory traversal for settings.json (the original ask of this issue) would partially help, but the core problem for multi-root workspace users is that the working directory itself is hardcoded to the first folder.

robbie-williams · 4 months ago

Same problem here

Use case: I have ~/dev/work/.claude/settings.json configured with AWS Bedrock settings for my company. Under ~/dev/work/ I have multiple repos (join-api/, web-app/, etc.). When I cd into any of these and run claude, the parent settings are not found — each project would need its own copy.

Outside ~/dev/work/, I use my personal Pro subscription, so user-level settings (~/.claude/settings.json) isn't the right place for the Bedrock config.

Current workarounds

None are clean:

  • Symlinks into each project (requires .gitignore updates, manual maintenance)
  • direnv for env vars only (doesn't cover model, hooks, awsAuthRefresh, etc.)
  • User-level settings with per-project opt-out (inverts the logic)
inancgumus · 3 months ago

For now, I've added a zsh wrapper that preserves settings from parent directories.

Works great for my use case. Different parent directories with different settings.

  • It walks up from CWD, collects all parent .claude/settings.json files
  • Deep-merges them into .claude/settings.local.json (Claude auto-gitignores it without .gitignore)
  • On Claude Code exit: restores the original settings.local.json with delta changes

The script is here.

yurukusa · 3 months ago

You can work around this with a Notification hook that copies the repo-root settings into subdirectories at session start:

GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
[ -z "$GIT_ROOT" ] && exit 0
CWD=$(pwd)
ROOT_SETTINGS="$GIT_ROOT/.claude/settings.json"
LOCAL_SETTINGS="$CWD/.claude/settings.json"
if [ "$CWD" != "$GIT_ROOT" ] && [ -f "$ROOT_SETTINGS" ] && [ ! -f "$LOCAL_SETTINGS" ]; then
    mkdir -p "$CWD/.claude"
    ln -sf "$ROOT_SETTINGS" "$LOCAL_SETTINGS"
    echo "Linked monorepo settings from $GIT_ROOT" >&2
fi
exit 0
GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
[ -z "$GIT_ROOT" ] && exit 0
CWD=$(pwd)
ROOT_SETTINGS="$GIT_ROOT/.claude/settings.json"
LOCAL_SETTINGS="$CWD/.claude/settings.json"
if [ "$CWD" != "$GIT_ROOT" ] && [ -f "$ROOT_SETTINGS" ]; then
    if [ -f "$LOCAL_SETTINGS" ]; then
        MERGED=$(jq -s '.[0] * .[1]' "$ROOT_SETTINGS" "$LOCAL_SETTINGS")
        echo "$MERGED" > "$LOCAL_SETTINGS.merged"
    else
        mkdir -p "$CWD/.claude"
        ln -sf "$ROOT_SETTINGS" "$LOCAL_SETTINGS"
    fi
fi
exit 0
// ~/.claude/settings.json (user-level, always loaded)
{
  "hooks": {
    "Notification": [{
      "matcher": "start",
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/monorepo-settings.sh" }]
    }]
  }
}

If the main thing you need inherited is permission allow-lists, a PreToolUse hook in ~/.claude/settings.json is simpler — it applies globally regardless of which subdirectory you're in. No symlinks needed.
The symlink approach is most useful when you have MCP servers, custom hooks, or other project-level settings defined at the monorepo root.

flound1129 · 2 months ago

Strong +1. The current behavior makes "project scope" effectively indistinguishable from "local scope" the moment you cd into any subdirectory — which is most of the time in a real repo. Installing a plugin at project scope and having it silently disappear when launching from src/ or packages/foo/ is deeply surprising and undermines the whole point of project-level config.

Every other tool in the ecosystem (git, npm, pnpm, cargo, tsconfig, .gitignore, direnv, editorconfig) walks up to find the project root. Claude Code not doing so means users have to either:

  • launch from repo root every time (fragile, easy to forget),
  • symlink .claude/ into every subdir (gross),
  • or fall back to user scope (defeats the purpose of per-project install).

This isn't a nice-to-have — it's a core correctness issue with the plugin/settings model. Please prioritize.

flound1129 · 2 months ago

Another pain point in the same vein: because there's no upward traversal, users end up with .claude/settings.local.json (and .claude/ directories generally) scattered across subdirectories of a single repo — one per directory they happened to launch from. It's a footgun:

  • No single source of truth for "what plugins does this repo use."
  • Easy to accumulate stale/conflicting configs in sibling subdirs.
  • Hard to audit or clean up — you have to find . -name settings.local.json and diff them.
  • Git hygiene becomes a mess (.gitignore has to cover arbitrary depths).

Upward traversal fixes this by giving the repo exactly one .claude/ at the root, the way every other tool works.

flound1129 · 2 months ago

One more — and this one is arguably the worst part: the /plugin install prompt literally describes project scope as "Install for all collaborators on this repository". That wording makes a concrete promise about repo-wide reach. The actual behavior is "install for collaborators who happen to launch claude from this exact directory," which is a completely different thing.

So this isn't just missing traversal — it's a mismatch between the documented contract in the UI and the implementation. Either the traversal needs to be added to match the promise, or the wording needs to change to something honest like "Install for collaborators who launch from this directory." The current state misleads every user who reads the prompt.

sofiane3106 · 2 months ago

I’m seeing the same issue, but for project subagents.

Docs say .claude/agents/ are discovered by walking up from the current working directory:
https://code.claude.com/docs/en/sub-agents

Repro:

```bash
mkdir -p parent/.claude/agents parent/child
# add any valid agent in parent/.claude/agents/test-agent.md

cd parent/child
claude agents # test-agent is visible

git init
claude agents # test-agent is no longer visible

So discovery appears to stop at the nearest .git root. This affects monorepos/workspaces where shared .claude/agents or .claude/skills live in a parent directory.

flound1129 · 2 months ago

There are other path traversal issues with subagents, sometimes they can't use project LSP servers either.

vpanta · 2 months ago

Gonna add on here for .mcp.json as well, as it lives outside of .claude directly. We are generally symlinking it into the sub-repo ($SUB_PROJECT_ROOT/.mcp.json is a link to $PROJECT_ROOT/.mcp.json), however using claude mcp add ... clobbers the symlink and writes it in $SUB_PROJECT_ROOT/.mcp.json as a regular file :(

As this doesn't seem to be anything which actually goes through the agent, I can't catch it as a PreToolUse hook etc etc as I could with Write|Edit usages which do similar things.

dragontheory · 2 months ago

Let's get on with this. It's important.

krisgeus · 28 days ago

It is a security issue being created. Claude should never escape the current root folder (except for some well known locations). The already implemented breakouts to parent directories should be removed.
Please stay into the bounding context of the folder where claude is started