Hooks#
Note
This is a new feature added in response to issue #156.
The hook system allows tools and plugins to register callbacks that execute at various points in gptme’s lifecycle. This enables powerful extensions like automatic linting, memory management, pre-commit checks, and more.
Hook Types#
The following hook types are available:
Turn And Step Lifecycle Hooks#
TURN_PRE: Once per submitted user prompt, before the turn startsSTEP_PRE: Before each generation/tool-execution step within a turnSTEP_POST: After each step completesTURN_POST: After the whole turn completesMESSAGE_TRANSFORM: Rewrite assistant message content before persistence/display
TURN_PRE is the clean “user prompt submitted” surface. STEP_PRE may run
multiple times inside one turn if the assistant keeps calling tools.
Tool Lifecycle Hooks#
TOOL_EXECUTE_PRE: Before executing any toolTOOL_EXECUTE_POST: After executing any toolTOOL_TRANSFORM: Transform tool executionTOOL_CONFIRM: Blocking confirmation/deny decision before execution (see Confirmation Hooks and Deny Decisions)
File Operation Hooks#
FILE_SAVE_PRE: Before saving a fileFILE_SAVE_POST: After saving a fileFILE_PATCH_PRE: Before patching a fileFILE_PATCH_POST: After patching a file
Session Lifecycle Hooks#
SESSION_START: At session startSESSION_END: At session end
Global and project configuration can attach bounded shell commands to these two
lifecycle events through [[hooks.scripts]]. The lists are additive and run
in descending priority order; global hooks win equal-priority ties, then
declaration order is preserved. See Global config and
Project config. Other hook types carry structured inputs or control-flow
semantics and are not exposed through the shell adapter.
Generation Hooks#
GENERATION_PRE: Before generating responseGENERATION_POST: After generating responseGENERATION_INTERRUPT: Interrupt generation
Usage#
Registering Hooks from Tools#
Tools can register hooks in their ToolSpec definition:
from gptme.tools.base import ToolSpec
from gptme.hooks import HookType
from gptme.message import Message
def on_file_save(path, content, created):
"""Hook function called after a file is saved."""
if path.suffix == ".py":
# Run linting on Python files
return Message("system", f"Linted {path}")
return None
tool = ToolSpec(
name="linter",
desc="Automatic linting tool",
hooks={
"file_save": (
HookType.FILE_SAVE_POST.value, # Hook type
on_file_save, # Hook function
10 # Priority (higher = runs first)
)
}
)
Registering Hooks Programmatically#
You can also register hooks directly:
from gptme.hooks import register_hook, HookType
from gptme.hooks.confirm import ConfirmationResult
def my_hook_function(manager):
"""Custom hook function."""
# Do something
return Message("system", "Hook executed!")
register_hook(
name="my_custom_hook",
hook_type=HookType.TURN_PRE,
func=my_hook_function,
priority=0,
enabled=True
)
Hook Function Signatures#
Hook functions receive different arguments depending on the hook type:
# Turn/step hooks
def turn_hook(manager):
pass
# Tool hooks
def tool_hook(log, workspace, tool_use):
pass
# File hooks
def file_hook(log, workspace, path, content, created=False):
pass
# Session hooks
def session_hook(logdir, workspace, initial_msgs):
pass
# Confirmation hooks (see "Confirmation Hooks and Deny Decisions" below)
def confirm_hook(tool_use, preview=None, workspace=None):
if is_dangerous(tool_use):
return ConfirmationResult.skip("Blocked: destructive path")
return None # fall through to the next confirmation hook
Most hook functions can:
Return
None(no action)Return a single
MessageobjectReturn a generator that yields
MessageobjectsRaise exceptions (which are caught and logged)
TOOL_CONFIRM hooks are the exception: they return a
ConfirmationResult rather than yielding messages. See below.
Confirmation Hooks and Deny Decisions#
TOOL_CONFIRM is the hook for deterministic, below-the-model guardrails: it
runs before a tool executes and can deny execution outright.
from gptme.hooks import HookType, register_hook
from gptme.hooks.confirm import ConfirmationResult
def block_secret_reads(tool_use, preview=None, workspace=None):
"""Deny any shell command that reads private keys."""
if tool_use.tool == "shell" and "id_rsa" in (tool_use.content or ""):
return ConfirmationResult.skip("Blocked: secret file access")
return None # not our decision — fall through
def register():
register_hook(
"guardrail.secrets",
HookType.TOOL_CONFIRM,
block_secret_reads,
priority=1000, # must exceed every built-in confirmation hook
)
The three results a confirmation hook can return:
ConfirmationResult.skip(message)— deny; the tool does not execute, andmessageis surfaced as the reason.ConfirmationResult.confirm()— approve without prompting the user.ConfirmationResult.edit(content)— execute with modified content.
Returning None falls through to the next TOOL_CONFIRM hook in priority
order; the first non-None result wins. If no hook is registered at all,
execution is auto-confirmed (autonomous mode).
Warning
A guardrail must out-rank every built-in confirmation hook, or it can be pre-empted before it ever runs. The built-ins register at:
server_confirm— priority 100cli_confirm— priority 0auto_confirm— priority 0
Hooks sort by (priority, name) descending, so equal priority is broken
by name in reverse alphabetical order — not by registration order. A
guardrail named guardrail.secrets registered at priority 100 therefore
loses to server_confirm ("s" > "g"), which returns a non-None
result and prevents the guardrail from running at all. Pick a priority
strictly greater than 100 (the example uses 1000) rather than relying on
the name tie-break.
Note
--no-confirm/-y only stops gptme from registering its interactive
confirmation hooks (cli_confirm/server_confirm). A plugin-registered
TOOL_CONFIRM hook is still called, so a skip result keeps blocking
execution in autonomous runs. This is what makes the hook usable as a real
guardrail rather than a prompt.
Managing Hooks#
Query Hooks#
from gptme.hooks import get_hooks, HookType
# Get all hooks
all_hooks = get_hooks()
# Get hooks of a specific type
tool_hooks = get_hooks(HookType.TOOL_EXECUTE_POST)
Enable/Disable Hooks#
from gptme.hooks import enable_hook, disable_hook
# Disable a hook
disable_hook("linter.file_save")
# Re-enable it
enable_hook("linter.file_save")
Unregister Hooks#
from gptme.hooks import unregister_hook, HookType
# Unregister from specific type
unregister_hook("my_hook", HookType.FILE_SAVE_POST)
# Unregister from all types
unregister_hook("my_hook")
Examples#
Pre-commit Hook#
Automatically run pre-commit checks after files are saved:
from pathlib import Path
from gptme.tools.base import ToolSpec
from gptme.hooks import HookType
from gptme.message import Message
import subprocess
def run_precommit(path: Path, content: str, created: bool):
"""Run pre-commit on saved file."""
try:
result = subprocess.run(
["pre-commit", "run", "--files", str(path)],
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
yield Message("system", f"Pre-commit checks failed:\n{result.stdout}")
else:
yield Message("system", "Pre-commit checks passed", hide=True)
except subprocess.TimeoutExpired:
yield Message("system", "Pre-commit checks timed out", hide=True)
tool = ToolSpec(
name="precommit",
desc="Automatic pre-commit checks",
hooks={
"precommit_check": (
HookType.FILE_SAVE_POST.value,
run_precommit,
5 # Run after other hooks
)
}
)
Memory/Context Hook#
Automatically add context at session start:
def add_context(logdir, workspace, initial_msgs):
"""Add relevant context at session start."""
context = load_relevant_context(workspace)
if context:
yield Message("system", f"Loaded context:\n{context}", pinned=True)
tool = ToolSpec(
name="memory",
desc="Automatic context loading",
hooks={
"load_context": (
HookType.SESSION_START.value,
add_context,
10
)
}
)
Linting Hook#
Automatically lint files after saving:
def lint_file(path: Path, content: str, created: bool):
"""Lint Python files."""
if path.suffix != ".py":
return
import subprocess
result = subprocess.run(
["ruff", "check", str(path)],
capture_output=True,
text=True
)
if result.returncode != 0:
yield Message("system", f"Linting issues:\n{result.stdout}")
tool = ToolSpec(
name="linter",
desc="Automatic Python linting",
hooks={
"lint": (HookType.FILE_SAVE_POST.value, lint_file, 5)
}
)
Built-in Hooks#
gptme ships with several built-in hooks that provide core functionality:
Session & Context
active_context: Selects relevant files to include before generationagents_md_inject: Loads AGENTS.md/CLAUDE.md when the working directory changescwd_tracking: Tracks the current working directory across tool callstime_awareness: Injects current time into contexttoken_awareness: Monitors token budget and warns when approaching limitscost_awareness: Tracks and reports LLM API costscache_awareness: Surfaces cache hit rates for prompt caching
Tool Confirmation
cli_confirm: Terminal-based tool confirmation with previewauto_confirm: Auto-approves tools in autonomous/non-interactive modeserver_confirm: Confirmation via WebUI/API for server mode
User Input
elicitation: Structured user input (forms, choices) in CLIserver_elicit: Elicitation via WebUI/API for server modeform_autodetect: Detects when assistant output contains form-like choices
Code Quality
markdown_validation: Detects codeblock cut-offs in generated content
Agent Awareness
workspace_agents: Detects parallel agents (gptme, Claude Code, Codex, Goose, OpenCode, Amp) running in the same workspace
Best Practices#
Keep hooks fast: Hooks run synchronously and can slow down operations
Handle errors gracefully: Use try-except to prevent hook failures from breaking the system
Use priorities wisely: Higher priority hooks run first (use for dependencies)
Return Messages appropriately: Use
hide=Truefor verbose/debug messagesTest hooks thoroughly: Hooks run in the main execution path
Document hook behavior: Explain what your hooks do and when they run
Consider disabling hooks: Make hooks easy to disable via configuration
Thread Safety#
The hook registry is thread-safe. Each thread maintains its own tool state, and hooks are registered per-thread.
When running in server mode with multiple workers, hooks must be registered in each worker process.
Configuration#
Hooks can be configured via environment variables:
# Example: disable specific hooks
export GPTME_HOOKS_DISABLED="linter.lint,precommit.precommit_check"
# Example: set hook priorities
export GPTME_HOOK_PRIORITY_LINTER=20
Migration Guide#
Converting Existing Features to Hooks#
If you have features that should be hooks:
Identify the appropriate hook type: Choose from the available hook types
Extract the logic: Move the feature logic into a hook function
Register the hook: Add it to a ToolSpec or register programmatically
Test thoroughly: Ensure the hook works in all scenarios
Update documentation: Document the new hook
Example: Converting pre-commit checks to a hook#
Before (hard-coded in chat.py):
# In chat.py
if check_for_modifications(log):
run_precommit_checks()
After (as a hook):
# In a tool
def precommit_hook(log, workspace):
if check_for_modifications(log):
run_precommit_checks()
tool = ToolSpec(
name="precommit",
hooks={
"check": (HookType.TURN_POST.value, precommit_hook, 5)
}
)
API Reference#
Hook system for extending gptme functionality at various lifecycle points.
This package provides a hook registry for registering and triggering hooks at various points in the gptme lifecycle. The system is split into:
types: Type definitions (Protocol classes, HookType enum, Hook dataclass)registry: Hook registry, registration, and execution infrastructureconfirm: Tool confirmation hookselicitation: Structured user input hooks
Individual hook implementations live in their own modules (e.g., cwd_changed,
time_awareness, workspace_agents).
- gptme.hooks.init_hooks(allowlist: list[str] | None = None, interactive: bool = False, no_confirm: bool = False, server: bool = False) None#
Initialize and register hooks in a thread-safe manner.
Mode detection for confirmation hooks: - Interactive CLI mode with confirmation: Registers cli_confirm hook - Server mode with confirmation: Registers server_confirm hook - Non-interactive mode: No confirmation hook (autonomous/auto-confirm)
- Parameters:
allowlist – Explicit list of hooks to register (replaces defaults). If not provided, defaults will be loaded from env/config.
interactive – Whether running in interactive mode (CLI).
no_confirm – Whether to skip tool confirmations.
server – Whether running in server mode (API/WebUI).
See Also#
Tools - Tool system documentation
Configuration - Configuration options
Issue #156 - Original feature request