The .claude/ Folder Has Two Control Mechanisms, and Most Teams Only Use One
Source: hackernews
Every project that has lasted more than a few months accumulates a set of files that tell tools how to behave. The lineage is long. Makefiles encoded build procedures in the 1970s so any developer with make could run them without reading documentation. .editorconfig in 2011 gave editors a standard way to read per-project formatting preferences — tab width, line endings, trailing whitespace — without each developer manually configuring their own environment. .eslintrc followed, encoding code quality rules the linter would enforce automatically. .github/workflows/ brought the same principle to CI: the pipeline configuration lives in the repository, so every clone includes the team’s build and deployment procedures.
The .claude/ folder fits into this pattern. The anatomy breakdown that surfaced on Hacker News this week catalogs its contents: the CLAUDE.md file for project instructions, settings.json for permissions and hooks, and a commands/ directory for custom slash commands. What the catalog view misses is what makes the folder different from every prior per-project config format — and understanding that difference determines whether teams get consistent behavior or just occasional compliance.
What Every Prior Config File Has in Common
The defining property of every per-project config format before CLAUDE.md is that it is consumed by a deterministic process. The linter either flags the function that violates the rule or it does not. The editor either indents with tabs because .editorconfig says indent_style = tab or it does not. The CI runner either executes the workflow you defined or it does not. The config specifies behavior and the tool implements it without judgment.
This is what makes per-project config files useful as team coordination tools. You do not rely on everyone remembering to run the formatter; you run it in CI. You do not rely on developers knowing which patterns are approved; you encode that in a linter rule that runs automatically. The config has force because the tool that reads it does not exercise judgment; it implements the rule.
CLAUDE.md Introduces Something That Cannot Be Parsed
CLAUDE.md breaks this pattern. It is read by a language model, not a parser, and the model can reason about what it contains. You can write instructions like “when modifying the authentication middleware, check with the team first — we had a production incident in January 2025 related to session token handling” and Claude will understand the instruction, its rationale, and how to apply it to new situations. No linter rule can represent contextual, historical knowledge of that kind.
The tradeoff is that the guarantee is probabilistic rather than categorical. The model follows the instructions because following instructions is what well-trained models do when they read them, not because anything prevents deviation. Research on context position bias from Stanford and UC Berkeley demonstrates that models have measurably worse recall for content positioned in the middle of a long context window. As a session grows, instructions in CLAUDE.md drift toward the middle of the accumulated context, and their effective weight decreases. A rule the model follows reliably in a 10-turn conversation may be inconsistently applied at turn 40 of a long autonomous task.
This is not a criticism of the format. It describes what natural language instructions fundamentally are. A style guide for human developers has the same property: people follow it in most situations, with judgment applied at the margins. You do not use a style guide to enforce security invariants. For security invariants, you use a linter or a CI gate. The question is whether you apply the same distinction when writing CLAUDE.md.
Hooks Recover the Determinism
The hooks configuration in settings.json is where the .claude/ folder recovers the reliability guarantee that CLAUDE.md cannot provide. Hooks are shell commands configured to fire at agent lifecycle points: PreToolUse before any tool call, PostToolUse after a tool completes, UserPromptSubmit when you send a message, and Stop at the end of a turn.
Each hook receives a JSON payload on stdin describing the tool call:
{
"session_id": "abc123",
"tool_name": "Bash",
"tool_input": {
"command": "git push origin main"
}
}
For PreToolUse, the exit code controls what happens next. Exit 0 and the tool runs normally. Exit 2 and the tool is blocked entirely, with whatever the hook wrote to stdout surfaced to Claude as the reason.
#!/bin/bash
if echo "$CLAUDE_TOOL_INPUT_COMMAND" | grep -qE 'git push.*(origin main|origin master)'; then
echo "Direct push to main is blocked. Open a pull request."
exit 2
fi
This fires regardless of what the model decides in a given session. The distinction from a CLAUDE.md instruction maps directly to the distinction between a code review comment and a CI gate: both encode the same rule, but only one prevents the violation. You can write “never push directly to main” in your project CLAUDE.md. The model will follow that instruction the majority of the time. The PreToolUse hook above is a categorical guarantee.
PostToolUse hooks are different in kind because the tool has already executed. They are suited to work that should follow every file write: running a formatter, injecting lint feedback back into the agent’s context, updating a changelog. The hook’s stdout is injected into the model’s context before the next step, giving Claude feedback it can act on:
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}]
}
]
With this configuration, formatting happens as infrastructure. The model does not need to remember to format its output, and you do not need to instruct it in CLAUDE.md.
The settings.json / settings.local.json Split
The permissions and hook configuration lives in two files: settings.json, committed to git and shared across the team, and settings.local.json, auto-gitignored and holding personal overrides. The pattern mirrors .env.example in the repository paired with a developer’s local .env — the example describes the expected shape, the local file holds values that cannot be committed.
When Claude prompts you during a session to approve a tool action and you select “always allow,” the rule is written to settings.local.json rather than settings.json. Your personal trust decisions do not modify the team’s shared policy, and you cannot accidentally commit personal allowances into the repository alongside the team configuration.
Commands as a Version-Controlled Prompt Library
Teams that use Claude Code intensively develop accumulated knowledge about how to prompt it effectively for their specific codebase. Which commands to run before a review. How to frame a debugging task. What context to provide when asking for help with a migration. This knowledge usually lives in personal notes, Slack messages, or wiki pages — the same places deployment runbooks lived before teams wrote them as Makefile targets or CI pipeline steps.
The .claude/commands/ directory version-controls this knowledge alongside the code it supports. Markdown files placed here become slash commands: commands/review.md becomes /project:review, commands/debug.md becomes /project:debug. The file content is a prompt template, with $ARGUMENTS substituted from user input. Lines prefixed with ! execute shell commands at invocation time and inject their output:
# Review Pull Request
Current branch:
!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
New team members who clone the repository get the project’s standard review workflow, debugging runbook, and migration procedure as slash commands, without any additional setup. The accumulated prompt engineering knowledge travels with the codebase, gets reviewed in pull requests, and improves as the team’s understanding evolves — the same arc that Makefiles followed when teams moved from informal runbooks to codified build procedures.
What Actually Belongs in CLAUDE.md
Given that hooks provide deterministic enforcement, the question of what belongs in CLAUDE.md sharpens. The Claude Code documentation recommends keeping the file under roughly 2,000 tokens, because every token competes for context budget with source files and conversation history. This constraint forces prioritization.
CLAUDE.md is the right home for conventions that benefit from judgment: architectural decisions with their rationale, preferred libraries when multiple options exist, non-obvious patterns the model should recognize, and constraints that require understanding rather than pattern matching. Generic advice that the model follows correctly from training belongs nowhere in the file; team-specific decisions that diverge from defaults belong in CLAUDE.md; categorical invariants that cannot have exceptions belong in hooks.
The @import syntax in CLAUDE.md helps when relevant documentation already exists in other files:
@./docs/architecture.md
@./docs/api-conventions.md
Referenced files are inlined at read time. An architecture document that is actively maintained does not need to be duplicated into CLAUDE.md; it can be imported directly, which keeps the two in sync as the architecture evolves.
The Folder as a Living Artifact
What the .claude/ folder represents across all its components is the same thing every prior per-project config format represented: the team’s accumulated knowledge about how to work with this codebase, encoded in files that live alongside the code and apply uniformly to everyone who works in it. The novel part is the split between two mechanisms with different reliability properties in the same directory.
Some project knowledge benefits from the model’s judgment; some requires categorical enforcement. The folder holds both, and the configuration work that actually improves agent reliability is deciding which constraints belong in which layer — not adding more instructions to CLAUDE.md and hoping the model weighs them correctly at step 35 of an unattended task.