Claude Code · Part 7

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.

Quick idea: Permission rules and hooks are enforced by Claude Code itself, not by the model choosing to follow an instruction, which is what makes them the right tool for anything that must never happen without exception.
Allow / Ask / Deny

Three tiers of rule that decide whether a tool call runs without a prompt, prompts you, or never runs.

settings.json

Where permission rules, hooks, and other configuration live, at several scopes with a fixed precedence.

Hooks

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.

Important: A broad deny rule beats a narrow allow rule. 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 denyBlocks 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
ManagedC:\Program Files\ClaudeCode\Highest, cannot be overridden
Local.claude\settings.local.json, not committedAbove project and user
Project.claude\settings.json, committed to source controlAbove user
User%USERPROFILE%\.claude\settings.jsonLowest

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/**)"
    ]
  }
}
Practical rule: Check the project-scoped .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
defaultPrompts on first use of each tool, standard behaviour
acceptEditsAuto-accepts file edits and common filesystem commands in the working directory
planRead-only exploration; source files are never edited
bypassPermissionsSkips prompts almost entirely; only for isolated containers or VMs
Production note: Leave 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.

Key difference: A CLAUDE.md rule saying “never run destructive commands without asking” is a request. A 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 knowCLAUDE.md
A repeatable multi-step procedure invoked by nameA Skill
Control over which tools run without a promptPermission rules
An action that must happen, or must never happen, no exceptionsA 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.

Key takeaway: If it must always or never happen, write a hook, not a CLAUDE.md instruction. Deny rules beat allow rules regardless of specificity, so a broad deny is the fastest way to close off an entire category of risk.
Next in this series

Next, a standing reference for common Claude Code problems beyond installation: permission surprises, hooks that do not fire, and sessions that behave unexpectedly.