[BUG] .claude/settings.json is ignored when starting from a subdirectory — hooks and plugins silently disabled (still repros on 2.1.228; see #10367, #8810)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When Claude Code is started from a subdirectory of a project, <project-root>/.claude/settings.json is never loaded. Every hook and every plugin declared there is inactive for that session, and no warning is printed.
This is not a ${CLAUDE_PROJECT_DIR} expansion problem. With the hook command written as a hard-coded absolute path — no variables at all — the hook still does not fire, so the settings file itself is not being read.
The part that makes this hard to notice: the same session does load <project-root>/CLAUDE.md. I verified this from <project-root>/functions on 2.1.228 by asking for a string that exists only in the root CLAUDE.md, with an explicit instruction not to read files. It answered correctly in ~2s with no tool calls, while the same session had Registered 0 hooks from 1 plugins.
So within a single session, project instructions are resolved by walking up to the project root, but .claude/ is resolved only against the startup directory. The session looks correctly configured and behaves normally, while every guard is off.
For us this silently disables a PreToolUse hook that blocks writes to .env files and requires confirmation for destructive commands. cd app && claude or cd functions && claude is a natural thing to do — several of our repositories keep the application under a subdirectory.
Note: this was reported before and closed without a fix — #10367 (v2.0.27, closed NOT_PLANNED by the autoclose bot) and #8810. #6305 is still open. I am re-filing because it still reproduces on 2.1.228, and because two things are new since those reports: plugins are affected as well as hooks, and the CLAUDE.md asymmetry above gives a concrete inconsistency inside the product rather than just "hooks don't run".
What Should Happen?
Either of these would resolve it:
- Resolve
.claude/by walking up to the project root, consistent with howCLAUDE.mdis already discovered in the same session.
- If changing resolution is too disruptive, at least make the failure visible: when a session starts inside a git repository whose root contains a
.claude/settings.jsonthat is not being loaded, print one line saying so at startup. Turning a silent, total failure into a visible one would be enough for us to work with.
git rev-parse --show-toplevel returns the correct project root from all of the directories involved, so the information needed is already available.
Error Messages/Logs
# Real repository with plugins installed at project scope (talktome-app), 2.1.228
# --debug hooks --debug-file, grep 'Registered [0-9]* hooks from [0-9]* plugins'
started from <project-root> -> Registered 2 hooks from 3 plugins
started from <project-root>/functions -> Registered 0 hooks from 1 plugins
# Same comparison on 2.1.223 (kaendar-lp)
started from <project-root> -> Registered 1 hooks from 3 plugins
started from <project-root>/src -> Registered 0 hooks from 1 plugins
# Minimal synthetic repo below, 2.1.228, grep -c HOOK_FIRED on the debug log
started from repro -> 1
started from repro/sub/deeper -> 0
# There is no error message. That is the core of the report: nothing is printed.
Steps to Reproduce
- Create a minimal project with a hook that is impossible to miss:
mkdir -p repro/.claude repro/sub/deeper
cd repro && git init -q .
cat > .claude/settings.json <<'JSON'
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command",
"command":"echo HOOK_FIRED >&2; exit 2"}]}]}}
JSON
- Start Claude Code from the project root and ask it to run any Bash command:
cd repro && claude
The hook fires and the command is blocked.
- Start Claude Code from the subdirectory and ask for the same command:
cd repro/sub/deeper && claude
The hook does not fire and the command runs. No warning is shown.
- Headless equivalent, if you want the count rather than the behaviour:
printf 'run: echo hi' | claude -p --debug hooks --debug-file /tmp/d.log
grep -c HOOK_FIRED /tmp/d.log
1 when started from repro, 0 when started from repro/sub/deeper.
- To confirm it is not variable expansion: replace the hook command with a
hard-coded absolute path to a script (no ${CLAUDE_PROJECT_DIR}, no variables).
It still does not fire from the subdirectory.
- To see the asymmetry: put a distinctive string in <project-root>/CLAUDE.md,
start from the subdirectory, and ask for it with "answer without reading any
files". It is answered from context, in the same session where the hook is dead.
Note for step 4: in a project with no plugins, the Registered N hooks from M plugins
line counts plugin hooks only and stays at 0 either way, so use HOOK_FIRED there.
In a project with plugins installed, that line is the clearer indicator.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.228 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Scope of the impact
- It is total, not partial. All hooks declared in settings.json and all entries in
enabledPlugins are inactive.
- It is silent. Nothing is printed at startup, and the session otherwise behaves normally.
/hookscannot be used to detect it. It lists only settings.json-derived hooks, so it
prints "0 hooks configured" even in a healthy session whose hooks come from plugins.
- Reproduced in two versions (2.1.223, 2.1.228) and in three repositories, on macOS.
Why this is hard to work around from the project side
- A plugin hook cannot detect that it was never loaded.
- Putting a check in CLAUDE.md does reach the model, since CLAUDE.md is loaded, but it
relies on the model choosing to run and act on it. In our testing, asking the session to
run which ainv-review produced an answer sourced from the installed-plugins registry
file rather than from PATH, which defeats the check.
- Placing .claude/settings.json in every subdirectory is not viable: the set of
directories cannot be enumerated, and duplicated configuration drifts.
- A shell wrapper that cd's to the git root before exec'ing claude does work, and is what
we currently use, but it is per-machine and per-shell, so a project cannot ship it.
Related issues
- #10367 — same bug, all hook types, v2.0.27. Closed NOT_PLANNED by the autoclose bot.
- #8810 — same bug, UserPromptSubmit hooks. Closed, also autoclose.
- #6305 — Pre/PostToolUse hooks not executing. Still open.
- #77464 — requests the opposite for the Agent SDK (settingSources: ["project"] should not
traverse parent directories). Worth reading together with this one, since a fix should
probably make the traversal rule explicit and consistent rather than implicit.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗