AI-Assisted Engineering

Claude Code hooks, skills and memory: an operating manual

Which Claude Code mechanism owns which job: CLAUDE.md, rules, auto memory, skills and hooks, with the limits, load behaviour and exact config for each.

Published

Updated

—

Reading time

12 min

Most Claude Code setups I review fail in one of two ways. Either everything lives in one enormous CLAUDE.md that nobody has pruned since March, or the team discovered hooks and skills and now has six overlapping mechanisms that each half-own the same rule. Both failures have the same root cause: the person who configured the tool never decided which mechanism is responsible for which kind of job. This article is the decision manual I use. It is for engineers who already run Claude Code daily and want their instructions to be predictable: what loads when, what is merely advice and what is enforced, what Claude writes for itself, and how to debug the case where a rule is being ignored. I cover CLAUDE.md and rules, auto memory, skills, and hooks, with the limits taken from the official documentation. For the workspace-level layout across many repositories, start with setting up Claude Code for a multi-repo workspace; this piece goes deeper on the four mechanisms themselves.

The constraints I design for#

  • Enforced things must be enforced. If a rule protects production or credentials, a model that reads it and decides otherwise is not acceptable.
  • Context is a budget. Everything loaded on every turn is paid for on every turn, in money and in attention. Instructions that matter for one task should not ride along on all of them.
  • Advice should fail visibly. When Claude ignores an instruction, I want a command that tells me whether the file loaded at all.
  • A new teammate gets the setup by cloning. Shared behaviour is committed. Personal preference stays out of the repository.
  • Nothing the model learns on its own is trusted blindly. Anything Claude writes for itself must be easy to read, edit and delete.

The four mechanisms, compared#

The memory documentation is explicit about the most important distinction: Claude treats CLAUDE.md content as context, not enforced configuration. To block an action regardless of what Claude decides, the documentation points to a PreToolUse hook. That one sentence organises the whole comparison.

MechanismWho writes itLoadsEnforcedBest for
CLAUDE.md and unscoped rulesYouAt launch, every sessionNo, it is contextBuild commands, conventions, always-true facts
Path-scoped rules (paths: frontmatter)YouWhen Claude reads a matching fileNoConventions tied to a file type or directory
Auto memoryClaudeMEMORY.md index at launch, topic files on demandNoYour corrections, preferences, facts not derivable from code
SkillsYouDescription always, body when invokedNoMulti-step procedures needed for some tasks
Hooks and permission rulesYouRegistered at startup, run at lifecycle eventsYesRules that must hold every time
Three questions, asked in this order, place almost every instruction.

The decision: ask three questions in order#

  1. Must it hold every time, with no judgement involved? It becomes a hook or a permission rule. Writing it in CLAUDE.md as well is fine as documentation, but the hook is the control.
  2. Is it a procedure with several steps that only some tasks need? It becomes a skill, so the steps stay out of context until they are useful.
  3. Is it a fact Claude needs in most sessions? If you write it, it goes in CLAUDE.md (or a path-scoped rule if it only applies to some files). If Claude learned it from your corrections, it lives in auto memory.

The order matters because the cheap mistake is to start at question three. A line like "never run terraform apply against production" feels like a fact, so it lands in CLAUDE.md, and it works until the one session where it does not.

Implementation#

1. CLAUDE.md and rules: what loads, and when#

Claude Code loads CLAUDE.md and CLAUDE.local.md from the working directory and every directory above it at launch, concatenated from the filesystem root down so the instructions closest to where you started are read last. Files in subdirectories load on demand, when Claude reads files there. The documentation suggests keeping each file under about 200 lines, and notes that @path imports do not reduce cost because imported files load at launch too.

Three details save real effort:

  • Block-level HTML comments are stripped before the content reaches Claude, so maintainer notes cost no context. Comments inside code blocks are kept.
  • Rules without paths frontmatter load at launch like .claude/CLAUDE.md. Rules with paths load only when Claude reads a matching file. paths is the only frontmatter field Claude Code reads from a rule.
  • Project-root CLAUDE.md survives /compact: Claude re-reads it from disk afterwards. An instruction given only in conversation does not survive, and neither does one in a nested file that has not reloaded yet.

A path-scoped rule looks like this:

.claude/rules/migrations.mdmarkdown
---
paths:
  - "db/migrations/**/*.sql"
---
 
# Migrations
 
- One schema change per migration file.
- Never edit a migration that has been merged; add a new one.
- New indexes on large tables use CREATE INDEX CONCURRENTLY.

If the repository also carries an AGENTS.md, note the default: Claude reads it only when there is no CLAUDE.md or CLAUDE.local.md in the working directory or above. The simplest way to share one file across tools is a CLAUDE.md whose first line is @AGENTS.md, with Claude-specific notes below it.

2. Auto memory: what Claude writes for itself#

Auto memory is on by default. Claude saves four kinds of notes, recorded in the file's type frontmatter: user, feedback, project and reference. It skips anything derivable from the code or already stated in your CLAUDE.md files.

The storage rules are worth knowing before you rely on it:

  • The directory is ~/.claude/projects/<project>/memory/, derived from the git repository, so all worktrees of one repository share it.
  • It is machine-local. It does not follow you to another machine or to a cloud environment.
  • MEMORY.md is an index, one line per memory. Only its first 200 lines or first 25KB, whichever comes first, load at session start. Topic files are read on demand.
  • Memory files are excluded from the transcript cleanup that follows cleanupPeriodDays; they stay until you or Claude changes them.

To turn it off globally use the toggle in /memory, or set CLAUDE_CODE_DISABLE_AUTO_MEMORY=1. For one project, put this in that project's settings:

.claude/settings.jsonjson
{
  "autoMemoryEnabled": false
}

My rule for auto memory is to treat it as a draft inbox. Every few weeks I run /memory, open the folder and read it. Anything that is really a team convention gets promoted into the committed CLAUDE.md, because auto memory is not shared across the team. Anything stale gets deleted. Because the files are plain markdown, this takes minutes.

3. Skills: procedures that cost nothing until used#

A skill is a directory containing a SKILL.md. Personal skills live in ~/.claude/skills/<name>/, project skills in .claude/skills/<name>/, and nested skills in a subdirectory's own .claude/skills/. The name and description of every skill sit in context on every turn; the body loads only when the skill is invoked.

That makes the description the expensive part. The documentation caps the combined description and when_to_use text at 1,536 characters per skill, and scales the whole listing budget to 1% of the context window; when it overflows, descriptions of the least-used skills are dropped first. So I put the trigger condition first and keep it short.

A release skill that only I can start, which pulls live data into the prompt before Claude sees it:

.claude/skills/release-notes/SKILL.mdmarkdown
---
name: release-notes
description: Draft release notes from the commits since the last tag. Use when preparing a release.
disable-model-invocation: true
arguments: [version]
---
 
Draft release notes for version $version.
 
## Commits since the last tag
 
!`git log --oneline "$(git describe --tags --abbrev=0)"..HEAD`
 
Group the commits under Added, Changed and Fixed. Skip merge commits and
dependency bumps. Keep each entry to one line and link the pull request
number where the commit message has one.

Four behaviours are easy to get wrong:

  • disable-model-invocation: true stops Claude from starting the skill on its own. Use it for anything with side effects.
  • The ! command runs before Claude sees the content, and its output replaces the placeholder. A non-zero exit aborts the whole skill invocation, so commands that may legitimately exit non-zero need || true. The git describe above fails in a repository with no tags, which is the right outcome: the skill should not run blind.
  • Invoked content stays in the conversation as a single message across later turns, and Claude Code does not re-read the file. Write standing instructions, not "step one, now do step two" prose that only makes sense at the moment of invocation.
  • allowed-tools grants clear on your next message. Do not rely on a skill to pre-approve something for a long session.

context: fork runs a skill in an isolated subagent context, which suits exploration-heavy tasks where you want the summary rather than the file dumps. paths limits when a skill activates, with the same glob style as rules.

4. Hooks: the enforced layer#

Hooks run at lifecycle events. They can be a shell command, an HTTP call, an MCP tool call, a single-turn model prompt or a subagent (the last is marked experimental). Hook entries from user, project and local settings merge rather than replace one another, and managed settings cannot be switched off by a user.

Three exit-code facts do most of the work for command hooks. Exit 0 means proceed, and for some events plain stdout is added to context. Exit 2 blocks the action and feeds stderr back to Claude as the reason. Any other non-zero code is a non-blocking error and the action proceeds. That last one catches people: a hook script that crashes with exit 1 does not block anything.

Here is a PreToolUse guard that denies a specific class of command, using JSON output rather than exit 2 so the reason is structured. The if field narrows it with the same syntax as permission rules, so the script only runs for matching commands:

.claude/settings.jsonjson
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(terraform apply *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-prod-apply.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
.claude/hooks/block-prod-apply.shbash
#!/usr/bin/env bash
# Reads the pending tool call as JSON on stdin.
input=$(cat)
command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
 
if printf '%s' "$command" | grep -Eq -- '-chdir=.*prod|workspace.*prod'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Production applies run from the pipeline, not from a session."
    }
  }'
fi
exit 0

Treat a pattern-matching guard like this as a seatbelt, not a boundary: a determined command can be rewritten to slip past a regular expression. The real boundary for production is that the session's credentials cannot apply to production at all, which is what least-privilege IAM is for.

Two other events earn their place in almost every setup:

  • SessionStart with additionalContext injects live state, such as the current branch and uncommitted files, as a system reminder before your first prompt. Its matcher filters on startup, resume, clear, compact or fork, so you can re-inject after compaction only.
  • Stop receives last_assistant_message and can block stopping with exit 2 or decision: "block", which makes Claude continue. I use it sparingly, because a Stop hook that blocks on a condition Claude cannot fix creates a loop; the documentation gives no built-in loop guard, so mine always checks a condition that Claude's next action can plausibly change, and has a short timeout.

For debugging, the InstructionsLoaded event fires when CLAUDE.md or a rules file loads, and its matcher filters on the load reason. A hook that appends the file path and reason to a log answers "did my path-scoped rule ever load?" in one look. Note that it does not fire for an AGENTS.md that Claude reads directly through the project-instructions setting.

CostWhere the tokens actually go

Context cost here is structural, not per feature. Anything that loads at launch is re-sent on every turn: the CLAUDE.md chain, unscoped rules, the first 200 lines of MEMORY.md, and the name and description of every skill. Skill bodies, path-scoped rules, nested CLAUDE.md files and memory topic files cost nothing until they are used. Hooks that run shell commands cost no tokens at all unless they print output into the conversation. I do not quote a per-token price because plans and rates change; check the Claude pricing page for current figures. The practical consequence is simple: moving a 40-line procedure from CLAUDE.md into a skill saves those 40 lines on every turn of every session that does not need it.

Trade-offs and failure modes#

  • A rule in CLAUDE.md is ignored. First run /context and check the Memory files list to confirm the file loaded. Then look for contradictions between layers, since Claude may pick either of two conflicting instructions. Then make the wording verifiable: "run pnpm test before committing" beats "test your changes". If it must happen at a fixed point, make it a hook.
  • A hook does not fire. Run /hooks to see what registered. Check the matcher: a plain string of letters, digits, _, -, spaces, commas and | is an exact match, and anything else is a regular expression. Tool names are written exactly as Claude Code names them, such as Bash or Edit.
  • A hook script crashes silently. Remember that exit 1 is non-blocking. Guard scripts that must block should either exit 2 deliberately or print the deny JSON, and I test them by piping a sample payload into the script by hand before trusting them.
  • Skills stop being used. The listing budget may have dropped their descriptions. /context shows the loaded sizes, and /skill-doctor finds skills worth disabling.
  • Instructions drift. Run /doctor prompt-audit periodically. It reports outdated instructions, references to files or commands that no longer exist, and files that contradict each other, and it changes nothing until you ask it to. It needs a recent Claude Code version (the documentation states v2.1.283 or later).
  • Auto memory records something wrong. It is plain markdown. Edit or delete it, and if the fact is a team convention, move it to the committed file.
  • Hooks in a shared repository run on everyone's machine. Review hook scripts in pull requests with the same care as a CI script, because a committed hook is code that executes locally.

Before you call the setup done

  • Every must-hold rule is a hook or permission rule, not only a sentence in CLAUDE.md
  • Hook scripts that block exit 2 or print deny JSON, and were tested with a sample payload
  • Each CLAUDE.md is under about 200 lines, and procedures have moved into skills
  • Skills with side effects set disable-model-invocation
  • Skill descriptions lead with the trigger and stay short
  • File-type conventions use path-scoped rules instead of launch-time rules
  • /context shows the memory files you expect, and nothing contradictory
  • Auto memory is reviewed periodically and team conventions are promoted into the committed file
  • Personal preferences live in CLAUDE.local.md or user settings, not in the repository

When not to do this#

For a small repository with one developer, most of this is overhead. A 30-line CLAUDE.md with the build and test commands, and the default auto memory, will carry you a long way; add a hook the first time Claude does something you would never want repeated, and a skill the first time you paste the same checklist twice. Configuration built before the need exists tends to encode guesses, and guesses are exactly the contradictory, stale instructions that /doctor prompt-audit ends up flagging later.

Share
All articles →

A layered CLAUDE.md, tight permissions, hooks, skills and scoped MCP servers: the setup that keeps Claude Code safe and cheap across dozens of repositories.

16 min

A scheduled sync pays to refresh every record, including the ones nobody reads. Refreshing stale data when someone looks makes cost follow attention.

18 min