A recent breakdown of the .claude/ folder made the rounds on Hacker News with 274 points and 138 comments, which tells you something: people are using Claude Code in earnest now, and they want to understand what it is actually doing on their filesystem. The post covers the basics well. What it does not cover is why this configuration system is designed the way it is, how the layers interact, and where the interesting leverage points are.
I have been running Claude Code on a reasonably complex TypeScript project for a while, and the .claude/ folder has become one of the more useful things I manage. Here is what I have learned.
The Hierarchy
The first thing to understand is that there are two separate .claude/ directories at play, and they compose rather than override each other.
The global one lives at ~/.claude/. It contains your user-level settings, any commands or hooks you want available in every project, and the conversation history Claude Code stores per project. The project-level one lives at .claude/ inside your working directory. Settings defined here apply only to that project, and they layer on top of global settings.
This mirrors a pattern you see throughout Unix tooling. Git has ~/.gitconfig and .git/config. ESLint has a global config and a per-project one. VSCode has user settings and workspace settings. The pattern exists because some preferences are personal (your preferred model, your API key behavior) while others are project-specific (which files the agent can write to, what commands it should know about).
There is also a third level that is less obvious: you can place CLAUDE.md files inside subdirectories of your project, and Claude Code will read them when operating in that scope. If your monorepo has a packages/api/ directory with its own conventions, a packages/api/CLAUDE.md can document them without polluting the root instructions.
CLAUDE.md: The System Prompt You Own
CLAUDE.md is the most immediately useful file in the directory. Claude Code reads it at startup and injects it as persistent context for every conversation in that project. Think of it as the system prompt that you write and maintain, rather than one hidden inside Anthropic’s infrastructure.
The format is plain Markdown. There are no special directives or schema to learn. A typical project file might look like this:
# Project Overview
This is a Discord bot built with discord.js v14 and TypeScript.
All bot commands live in src/commands/. Services are in src/services/.
# Commands
- `npm run build` to compile
- `npm test` to run the test suite
- `npm run dev` for development with ts-node
# Conventions
- Use named exports, not default exports
- Error handling via Result types, not thrown exceptions
- Never commit to master directly
Because Claude Code reads this before any conversation starts, you stop re-explaining your project structure every session. The file also commits to your repository, which means new contributors get the same baseline context when they start using Claude Code on the project.
The global ~/.claude/CLAUDE.md is where personal preferences go: your preferred code style, how verbose you want explanations, any standing instructions that apply across all your projects.
settings.json and the Permission Model
.claude/settings.json (and its global counterpart) controls what Claude Code is allowed to do. The most important section is permissions, which takes allow and deny arrays:
{
"permissions": {
"allow": [
"Bash(git:*)",
"Bash(npm run:*)",
"Read(**)",
"Write(src/**)"
],
"deny": [
"Bash(rm:*)",
"Bash(curl:*)"
]
}
}
The syntax is ToolName(pattern). Bash(git:*) allows any git command. Write(src/**) allows writes only inside src/. This is a capability-based permission model: instead of prompting the user on every action, you define upfront what is allowed, and anything outside that boundary triggers a confirmation dialog.
The security implications are worth thinking about. If you are using Claude Code in a project that handles credentials or sensitive config files, restricting Write to specific directories means a wayward tool call cannot touch your .env. The deny rules are evaluated first, so a deny on Bash(rm:*) blocks deletions even if a broad Bash(*:*) allow is also present.
The model key in settings lets you pin a specific model per project:
{
"model": "claude-opus-4-6"
}
This is useful when you want your most capable model on a complex codebase but are willing to use a faster, cheaper model for a simpler utility project.
Custom Slash Commands
The .claude/commands/ directory is where Claude Code’s reusability story gets interesting. Any .md file you place there becomes a slash command available inside Claude Code sessions for that project.
Create .claude/commands/review.md with content like:
Review the staged git diff for correctness, potential bugs, and style issues.
Focus on $ARGUMENTS if specified, otherwise review everything.
Output a structured list of findings with severity levels.
Now /review is a command you can invoke, and /review authentication logic passes authentication logic as the $ARGUMENTS substitution.
Global commands in ~/.claude/commands/ are available everywhere. This is where I put things like a /standup command that summarizes recent git commits in a format suitable for a team update, and a /deps command that audits package.json for outdated or vulnerable packages.
The comparison to other AI tool configuration is instructive here. Cursor has .cursorrules, which is a single flat file for project instructions. GitHub Copilot has .github/copilot-instructions.md, similar in spirit to CLAUDE.md. Aider uses .aider.conf.yml for model and behavior configuration. None of them have the composable command system that .claude/commands/ provides. The closest analogue is VS Code snippets, but those are static text expansions rather than dynamically executed AI prompts.
Hooks: The Underused Layer
Hooks are configured in settings.json and run shell commands in response to Claude Code lifecycle events. There are four event types: PreToolUse, PostToolUse, Notification, and Stop.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npm run lint --fix 2>/dev/null || true"
}
]
}
]
}
}
This hook runs your linter automatically after every file write. Claude Code writes a file, the hook fires, the formatter runs, and the file is already clean before Claude Code reads it back. You close a feedback loop that would otherwise require explicit instructions in every session.
PreToolUse hooks can block tool execution by exiting with a non-zero code, which is how you implement project-level guardrails. A PreToolUse on Bash that validates the command against a blocklist gives you a second layer of protection beyond the permissions.deny configuration.
The Stop hook fires when an agent session ends, which is useful for cleanup: sending a notification, writing a summary to a file, or committing a journal entry.
Hooks are where Claude Code starts to feel less like a chat interface and more like a build tool with an AI layer. The event model is simple, but the surface area it gives you over the agent loop is substantial.
What to Commit
The right .gitignore strategy is to commit everything in .claude/ except the things that contain personal data or project state. Specifically:
- Commit:
CLAUDE.md,settings.json(if it contains no secrets),commands/,hooks/configuration - Do not commit:
~/.claude/projects/(conversation history), anything containing API keys
The conversation history in ~/.claude/projects/ is stored per project using a hash of the project path as the directory name. Each session is a .jsonl file with the full turn-by-turn exchange. This is stored globally, not in the project repo, so it does not end up in version control accidentally.
Committing CLAUDE.md and the commands/ directory is the most valuable part of this. It means the AI configuration for your project travels with the code, the same way .eslintrc, tsconfig.json, and .prettierrc do. A team where everyone has the same CLAUDE.md gets consistent behavior across sessions without any coordination overhead.
The Design Is Deliberate
Looking at this system as a whole, the choices Anthropic made follow well-established conventions in developer tooling. Layered config (global beats nothing, project beats global, subdirectory beats project) is the same model ESLint and Prettier use. Plain Markdown for instructions rather than a DSL keeps the barrier to entry low. The permission model prioritizes explicit over implicit, which reduces surprise in a tool that can modify your filesystem.
The hooks and commands system is where the design goes beyond convention management and into genuine workflow automation. Most users are probably using Claude Code as a smarter autocomplete. The ones getting outsized value from it are the ones who have invested twenty minutes in building out .claude/commands/ for their common workflows and wiring up a few PostToolUse hooks to close the feedback loops that would otherwise require manual re-prompting.
The configuration surface is not large. It is worth understanding completely.