How to Define a PreToolUse Hook in Claude Code: A Complete Implementation Guide
PreToolUse hooks in Claude Code execute custom JavaScript logic immediately before any tool invocation by registering a script in hooks/hooks.json with "event": "preToolUse" and exporting an async run(context) function that receives and can modify the tool name, arguments, and session state.
Hooks are a core extension mechanism in the WorldFlowAI/everything-claude-code repository. The PreToolUse hook specifically intercepts tool calls at the moment Claude Code is about to execute any tool—whether built-in (file read, search) or custom. This capability enables argument validation, security filtering, logging, and dynamic transformation of tool inputs before execution proceeds.
PreToolUse Hook Architecture
The hook system follows a three-layer design implemented across the repository's scripts/hooks/, hooks/, and tests/hooks/ directories.
Core Components
| Component | Location | Purpose |
|---|---|---|
| Hook script | scripts/hooks/*.js |
Contains the executable logic; must export run(context) |
| Hook manifest | hooks/hooks.json |
Registers when the hook fires via "event": "preToolUse" |
| Test suite | tests/hooks/hooks.test.js |
Validates hook behavior and side effects |
Execution Flow
User Request → Claude Code → PreToolUse Hook → Tool Execution → Response
↑_______________|
(mutated context returned)
When a tool call occurs, Claude Code queries hooks/hooks.json for entries matching "event": "preToolUse", loads the specified script, and awaits its run function. The hook may return a modified context object to alter tool behavior or throw an error to abort execution entirely.
Creating a PreToolUse Hook: Step-by-Step
Step 1: Implement the Hook Script
Create a JavaScript file in scripts/hooks/ that exports an async run function. The function receives a context object with these properties:
context.tool(string): Name of the tool about to executecontext.args(Array): Arguments originally passed to the toolcontext.session(Object): Current session state object
// scripts/hooks/my-pre-tool.js
/**
* PreToolUse hook for Claude Code.
* Executes before any tool invocation to validate or transform inputs.
*
* @param {Object} context - Execution context
* @param {string} context.tool - Tool name being invoked
* @param {Array} context.args - Tool arguments
* @param {Object} context.session - Mutable session state
* @returns {Promise<Object>} - Modified or original context
*/
export async function run(context) {
// Block dangerous tools by name
const blockedTools = ['dangerousTool', 'unauthorizedExec'];
if (blockedTools.includes(context.tool)) {
throw new Error(`PreToolUse hook blocked: "${context.tool}" is not allowed`);
}
// Transform arguments: prefix all strings with execution marker
context.args = context.args.map(arg =>
typeof arg === 'string' ? `[AUDITED] ${arg}` : arg
);
// Log tool invocations for audit trail
console.error(`[PreToolUse] ${context.tool}: ${JSON.stringify(context.args)}`);
// Return mutated context to propagate changes
return context;
}
The run function must be exported as a named export. The repository's hook loader in scripts/hooks/pre-compact.js demonstrates similar mutation patterns for session state management.
Step 2: Register in hooks.json
Add an entry to hooks/hooks.json with the exact "preToolUse" event identifier. The "script" path is relative to the repository root.
// hooks/hooks.json
{
"hooks": [
{
"name": "my-pre-tool",
"event": "preToolUse",
"script": "scripts/hooks/my-pre-tool.js",
"description": "Validates and audits tool invocations before execution"
}
]
}
The "name" field is used for debugging and logging. Multiple PreToolUse hooks can be registered; they execute in manifest order.
Step 3: Write Tests
Validate hook behavior using the repository's test utilities. The tests/hooks/hooks.test.js file contains patterns for hook testing.
// tests/hooks/hooks.test.js
import { describe, it, expect } from 'vitest';
import { runScript } from '../lib/utils.js';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('PreToolUse hook: my-pre-tool', () => {
it('blocks dangerous tools with clear error message', async () => {
const hookPath = path.join(process.cwd(), 'scripts/hooks/my-pre-tool.js');
const { run } = await import(hookPath);
await expect(
run({ tool: 'dangerousTool', args: ['rm -rf /'], session: {} })
).rejects.toThrow('blocked');
});
it('transforms string arguments with audit prefix', async () => {
const { run } = await import(
path.join(process.cwd(), 'scripts/hooks/my-pre-tool.js')
);
const result = await run({
tool: 'read',
args: ['sensitive/file.txt'],
session: {}
});
expect(result.args[0]).toMatch(/^\[AUDITED\]/);
});
it('preserves non-string arguments unchanged', async () => {
const { run } = await import(
path.join(process.cwd(), 'scripts/hooks/my-pre-tool.js')
);
const result = await run({
tool: 'compute',
args: [42, { key: 'value' }, null],
session: {}
});
expect(result.args[0]).toBe(42);
expect(result.args[1]).toEqual({ key: 'value' });
expect(result.args[2]).toBeNull();
});
});
PreToolUse Hook Use Cases
Security and Access Control
// scripts/hooks/security-gate.js
export async function run(context) {
const sensitivePaths = ['/etc/', '/root/', '.env', 'secrets/'];
const hasSensitiveArg = context.args.some(arg =>
typeof arg === 'string' && sensitivePaths.some(p => arg.includes(p))
);
if (hasSensitiveArg && !context.session.authenticated) {
throw new Error('Authentication required for sensitive path access');
}
return context;
}
Argument Sanitization
// scripts/hooks/sanitizer.js
export async function run(context) {
// Prevent shell injection in string arguments
const SHELL_CHARS = /[;&|`$]/;
context.args = context.args.map(arg => {
if (typeof arg === 'string' && SHELL_CHARS.test(arg)) {
throw new Error(`Potentially unsafe characters in argument: "${arg}"`);
}
return arg;
});
return context;
}
Rate Limiting and Quotas
// scripts/hooks/rate-limiter.js
const toolCallCounts = new Map();
export async function run(context) {
const sessionId = context.session.id;
const key = `${sessionId}:${context.tool}`;
const current = toolCallCounts.get(key) || 0;
const limit = context.session.toolLimits?.[context.tool] || 100;
if (current >= limit) {
throw new Error(`Rate limit exceeded for tool: ${context.tool}`);
}
toolCallCounts.set(key, current + 1);
return context;
}
Key Reference Files in everything-claude-code
| File | Path | Description |
|---|---|---|
pre-compact.js |
scripts/hooks/pre-compact.js |
Reference implementation showing session state mutation patterns |
hooks.json |
hooks/hooks.json |
Central registry defining all hook event bindings |
hooks.test.js |
tests/hooks/hooks.test.js |
Comprehensive test suite for hook validation |
hooks.md |
rules/hooks.md |
Documentation of supported hook events including preToolUse |
The rules/hooks.md file specifies that preToolUse is one of several lifecycle events, alongside postToolUse, preCompact, and sessionStart. Each event receives a context object with event-specific properties.
Common Pitfalls and Solutions
- Forgetting to return context: Always return the context object, even if unmodified, or subsequent hooks and tool execution receive
undefined. - Synchronous handlers: Wrap synchronous logic in
Promise.resolve()or markrunasasyncto ensure compatibility with the hook orchestrator. - Context mutation side effects: Clone deep objects before mutation to avoid unintended cross-hook pollution:
context.session = JSON.parse(JSON.stringify(context.session)).
Summary
- PreToolUse hooks execute JavaScript before every tool invocation in Claude Code, enabling validation, transformation, and logging.
- Implementation requires: a script in
scripts/hooks/exportingrun(context), registration inhooks/hooks.jsonwith"event": "preToolUse", and tests intests/hooks/. - The
contextobject providestool(name),args(array), andsession(state) for inspection and mutation. - Hooks can abort execution by throwing errors or modify behavior by returning mutated context.
- Reference implementations in
WorldFlowAI/everything-claude-codedemonstrate production patterns for security, sanitization, and rate limiting.
Frequently Asked Questions
What happens if multiple PreToolUse hooks are registered?
Claude Code executes PreToolUse hooks in the order they appear in hooks/hooks.json. Each hook receives the context returned by the previous hook, creating a pipeline. If any hook throws an error, the tool invocation aborts immediately and subsequent hooks do not run.
Can a PreToolUse hook modify which tool actually executes?
No. The context.tool property is read-only within the hook contract. While you can read the tool name to apply conditional logic, you cannot redirect execution to a different tool. To achieve tool substitution, implement custom tool wrappers or use session state to influence downstream behavior.
How do I debug a PreToolUse hook that isn't firing?
Verify three elements: (1) the JSON entry in hooks/hooks.json uses the exact string "preToolUse" for the event field, (2) the script path is correct relative to the repository root, and (3) the script exports run as a named export (not default export). Check rules/hooks.md for event name spelling and examine tests/hooks/hooks.test.js for working examples.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →