· 5 min read ·

How Claude Code's Hooks System Turns the .claude/ Folder Into a Policy Layer

Source: hackernews

Most developers using Claude Code treat the .claude/ folder as a place to drop CLAUDE.md and move on. The anatomy article that landed on Hacker News this week catalogs the full directory structure, and reading it alongside the official Claude Code documentation reveals that most of the folder’s leverage sits in two sections most users have never opened: the hooks configuration and the permissions model in settings.json.

The folder has three distinct layers that operate independently. The first, CLAUDE.md, has been covered extensively elsewhere. The second and third, behavioral policy and extensibility, do work that does not depend on prompt quality at all.

Configuration Layers and How They Compose

Claude Code reads from two settings.json files: a global one at ~/.claude/settings.json and a project-local one at .claude/settings.json in the repository root. Project-level values take precedence for keys that appear in both; everything else merges. This separation is worth using deliberately. Global settings carry personal preferences and API configuration; project settings encode the policy appropriate for that specific codebase.

CLAUDE.md follows the same composition. The global ~/.claude/CLAUDE.md loads in every session. The project-level ./CLAUDE.md appends on top. Subdirectory-level CLAUDE.md files load when the agent operates inside that subtree. The model receives a concatenation of all applicable layers at the top of every context window, before any conversation history.

The Permissions Model

The permissions key in settings.json constrains which tools the agent can invoke:

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm run *)",
      "Read",
      "Write",
      "Edit",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(curl *)",
      "Bash(wget *)"
    ]
  }
}

Bare tool names like Read match the entire tool. Bash(pattern) matches specific shell commands using glob syntax. Deny rules take priority over allow rules when both apply. An agent with these permissions can read and write files, invoke git commands, and run npm scripts, but cannot make outbound HTTP requests from the shell or recursively delete directories.

This is a behavioral guardrail for expected workflows, not a security boundary. A sufficiently determined agent could construct commands that slip past glob matching, and shell quoting introduces edge cases. For hard isolation, you need OS-level sandboxing. For constraining agent behavior during normal operation within a known workflow, the permissions model is straightforward and effective.

Hooks: Middleware for Tool Calls

Hooks are the most underexplored part of the folder. They define shell commands that Claude Code executes in response to tool lifecycle events. Four event types are available:

  • PreToolUse: fires before any tool call, with access to the tool name and its inputs
  • PostToolUse: fires after a tool completes, with access to its result
  • UserPromptSubmit: fires when the user submits a message
  • Stop: fires when the agent finishes a turn

Each hook entry specifies a matcher (a regex matched against the tool name) and a list of commands to execute. The commands run as subprocesses, with tool context injected as environment variables.

{
  "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 +%Y-%m-%dT%H:%M:%SZ) $CLAUDE_TOOL_INPUT_COMMAND\" >> ~/.claude/audit.log"
          }
        ]
      }
    ]
  }
}

The first hook runs Prettier on every file the agent writes or edits. Formatting happens as infrastructure; you do not need to include formatting instructions in CLAUDE.md or remind the agent in conversation. The second hook writes every shell command the agent executes to an audit log, which is useful when reviewing what a long autonomous session actually did or diagnosing why a build broke after an unattended run.

The PreToolUse hooks have one additional capability that makes them qualitatively different from the rest: if the hook process exits with code 2, Claude Code treats it as a block and does not execute the tool. This supports custom enforcement logic that cannot be expressed as a simple permission pattern:

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

The distinction between hooks and CLAUDE.md instructions matters here. Instructions in CLAUDE.md are context the model can reason around if it judges the situation to be an exception. Hooks fire regardless of what the model decides. A PreToolUse block that prevents direct pushes to main will fire whether or not the model thinks pushing to main is justified in this specific situation. That is a meaningfully different reliability guarantee.

Environment variables available inside hooks include CLAUDE_TOOL_NAME, tool-specific inputs prefixed with CLAUDE_TOOL_INPUT_, and for PostToolUse, a JSON-encoded CLAUDE_TOOL_RESULT. The exact variables available depend on the tool. For Write and Edit, $CLAUDE_TOOL_INPUT_FILE_PATH contains the target file path. For Bash, $CLAUDE_TOOL_INPUT_COMMAND contains the shell command. The Claude Code documentation lists the full set per tool.

MCP Servers

The mcpServers key extends the tool set the agent can access. Model Context Protocol is an open standard for exposing tools, resources, and prompts to LLM clients; Claude Code is a first-class MCP consumer.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/sandbox"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
      }
    }
  }
}

Each server runs as a subprocess communicating over stdio. The server advertises its tools and they become available to the agent alongside built-ins. An SSE transport option supports remote servers, which opens this to shared tooling across a team. Anthropic maintains a reference set of servers at github.com/modelcontextprotocol/servers, covering databases, browsers, GitHub, Slack, and several other systems. The organizational pattern that works well is global settings carrying infrastructure-level servers (database access, internal APIs) and project settings carrying project-specific tools.

Custom Slash Commands

The .claude/commands/ directory holds Markdown files where each filename becomes a slash command. A file named review.md becomes /review. The file content is the prompt template, with $ARGUMENTS as a placeholder for whatever the user passes after the command name.

# Review Pull Request

Fetch the diff for PR $ARGUMENTS using `gh pr diff $ARGUMENTS`, then:

1. Check for logic errors and edge cases
2. Identify any security issues  
3. Note any missing test coverage
4. Write a three-sentence summary

These commands version-control alongside the code they support. When a new team member clones the repository, they get the project’s standard review workflow, debugging runbook, and migration procedure automatically, without any additional setup step. The commands directory is effectively a shared prompt library that ships with the repository, rather than living in personal notes or a team wiki.

Treating the Folder as Infrastructure

The .claude/ folder is worth the same treatment as CI configuration: versioned alongside code, reviewed in pull requests, updated when workflows change, and owned by the team rather than maintained by whoever wrote the first version. A settings.json that enforces formatting on every write and blocks direct pushes to main does reliable, repeatable work that does not depend on prompt phrasing or model behavior. That category of reliability compounds across every session the team runs, and it is sitting in the folder most people have only half-configured.

Was this interesting?