---
title: LLM Call Hooks
description: Learn how to use LLM call hooks to intercept, modify, and control language model interactions in CrewAI
mode: "wide"
---

LLM Call Hooks provide fine-grained control over language model interactions
during agent execution. These hooks allow you to intercept LLM calls, modify
prompts, transform responses, implement approval gates, and add custom logging
or monitoring.

## Overview

LLM hooks are executed at two interception points:

| Point | When | Hook receives |
|-------|------|---------------|
| `PRE_MODEL_CALL` | Before every LLM call | `LLMCallHookContext` |
| `POST_MODEL_CALL` | After every LLM call | `LLMCallHookContext` (with `response` set) |

Write them with the [`@on` decorator](/edge/en/learn/execution-hooks). The
[legacy `@before_llm_call` / `@after_llm_call` decorators](#legacy-decorators)
keep working unchanged — both styles register on the same engine and run in one
ordered chain.

## Hook Signature

```python
from crewai.hooks import on, HookAborted, InterceptionPoint, LLMCallHookContext

@on(InterceptionPoint.PRE_MODEL_CALL)
def before_hook(ctx: LLMCallHookContext) -> None:
    # Mutate ctx.messages in place, or
    # raise HookAborted(reason, source) to block the call
    ...

@on(InterceptionPoint.POST_MODEL_CALL)
def after_hook(ctx: LLMCallHookContext) -> str | None:
    # Return a string to replace ctx.response
    # Return None to keep the original response
    ...
```

Unlike the boundary and step points, the model-call points pass the rich
`LLMCallHookContext` directly as the hook argument (there is no separate
`ctx.payload`): mutate `ctx.messages` in place before the call, and return a
string to replace the response after it.

Blocking a call raises `ValueError("LLM call blocked by before_llm_call hook")`
inside the executor; the `HookAborted` reason and source are recorded in
[telemetry](/edge/en/learn/execution-hooks#telemetry).

## LLM Hook Context

The `LLMCallHookContext` object provides comprehensive access to execution state:

```python
class LLMCallHookContext:
    executor: CrewAgentExecutor | LiteAgent | None  # Executor (None for direct LLM calls)
    messages: list               # Mutable message list
    agent: Agent | None          # Current agent (None for direct LLM calls)
    task: Task | None            # Current task (None for direct calls or LiteAgent)
    crew: Crew | None            # Crew instance (None for direct calls or LiteAgent)
    llm: BaseLLM | None          # LLM instance
    iterations: int              # Current iteration count (0 for direct calls)
    response: str | None         # LLM response (POST_MODEL_CALL only)
```

The context also exposes `request_human_input(prompt, default_message)`, which
pauses live console updates and collects input from the terminal — useful for
approval gates.

### Modifying Messages

**Important:** Always modify messages in-place:

```python
# ✅ Correct - modify in-place
@on(InterceptionPoint.PRE_MODEL_CALL)
def add_context(ctx: LLMCallHookContext) -> None:
    ctx.messages.append({"role": "system", "content": "Be concise"})

# ❌ Wrong - replaces list reference and breaks the executor
@on(InterceptionPoint.PRE_MODEL_CALL)
def wrong_approach(ctx: LLMCallHookContext) -> None:
    ctx.messages = [{"role": "system", "content": "Be concise"}]
```

## Registration Methods

### 1. Global Hooks

Apply to all LLM calls across all crews. Use the `agents=` filter to scope a
hook to specific agent roles:

```python
from crewai.hooks import on, InterceptionPoint

@on(InterceptionPoint.PRE_MODEL_CALL)
def log_llm_call(ctx):
    print(f"LLM call by {ctx.agent.role} at iteration {ctx.iterations}")

@on(InterceptionPoint.POST_MODEL_CALL, agents=["Researcher"])
def log_researcher_responses(ctx):
    print(f"Response length: {len(ctx.response)}")
```

### 2. Crew-Scoped Hooks

Apply the same decorator to a method inside a `@CrewBase` class to scope the
hook to that crew only:

```python
from crewai.hooks import on, InterceptionPoint

@CrewBase
class MyProjCrew:
    @on(InterceptionPoint.PRE_MODEL_CALL)
    def validate_inputs(self, ctx):
        # Only applies to this crew
        if ctx.iterations == 0:
            print(f"Starting task: {ctx.task.description}")

    @crew
    def crew(self) -> Crew:
        return Crew(agents=self.agents, tasks=self.tasks, process=Process.sequential)
```

## Common Use Cases

### 1. Iteration Limiting

```python
@on(InterceptionPoint.PRE_MODEL_CALL)
def limit_iterations(ctx: LLMCallHookContext) -> None:
    if ctx.iterations > 15:
        raise HookAborted(reason="exceeded 15 iterations", source="loop-guard")
```

### 2. Human Approval Gate

```python
@on(InterceptionPoint.PRE_MODEL_CALL)
def require_approval(ctx: LLMCallHookContext) -> None:
    if ctx.iterations > 5:
        response = ctx.request_human_input(
            prompt=f"Iteration {ctx.iterations}: Approve LLM call?",
            default_message="Press Enter to approve, or type 'no' to block:",
        )
        if response.lower() == "no":
            raise HookAborted(reason="blocked by user", source="approval-gate")
```

### 3. Adding System Context

```python
@on(InterceptionPoint.PRE_MODEL_CALL)
def add_guardrails(ctx: LLMCallHookContext) -> None:
    ctx.messages.append({
        "role": "system",
        "content": "Ensure responses are factual and cite sources when possible."
    })
```

### 4. Response Sanitization

```python
import re

@on(InterceptionPoint.POST_MODEL_CALL)
def sanitize_sensitive_data(ctx: LLMCallHookContext) -> str | None:
    if not ctx.response:
        return None
    sanitized = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN-REDACTED]', ctx.response)
    return re.sub(r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', '[CARD-REDACTED]', sanitized)
```

### 5. Debug Logging

```python
@on(InterceptionPoint.PRE_MODEL_CALL)
def debug_request(ctx: LLMCallHookContext) -> None:
    print(f"Agent: {ctx.agent.role}, iteration {ctx.iterations}, "
          f"{len(ctx.messages)} messages")

@on(InterceptionPoint.POST_MODEL_CALL)
def debug_response(ctx: LLMCallHookContext) -> None:
    if ctx.response:
        print(f"Response preview: {ctx.response[:100]}...")
```

## Hook Management

```python
from crewai.hooks import (
    InterceptionPoint,
    clear_all_hooks,
    clear_hooks,
    get_hooks,
    unregister_hook,
)

# Unregister a specific hook
unregister_hook(InterceptionPoint.PRE_MODEL_CALL, my_hook)

# Clear one point, or everything (e.g. between tests)
clear_hooks(InterceptionPoint.POST_MODEL_CALL)
clear_all_hooks()

# Inspect what's registered
print(len(get_hooks(InterceptionPoint.PRE_MODEL_CALL)))
```

The legacy management API (`register_before_llm_call_hook`,
`unregister_before_llm_call_hook`, `clear_before_llm_call_hooks`,
`clear_all_llm_call_hooks`, `get_before_llm_call_hooks`, and their `after_`
counterparts) operates on the same underlying registries, so either API can
manage hooks registered by the other.

## Legacy Decorators

The original per-point decorators keep working unchanged and run in the same
registration-order chain as `@on` hooks:

```python
from crewai.hooks import before_llm_call, after_llm_call

@before_llm_call
def validate_iteration_count(context):
    if context.iterations > 10:
        return False  # Block execution
    return None

@after_llm_call(agents=["Researcher"])
def sanitize_response(context):
    if context.response and "API_KEY" in context.response:
        return context.response.replace("API_KEY", "[REDACTED]")
    return None
```

Differences from `@on`:

- **Blocking** is `return False` from a before hook — equivalent to raising
  `HookAborted`, but without a custom reason or source for telemetry.
- **Signatures** are point-specific: before hooks return `bool | None`, after
  hooks return `str | None`. The context object is the same
  `LLMCallHookContext`.
- **Filters and crew-scoping** work the same way: `@before_llm_call(agents=[...])`,
  and applying the decorator to a `@CrewBase` method scopes it to that crew.

Prefer `@on` for new code; keep the legacy style where it is already in use —
there is no behavioral penalty.

## Best Practices

1. **Keep hooks focused and fast** — they run on every LLM call
2. **Modify in-place** — always mutate `ctx.messages`, never replace the list
3. **Use type hints** — annotate with `LLMCallHookContext` for IDE support
4. **Abort loudly** — raise `HookAborted` with a meaningful reason and source;
   any other exception is swallowed (fail-open)
5. **Clear hooks in tests** — call `clear_all_hooks()` between test runs

## Troubleshooting

### Hook Not Executing
- Verify the hook is registered before crew execution
- Check whether an earlier hook aborted (subsequent hooks don't run)

### Message Modifications Not Persisting
- Use in-place modifications: `ctx.messages.append(...)`
- Don't replace the list: `ctx.messages = []`

### Response Modifications Not Working
- Return the modified string from a `POST_MODEL_CALL` hook
- Returning `None` keeps the original response

## Related Documentation

- [Execution Hooks Overview →](/edge/en/learn/execution-hooks)
- [Tool Call Hooks →](/edge/en/learn/tool-hooks)
- [Execution Boundary Hooks →](/edge/en/learn/execution-boundary-hooks)
- [Step Hooks →](/edge/en/learn/step-hooks)
