Most developers who use Claude Code know about CLAUDE.md. Drop a file at the root of your project, describe your conventions, and Claude picks it up automatically. That part is well-documented and well-understood.
What gets less attention is everything else in .claude/. The full directory has five distinct configuration surfaces, each with different purposes, and one of them — the hooks system — enables patterns that most people haven’t explored.
A recent breakdown on Daily Dose of DS catalogued what lives in the folder, prompting a healthy discussion on HackerNews. This post goes deeper on the parts most worth understanding.
The CLAUDE.md Hierarchy
CLAUDE.md is injected into every conversation as part of the system prompt. Claude Code reads it automatically at startup, which means anything you put there is available without any explicit retrieval step. That makes it genuinely different from documentation that lives in a README or a wiki — it’s always present.
What’s less obvious is that there are three levels:
~/.claude/CLAUDE.md— global, applies to every project on your machine<project>/CLAUDE.md— project-level context<project>/<subdir>/CLAUDE.md— scoped context for specific directories
The scoped version is the most useful for larger codebases. In a monorepo, you can put a CLAUDE.md in packages/api/ that contains API-specific conventions without polluting the top-level context for work happening in packages/frontend/. Claude reads the applicable files based on where you’re working and concatenates them in order, from global to local.
The practical implication: stop treating CLAUDE.md as a single file and start treating it as a layered context system. Global for machine-wide conventions, project root for architecture and tooling, subdirectories for subsystem-specific rules.
settings.json and the Permission Model
Two settings files exist alongside each other: settings.json (committed) and settings.local.json (typically gitignored for per-machine overrides). Both use the same schema.
The most important section is permissions:
{
"permissions": {
"allow": [
"Bash(git *)",
"Bash(npm run *)",
"Read(**)",
"Write(src/**)"
],
"deny": [
"Bash(git push *)",
"Bash(rm -rf *)"
]
}
}
These are glob-style matchers against tool call signatures. The deny list takes precedence over allow. This lets you set a default posture for a project — a shared settings.json that allows read access and safe build commands but requires a prompt for anything that modifies infrastructure or pushes code.
settings.local.json is where individual developers can relax or tighten those constraints on their own machine without touching the committed config. It’s the right place for personal trust levels that shouldn’t be imposed on teammates.
The Hooks System
This is the part that changes what Claude Code can do, not just how it behaves.
Hooks let you run arbitrary shell commands at four lifecycle points:
PreToolUse— before Claude executes a tool callPostToolUse— after a tool call completesNotification— when Claude sends a status notificationStop— when a conversation ends
They’re configured in settings.json under a hooks key:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cat >> ~/.claude/bash-audit.log"
}
]
}
]
}
}
Claude Code passes the tool invocation as JSON on stdin to the hook command. For a Bash tool call, the stdin looks like:
{
"tool_name": "Bash",
"tool_input": {
"command": "npm run build",
"description": "Build the project"
}
}
The hook can exit with code 0 to allow the call to proceed, or exit with code 2 to block it, with the reason printed to stdout becoming the rejection message Claude sees. This makes hooks a programmable firewall on Claude’s actions.
A few patterns worth calling out:
Audit logging. Pipe every Bash tool call to a log file. When something goes wrong in a long autonomous session, you have a complete record of what ran.
Safety enforcement. A PreToolUse hook that scans Bash commands for patterns you never want to run — git push --force, DROP TABLE, credential-related flags — and blocks them with a clear message. This is defense in depth on top of the permissions deny list, useful when the deny list patterns would be too broad.
PostToolUse side effects. After a Write tool call, run a formatter or linter against the written file automatically, without Claude needing to be instructed to do so.
Notification routing. The Notification hook fires when Claude sends progress updates. Hook it into whatever alerting system you use — a curl to a webhook, a Discord message, a desktop notification via notify-send.
The Stop hook is underrated for batch workflows. When a conversation ends, automatically commit work-in-progress, push to a remote, or run a test suite. For sessions where Claude handles a task and you check results later, this closes the loop without manual follow-up.
Custom Commands
The .claude/commands/ directory holds markdown files that become custom slash commands. A file at .claude/commands/pr-review.md becomes /project:pr-review. The markdown content is the prompt template, with $ARGUMENTS as a placeholder for anything typed after the command name:
Review the GitHub pull request at $ARGUMENTS.
Focus on:
- Security implications of any new dependencies
- Test coverage for changed code paths
- API contract changes that could break consumers
This is the right place for team-wide prompt conventions. Instead of each developer reconstructing the same review scaffolding from memory, the command encodes it once. The file lives in version control, so prompt improvements are reviewed like code changes.
Global commands live at ~/.claude/commands/ and are accessible across all projects. Utility prompts that apply everywhere — generating test cases for a function signature, refactoring to match a style guide — belong there rather than duplicated across project directories.
MCP Server Configuration
Model Context Protocol server definitions can live at .claude/mcp.json for project-scoped servers, or in the global settings for servers you want available everywhere:
{
"mcpServers": {
"sqlite": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite", "./data/dev.db"]
}
}
}
Putting MCP config in .claude/mcp.json rather than global settings means the project carries its own tool dependencies. Someone cloning the repo and opening Claude Code gets the right servers without additional configuration steps.
Comparing Across the Ecosystem
Every major AI coding tool is developing a similar pattern. Cursor uses .cursorrules, recently evolved into per-directory rule files under .cursor/rules/. GitHub Copilot reads .github/copilot-instructions.md. Windsurf has .windsurfrules. Aider uses .aider.conf.yml.
What distinguishes the .claude/ approach is scope. The others are mostly context injection — you give the model instructions about your project. Claude Code adds permissions, hooks, and commands on top of that, making the config directory a genuine interface layer rather than a documentation drop.
The hooks system has no direct equivalent in those tools. It’s closer to git hooks conceptually: a defined set of lifecycle events where you can inject behavior. The key difference is that Claude Code hooks can inspect and block the AI’s actions, not just react to them after the fact.
What Belongs in .gitignore
settings.local.json should typically be gitignored. It’s for personal preferences, local path overrides, and trust levels that are specific to one machine.
The rest of .claude/ — settings.json, CLAUDE.md, commands/, mcp.json — belongs in version control. Treating it as shared team configuration rather than personal tooling is what makes it pay off. When a new developer joins the project, they inherit the accumulated context, the permission defaults, and the shared commands, without any onboarding ceremony.
The .claude/ folder has the same character as a well-maintained Makefile or .editorconfig: easy to ignore, quietly useful to inherit.