A Claude Code configuration is a small piece of JSON plus a few scripts, and the tool is forgiving about it. A rule with a wrong shape is usually skipped, a hook that never matches simply never runs, and the session starts as normal. That is convenient while you experiment and dangerous once a team relies on the file, because the failure has no message. You find out when the protection was supposed to act and did not. This guide goes through the places where a setting reads as correct and does nothing: hooks, permission rules, the settings files themselves and MCP server switches. Each one comes from the official documentation, and each can be checked in a few minutes.
The file may not be loading at all
Start with the cheapest check. The settings files are strict JSON. Comments and dangling commas count as errors, and an interactive session reports a broken user, project or local file in a dialog when it starts (Claude Code docs, settings). The docs draw a second distinction: when only individual entries are bad, for instance a malformed rule or an event name that does not exist, those entries are left out with a warning, while the remainder is applied.
The quiet case is a non-interactive run. A claude -p run opens no dialog, drops the broken file or entries and goes on, so a script or a CI job can run for weeks without the rules you wrote (Claude Code docs, settings). After every edit, run a JSON parser over the file, then use /status to see which files were loaded and claude doctor to see which entries were rejected.
Hooks: the exit code decides, not the intent
A hook that is meant to stop a tool call has to exit with code 2, and the message Claude receives is whatever the script wrote to stderr. Exit code 1 is the usual failure code on Unix, so scripts reach for it by habit, but for hooks it is only a non-blocking error and the action goes ahead (Claude Code docs, hooks). The docs say it outright: a policy hook should exit with 2.
A related trap sits in the JSON route. For PreToolUse, the current shape is a hookSpecificOutput object that carries the event name, a permissionDecision and a reason, and the older top-level decision field now accepts only the value block (Claude Code docs, hooks). Older examples may still use the old top-level shape. Copy the current shape, and test it with a command you are sure should be refused.
Hooks: timeouts do not act as a gate
A scanner that inspects a command, with the expectation that a slow scan stops the command, is a common design. It does not behave like that. When a command hook runs past its limit, Claude Code cancels it and throws away what it printed; for PreToolUse the tool call then proceeds along the ordinary permission path (Claude Code docs, hooks). Per the same page, the limit defaults to 600 seconds. The docs offer an onFailure setting for blocking in that case; if you cannot use it, let the script watch its own clock and exit with 2 when time runs out.
Hooks: matchers are two different languages
The matcher field changes meaning depending on its characters. When it holds only letters, digits, underscore, hyphen, spaces, commas and the pipe sign, it names one tool or a list of tools, and each must be spelled in full. When it holds anything else, it becomes a pattern in the JavaScript regex flavour that may hit anywhere inside the tool name (Claude Code docs, hooks). This produces two opposite mistakes.
- Too narrow. A matcher such as
mcp__memoryis made only of plain characters, so it is read as a full tool name and no tool carries that name. A whole server needsmcp__memory__.*. The same page notes that tools from a server bundled in a plugin have a longer name that includes the plugin, so a pattern built from the plain server key does not fire for them. - Too wide.
Edit.*is a regular expression, so it also catchesNotebookEdit. Anchor it with^and$, or list the exact names separated by a pipe sign.
An event name that does not exist is the third silent failure. The entry is dropped with a warning while other hooks stay active (Claude Code docs, settings), so a misspelt event means the hook never runs.
Permission rules: which tool name is actually checked
Deny rules are evaluated first, then ask rules, then allow rules, across all files, so a deny anywhere beats an allow elsewhere (Claude Code docs, permissions). That makes deny rules the right place for secrets, but only if they are written in a form that is consulted. The documentation says that path rules belong to Read and Edit: use Edit(docs/**) instead of the same pattern under Write, NotebookEdit or MultiEdit, and Read instead of Glob. A line such as Write(.env) in the deny list is accepted and looks protective, but the docs point you to Edit for such rules, so test that it really refuses before trusting it.
Permission rules: where a slash points
One leading slash does not make an absolute path. It is anchored where the rule is defined: the project folder for the project files, and the Claude folder in your home directory for the user file. For a real absolute path the pattern starts with two slashes, and for the home folder with a tilde (Claude Code docs, permissions). The docs give the consequence: a rule like Read(/secrets/**) in the user file blocks a folder inside your Claude directory, not the secrets folder of your project.
Depth is the next surprise. As an allow rule, Edit(src/**) matches only the src directly under the working directory, so a monorepo with several nested src folders needs Edit(**/src/**). As a deny or ask rule, the same short form reaches a folder of that name at every level (Claude Code docs, permissions). A deny rule in that shape is therefore fine, while its allow twin quietly does less than you expect.
For shell commands, the space before the final wildcard is part of the rule. Bash(ls *) matches ls and ls -la but not lsof, while Bash(ls*) matches all three, and the :* ending is just another spelling of the closing wildcard (Claude Code docs, permissions). Whether a colon placed earlier in a pattern still acts as a wildcard is not stated on that page, so test such a rule before trusting it.
Modes that a repository file cannot set
Two values of permissions.defaultMode, auto and bypassPermissions, are ignored when they sit in project or local settings. They have to come from the user file, from managed settings or from a flag for one session. Releases older than 2.1.257 accepted bypassPermissions from every file (Claude Code docs, settings). A team file that contains one of those modes therefore does nothing, or on an old version more than anyone intended.
MCP: the switch that controls a different list
Claude Code has several similarly named keys for MCP servers. The enabledMcpjsonServers and disabledMcpjsonServers pair only governs the approval of servers that originate in a .mcp.json file at the project root, and the keys enabledMcpServers and disabledMcpServers are a separate pair (Claude Code docs, MCP). Putting the name of a server you added by command, or one that arrives through a plugin, into the .mcp.json list does not turn it off. Plugin servers are registered under a plugin: name with the plugin and server joined, which is the string you would need when looking for them.
Approval has its own gap. In claude -p runs and in SDK sessions the approval prompt cannot be shown, so project-scoped servers load without asking (Claude Code docs, MCP). A shared .mcp.json therefore needs review as code, since automation will start whatever it lists. We have not checked whether the docs offer an organisation-level deny list for MCP servers; if you need a hard block at every scope, look for one in the current settings reference before relying on the per-project switches.
A ten-minute check
- Parse each settings file, then run
/statusandclaude doctor. - For every blocking hook, run the script by hand with a bad input and look at its exit code. It should be 2.
- For every matcher, ask whether it contains a character outside the plain set, and what name it is really compared against.
- For every path rule, ask which tool name it uses, where its first slash points and whether it is an allow or a deny rule.
- For every MCP switch, ask where the server is defined and which list the key belongs to.
- Try the protection once on purpose, for example a read of a decoy file that a deny rule should refuse.
Before you add a server to a shared file, read our piece on what to check when you add an MCP server, where the question is which tools a server hands to the agent.
When this does not apply
Our reading rests on the documentation pages as read on 28 October 2026, and Claude Code moves fast; keys, defaults and version thresholds shift, so confirm against the current pages before editing a team file. The scope is local use from a terminal, not cloud sessions or organisation-managed deployments, where some settings are fixed from above. No experiments were run for this post, so the behaviours described are the documented ones, not something we measured, and your version may differ. A rule that looks wrong is not necessarily harmful: a narrow allow rule only costs you extra prompts. Finally, a settings review cannot tell you whether your policy is wise, only whether the lines do what their author thinks.
Reviewing a configuration
Before a team starts depending on a configuration, the claude-code-settings-review skill reads a pasted settings file, hook script or permission list and reports each place where a key or pattern has a different effect from the one intended. It covers wrongly aimed MCP switches, syntax errors, hooks that exit with the wrong code, matcher mistakes, path rules for the wrong tool, misplaced slashes, wildcard shapes, timeouts and unsafe modes in project files, and replies with one finding per problem plus a verdict. Its scope is the pasted text alone, and matters of style are left to you.
Read this post as Markdown: /blog/claude-code-settings-hooks-permissions.md · Atom feed.
