Hooks, Commands, and Memory: Claude Code's .claude/ Folder Beyond the Basics
Source: hackernews
Most developers who use Claude Code discover CLAUDE.md within the first hour. You drop it in the project root, write some context about the codebase, and Claude picks it up. That part is well-documented and intuitive. What gets less attention is that CLAUDE.md is only one file in a larger configuration system spanning permissions, custom commands, hooks, MCP server definitions, and persistent memory. The Daily Dose of DS article that surfaced on Hacker News provides a useful map of what’s in .claude/. This post goes deeper into how the pieces interact and where the design choices matter.
The Two-Level Layout
The first thing worth understanding is that .claude/ exists at two distinct levels. The global config lives at ~/.claude/ and applies to every project. The project-level config lives at .claude/ in your repository root. Settings at the project level extend or override global ones depending on the specific field.
CLAUDE.md itself does not live inside .claude/. The project-level version goes at the repository root; the global version lives at ~/.claude/CLAUDE.md. When Claude Code starts a session, it reads both and concatenates them into the system prompt context. Subdirectory-level CLAUDE.md files are also picked up when Claude is actively working within that subdirectory, giving you a way to document specific subsystems in place rather than cramming everything into a root-level file.
This hierarchy is useful in practice. Global CLAUDE.md is the right place for preferences that should not need repeating: your preferred code style, tools you always use, things you never want generated without asking. Project-level CLAUDE.md handles the specifics: architecture decisions, module conventions, known gotchas. They compose rather than compete.
settings.json and the Permission Model
The most operationally significant file is .claude/settings.json. Its permission section controls which tool invocations require interactive approval and which are silently allowed:
{
"permissions": {
"allow": [
"Bash(npm run test:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Read(**)",
"Edit(src/**)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force*)"
]
}
}
Bash() wraps shell command patterns. File operation tools like Read(), Write(), and Edit() take glob patterns. The deny list takes precedence over the allow list when both match the same tool call. By default, Claude Code prompts you for approval on every tool use; entries in allow suppress those prompts for matched patterns.
There are two settings files: settings.json for shared project config (committed to version control) and settings.local.json for personal overrides (gitignored by default). This split maps cleanly onto the shared vs. personal config problem that tools like git and editors have handled for years. Your team agrees on a baseline set of allowed commands in settings.json; individuals add their own trusted patterns in settings.local.json without touching shared state.
Custom Commands
Any Markdown file placed in .claude/commands/ becomes a slash command accessible in Claude Code sessions. Create .claude/commands/pr-review.md and you get /pr-review. The file content is the prompt Claude receives when you invoke the command:
Review the staged changes in this PR. Look for:
- Breaking changes to public API contracts
- Missing test coverage for new code paths
- Input validation gaps or injection risks
List findings with severity: critical, warning, or informational.
Commands support $ARGUMENTS substitution, so /pr-review auth-service passes “auth-service” into wherever $ARGUMENTS appears in the template. The global counterpart at ~/.claude/commands/ makes commands available across all projects.
This is a lightweight macro system for teams that want to enforce consistent review criteria, generate boilerplate in a documented way, or give less-experienced contributors a library of tested prompt patterns without requiring them to understand prompt engineering from first principles.
Hooks: Claude Code as a Programmable Runtime
Hooks are where the configuration system gets genuinely powerful. Defined in settings.json under a hooks key, they fire in response to events in Claude Code’s execution lifecycle:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) $CLAUDE_TOOL_INPUT\" >> ~/.claude/audit.log"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --check $CLAUDE_TOOL_OUTPUT_PATH 2>&1 || exit 1"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code session ended'"
}
]
}
]
}
}
The four event types are PreToolUse, PostToolUse, Notification, and Stop. The matcher field filters to specific tool names; omit it to match all tool invocations. Context flows through environment variables: CLAUDE_TOOL_INPUT carries the tool call as JSON before execution, CLAUDE_TOOL_OUTPUT carries the result afterward.
The veto mechanism is the important design detail here: if a PreToolUse hook exits with a non-zero code, Claude Code treats it as a rejection of that tool call. This lets you build enforcement logic that goes beyond the static pattern matching in permissions. A hook script can inspect the full tool input, call external services, check against a policy database, or run arbitrary validation before deciding whether to allow the operation.
Practical uses include audit logging for agentic sessions, pre-flight linting on file writes, integration with external approval workflows for sensitive operations, and triggering notifications when long-running tasks complete. If you are running Claude in any kind of autonomous or long-running loop, the ability to inject validation steps at tool boundaries becomes load-bearing. Hooks are the mechanism that makes Claude Code behave like a supervised runtime rather than an unconstrained shell.
MCP Server Configuration
Model Context Protocol server definitions can live in .claude/mcp.json at the project level, specifying additional tool providers Claude has access to in that project context:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
},
"sqlite": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite", "./data/dev.db"]
}
}
}
Claude Code spawns MCP servers as child processes and communicates over stdio or HTTP. Each server can expose tools, resources, and prompt templates that become available during the session. The ${VARIABLE} interpolation syntax keeps secrets out of committed config while making the server definition self-contained and shareable across a team.
The project-level placement is deliberate. Committing .claude/mcp.json to the repository means the full tool set is available to anyone who clones it, without requiring per-machine setup beyond the right tokens in environment variables. The server configuration is part of the project’s development environment, not a personal preference.
Memory
The ~/.claude/projects/ directory holds per-project persistent state, keyed by a hash of the project path. Within each project directory, a memory/ folder stores Markdown files that can be loaded as context across sessions:
---
name: database-notes
description: Notes about schema state and pending migrations
type: project
---
The users table uses a soft-delete pattern via deleted_at timestamp.
The payments table migrated to UUID primary keys in v2.1...
A MEMORY.md index file in the same directory is loaded at the start of every session. Individual memory files are loaded on demand based on their descriptions. This two-level structure is a practical response to context window limits: you can accumulate detailed notes without burning token budget loading all of them on every session start.
The memory system is most useful for things that change slowly and matter consistently, like architectural decisions, known technical debt, the rationale behind non-obvious patterns. It is less useful for ephemeral task state, which belongs in the conversation itself.
What the Design Reflects
The consistent pattern across all of these files is additive composition rather than replacement. Project config extends global config. Project CLAUDE.md adds to global CLAUDE.md. settings.local.json overrides settings.json for personal preferences without touching shared state. MCP servers and commands at the project level add to whatever you have configured globally.
The original article is a solid reference for seeing these files laid out in one place; the Hacker News thread has useful examples of workflows people have built around them. What is underrepresented in most introductions to this system is the hooks section. Permissions and CLAUDE.md are discoverable early through documentation and example repos. Hooks require thinking about Claude Code as a programmable event-driven runtime, which takes longer to arrive at as a mental model.
Once you are past interactive use and running Claude in longer agentic workflows, the full shape of this configuration surface starts to matter. The folder is small but it covers a lot of ground, and understanding the pieces before you need them saves building workarounds for problems the built-in mechanisms already solve.