The .claude/ Folder Is a Configuration System, Not Just a Config File
Source: hackernews
A recent post on Daily Dose of DS broke down the contents of Claude Code’s .claude/ folder, and it got significant traction on Hacker News. The breakdown is useful as a map, but what the article doesn’t get into is why the folder is structured the way it is, and what you can do once you understand the design rather than just the file list.
I’ve been living inside Claude Code for months, using it to build and iterate on Ralph, a Discord bot. The .claude/ folder has become a meaningful part of that workflow, so here’s a more opinionated read on what it contains and what it enables.
Three Tiers, One System
The first thing worth understanding is that .claude/ isn’t a single config location. It’s three overlapping layers that compose at runtime.
Global: ~/.claude/ on your machine. Settings here apply to every Claude Code session across every project. Your global CLAUDE.md, your global custom commands, your API key, your model preference.
Project: .claude/ (or CLAUDE.md) at your project root. Checked into version control. Shared with everyone on the team.
Local: .claude/settings.local.json. Not checked in. Your personal overrides for a specific project, things like a different model for this repo, or an API key override.
The layering is conventional config design, but it matters a lot for teams using Claude Code. Your project’s .claude/settings.json can define permitted tool patterns for CI or review purposes, and individual developers can layer personal overrides on top without touching the shared file. The permission allow/deny system stacks across all three layers.
CLAUDE.md Is a Spec That Runs
Most documentation about CLAUDE.md treats it as “instructions for Claude.” That framing is accurate but undersells it. A well-written CLAUDE.md functions as a living spec: it encodes architectural decisions, naming conventions, testing requirements, and deployment notes in a format that gets injected directly into Claude’s context at session start.
The file supports import syntax for composition:
@docs/architecture.md
@.claude/commands/deploy.md
This means you can keep CLAUDE.md as a thin index that pulls in detailed specs from wherever they actually live in your repo. If your architecture doc already exists in docs/, you don’t duplicate it. Claude reads it from source.
For a project like Ralph, CLAUDE.md carries things like: which Discord.js patterns are in use, how the command handler is structured, what the data directory layout looks like, and what the restart/reload lifecycle expects. Without it, every session starts from zero. With it, Claude begins with the same context a senior contributor would have.
The global ~/.claude/CLAUDE.md is worth setting up too. Mine holds things that apply everywhere: my preferred code style, the languages I work in most, how I like errors explained, and the reminder that I care about reversible operations before destructive ones.
settings.json and the Permission Model
The settings.json structure is where Claude Code’s security model lives. The permissions block accepts allow and deny arrays of tool patterns:
{
"permissions": {
"allow": [
"Bash(git *)",
"Bash(npm test)",
"Read",
"Edit"
],
"deny": [
"Bash(rm -rf *)",
"Bash(curl *)"
]
}
}
Patterns use glob-style matching against the tool name and its arguments. A project .claude/settings.json checked into a repo communicates to everyone what Claude is and isn’t permitted to do automatically, without a confirmation prompt. Anything not in allow still gets asked about; the deny list blocks outright.
This is a meaningful capability boundary, particularly for repos where other people will run Claude Code. You’re not just trusting the model; you’re expressing in code what operations are in scope.
The env block in settings lets you set environment variables for the session, which is useful for toggling feature flags or pointing at a local API without modifying shell config:
{
"env": {
"NODE_ENV": "development",
"BOT_DATA_DIR": "/tmp/ralph-dev"
}
}
Hooks: The Reactive Layer
Hooks are the most underused feature in .claude/. They let you attach shell commands to lifecycle events in Claude’s tool execution loop. The events are PreToolUse, PostToolUse, Notification, Stop, and SubagentStop.
A hook definition looks like this:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"[audit] Bash called\" >> ~/.claude/audit.log"
}
]
}
]
}
}
A PreToolUse hook that exits with a non-zero code will block the tool call entirely. That’s a programmable safety layer. You can write a hook that checks whether a Bash command touches certain paths and refuses it before Claude ever runs it.
PostToolUse hooks fire after a tool completes and receive the tool result. This is useful for things like: automatically running a linter after any file edit, logging every command that Claude executes to an audit trail, or triggering a rebuild after source files change.
Stop hooks fire when the main agent’s turn ends, which makes them useful for notifications: send a desktop alert or a Discord message (relevant for my setup) when a long-running task completes.
Hook processes receive a JSON payload over stdin describing the event, the tool name, the arguments, and the result where applicable. Writing a useful hook is a small shell or Python script. The interface is minimal on purpose.
Custom Commands as Vocabulary
The .claude/commands/ directory is where you define custom slash commands. Each .md file in that directory becomes a /command-name shortcut in the CLI. A file at .claude/commands/review.md becomes /review.
The file content is a prompt template. You can use $ARGUMENTS to pass arguments from the command invocation:
Review the following code for security issues and suggest fixes:
$ARGUMENTS
Global commands live in ~/.claude/commands/. Project commands live in the project’s .claude/commands/.
What this enables is a command vocabulary specific to your project. Rather than typing the same context-heavy prompt repeatedly, you encode it once. For Ralph’s repo, a /selfcheck command runs a sequence of build verification and health checks without me having to describe what that means each time.
The command files also support a --- separator at the top for a description string, which shows up in the /help listing. The ergonomics here are simple, but the composability is high. Commands can reference paths in your project, import context with @, and be shared with everyone who clones the repo.
The Memory Directory
The ~/.claude/projects/ directory stores per-project memory files. These are written by Claude during sessions when you ask it to remember something, and read back at the start of subsequent sessions. The structure is JSON files keyed by a hash of the project path.
This is separate from CLAUDE.md. CLAUDE.md is static, human-written spec. The memory directory is dynamic, Claude-written state. Both get injected into context, but they serve different purposes. One is architecture documentation; the other is accumulated session learning.
The Design Philosophy
Looking at all of this together, the .claude/ folder is not a settings file. It’s a composable configuration system that covers three distinct concerns: context injection (CLAUDE.md), capability boundaries (settings.json permissions), reactive behavior (hooks), and command vocabulary (.claude/commands/). The three-tier hierarchy means the system works for solo developers, for teams, and for per-machine overrides without any of those concerns stepping on each other.
The original article is worth reading as a reference for what files exist and where. But the more interesting question is how these pieces compose. A project that uses all four subsystems has effectively defined a programmable development environment, not just a chat interface. For the kind of iterative, context-heavy work that building a bot involves, that distinction matters considerably.