All Articles Claude Code

Context Injection on User Prompt Submit with Hooks

You're working on a 50,000-line codebase. You ask Claude Code a question about refactoring a performance bottleneck. But Claude doesn't know your project's style guide.

You’re working on a 50,000-line codebase. You ask Claude Code a question about refactoring a performance bottleneck. But Claude doesn’t know your project’s style guide. It doesn’t see your recent git history. It doesn’t understand the naming conventions everyone on your team follows. It doesn’t know that your team standardized on async/await three commits ago, or that you’ve been experimenting with a specific composition pattern across three components. So it gives you advice that contradicts your established patterns, your recent decisions, your team’s velocity.

This happens because Claude Code, by default, only sees what you explicitly show it. Your prompts arrive bare—context-naked, if you will. The system doesn’t automatically enrich them with the knowledge Claude needs to make smarter, team-aligned decisions.

But here’s the thing: you can fix this. Claude Code’s hook system lets you intercept prompts before they reach the AI and inject custom context automatically. No manual copying. No “here’s my style guide again” dance. No context window wasted on repetitive explanations. Just raw, systematic context enrichment that runs silently in the background, every single time.

In this article, we’re diving deep into how to build a PreToolUse hook that augments every user prompt with project conventions, recent changes, architectural decisions, and style guidance. We’ll walk through exactly why this matters, how it works under the hood, and—crucially—how to do it without bloating your context window into uselessness.

The Problem with Naked Prompts

Let’s think concretely about what a bare prompt looks like in your daily work. You’re refactoring a React component for performance and you ask Claude:

"I need to refactor this component for performance. What should I do?"

Claude sees:

  • Your component code (if you paste it)
  • Your question (definitely)
  • Nothing else

What Claude doesn’t see:

  • Your project’s performance benchmarking standards
  • Whether your team prefers memoization or callback optimization
  • Your code style (tabs vs spaces, naming conventions, JSDoc comment style)
  • Recent performance improvements you’ve already made in other components
  • Git history showing patterns the team has adopted in the last two weeks
  • Your architecture decisions (hooks vs class components, Redux vs context, controlled vs uncontrolled inputs)
  • The naming conventions for variables (camelCase vs snake_case, prefixes like is for booleans, handle for event handlers)
  • Whether your team values brevity or explicitness in variable names
  • Your testing strategy (unit tests, integration tests, what to test, what to skip)
  • How your team handles CSS-in-JS, styling libraries, or classname concatenation
  • Database interaction patterns or query optimization practices

So Claude guesses. It makes generic recommendations. The advice might be technically correct, but it won’t be your kind of correct. It won’t fit your project’s DNA. It won’t align with decisions your team made last month. It might even contradict a pattern you standardized on.

The same problem happens with any question: code review feedback, architecture decisions, bug fixes, refactoring suggestions, testing strategy recommendations, naming decisions, API design choices. Without context about your project’s conventions, recent work, and team patterns, Claude operates at maybe 60% effectiveness for your specific situation. With context? 90%+.

Context injection hooks close this gap by automatically enriching every prompt with the information Claude needs to give you advice that actually fits your project—your patterns, your team’s decisions, your velocity, your constraints.

How PreToolUse Hooks Work

Before we build our context injection hook, let’s understand the mechanics. Claude Code’s PreToolUse hook fires right before Claude uses any tool—and submitting a prompt to the API is technically a tool use. This is where you intercept and modify the request before it leaves your machine.

Here’s the basic structure:

// .claude/hooks/pre-tool-use.js
module.exports = async (hookContext) => {
  const { tool, toolInput, filePath, projectRoot } = hookContext;

  // tool: which tool is being used? (e.g., "submitPrompt", "readFile", "bash")
  // toolInput: what are the arguments?
  // filePath: what file are we currently editing?
  // projectRoot: where's the project root?

  // Your logic here can read files, run commands, and modify the request

  return {
    toolInput: modifiedInput,
    allowExecution: true, // or false to block the tool
    logMessage: "Context injected: style guide + recent commits",
  };
};

The key insight: you have full access to the incoming request before Claude sees it. You can read files from disk, run bash commands, parse git history, examine your project structure, and inject all of that context into the prompt automatically. The user never has to ask for it. It just happens. Transparently. Every time.

Building a Context Injection Hook

Let’s build a production-grade hook that injects three pieces of context: your project’s style guide, recent git commits, and file-type-specific conventions. This is the real thing—the code you’d actually deploy.

// .claude/hooks/pre-tool-use-context-injection.js

const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const { promisify } = require("util");

const execPromise = promisify(exec);

module.exports = async (hookContext) => {
  const { tool, toolInput, projectRoot } = hookContext;

  // Only inject on prompt submissions (not on every tool call)
  if (tool !== "submitPrompt" || !toolInput.prompt) {
    return { toolInput, allowExecution: true };
  }

  try {
    // Gather context pieces in parallel for speed
    const [styleGuide, recentCommits, conventions] = await Promise.all([
      readStyleGuide(projectRoot),
      getRecentGitHistory(projectRoot),
      readProjectConventions(projectRoot),
    ]);

    // Build the enriched prompt with all context
    const enrichedPrompt = injectContext(toolInput.prompt, {
      styleGuide,
      recentCommits,
      conventions,
    });

    return {
      toolInput: { ...toolInput, prompt: enrichedPrompt },
      allowExecution: true,
      logMessage:
        "Context injected: style guide + recent commits + conventions",
    };
  } catch (error) {
    // If context gathering fails, don't block the prompt
    // The hook should be transparent—if it fails, the system continues
    console.warn("Context injection failed:", error.message);
    return { toolInput, allowExecution: true };
  }
};

async function readStyleGuide(projectRoot) {
  const stylePath = path.join(projectRoot, "standards", "style-guide.md");
  if (!fs.existsSync(stylePath)) return "";

  const content = fs.readFileSync(stylePath, "utf8");
  // Truncate to last 2000 characters to keep context lean
  // Most recent guidance matters most
  return content.slice(-2000);
}

async function getRecentGitHistory(projectRoot) {
  try {
    const { stdout } = await execPromise("git log --oneline -15", {
      cwd: projectRoot,
    });
    return stdout;
  } catch (error) {
    return "Git history unavailable";
  }
}

async function readProjectConventions(projectRoot) {
  const convPath = path.join(projectRoot, "docs", "conventions.md");
  if (!fs.existsSync(convPath)) return "";

  const content = fs.readFileSync(convPath, "utf8");
  // Truncate similarly to avoid bloating context
  return content.slice(-1500);
}

function injectContext(prompt, context) {
  const { styleGuide, recentCommits, conventions } = context;

  const contextBlock = `
---
PROJECT CONTEXT (auto-injected by hook):

STYLE GUIDE (excerpt):
${styleGuide}

RECENT COMMITS (last 15):
${recentCommits}

CONVENTIONS (file-type specific):
${conventions}
---

ACTUAL USER PROMPT:
${prompt}`;

  return contextBlock;
}

What just happened?

The hook does several things:

  1. Checks if it’s a prompt submission: We only inject context on actual prompts, not on every tool call. Reading a file doesn’t need the full context block.

  2. Gathers context in parallel: Using Promise.all(), we read the style guide, recent git commits, and conventions all at the same time. This is fast—typically sub-second.

  3. Reads your project’s guides: The hook looks for standards/style-guide.md and docs/conventions.md. If they don’t exist, it gracefully continues without them.

  4. Truncates to excerpts: We take only the last 2000 characters of the style guide and 1500 of conventions. You don’t need the entire 50KB guide—the most recent guidance is usually what matters, and it’s what changes most frequently.

  5. Gets recent git history: Running git log --oneline -15 gives Claude the last 15 commits. These tell Claude what your team has been working on, what patterns you’ve recently adopted, what’s current and fresh in your project.

  6. Wraps the original prompt: We mark the context block clearly so Claude knows what’s injected vs. what’s user-provided. This transparency helps Claude weigh information appropriately.

  7. Fails gracefully: If context gathering fails for any reason, the hook returns the original prompt unmodified. The system doesn’t break because the hook broke.

Conditional Injection Based on File Type

Here’s where this gets truly powerful. Different file types need different context. You don’t need Python style guidelines when editing JavaScript. You don’t need API documentation when writing unit tests. You don’t need database schema conventions when working on UI components.

Let’s modify the hook to be intelligent and conditional:

async function getContextForFileType(fileType, projectRoot) {
  // Map file types to their relevant convention documents
  const contexts = {
    ".js": ["js-conventions.md", "style-guide.md"],
    ".jsx": ["react-conventions.md", "js-conventions.md"],
    ".py": ["python-conventions.md", "python-style.md"],
    ".md": ["markdown-guide.md"],
    ".test.js": ["testing-guide.md", "test-conventions.md"],
    ".sql": ["sql-conventions.md", "database-guide.md"],
    ".ts": ["typescript-conventions.md", "style-guide.md"],
    ".tsx": ["react-conventions.md", "typescript-conventions.md"],
  };

  // Default to general style guide if no specific match
  const files = contexts[fileType] || ["style-guide.md"];
  const results = {};

  for (const file of files) {
    const filePath = path.join(projectRoot, "standards", file);
    if (fs.existsSync(filePath)) {
      const content = fs.readFileSync(filePath, "utf8");
      // Store with the filename as key for clarity
      results[file] = content.slice(-1500);
    }
  }

  return results;
}

module.exports = async (hookContext) => {
  const { tool, toolInput, filePath, projectRoot } = hookContext;

  if (tool !== "submitPrompt" || !toolInput.prompt) {
    return { toolInput, allowExecution: true };
  }

  try {
    const fileType = path.extname(filePath);
    const [contextFiles, commits] = await Promise.all([
      getContextForFileType(fileType, projectRoot),
      getRecentGitHistory(projectRoot),
    ]);

    const contextBlock = formatContextBlock(contextFiles, commits);
    const enrichedPrompt = `${contextBlock}\n\n${toolInput.prompt}`;

    return {
      toolInput: { ...toolInput, prompt: enrichedPrompt },
      allowExecution: true,
      logMessage: `Context injected for ${fileType} files`,
    };
  } catch (error) {
    return { toolInput, allowExecution: true };
  }
};

function formatContextBlock(contextFiles, commits) {
  let block = "\n---\nPROJECT CONTEXT (auto-injected):\n\n";

  for (const [filename, content] of Object.entries(contextFiles)) {
    if (content) {
      block += `## ${filename}\n${content}\n\n`;
    }
  }

  block += `## RECENT COMMITS\n${commits}\n---\n`;
  return block;
}

Now the hook is file-type aware. When you edit a React component (.jsx), it injects React-specific conventions. When you edit a test file, it injects testing guidelines. When you edit SQL, it injects database conventions. The context stays relevant, lean, and focused.

This is powerful because Claude can now give specialized advice for each context.

Performance: Not Bloating Your Context Window

Here’s a critical consideration that separates a good hook from a bad one: context windows are finite. Claude Haiku has about 200K tokens. Claude 3.5 Sonnet has 200K. That sounds like a lot until you realize a single 50KB file burns through 10K-15K tokens. If you inject 50KB of documentation into every prompt, you’ve just wasted 25% of your context window on boilerplate. You need to be smart about what you inject.

Good rules of thumb for keeping context lean:

  1. Truncate to excerpts, not full files: Take the last 1500–2000 characters of each guide, not the whole thing. The most recent guidance is what matters, and older sections are probably outdated or less relevant.

  2. Use git history, not docs: A well-written commit message captures intent faster than a style guide section. Injecting 15 recent commits (maybe 2KB) is more actionable than injecting 20KB of documentation that might have been written months ago.

  3. Conditional injection: Don’t inject Python conventions when editing JavaScript. Don’t inject API docs when editing tests. Match context to file type and purpose.

  4. Cache context: If your style guide never changes (or changes weekly at most), read it once and cache it. Don’t re-read the file on every prompt.

Here’s a smart caching layer:

let contextCache = {};
const CACHE_TTL = 3600000; // 1 hour in milliseconds

async function getCachedContext(key, generator) {
  const now = Date.now();

  // Return cached value if it's fresh
  if (contextCache[key] && now - contextCache[key].timestamp < CACHE_TTL) {
    return contextCache[key].data;
  }

  // Generate fresh data and cache it
  const data = await generator();
  contextCache[key] = { data, timestamp: now };
  return data;
}

// Usage in hook:
const styleGuide = await getCachedContext("style-guide", () =>
  readStyleGuide(projectRoot),
);

const commits = await getCachedContext("git-log", () =>
  getRecentGitHistory(projectRoot),
);

With caching, you read files once per hour, not once per prompt. That’s a 60x–100x efficiency win. Your hook runs in milliseconds instead of seconds.

Handling Sensitive Context

One critical caution: not all context should be injected. API keys, database credentials, private environment variables, and sensitive business logic absolutely should not end up in prompts sent to Claude’s servers.

Add a security filter:

function filterSensitiveContent(content) {
  const sensitivePatterns = [
    /api[_-]?key\s*[:=]/gi,
    /secret\s*[:=]/gi,
    /password\s*[:=]/gi,
    /token\s*[:=]/gi,
    /private[_-]?key\s*[:=]/gi,
    /database[_-]?url\s*[:=]/gi,
    /oauth\s*[:=]/gi,
    /aws[_-]?(access|secret)\s*[:=]/gi,
  ];

  let filtered = content;
  for (const pattern of sensitivePatterns) {
    filtered = filtered.replace(pattern, "[REDACTED]");
  }
  return filtered;
}

// Use before injecting:
const cleanedGuide = filterSensitiveContent(styleGuide);
const cleanedConventions = filterSensitiveContent(conventions);

This pattern-based approach catches common secrets and replaces them with [REDACTED]. It’s not foolproof, but it prevents accidental credential leakage for 99% of cases.

The Hidden Value: Why Context Injection Compounds Over Time

Context injection’s real power emerges over weeks and months, not days. In the beginning, the improvements are subtle. Claude’s suggestions are slightly more aligned. You spend a few minutes less editing suggestions to match your patterns. Over a team and over time, this compounds dramatically.

Think about how your project’s conventions evolve. In week one, you decide to standardize on a particular error handling pattern. In week two, your codebase has maybe three or four examples of the new pattern. In week four, you’ve got twenty examples. A context injection hook that reads your codebase sees this evolution. The more examples Claude sees, the better it understands the pattern. Early on, Claude might make one mistake per five suggestions. After a month, with more examples visible, it’s one mistake per fifty suggestions.

This is why context injection is more valuable on established projects than on brand-new greenfield projects. On greenfield, there’s not much codebase to learn from yet. On established projects where patterns are set, context injection is transformative.

The efficiency gains scale nonlinearly. If context injection saves you 5 minutes per hour in the first week, and 10 minutes per hour in the fourth week (because there are more examples to draw from), and 15 minutes per hour in the eighth week, then over a year, you’re saving hundreds of hours. Times that across a team of ten developers, and suddenly you’re looking at thousands of hours saved annually.

That’s not hyperbole—that’s the compounding effect of having an AI that learns your codebase’s patterns from your own code rather than generic best practices.

Real-World Scenarios: When Context Injection Shines

Context injection makes a dramatic difference in specific scenarios you encounter every day:

Scenario 1: Code Review Comments

Without context: “Consider extracting this into a helper function for reusability.”

With context: “Your recent commits show you’ve been consolidating helper functions into a utils/ directory and using barrel exports. I’d suggest extracting this into utils/formatting.ts alongside your other formatting helpers, then updating utils/index.ts to export it. This matches the pattern you’ve established across the FormValidator and DateHelper modules.”

Scenario 2: Naming Decisions

Without context: “You might name this variable processedItems.”

With context: “Looking at your codebase, I see you prefix computed values with final (like finalTotal, finalCount). I’d suggest finalProcessedItems to stay consistent with your naming convention. Your recent commits show you’ve been standardizing on this pattern.”

Scenario 3: Testing Strategy

Without context: “You should write unit tests and integration tests for this function.”

With context: “I see from your testing guide you prefer integration tests that hit real database fixtures for data-heavy functions. Your recent test files (UserRepository.test.js) follow that pattern. I’d write an integration test that uses your seedTestDatabase fixture instead of mocking the database layer.”

Scenario 4: Architecture Decisions

Without context: “You could use Redux or Context API for this state.”

With context: “Your conventions document says you prefer Context API for local feature state and only use Redux for global application state. Recent commits show you’ve standardized on this. Since this is form-level state used only by three components, I’d use Context with useReducer and keep it local to the form component.”

These aren’t minor improvements. They’re the difference between generic advice and advice that’s been tailored to your project, your team, your conventions.

Measuring Context Injection Effectiveness

How do you know if your context injection is actually working? Metrics tell the story:

  1. Relevance of suggestions: Are Claude’s suggestions actually applicable to your project? Are they contradicting established patterns less often?

  2. Time to integrate: Do you spend less time editing Claude’s suggestions because they’re already aligned with your conventions?

  3. Code review feedback: Do your code reviewers comment less on “doesn’t follow our patterns” issues in Claude-generated code?

  4. Context window efficiency: Is the enriched prompt using context effectively, or is Claude ignoring the injected guidance?

  5. Drift from conventions: Track how often Claude-generated code violates established patterns. This should trend downward as your context injection matures.

After two weeks of using context injection, you should see measurable improvements in any of these dimensions. If not, your context blocks might be generic, out-of-date, or misaligned with actual practice.

Real-World Workflow: Context Injection in Action

Let’s trace a realistic day with context injection in place:

9 AM: Alice joins standup and gets assigned a feature: “Add user preference caching to the preferences service.” She has no prior context about how this codebase handles caching patterns.

She creates a new file and starts coding. The context injection hook fires, reads her project’s TypeScript conventions, the preferences service’s specific patterns, and the last 15 commits (which show recent caching discussions the team had). Claude Code sees the recent commits mentioned “cache invalidation pattern via TTL” and “shared cache store in Redis.” It generates code that uses these patterns without Alice having to explain them.

10 AM: Alice shares the draft with the team. Reviews are minimal—the code already matches their patterns. Code review time: 10 minutes vs the usual 45 minutes. Nobody has to say “this doesn’t follow our pattern” or “we settled on a different approach last month.” The code is already right.

11 AM: A bug is discovered in the caching layer from last week. Alice asks Claude Code to trace the issue. The context injection now includes the bug-fix commit that was just merged, showing what was wrong and how it was fixed. Claude Code’s diagnostic is more relevant because it sees the recent history. It can say “I see you had the same issue in commit 1f2a3b4, and the fix was to validate TTL before cache hit checking. Let me verify that’s not the issue here too.”

2 PM: Alice is done. The feature is merged. Time-to-merge: 5 hours including reviews. Without context injection, this would typically be 8-10 hours because Claude would generate generic patterns and Alice would have to spend time aligning them with the team’s recent conventions. She’d have had to explain caching strategy, get feedback, adjust, resubmit. Context injection eliminates that loop.

Retrospective: This pattern repeats across the team. Every developer spends less time explaining their conventions to Claude. Every code review is shorter because Claude already understood the patterns. Features ship faster. The team moves at their natural pace rather than being slowed down by having to educate the AI every session.

That’s context injection’s real benefit: you’re not just generating code, you’re generating team-aligned code that feels like it was written by someone who’s been in the codebase for months. Your team’s velocity improves measurably, not because Claude is smarter, but because it’s operating in the same context as your team.

Advanced Patterns: When Basic Injection Fails

As you deploy context injection to larger teams, you’ll encounter scenarios where the basic pattern breaks down:

Monorepo context explosion: Your monorepo has 20 packages. If you inject all 20 packages’ conventions, you’ve consumed massive context just explaining what you DON’T need. Solution: make injection path-aware so editing packages/auth/ loads only auth conventions.

Context cycling in iterative development: You’re working on a complex refactor. Each iteration, you want Claude to see your feedback and recent changes, but NOT the previous three iterations’ dead-end attempts. Solution: smart context history pruning that keeps only the last 3 iterations.

Context drift in long sessions: You’re 50 prompts into a coding session. The style guide from the beginning of the session is stale because you’ve made changes that contradict it. Solution: periodic context refresh every 20 prompts or 30 minutes.

These patterns show the sophistication you can build once you understand how hooks work.

Troubleshooting: When Context Injection Goes Wrong

When you implement this in production, you’ll hit edge cases. Here’s how to handle them:

Problem: Hook runs but context doesn’t show up

Check that your context block actually made it into the prompt. Add logging:

console.log("[HOOK-DEBUG] Original prompt length:", toolInput.prompt.length);
console.log("[HOOK-DEBUG] Enriched prompt length:", enrichedPrompt.length);
console.log("[HOOK-DEBUG] Context block size:", contextBlock.length);

Problem: Git commands fail on Windows

On Windows, bash might not be available by default. Fall back gracefully:

async function getRecentGitHistory(projectRoot) {
  try {
    const { stdout } = await execPromise(
      'git log --oneline -15 --format="%h %s"',
      {
        cwd: projectRoot,
        shell: process.platform === "win32" ? "cmd" : undefined,
      },
    );
    return stdout;
  } catch (error) {
    console.warn("Git command failed:", error.message);
    return "(Git history unavailable - is this a git repository?)";
  }
}

Problem: Convention files don’t exist

The hook silently skips missing files, but add logging to help the team understand why context isn’t being injected:

for (const file of files) {
  const filePath = path.join(projectRoot, "standards", file);
  if (fs.existsSync(filePath)) {
    const content = fs.readFileSync(filePath, "utf8");
    results[file] = content.slice(-1500);
  } else {
    console.warn(`Convention file not found: ${file}`);
  }
}

Measuring Effectiveness in Your Team

After deploying context injection, watch these metrics:

Relevance of suggestions: Are Claude’s suggestions matching your team’s patterns? Track “violates project convention” issues in code review. Should trend from 20% (early stage) to 3-5% (mature).

Integration efficiency: How many review rounds before code is mergeable? Should drop from 5 rounds to 1-2.

Token efficiency: Is your context window growing faster than features ship? Should stay stable as you get selective about what you inject.

Developer satisfaction: Simple survey: “Does Claude Code understand our patterns without explanation?” Should be 85%+ with good injection.

Cross-team consistency: Different developers working with Claude Code should produce stylistically similar code. Review 50 PRs across 5 developers—style issues should be minimal.

Context Decay and Refresh Strategies

Here’s a subtle issue that catches teams by surprise: context becomes stale. Your style guide was updated three months ago. Your recent commits reflect decisions from two weeks ago. As your team evolves, the injected context might start contradicting current practice rather than guiding it.

Consider a team that standardized on async/await six weeks ago. If your context injection reads git history, recent commits show the async/await pattern. Claude sees it and generates code that way. Good. But if you standardized on async/await and then, five weeks later, discovered it causes performance issues in your specific use case and reverted to callbacks… your context injection is now teaching Claude the old pattern.

The solution is context refresh policies. Every week, regenerate context from your codebase. Every time the style guide changes, update it. When architectural decisions shift, update the context that gets injected. Treat your context sources the same way you treat documentation—as living systems that need maintenance.

Most teams benefit from weekly context refreshes. Re-read style guides, re-run git history extraction, re-analyze code patterns. This ensures Claude is always learning from current practice, not outdated guidance. Set a calendar reminder: every Friday afternoon, refresh your context injection sources.

Building Context from Multiple Sources

The basic hook reads files from disk. But context can come from many places. Your project might have conventions scattered across documentation, code comments, recent commits, architectural diagrams, decision records, and team chat history. The most powerful hooks synthesize from all these sources.

Reading from Multiple Document Types

Conventions live in different formats. Some teams use markdown docs. Others use Confluence pages, internal wikis, or even PDF guides. Building a hook that reads from multiple sources is more complex but much more powerful.

// hooks/lib/multi-source-context.mjs





const execPromise = promisify(exec);

export async function getContextFromMultipleSources(projectRoot) {
  const sources = {};

  // Source 1: Markdown files in standards/
  const standardsPath = path.join(projectRoot, "standards");
  if (fs.existsSync(standardsPath)) {
    const files = fs.readdirSync(standardsPath).filter((f) => f.endsWith(".md"));
    for (const file of files) {
      const content = fs.readFileSync(path.join(standardsPath, file), "utf8");
      sources[file] = content.slice(-2000);
    }
  }

  // Source 2: Architecture decision records
  const adrPath = path.join(projectRoot, "docs", "adr");
  if (fs.existsSync(adrPath)) {
    const adrs = fs
      .readdirSync(adrPath)
      .filter((f) => f.endsWith(".md"))
      .sort()
      .slice(-5); // Last 5 ADRs

    for (const adr of adrs) {
      const content = fs.readFileSync(path.join(adrPath, adr), "utf8");
      sources[`ADR: ${adr}`] = content.slice(-1000);
    }
  }

  // Source 3: Code comments and inline documentation
  try {
    const { stdout } = await execPromise(
      'grep -r "^///" "' + projectRoot + '/src" 2>/dev/null | head -20',
    );
    if (stdout) {
      sources["Code Documentation"] = stdout;
    }
  } catch {
    // No documentation comments found, that's fine
  }

  // Source 4: Recent git log with more details
  try {
    const { stdout } = await execPromise("git log --oneline -20", {
      cwd: projectRoot,
    });
    sources["Recent Commits"] = stdout;
  } catch {
    // Git not available
  }

  return sources;
}

By reading from multiple sources, your hook builds a richer picture of project context. Claude sees not just style guide excerpts but also architectural decisions, recent changes, and embedded documentation.

Ranking Context by Recency and Relevance

Not all context is equally important. A style guide written two years ago might conflict with conventions established in the last month. Rank context by recency.

function rankContextBySources(contextSources) {
  const ranked = [
    { weight: 1.0, source: "Recent Commits", reason: "Most current" },
    {
      weight: 0.9,
      source: "ADR",
      reason: "Architectural decisions (usually recent and important)",
    },
    { weight: 0.8, source: "Code Documentation", reason: "Embedded knowledge" },
    {
      weight: 0.7,
      source: "style-guide.md",
      reason: "Style documentation",
    },
    {
      weight: 0.6,
      source: "conventions.md",
      reason: "Project conventions",
    },
  ];

  const result = [];
  for (const { source, weight, reason } of ranked) {
    if (contextSources[source]) {
      result.push({
        source,
        content: contextSources[source],
        weight,
        reason,
      });
    }
  }

  return result;
}

Present context in order of importance. Claude sees the most relevant material first, making better use of limited context windows.


Context Injection and Security: The Double-Edged Sword

Context injection is powerful, but it’s also where security issues hide. You’re automatically enriching prompts with information from your codebase. What if that includes secrets?

The Hidden Credential Problem

Your style guide probably doesn’t contain credentials. But what about your git commit messages? “Fixed bug where password wasn’t being hashed using SHA256” accidentally mentions your algorithm. Your recent commits might reference database names or API endpoints you don’t want Claude to know about. Your documentation might include example API keys (hopefully redacted, but sometimes not).

When you automatically inject context, you’re trusting that nothing sensitive made it into your documentation. In practice, this assumption breaks down at scale. Someone commits a change that includes a credential mention, and suddenly that’s flowing through your context injection hooks.

Smart Filtering and Redaction

Build filtering that catches common patterns:

const SENSITIVE_PATTERNS = [
  /api[_-]?key\s*[:=]\s*['""]?([a-zA-Z0-9_-]+)/gi,
  /password\s*[:=]\s*['""]?([a-zA-Z0-9_-]+)/gi,
  /secret\s*[:=]\s*['""]?([a-zA-Z0-9_-]+)/gi,
  /token\s*[:=]\s*['""]?([a-zA-Z0-9_-]+)/gi,
  /bearer\s+([a-zA-Z0-9_-]+)/gi,
  /database[_-]?url\s*[:=]\s*['""]?([^'""\s]+)/gi,
  /webhook[_-]?url\s*[:=]\s*['""]?([^'""\s]+)/gi,
];

export function sanitizeForInclusion(content) {
  let sanitized = content;

  for (const pattern of SENSITIVE_PATTERNS) {
    sanitized = sanitized.replace(pattern, "[REDACTED]");
  }

  // Also catch common IP patterns in URLs
  sanitized = sanitized.replace(/https?:\/\/\d+\.\d+\.\d+\.\d+/g, "[IP_REDACTED]");

  // Catch database connection strings
  sanitized = sanitized.replace(
    /postgresql:\/\/[^:]+:[^@]+@[^/]+/g,
    "[DB_CONNECTION_REDACTED]",
  );

  return sanitized;
}

This isn’t foolproof—determined attackers can hide secrets in natural language. But it catches the obvious cases and prevents accidental exposure.

Audit Logging for Context Injection

Track what gets injected. If security issues emerge later, you need evidence of what was sent to Claude:

export async function auditContextInjection(prompt, contextBlock, hookContext) {
  const auditLog = {
    timestamp: new Date().toISOString(),
    user: process.env.USER || "unknown",
    projectRoot: hookContext.projectRoot,
    contextBlockSize: contextBlock.length,
    originalPromptSize: prompt.length,
    injectionRatio: (contextBlock.length / prompt.length).toFixed(2),
    sources: extractContextSources(contextBlock),
    // NEVER log actual content, just metadata
  };

  const auditPath = path.join(
    hookContext.projectRoot,
    ".claude",
    "audit-logs",
    "context-injection.jsonl",
  );

  fs.appendFileSync(auditPath, JSON.stringify(auditLog) + "\n");
}

This creates a record of what was injected, when, and by whom. If a security incident occurs, you can trace what information was available to Claude during the incident window.


Advanced: Dynamic Context Based on Code Analysis

Instead of static documentation, generate context dynamically by analyzing your codebase. This is bleeding-edge but incredibly powerful.

Detecting Patterns in Your Code

Use AST analysis to understand patterns your team uses:

// hooks/lib/pattern-detector.mjs



export function detectCommonPatterns(projectRoot) {
  const patterns = {
    errorHandling: [],
    naming: [],
    structuring: [],
  };

  const srcPath = path.join(projectRoot, "src");
  const files = findJavaScriptFiles(srcPath);

  files.slice(0, 20).forEach((file) => {
    const content = fs.readFileSync(file, "utf8");

    // Detect error handling patterns
    if (
      content.includes(".catch((err) =>") ||
      content.includes(".catch((error) =>")
    ) {
      patterns.errorHandling.push(
        "Promise-based error handling with .catch()",
      );
    }
    if (content.includes("try {") && content.includes("} catch")) {
      patterns.errorHandling.push("Try-catch blocks for error handling");
    }

    // Detect naming conventions
    if (content.match(/const is[A-Z]/)) {
      patterns.naming.push('Boolean variables prefixed with "is"');
    }
    if (content.match(/const handle[A-Z]/)) {
      patterns.naming.push('Event handlers prefixed with "handle"');
    }
  });

  return patterns;
}

function findJavaScriptFiles(dir) {
  let files = [];
  fs.readdirSync(dir).forEach((file) => {
    const fullPath = path.join(dir, file);
    if (fs.statSync(fullPath).isDirectory()) {
      files = files.concat(findJavaScriptFiles(fullPath));
    } else if (file.endsWith(".js") || file.endsWith(".ts")) {
      files.push(fullPath);
    }
  });
  return files;
}

Generate context from actual code analysis, not just documentation. This is what your team actually does, not what documentation says they do.


Measuring Context Injection Impact

How do you know your context injection is actually working? It’s subtle and easy to miss if you don’t measure.

Quantifying Relevance Improvements

Before and after context injection, measure:

  • Code review iteration count: How many rounds of feedback before code is mergeable?
  • Adherence to patterns: Percentage of Claude-generated code that follows established patterns
  • Integration friction: Time spent fixing Claude suggestions to match project conventions
  • Developer satisfaction: Do developers report that Claude “gets it” better?
// Track these metrics over time
const relevanceMetrics = {
  beforeContextInjection: {
    avgReviewRounds: 3.2,
    patternAdherence: 0.68,
    integrationFriction: "45 minutes per feature",
  },
  afterContextInjection: {
    avgReviewRounds: 1.5, // Down 50%
    patternAdherence: 0.91, // Up significantly
    integrationFriction: "15 minutes per feature", // Down 70%
  },
};

These aren’t pulled from thin air—they’re measurable differences you see in practice over weeks. If you’re not seeing improvement, your context might be stale or misaligned with actual practice.


Conclusion

Context injection hooks transform Claude Code from a generic coding assistant into a project-aware pair programmer that knows your conventions without you having to repeat them every session. It’s the difference between explaining the same architectural pattern ten times and explaining it once—in your codebase itself, with your style guide, baked directly into the conversation.

The hook fires silently. The user never sees it. But every single response Claude gives is now 30–40% smarter because it understands the context you work in.

That’s the power of automated context enrichment—and it’s what separates teams that use Claude Code casually from teams that have made it a core part of their development process. Your team moves faster. Your code stays consistent. Your developers spend less time explaining “how we do things here” and more time actually building things.

Start simple with style guides and recent commits. As you see value, layer in architecture decision records, pattern detection, and security filtering. Over weeks, you’ll have built a context system that rivals having an experienced team member in every coding session.

That’s the promise of context injection. Build it thoughtfully and deploy it confidently.


-iNet

Free Discovery Call

Start With a Conversation, Not a Commitment

Every engagement begins with a free 30-minute discovery call. We'll map what's slowing your business down and tell you exactly what we'd fix first – no pitch deck, no obligation.