Sign inStart free
§Learn

CLAUDE.md, hooks, and permissions: how Claude Code decides what it can do

Claude Code reads configuration in layers: user-level CLAUDE.md (all projects), project-level (shared, versioned), and CLAUDE.local.md (personal, for .gitignore). Permissions follow the same scope logic, but with one fixed rule: a deny always beats an allow, regardless of specificity or which scope each rule comes from. CLAUDE.md shapes what Claude attempts to do; hooks like PreToolUse enforce what it is actually allowed to execute, deterministically, even if the model decides otherwise.

Claude Code decides what to do across two different layers: CLAUDE.md shapes behavior, and permissions — allow, deny, hooks — decide what it's authorized to execute. Confusing the two is the most common mistake when configuring Claude Code for the first time.

CLAUDE.md isn't permission — it's context

CLAUDE.md exists in layers: a user file (applies to every project), a project file (shared, versioned in git), and a personal CLAUDE.local.md, which stays out of version control. What you write there shapes what Claude attempts to do — but it's a natural-language instruction, and a natural-language instruction can be forgotten partway through a long conversation or lose out to a more recent request.

That's why writing "never run DROP TABLE in production" in CLAUDE.md is a good habit, but not a guarantee. It guides behavior most of the time; it doesn't enforce anything at the execution level.

Deny always beats allow — with no exception for specificity

Permissions follow scopes similar to CLAUDE.md, but with one fixed rule that has no exception: when a deny rule and an allow rule both apply to the same call, deny wins — no matter which scope each one comes from, and no matter which of the two is more specific.

This overturns a common intuition: a tightly scoped allow, like permitting only `git push origin main`, doesn't escape a broader deny, like blocking `git push *`. The hierarchy is fixed on purpose, so a security restriction can't be worked around by a finer-grained allow written somewhere else.

Hooks are the only layer that's actually deterministic

A PreToolUse hook runs before the tool executes and can block the call for real, regardless of what the model decided to do. That's the difference between asking Claude to avoid something and preventing it from happening.

PostToolUse runs afterward: useful for reacting to a result, like running the linter after an edit, but useless as a barrier — by the time it fires, the destructive command has already run. Picking the wrong hook for the goal is the most common mistake: if you want to block something, use PreToolUse; if you want to react to it, use PostToolUse.

Where to configure what

A personal preference that doesn't make sense to impose on the team goes in CLAUDE.local.md, outside version control. A rule the whole team needs to follow, and that can't be silently ignored by one developer, goes in the project's CLAUDE.md or a versioned hook — because only what lives in the repository is shared by construction.

The question that decides where something belongs isn't "is this important?" — it's "does this need to hold for everyone, even if nobody remembers to configure it again?" If yes, it's a hook or project CLAUDE.md. If it's just your own preference, it's local.

Beyond asking every time: permission modes

Asking before every edit isn't the only way to operate. `acceptEdits` mode lets Claude write and edit files without confirming one by one; `plan` mode blocks all writes during the session, useful when you only want it to explore and propose, without executing anything. Each mode trades safety for speed in a different way — the right choice depends on how much you already trust the task being automated, not on a fixed preference.

This is session configuration, so it only holds while the session lasts — it doesn't replace a permanent deny or a hook. A more permissive mode is a deliberate choice for a specific task, not a way around a rule that should stay fixed.

Takeaway: CLAUDE.md persuades; hooks enforce. If a rule can't have an exception, it doesn't belong in an instruction file — it belongs in a PreToolUse hook.

Try it yourself

An internal tooling team wants to guarantee that Claude Code can never execute a raw `DROP TABLE` or `TRUNCATE` statement against their production Postgres database through the Bash tool, regardless of what the model decides to do mid-conversation.

Which mechanism enforces this deterministically, rather than merely encouraging the model to avoid it?

Read next

Study this for real

This is one concept out of the whole certification path. AgentPrep turns all of them into a daily quest, inside Claude Code.

Start free