Lesson 26 of 32
Debugging Your Configuration
A diagnostic order for configuration that does not work — what actually loaded, which copy of it won, whether it ever fired, and how to bisect the rest.
Everything in this part of the course has a failure mode in common. You write the file, you start a session, and nothing changes. The instinct at that point is to conclude that Claude is ignoring you, and to write the instruction again, louder.
Almost always something more boring is true. The file did not load. It loaded from a different place than you thought. Another file overrode it. The hook is registered but its matcher never matches. Each of those has a different fix and all of them look identical from the outside.
So the skill worth having is not a list of symptoms. It is an order of questions.
The order
- Step 1
Did it load?
Look at what is actually in the session rather than what you believe you configured. If it is absent, this is a location or syntax problem and nothing else matters.
- Step 2
Is it binding?
If it loaded and is being disregarded, check what kind of thing it is. Advisory mechanisms are allowed to be disregarded, which makes this a wording problem, not a wiring one.
- Step 3
Which copy won?
The same key can exist in four scopes. When a value seems inert, something above it in the precedence order is usually setting it too.
- Step 4
Did it fire?
Registered is not the same as triggered. For anything event-driven, confirm the event happened and the matcher matched before doubting the command.
The commands that answer them
/context is the first move for almost everything. It reports what is occupying
the window right now, broken down: the system prompt, tool definitions, MCP
tools, custom subagents and where each was loaded from, memory files, skills, and
the conversation itself. If your CLAUDE.md is not in that list, Claude cannot
see it, and no amount of rewording will help.
From there the specific commands are narrower and faster.
| Command | What it tells you |
|---|---|
/context | Everything in the window, and how much space each part takes |
/memory | Where your memory files are, across user and project scope |
/skills | Which skills were found, and from which source |
/hooks | Every registered hook, grouped by event |
/mcp | Each configured server, its connection status and its tool count |
/permissions | The allow and deny rules actually in effect |
/status | Which settings sources loaded, including managed ones |
/doctor | A setup checkup: invalid settings files, duplicate definitions, and proposed fixes |
/doctor is the one to reach for when you do not yet have a hypothesis, because
it looks for the mistakes you would not think to check — a settings file that
failed to parse, two subagents in one directory claiming the same name. It
proposes fixes and applies them only after you confirm. From your shell,
claude doctor prints the read-only version without starting a session, which is
what you want when Claude Code will not start.
One more, for a genuinely opaque problem: /debug turns on debug logging for the
session and asks Claude to diagnose using the log and your settings paths.
Precedence, which explains most silent failures
Settings merge across four scopes, and when the same key appears twice the narrower one wins. Managed settings deployed by your organisation sit at the top and cannot be overridden. Below that come command-line flags for the session, then the gitignored local file, then the project file, then your personal user file.
That order produces one specific, extremely common confusion. Your
.claude/settings.local.json is not committed, and you almost never open it, so
a value you set there once will quietly beat the project setting you are
currently editing. /status tells you which sources are loaded at all; the local
file is where to look next.
Two more traps in this area are worth memorising, because neither produces an error message.
~/.claude.json and ~/.claude/settings.json are different files. The first
holds application state — your login, per-project trust decisions, your personal
MCP servers. Permissions, hooks and environment variables belong in the second.
Putting them in the first is silently ignored.
And a user, project or local settings file that fails validation is rejected as a
whole, not partially. One trailing comma and every hook in that file disappears
from /hooks. Managed settings behave differently on purpose — an invalid entry
there is stripped and the rest of the policy still applies — which is why a typo
cannot disarm an organisation's policy.
Not everything follows the override rule, either. Permission rules merge across
scopes rather than replacing one another, and CLAUDE.md files accumulate rather
than override, which is why two contradictory rules produce inconsistent
behaviour instead of a clear winner.
Walking it once
WhyThe setup: a hook that should run the type checker after every file edit. It was working last week. Today it is not, and the temptation is to start editing the script.
You typed: /hooks
Output: No hooks configured.WhyQuestion one, answered in four seconds. This is not a matcher problem or a script problem — nothing is registered at all. Every hypothesis about the command was wasted breath.
You typed: /doctor
Output: Installation OK Settings .claude/settings.json invalid JSON at line 14 ~/.claude/settings.json OKWhyThere it is. A settings file that fails to parse is rejected entirely, so the hook did not half-load — it was never read. Note that the file was still edited successfully and committed; nothing warned anybody.
You typed: Fix line 14 of .claude/settings.json.
Claude ran: Read .claude/settings.json
Claude ran: Edit .claude/settings.json
Claude replied: A trailing comma after the last hook entry. Removed. Settings files reload without a restart, so /hooks should show it now.
You typed: /hooks
Output: PostToolUse (1) matcher Edit|Write type command source .claude/settings.jsonWhyRegistered is still not the same as working. One more step: cause the event and look for the effect, rather than assuming.
You typed: add a nullable note field to the Invoice type
Claude ran: Edit src/types/invoice.ts
Output: tsc --noEmit: 0 errorsWhyFour commands, no guessing, and at no point did anyone read the hook script. The order did the work: is it there, why not, is it there now, does it fire.
The three that come up most
Each of these is the general procedure applied to one mechanism.
A hook that does not fire. Run /hooks. If it is absent, the settings file
is the problem: hooks live under a hooks key in a settings file, and there is no
standalone hooks file for project or user configuration — only plugins use a
separate hooks/hooks.json. If it is present, suspect the trigger. Matchers are
case-sensitive and match tool names exactly, so bash matches nothing. Use |
to list several tools rather than an array, which is a schema error and takes the
whole file down with it. And remember which tool actually did the thing: a matcher
on Edit never sees a file rewritten by a shell command. If it is registered and
the matcher looks right, start the session with claude --debug and trigger it
while watching the log, which records what matched and how it exited.
A skill that is never picked up. Run /skills. If it is missing, check the
layout: a skill is a directory with SKILL.md inside it, not a single markdown
file named after the skill. If it is listed but Claude never reaches for it,
either it is marked as yours to invoke only, or the description does not use the
vocabulary of your actual request. Say the request out loud, then look at whether
those words appear in the description.
An MCP server that will not connect. Run /mcp. A project-scoped server from
.mcp.json needs a one-time approval, and if you dismissed that prompt the server
stays disabled until you approve it there. A server that fails to start is
usually a relative path in its command or arguments, which resolves against the
directory you launched from rather than the location of the config file — use
absolute paths. A server that shows as connected with zero tools has started but
is not returning a tool list; reconnect it from /mcp. And check the location
before anything else: .mcp.json belongs at the repository root, not inside
.claude/, and a settings file does not read an mcpServers key at all.
When the window is the problem
/context earns its place twice. Beyond confirming what loaded, it shows you
what is taking up room, which diagnoses a different class of complaint — Claude
losing track of your conventions, or skills that stop triggering reliably.
Look for a CLAUDE.md that has grown past a couple of hundred lines, a large
skill body that was pulled in early and has stayed since, or several MCP servers
whose tool listings you are paying for in every request. A configuration problem
and a crowding problem feel the same from the outside, and only one of them is
fixed by editing files.
The same instinct applies to anything else you might be assuming about your session, permission mode included. Do not reason from what the default ought to be — the status bar tells you which mode you are in.
When you cannot tell
If the four questions have not resolved it, stop narrowing and start bisecting.
claude --safe-mode starts a session with your customisations disabled —
CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands and agents.
Authentication, models, built-in tools and permissions still work. If the problem
vanishes, it lives in something you added, and the targeted checks above will find
which. If it survives, you have ruled out all of it in one step.
For the harder case where your settings themselves are suspect, launch from a
directory with no project configuration and point CLAUDE_CONFIG_DIR at an empty
directory, which bypasses everything under ~/.claude. You will see the
first-run setup screens, which is how you know it worked. Then reintroduce your
files one at a time.
What to take away
Work down four questions in order: did it load, is it the kind of thing that is
binding, which copy of it won, and did it actually fire. /context answers the
first for almost everything, and answering it first is what stops you rewriting an
instruction that never arrived. Settings merge with managed policy above
command-line flags, then the local file, then the project file, then your user
file, while permission rules merge instead of overriding and CLAUDE.md files
accumulate — and a user or project settings file with one syntax error is
rejected in full, silently. The specific commands narrow quickly: /hooks for
registration, /skills for discovery, /mcp for connection and approval,
/status for which sources loaded, /doctor when you have no hypothesis at all.
When nothing has resolved, claude --safe-mode splits the problem cleanly in two,
and a session pointed at an empty configuration directory rules out your own
files entirely.
Next: Part 4, and the shift from sessions you sit in to Claude running without
you — starting with claude -p, which turns the agent into something you can put
in a script.
Check yourself
5 questions · pass 4/5 to unlock Non-Interactive Mode
1.A rule you added to CLAUDE.md is being ignored. What is the first thing to establish?
2.A key in your project's
.claude/settings.jsonappears to have no effect, and nothing else obviously conflicts. What is worth checking?3.Your hook appears in
/hooksunder the right event but never runs. Which explanation fits that evidence?4.Why is
claude --safe-modea useful step rather than a last resort?5.You added an
mcpServersblock to.claude/settings.jsonand the server never appears. What is going on?
5 left to answer