Claude Code — Part 7 — Permissions, settings.json, and Hooks
CLAUDE.md shapes what Claude tries to do. Permissions and hooks control what Claude Code actually allows, enforced by the tool itself rather than by the model choosing to comply.
Three tiers of rule that decide whether a tool call runs without a prompt, prompts you, or never runs.
Where permission rules, hooks, and other configuration live, at several scopes with a fixed precedence.
Shell commands that fire at fixed points in Claude Code’s lifecycle, regardless of what the model decides.
Introduction
Part 4 made a point of noting that CLAUDE.md is context, not enforcement. Claude reads it and tries to follow it, but nothing physically stops it from deciding otherwise on a bad day. This post covers the two mechanisms that do physically stop it: permission rules and hooks.
The Permission System
Every tool call Claude Code makes is checked against a set of rules before it runs. There are three outcomes: an allow rule lets it run without a prompt, an ask rule prompts you for confirmation, and a deny rule stops it outright. Rules are evaluated in that order, deny first, then ask, then allow, and the first match wins regardless of how specific a competing rule is.
Bash(aws *) as a deny rule blocks every AWS CLI call, even one that also matches a specific Bash(aws s3 ls) allow rule.
Read-only actions like file reads need no approval within your working directory. Bash commands and file edits need approval by default, with edits generally auto-accepted for the rest of a session once you approve the first one.
Permission Rule Syntax
Rules follow the pattern Tool or Tool(specifier). A bare tool name matches every use of that tool. A specifier narrows it to specific commands, paths, or domains.
| Rule | Matches |
|---|---|
Bash(npm run build) | Exactly that command, nothing else |
Bash(git commit *) | Any command starting with git commit |
Bash(git push *) in deny | Blocks all push variants regardless of other allow rules |
Read(./.env) | Reading that one file |
WebFetch(domain:example.com) | Fetches to that domain only |
Wildcards can appear anywhere in a Bash pattern. Bash(* --version) matches any command ending in --version, and a single * can span multiple words, so Bash(git * main) matches both git checkout main and git merge main.
Where Rules Live: settings.json
Permission rules and hooks are configured in a settings.json file, at one of several scopes. Higher scopes win when the same rule appears in more than one place.
| Scope | Location (Windows) | Precedence |
|---|---|---|
| Managed | C:\Program Files\ClaudeCode\ | Highest, cannot be overridden |
| Local | .claude\settings.local.json, not committed | Above project and user |
| Project | .claude\settings.json, committed to source control | Above user |
| User | %USERPROFILE%\.claude\settings.json | Lowest |
A minimal project settings file, denying access to secrets while allowing routine commands, looks like this:
{
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)"
],
"deny": [
"Read(./.env)",
"Read(./secrets/**)"
]
}
}
.claude\settings.json into source control so the whole team gets the same baseline. Keep personal exceptions in settings.local.json, which is gitignored.
Permission Modes
Beyond individual rules, a session-wide mode controls the general approval behaviour.
| Mode | Behaviour |
|---|---|
default | Prompts on first use of each tool, standard behaviour |
acceptEdits | Auto-accepts file edits and common filesystem commands in the working directory |
plan | Read-only exploration; source files are never edited |
bypassPermissions | Skips prompts almost entirely; only for isolated containers or VMs |
bypassPermissions alone on a real work machine. It exists for disposable, isolated environments, not for a session touching production infrastructure.
Hooks: Enforcement, Not Instruction
A hook is a shell command that fires at a fixed point in Claude Code’s lifecycle, such as before a tool runs or after a file edit completes. Unlike a CLAUDE.md instruction, a hook always runs. It does not depend on Claude choosing to comply.
PreToolUse hooks can inspect a tool call before it happens and block it outright. This example logs every Bash command to a file, then runs a script that denies anything matching a destructive pattern by exiting with status 2.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r .tool_input.command >> ~/.claude/bash.log"
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
]
}
]
}
}
block-rm-rf.sh receives the tool call as JSON on standard input, checks whether the command contains rm -rf, and exits with status 2 to deny it if so. Claude Code enforces that exit code regardless of what the model wanted to do. The same lifecycle mechanism applies whichever shell tool a session is using, so the same pattern covers a Windows-native session as much as a Bash one.
PreToolUse hook that exits 2 on a destructive pattern is a wall. Use the second one for anything that genuinely must never happen.
CLAUDE.md, Skills, Permissions, or Hooks?
By this point in the series it is worth pulling the four mechanisms together, since they solve genuinely different problems.
| Need | Use |
|---|---|
| A standing fact or convention Claude should always know | CLAUDE.md |
| A repeatable multi-step procedure invoked by name | A Skill |
| Control over which tools run without a prompt | Permission rules |
| An action that must happen, or must never happen, no exceptions | A hook |
Final Thoughts
CLAUDE.md, Skills, permissions, and hooks are not competing ways to do the same thing. They sit at different levels of strictness, from a suggestion Claude tries to follow, to a wall it cannot cross regardless of what it decides. On a machine with real access to real infrastructure, that distinction is the whole point.
Most of what has gone wrong in this series so far has been “Claude did something I didn’t expect.” The next post covers what to do when something goes wrong at the tool level itself.
Next, a standing reference for common Claude Code problems beyond installation: permission surprises, hooks that do not fire, and sessions that behave unexpectedly.