The anatomy of the .claude/ folder has been getting attention on Hacker News lately, and rightfully so. Most people using Claude Code treat it as a chat interface for code. The configuration layer sitting in .claude/ tells a different story: this is closer to a programmable development environment than a chatbot wrapper.
I’ve been running Claude Code heavily on a few projects, including a Discord bot with a fairly complex MCP toolchain. The more I’ve shaped .claude/ to fit the work, the less it feels like “configuring an AI” and the more it feels like setting up a development environment the way you’d configure a language server or a linter. The design decisions behind the folder structure are worth understanding on their own terms.
The Two-Tier Layout
Claude Code operates with two separate .claude/ directories at all times: a global one at ~/.claude/ and a project-level one at .claude/ in your repository root. Both can contain settings.json, a CLAUDE.md file, and a commands/ subdirectory. The project level overrides the global level for settings, and both levels’ CLAUDE.md files get injected into context simultaneously.
This mirrors the design of git config almost exactly: system-level defaults, user-level preferences, and repository-level overrides, each scoped appropriately. It is a well-understood layering pattern, and Claude Code extends it with a third tier: .claude/settings.local.json. That file is intended to stay out of version control and handles per-machine configuration, which is the same pattern Next.js uses with .env.local or Vite with .env.local. You commit the shared project config; you keep the machine-specific stuff out of the repo.
CLAUDE.md: Persistent Context, Not a Memory Database
The thing most people get wrong about CLAUDE.md is treating it as a magic memory store. It is not a database. Claude Code reads the file and injects its contents into the model’s context at session start. There is no retrieval, no embedding lookup, no semantic search. If you write it down in CLAUDE.md, the model sees it. If you don’t, it doesn’t.
This has real implications for how you structure the file. Dense walls of prose get processed as dense walls of prose. Structured, specific instructions work better:
## Build Commands
- Build: `npm run build`
- Type check: `npm run typecheck`
- Test: `npm test -- --run`
## Conventions
- Zod for all validation, not Yup or Joi
- Named exports only, no default exports
- No comments unless logic is genuinely non-obvious
The docs actually call this out explicitly: Claude Code will ask you for lint and typecheck commands if it can’t find them, and will offer to write them to CLAUDE.md so it knows them in future sessions. That is the intended workflow. The model writing to CLAUDE.md via the Edit tool, on your request, is the persistence mechanism. It is simple and it works.
CLAUDE.md files can also live in subdirectories. If you have a frontend/CLAUDE.md and Claude Code is working inside frontend/, that file gets loaded too. This lets you scope framework-specific conventions to the parts of the codebase where they apply, without polluting the root-level file with React-specific notes that don’t matter when Claude is working on the backend.
The Hooks System
Hooks are where Claude Code moves from “configurable” to genuinely programmable. They are shell commands that fire at specific points in the tool-use lifecycle, configured in settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint -- $CLAUDE_FILE_PATHS"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code finished'"
}
]
}
]
}
}
Four lifecycle events are available: PreToolUse, PostToolUse, Notification, and Stop. The PreToolUse hook is the most powerful one because a non-zero exit code from that hook blocks the tool call entirely. Claude Code receives the hook’s stdout and can adjust its approach based on what the hook reports. This means you can build guardrails that actually stop things before they happen, not just log after the fact.
Common patterns: run your linter on every file Claude edits, block git push unless you’re in a specific branch, send a desktop notification when a long task finishes, log every bash command to an audit file. The $CLAUDE_FILE_PATHS environment variable gives hooks access to the files involved in an Edit or Write operation, so the linting hook above only checks what actually changed.
This architecture resembles Git hooks more than it resembles typical AI tool configuration. Git hooks are shell scripts that fire at lifecycle events and can abort operations. The parallel is deliberate, and the effect is the same: your project gets a programmable layer that enforces invariants without relying on the model to remember them.
Custom Slash Commands
Files in .claude/commands/ become slash commands. The filename (minus .md) is the command name. A file at .claude/commands/review.md registers as /review. Subdirectories create namespaced commands: .claude/commands/db/migrate.md becomes /db:migrate.
The file format accepts optional frontmatter and a body that becomes the prompt:
---
description: "Code review focusing on security and performance"
---
Please do a thorough review of $ARGUMENTS. Check for:
- SQL injection and XSS vectors
- N+1 query patterns
- Unhandled promise rejections
- Missing input validation at API boundaries
Format findings as a numbered list ordered by severity.
$ARGUMENTS gets replaced with everything typed after the command name. /review src/api/users.ts passes src/api/users.ts as the argument. Commands in ~/.claude/commands/ are global and available in every project; commands in .claude/commands/ are project-scoped.
This is stored-prompt management with a clean interface. The comparison to shell aliases understates it: aliases don’t take structured arguments, don’t appear in a discoverable picker, and don’t let you write multi-paragraph prompts with careful framing. These feel more like Vim macros or VS Code tasks, except they target a language model instead of a build system.
The Settings Permission Model
The permissions field in settings.json lets you express explicit allow and deny rules for tool use:
{
"permissions": {
"allow": [
"Bash(npm run test:*)",
"WebFetch(domain:docs.anthropic.com)"
],
"deny": [
"Bash(git push:*)",
"Bash(rm -rf:*)"
]
}
}
Deny rules take precedence over allow rules. The pattern syntax supports wildcards for arguments, so you can allow an entire category of commands while blocking specific dangerous variants. This integrates with the PreToolUse hook system: if you want to block a command and explain why to the model, a hook is more expressive. If you want a hard block with no explanation needed, the deny list is simpler.
For teams, this is also where the project vs. local split matters. You can commit a project-level settings.json that defines what tools are allowed in the project, and individual developers can add further restrictions in their settings.local.json without affecting anyone else.
What This Adds Up To
Taken together, CLAUDE.md for persistent context, hooks for lifecycle automation, custom commands for stored workflows, and a layered settings system with explicit permissions produces something that behaves less like a configurable chat tool and more like a dev environment with a first-class AI participant. The analogy to .vimrc or settings.json in VS Code is apt: these are the files where you decide what the tool knows about your project and what it is allowed to do.
The design choices are conservative in good ways. CLAUDE.md is a markdown file, not a proprietary knowledge format. Hooks are shell commands, not a plugin API. Custom commands are markdown files with frontmatter, not compiled extensions. Everything is readable, diffable, and storeable in version control. The .local.json split keeps machine-specific config out of the shared history.
If you are using Claude Code without a populated .claude/ folder, you are leaving a significant amount of its capability on the table. The official settings documentation and hooks reference cover the full options. Start with a CLAUDE.md that captures your build commands and code conventions, add a PostToolUse hook that runs your linter, and see how the experience changes. The folder is small; what it enables is not.