DEV Community

Sasha Podlesniuk
Sasha Podlesniuk

Posted on

Claude Code: Using Hooks for Guaranteed Context Injection

When configuring Claude Code, most teams use CLAUDE.md or skills files to define coding standards. These work well for general guidance, but they share a limitation: the model decides when to read them.

This post explores Claude Code hooks—a mechanism that guarantees your critical rules are injected into context before every file operation.

The Context Loading Problem

Vercel's engineering team ran into this with their AI agent evaluations. In their research comparing skills vs embedded documentation, they found that skills-based retrieval was skipped in 56% of eval cases. The agent simply didn't invoke them.

Passive context (like CLAUDE.md) performed better because it's always present. But even passive context has limits—it's read once at session start, and the model still decides how much attention to give it during each task.

For rules that must be followed every time—security practices, architectural constraints—we need something stronger.

Claude Code Hooks: Guaranteed Execution

Hooks are shell commands that execute automatically at specific points in Claude Code's lifecycle. The key difference: hooks run on every matching tool call, regardless of what the model thinks is relevant.

Mechanism When It Loads Reliability
CLAUDE.md Session start Read once, attention varies
Skills When model invokes May be skipped by the model
Hooks Every matching tool call Guaranteed execution

The PreToolUse hook fires before Claude executes any tool—including Read, Edit and Write. This lets you inject rules directly into context at the moment they matter most.

User: "Add a new endpoint"
    ↓
Claude prepares Read tool call
    ↓
PreToolUse hook fires ← Guaranteed, every time
    ↓
Your rules injected into context
    ↓
Claude writes code with rules present
Enter fullscreen mode Exit fullscreen mode

Implementation: Package-Aware Rule Injection

Consider a monorepo where different packages have different coding standards. The API package requires Zod validation and parameterized SQL. The UI package mandates React Query over raw useEffect fetching.

Project Structure

project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── inject-rules.py
└── packages/
    ├── api/
    │   └── RULES.md
    └── ui/
        └── RULES.md
Enter fullscreen mode Exit fullscreen mode

Package Rules

packages/api/RULES.md

# API Package Rules

## Required Patterns
- All endpoints must have input validation using Zod schemas
- Wrap async handlers with error middleware
- Use `logger.info/error` instead of console.log
- All database queries must use parameterized statements

## Naming Conventions
- Controllers: `*.controller.ts`
- Routes: `*.routes.ts`
- Validators: `*.schema.ts`
Enter fullscreen mode Exit fullscreen mode

packages/ui/RULES.md

# UI Package Rules

## Required Patterns
- Use React Query for data fetching (no useEffect + fetch)
- Components must be functional with TypeScript props interface
- Always handle loading and error states

## Naming Conventions
- Components: PascalCase directories with index.tsx
- Hooks: `use*.ts`
- Styles: `*.module.css`
Enter fullscreen mode Exit fullscreen mode

Hook Implementation

The hook reads the file path from stdin, detects the package, and injects the corresponding rules.

.claude/hooks/inject-rules.py

#!/usr/bin/env python3
import sys, json
from pathlib import Path

input_data = json.load(sys.stdin)
file_path = input_data.get("tool_input", {}).get("file_path", "")

PACKAGE_RULES = {
    "/packages/api/": "packages/api/RULES.md",
    "/packages/ui/": "packages/ui/RULES.md",
}

for pkg_path, rules_path in PACKAGE_RULES.items():
    if pkg_path in file_path:
        package = pkg_path.strip("/").split("/")[-1]
        rules = Path(rules_path).read_text()
        print(json.dumps({
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "additionalContext": f"[{package}] rules:\n{rules}"
            }
        }))
        break
Enter fullscreen mode Exit fullscreen mode

Configuration

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "python3 .claude/hooks/inject-rules.py"
        }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The matcher uses regex—Read|Edit|Write captures both file editing and creation.

Results

With this configuration, requesting a new API endpoint produces code that follows all injected rules:

Request: "Add a createUser endpoint to packages/api/users.controller.ts"

Output:

import { z } from 'zod';
import { logger } from '../shared/logger';
import { asyncHandler } from '../middleware/errorHandler';

const createUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
});

export const createUser = asyncHandler(async (req, res) => {
  const { name, email } = createUserSchema.parse(req.body);
  logger.info('Creating user', { email });
  const result = await db.query(
    'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
    [name, email]
  );
  res.status(201).json(result.rows[0]);
});
Enter fullscreen mode Exit fullscreen mode
Rule Applied
Zod schema validation
asyncHandler wrapper
logger instead of console.log
Parameterized SQL query

While the same outcome can be achieved with skills, a growing skills library or codebase increases contextual noise and raises the risk that the model overlooks the correct skill.

Advanced Patterns

Compress Rules for Context Efficiency

Every hook injection adds to the context window. As context length increases, LLM performance can drop—a phenomenon called lost in the middle. Compression isn't just about cost; it's about effectiveness.

Borrowing techniques from prompt compression research, here's how to minimize your rules footprint:

Technique 1: Remove Filler Words

Strip articles (a, the), hedging words (should, please, always), and redundant phrases.

Before After
"All endpoints must have input validation" "endpoints: validate input"
"You should always use parameterized queries" "use parameterized queries"
"Please make sure to wrap handlers" "wrap handlers"

Technique 2: Use Symbols and Shorthand

Replace words with universally understood symbols—similar to how minified JS uses short variable names.

Pattern Meaning
leads to, use, becomes
!= not, avoid, don't use
required, do this
forbidden, never
* wildcard (any)
$1, $2 parameters

Technique 3: Code-Like Syntax

LLMs parse structured formats efficiently. Use patterns they recognize from code:

# BEFORE
## API Package Rules

### Required Patterns
- All endpoints must have input validation using Zod schemas
- Always wrap async handlers with error middleware
- Use `logger.info/error` instead of console.log for all logging
- All database queries must use parameterized statements to prevent SQL injection

### Naming Conventions
- Controllers should be named: `*.controller.ts`
- Routes should be named: `*.routes.ts`
- Validators should be named: `*.schema.ts`
Enter fullscreen mode Exit fullscreen mode
# AFTER
[api] validate:zod | wrap:asyncHandler | log:logger.* !=console.* | sql:parameterized($1,$2)
naming: *.controller.ts, *.routes.ts, *.schema.ts
Enter fullscreen mode Exit fullscreen mode

Technique 4: Relevance Filtering

Not every rule applies to every file. Filter dynamically in your hook:

# Only inject SQL rules for database-related files
if "repository" in file_path or "query" in file_path:
    rules += "\nsql: parameterized queries only"
Enter fullscreen mode Exit fullscreen mode

The compressed format remains fully interpretable by Claude while using a fraction of the context space.

Preserve Through Compaction

Use PreCompact to ensure critical rules survive context compaction:

{
  "PreCompact": [{
    "hooks": [{ "type": "command", "command": "python3 .claude/hooks/inject-critical-rules.py" }]
  }]
}
Enter fullscreen mode Exit fullscreen mode

Debugging

Run claude --debug to see hook execution:

[DEBUG] Executing hooks for PreToolUse:Write
[DEBUG] Hook command completed with status 0: {"hookSpecificOutput":...}
Enter fullscreen mode Exit fullscreen mode

Try It Yourself

  1. Create .claude/settings.json with the PreToolUse configuration
  2. Add .claude/hooks/inject-rules.py with your package detection logic
  3. Create RULES.md files in each package directory
  4. Restart Claude Code to load the new hooks
  5. Edit a file and observe the injected context with claude --debug

Top comments (0)