What it does
PreToolUse hooks intercept every tool call before Claude Code executes it. They run in the Claude Code harness, not in Claude itself, so they can block, transform, or augment the call with fresh state (environment variables, secrets, validation errors) that the model cannot directly modify.
A PreToolUse hook receives the tool name, parameters, and execution context, then returns one of three verdicts: approve the call as-is, reject it with a reason, or approve it with modified parameters.
When to use it
Blocking destructive operations. You can refuse git reset --hard, rm -rf, or npm audit fix unless the user explicitly types a confirmation phrase. This prevents Claude from accidentally trashing your work.
# Example rule: block force-push unless user types "I understand"
if (tool === 'Bash' && params.command.includes('git push --force')) {
if (!context.userInput.includes('I understand')) {
return { approved: false, reason: 'Force push blocked; type "I understand" to override' };
}
}
Injecting secrets and environment variables. You can intercept API tool calls and inject ANTHROPIC_API_KEY, database credentials, or OAuth tokens into the parameters without storing them in .env.local. The hook runs server-side, so secrets never touch the user's disk.
if (tool === 'Bash' && params.command.includes('curl')) {
params.env = { ...params.env, API_TOKEN: process.env.SECRET_API_TOKEN };
return { approved: true, params };
}
Enforcing input contracts. Before Claude calls a sensitive endpoint (e.g., a delete API), you can validate that the payload matches your schema — e.g., the record ID is a UUID, the count is within bounds, or a required field is present. If validation fails, reject and tell Claude why.
Rate limiting or quota tracking. Count how many times Claude has called expensive operations (Claude API calls, database writes, file uploads) in this session and block if a threshold is exceeded.
Try it yourself
Open ~/.claude/settings.json and add a hooks section with a preToolUse handler. Write a rule that blocks rm -rf commands unless the user's message contains the phrase "delete everything." Then try triggering a destructive Bash command in Claude Code and watch it refuse — you'll see the rejection reason inline. Next, re-submit the same command with your confirmation phrase and watch it approve.
Gotchas
Hooks are JSON contracts. Your preToolUse function receives tool metadata as a JSON object and must return a JSON-serializable verdict. You cannot inspect the Claude model's reasoning or state — only the tool call itself. If you need to block based on intent, you'll have to infer it from the command (e.g., checking if a Bash command contains git push).
Approval is per-call, not per-tool. Returning { approved: true } only approves this call. If Claude makes the same call again two seconds later, your hook runs again and must approve again. Use this to implement stateful rate limits — store counters server-side, not in the hook code.
Modified parameters must be valid. If you transform params and return them, Claude Code will pass the modified parameters to the tool. If they're invalid (e.g., a malformed JSON for an API call), the tool fails. Test your transformations before deploying.
Hooks block on tool execution, not submission. The model can see the tool in its tool list and call it — your hook decides whether it runs. This means Claude might waste tokens designing a call that your hook will reject. For truly off-limits tools, use Claude Code's permission system instead.
Try it yourself
Type the command in the fake terminal. Nothing leaves your browser.