· 6 min read ·

Context Files Are Where AI Coding Tools Converge. Hooks and Permissions Are Where They Diverge.

Source: hackernews

Every significant AI coding tool has converged on some form of per-project context file. Drop a file in the repository root, fill it with project conventions, and the tool picks it up automatically. The format varies and the names differ, but the basic mechanism is consistent across Cursor, GitHub Copilot, Windsurf, and Claude Code alike.

What this convergence conceals is a more meaningful divergence in what these files can do, and why that divergence exists. The recent breakdown of Claude Code’s .claude/ folder is getting passed around because most developers know about CLAUDE.md but have not explored the rest of the directory. Comparing what .claude/ contains to the equivalent configuration in other tools makes the design rationale considerably clearer.

The Flat File Baseline

The simplest version of per-project AI configuration is a single markdown file at the repository root. GitHub Copilot uses .github/copilot-instructions.md. Windsurf uses .windsurfrules. Early Cursor versions used .cursorrules. Each file does the same thing: its contents are prepended to the model’s context when generating suggestions, giving you a place to encode project conventions the model cannot infer from reading individual files.

This is a useful primitive. You can specify technology choices, coding style, which libraries to prefer, which patterns to avoid. The file is simple enough that any developer can understand and contribute to it without knowing anything about the underlying tool.

The limitation is equally clear. These files can only inform the model. There is no way to attach executable behavior to the instructions, no way to constrain which file paths the model can modify, no way to separate team-shared configuration from personal preferences. The file is read, its contents influence the model’s outputs, and that is the extent of the integration.

Cursor moved beyond the single-file model with the .cursor/rules/ directory. Instead of a monolithic .cursorrules file, you create individual .mdc files inside that directory, each with metadata specifying which file patterns it applies to:

---
description: API route handler conventions
globs: ["src/api/**/*.ts"]
alwaysApply: false
---

API route handlers should always validate request body against a Zod schema before processing...

This is a meaningful improvement over a flat file. Rules are scoped to the files they are relevant to, which reduces noise in the model’s context and makes large rule sets manageable. The mechanism is still context injection: rules inform, they do not enforce.

What Claude Code Adds

The .claude/ directory includes CLAUDE.md for context injection, and it follows a similar design to Cursor’s rule files: hierarchical loading from global to project to subdirectory, with the relevant layers concatenated at session start. An @import syntax allows referencing external documentation files without duplicating their content:

# Project Context

@./docs/architecture.md
@./docs/api-conventions.md

## Additional constraints
Always run the test suite after modifying core services.

That part is evolutionary. What makes the directory different from every other tool’s configuration is the settings.json content that goes beyond context.

The permissions model constrains which tool invocations the agent can make without prompting:

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm run *)",
      "Read(**)",
      "Write(src/**)"
    ],
    "deny": [
      "Bash(curl *)",
      "Bash(wget *)",
      "Write(.env*)"
    ]
  }
}

This configuration blocks Claude from making outbound HTTP requests via the shell, prevents writes to environment files, and limits file writes to the src/ directory. These constraints are enforced by the tool, not expressed as suggestions to the model. Deny rules take priority over allow rules unconditionally. When Claude attempts something outside the allow list, it prompts you interactively, and any approval you grant gets written to settings.local.json, which is automatically gitignored.

The hooks system takes this further. Shell commands configured under PreToolUse, PostToolUse, Stop, and UserPromptSubmit fire at the corresponding lifecycle points, receiving tool context on stdin:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
      }]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "echo \"$(date -u) | $CLAUDE_TOOL_INPUT_COMMAND\" >> ~/.claude/audit.log"
      }]
    }]
  }
}

A PostToolUse hook running Prettier on every file write guarantees formatting regardless of whether the model followed the formatting instruction in CLAUDE.md. A PreToolUse hook that exits with code 2 blocks the tool call unconditionally, not subject to the model’s judgment about whether the situation warrants an exception.

Finally, settings.local.json is automatically gitignored and holds machine-specific or personal configuration that should not land in version control alongside team-shared settings. This is the same pattern as .env.local in Next.js projects: shared policy goes in the committed file, personal overrides stay out of the repo.

Why the Extra Machinery Exists

None of the simpler tools have anything like hooks, a permissions model, or a local-versus-shared settings split. The reason is visible when you consider the operational context each tool was designed for.

Cursor, Copilot, and Windsurf are primarily completion and suggestion tools. They propose changes; the developer accepts or rejects them. In that model, the configuration needs to do one thing: inform the model’s suggestions. The developer is the execution layer. A context injection file is sufficient because no irreversible action takes place without human review at every step.

Claude Code is designed around autonomous action. It runs bash commands, writes files, creates commits, and can complete multi-step tasks across dozens of tool invocations before presenting any output. When the tool is the execution layer, the design requirements change. Context injection alone is not sufficient when the tool can overwrite environment variables or push to a remote branch before you can intervene.

The permissions model answers the question: what should the agent be allowed to do in this project without prompting? The hooks system answers: what should always happen at each lifecycle point, regardless of what the model decided? The settings.local.json split answers: how do you separate policy appropriate for the project from preferences appropriate for the person?

These questions only arise in the agentic context. Completion tools do not encounter them.

The Reliability Gap Between Context and Enforcement

There is a subtler reason the extra machinery matters, beyond the obvious point that autonomous tools need guardrails.

Instructions in any context file, whether CLAUDE.md, .cursorrules, or .github/copilot-instructions.md, are suggestions to a probabilistic system. Models have measurably reduced recall for content that drifts toward the middle of a long context window as conversation history accumulates. An instruction the model follows reliably in a short session may be inconsistently applied at turn 40 of a long autonomous task. This is the nature of language models, not a specific implementation flaw.

Hooks and permissions provide a different reliability guarantee. They run as external processes managed by the tool itself. A PreToolUse hook that checks for direct pushes to main does not depend on the model having attended to the corresponding instruction in CLAUDE.md. It runs on every bash invocation, period:

#!/bin/bash
if echo "$CLAUDE_TOOL_INPUT_COMMAND" | grep -qE 'git push.*(origin main|origin master)'; then
  echo "Direct push to main is blocked. Create a PR instead."
  exit 2
fi

A team that encodes only the former has probabilistic compliance. A team that encodes both has probabilistic compliance for everything expressed in natural language, and hard enforcement for the constraints where exceptions would cause real problems.

The distinction matters more as the autonomy level of the sessions increases. For interactive work where a developer is reviewing every suggestion before it applies, the model’s context adherence is secondary because the developer catches deviations. For extended autonomous runs on tasks like dependency upgrades, migration scripts, or test suite generation, the model’s consistency over a long session is the reliability guarantee, and it is a weaker one than most people realize.

Choosing What Belongs Where

For developers running Claude Code, the practical implication is that the .claude/ folder has two distinct functions that should be filled deliberately rather than by default.

CLAUDE.md is the right place for project conventions, architectural decisions, module structure, tooling preferences, and anything where the model applying judgment is appropriate and sometimes useful. These are the instructions where you want the model to understand the intent, not just the rule.

Hooks and the permissions model are the right place for constraints that should hold regardless of context, things where an exception would cause real harm: never write to production configuration files, always format before committing, block certain destructive shell patterns. These belong in enforcement infrastructure, not in a context file.

The tools that only offer context injection are not wrong to do so. They are designed for a mode of use where that is sufficient. Claude Code’s additional machinery exists because it is doing something architecturally different, and the anatomy of its configuration directory reflects that difference directly.

Was this interesting?