Claude Code's .claude/ Directory Is More Than Config — It's AI Infrastructure
Source: hackernews
The .claude/ directory showed up in my repos quietly. At first it was just a CLAUDE.md file I half-heartedly filled with project notes. A few months in, it had grown into something I actively maintain: hooks that auto-format files, permission rules that prevent Claude from touching environment files, custom slash commands for common workflows, and a settings.local.json with my personal tool preferences. The directory is getting more attention now, and the HackerNews discussion has surfaced a lot of interesting use patterns.
What follows is a deeper look at what the directory actually contains and why the design choices matter.
Two Scopes, One Structure
Claude Code operates with two configuration scopes: a global one at ~/.claude/ and a project-level one at <project-root>/.claude/. They share the same internal structure. The global directory holds your personal preferences and tools; the project directory holds team conventions and project-specific configuration that gets committed to git.
~/.claude/
├── settings.json # Global defaults, personal MCP servers
├── CLAUDE.md # Your personal coding style and habits
└── commands/ # Your personal slash commands
<project-root>/
├── CLAUDE.md # Project architecture, conventions (committed)
└── .claude/
├── settings.json # Project permissions, hooks, MCP (committed)
├── settings.local.json # Personal overrides, never committed
└── commands/ # Project slash commands
The split between settings.json and settings.local.json solves a problem that plagues dotfile-based tooling: some configuration is team-wide, some is personal, and you cannot commit both to the same file without one developer’s preferences overwriting another’s. Claude Code handles this by having the local file be automatically gitignored, while the main settings.json is shared. When both exist, they are merged, with local taking precedence on conflicts.
CLAUDE.md: Not Just a README for Claude
The CLAUDE.md file is injected into Claude’s context at session start. The key thing that often gets missed is that this loading is hierarchical. Claude Code reads multiple CLAUDE.md files per session:
~/.claude/CLAUDE.md(global, loads for every project)<project-root>/CLAUDE.md(project root, committed to git)- Any
CLAUDE.mdfiles in subdirectories that Claude is actively working in
All found files are concatenated and stacked — they do not override each other. This matters in monorepos. A structure like this works exactly as you would expect:
project/
├── CLAUDE.md # "This is a monorepo with three packages"
├── frontend/
│ └── CLAUDE.md # "React 18, Tailwind, no default exports"
└── backend/
└── CLAUDE.md # "Node 20, Prisma ORM, always use transactions"
When Claude is editing a file in frontend/, it sees both the root instructions and the frontend-specific ones. The backend instructions are irrelevant and excluded.
Claude Code also supports an @import syntax inside CLAUDE.md files:
# Project Instructions
@./docs/architecture.md
@./docs/api-conventions.md
This is useful when you already maintain documentation in specific files and do not want to duplicate it. The referenced files are inlined at read time.
Comparing this to equivalent tooling: Cursor uses .cursorrules (a single flat file), GitHub Copilot uses .github/copilot-instructions.md, and Windsurf uses .windsurfrules. Claude Code’s hierarchical, multi-file approach is more flexible, though it requires understanding the loading order to use well.
The Permissions System
The permissions block in settings.json is a whitelist/blacklist for tool invocations. Rules follow the format ToolName(pattern) and support glob matching:
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git log *)",
"Bash(git diff *)",
"Read(**)",
"Write(src/**)",
"Write(tests/**)"
],
"deny": [
"Bash(curl *)",
"Bash(wget *)",
"Write(.env*)",
"Write(secrets/**)"
]
}
Deny rules take precedence over allow rules unconditionally. If you broadly allow Write(**) but have Write(.env*) in deny, Claude will be blocked from writing to env files regardless of the allow rule.
When Claude attempts something not covered by an allow rule, it prompts you interactively. If you approve and choose “always allow,” that rule gets written to settings.local.json rather than settings.json, keeping the team-shared file clean.
Hooks: The Most Underused Feature
Hooks are where the .claude/ directory becomes genuinely interesting. They are shell commands configured to run at specific lifecycle points: before a tool executes (PreToolUse), after a tool completes (PostToolUse), when Claude finishes a turn (Stop), and on notifications.
Each hook receives a JSON payload on stdin describing the tool call:
{
"session_id": "abc123",
"tool_name": "Write",
"tool_input": {
"file_path": "/project/src/index.ts",
"content": "..."
}
}
For PreToolUse hooks, the exit code controls behavior. Exit 0 means proceed normally. Exit 2 blocks the tool call entirely, and whatever the hook wrote to stdout is surfaced to Claude as the reason for the block. This is a real middleware pattern: you can intercept, inspect, and gate every tool call Claude makes.
Some configurations I have found genuinely useful:
Auto-format on file writes. Claude’s edits bypass your editor’s format-on-save. A PostToolUse hook fixes this:
"PostToolUse": [
{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
}]
}
]
Audit logging. Capturing a log of every tool call Claude makes is straightforward and surprisingly valuable for debugging sessions:
#!/bin/bash
# ~/hooks/audit.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin)['tool_name'])")
echo "$(date -Iseconds) | $CLAUDE_SESSION_ID | $TOOL" >> ~/claude-audit.log
Block dangerous patterns before they execute:
import json, sys
payload = json.load(sys.stdin)
if payload["tool_name"] == "Bash":
cmd = payload["tool_input"].get("command", "")
if "rm -rf" in cmd and "/" in cmd.split("rm -rf")[-1].strip()[:2]:
print("BLOCKED: Refusing recursive delete on root-level path")
sys.exit(2)
sys.exit(0)
The available environment variables inside hook scripts include CLAUDE_SESSION_ID, CLAUDE_PROJECT_DIR, and CLAUDE_FILE_PATH for file operations. Any variables you define in the env block of settings.json are also available.
MCP Servers in settings.json
The mcpServers block in settings.json is where the file starts to feel like a services manifest. MCP (Model Context Protocol) servers extend Claude Code with additional tools, and their configuration lives right alongside permissions and hooks:
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"POSTGRES_CONNECTION_STRING": "${DATABASE_URL}"
}
}
}
The ${DATABASE_URL} syntax resolves from your shell environment at startup. MCP server tools show up in Claude’s toolset as mcp__<server-name>__<tool-name>, and you can reference this naming in hook matchers and permission rules, so the same permission and auditing infrastructure covers both built-in tools and MCP tools.
For project-level MCP servers, the connection string or API token usually needs to come from environment variables rather than hardcoded values. settings.local.json is the right place for anything you cannot express as a ${VAR} interpolation.
Custom Slash Commands
Markdown files placed in .claude/commands/ become project-scoped slash commands. The filename determines the command name. A file at .claude/commands/review.md creates /project:review. Global commands in ~/.claude/commands/ are accessible as /user:<name> or simply /<name>.
Commands support a $ARGUMENTS placeholder for inline input and can embed !-prefixed lines that execute shell commands and inject their output:
# PR Review
Current branch status:
!git log --oneline -5
!git diff main...HEAD --stat
Review the above diff with focus on: $ARGUMENTS
Usage: /project:review error handling and edge cases
What This Directory Represents
There is a broader pattern here worth noticing. The .claude/ directory is one instance of a new category of project artifact: AI context infrastructure. Every AI coding tool has one. The contents of these directories, not just the model choice, increasingly determine how useful AI assistance is on a given codebase.
The interesting design choice Anthropic made with .claude/ is that it blurs the line between configuration and behavior. A hooks file is not just a preference; it is executable logic that runs in the AI’s tool execution path. A CLAUDE.md with well-structured architecture documentation is not just documentation; it actively shapes every code change Claude proposes. The directory has gravity.
For teams adopting Claude Code, treating .claude/settings.json and the project CLAUDE.md as first-class project artifacts, reviewed in PRs and updated as the codebase evolves, produces noticeably better results than leaving them as afterthoughts. The configuration is not a one-time setup; it is an ongoing description of how your project works and what safe operation looks like within it.