Design: Hook-Based Tool Confirmations#
Issue: #1104 Author: Bob Date: 2026-01-10 Status: In Progress
Implementation Status#
Phase |
Component |
Status |
|---|---|---|
1 |
HookType.TOOL_CONFIRM |
✅ Complete |
1 |
ConfirmationResult dataclass |
✅ Complete |
1 |
ToolConfirmHook protocol |
✅ Complete |
1 |
get_confirmation() function |
✅ Complete |
2 |
cli_confirm_hook |
✅ Complete |
2 |
auto_confirm_hook |
✅ Complete |
2 |
Hook registration in chat.py |
✅ Complete |
2 |
confirm_bridge utilities |
✅ Complete |
2 |
confirm_func integration |
✅ Complete |
3 |
server_confirm_hook |
✅ Complete |
3 |
HTTP endpoint integration |
✅ Complete |
3 |
Tests (32 passing) |
✅ Complete |
4 |
Server context vars for SSE |
✅ Complete |
4 |
Server hook registration |
✅ Complete |
4 |
V1 API hook-aware confirm_func |
✅ Complete |
5 |
Tool migration |
❌ Reverted (see notes) |
6 |
Simplification & cleanup |
✅ Complete |
6.1 |
Consolidate preview printing |
✅ Complete |
6.2 |
Centralize auto-confirm state |
✅ Complete |
6.3 |
Unify server auto-confirm |
✅ Complete |
6.4 |
Consolidate help text |
✅ Complete |
7 |
Remove ask_execute fallback |
✅ Complete |
7.1 |
Simplify confirm_func |
✅ Complete |
7.2 |
Move CLI hook to init_hooks |
✅ Complete |
7.3 |
Use contextvars for auto-confirm |
✅ Complete |
8 |
Tool auto-approve via ToolSpec hooks |
✅ Complete |
8.1 |
Hook fall-through support |
✅ Complete |
8.2 |
Shell allowlist hook |
✅ Complete |
8.3 |
Tests for fall-through & allowlist |
✅ Complete |
Current state: Phases 1-4, 6, 7, 8 complete. Phase 5 was reverted.
Implemented:
confirm_funcinchat.pyalways uses hooks (noask_executefallback)confirm_funcinapi.py(v1) uses hooks when available, falling back to auto-confirmServer’s HTTP endpoint resolves hook-based confirmations via
_resolve_hook_confirmationServer hook now emits SSE events and blocks until client responds via HTTP endpoint
Context vars (
current_conversation_id,current_session_id) provide session context to hooks
Phase 5 Reversion Notes: The Phase 5 “tool migration” was reverted because:
It added ~88 lines without removing any (violated simplification goal)
Tools were creating ToolUse objects just to pass to confirmation - this is redundant since ToolUse already exists at the
ToolUse.execute()levelThe
_execute_with_hook_confirmation()helper duplicated logic fromexecute_with_confirmation()
The hook system works correctly through the confirm_func bridge without requiring tools to create ToolUse objects.
Architecture notes:
V1 API: Uses hook-aware confirm_func, auto-confirms when no context vars set (legacy behavior)
V2 API: Uses separate
pending_toolsmechanism + hook resolution for HTTP confirmationsCLI: Uses hook-aware confirm_func, routes through cli_confirm_hook when registered
Hooks receive confirmation requests via
make_confirm_func_from_hooks()bridge
Phase 6.1 Notes (Completed):
Consolidated duplicate _print_preview in cli_confirm.py by importing shared print_preview
from ask_execute.py. This reduced cli_confirm.py by 10 lines (245 → 235) and eliminates
duplicate preview logic.
Phase 6.4 Notes (Completed):
Extracted shared print_confirmation_help() function in ask_execute.py. Both
ask_execute and cli_confirm_hook now use this shared function instead of
maintaining duplicate help text. Reduced cli_confirm.py by 19 lines.
Phase 6.2-6.3 Notes (Completed):
Centralized auto-confirm state in confirm.py with unified functions:
set_auto_confirm(count)- Set auto-confirm (count or infinite)reset_auto_confirm()- Reset to defaultscheck_auto_confirm()- Check and decrement (returns tuple)is_auto_confirm_active()- Check without decrementing
Both cli_confirm.py and ask_execute.py now use this centralized state instead of
maintaining their own duplicate globals. Server auto-confirm is also unified -
server_confirm_hook now checks centralized state first before checking session context.
Phase 7 Notes (Completed): Removed ask_execute fallback from chat.py per Erik’s suggestion:
confirm_func now always uses hooks via make_confirm_func_from_hooks()
CLI hook registration moved into init_hooks() via hook_allowlist parameter
Auto-confirm state converted to ContextVars for thread safety in server mode
When no_confirm=True, no CLI hook is registered, so get_confirmation() auto-confirms
Phase 8 Notes (Completed): Tools register their own auto-approve hooks via ToolSpec.hooks per Erik’s suggestion:
Modified
get_confirmation()to support fall-through: hooks returning None pass to next hookHooks are tried in priority order (highest first), first non-None result wins
Updated
ToolConfirmHookprotocol: now returnsConfirmationResult | NoneShell tool registers
shell_allowlist_hookwith priority 10 (higher than CLI hook at 0)Shell allowlist hook auto-confirms allowlisted commands, returns None for others
This keeps ToolSpec clean (no new fields) while enabling tool-specific auto-approve
Tests added: 3 fall-through tests + 5 shell allowlist tests (27 total passing)
Example usage for other tools:
def my_tool_auto_approve(tool_use, preview=None, workspace=None):
"""Auto-approve safe operations, fall through for others."""
if is_safe(tool_use):
return ConfirmationResult.confirm()
return None # Fall through to CLI/server hook
tool = ToolSpec(
name="my_tool",
hooks={
"auto_approve": ("tool_confirm", my_tool_auto_approve, 10),
},
...
)
Next steps:
✅ Phase 6.1-6.4: Consolidation complete
✅ Phase 7: Remove ask_execute fallback
✅ Phase 8: Tool auto-approve via ToolSpec hooks
Phase 6.5: Document hook API for custom confirmation backends
Phase 6.6: Add examples for new backends (GUI, Discord bot)
Future: Consider moving confirmation to ToolUse.execute()
Problem Statement#
gptme currently has two separate implementations for tool confirmation:
CLI (
ask_execute.py): Interactive terminal-based confirmation with rich featuresServer V2 (
api_v2_sessions.py): SSE event-based confirmation with pending tool queue
These implementations are not harmonized:
CLI uses
ask_execute()called directly from tools viaConfirmFuncServer V2 uses
pending_toolsdict withToolExecutionstate machineServer V1 has no real confirmation support (always auto-confirms)
No shared abstraction for confirmation logic
Goals#
Harmonize CLI and Server confirmation implementations
Leverage hooks for extensibility and clean separation
Maintain existing functionality (edit, copy, auto-confirm)
Simplify tool implementations by removing confirmation boilerplate
Enable new confirmation backends (e.g., GUI, Discord bot)
Current Architecture#
CLI Flow#
User/Tool → execute_with_confirmation() → ask_execute() → User Input → Execute
↓
print_preview()
↓
editable/copiable state
Server V2 Flow#
Tool Execute → Store in pending_tools → SSE Event (tool_pending) → Client Decides
↓
/api/v2/.../tool/confirm
↓
Execute or Skip
Key Differences#
Aspect |
CLI |
Server V2 |
|---|---|---|
Blocking |
Synchronous (blocks thread) |
Async (event-based) |
Input |
Terminal prompt |
HTTP endpoint |
Features |
edit, copy, auto |
confirm, edit, skip, auto |
State |
Global variables |
Session object |
Notification |
Bell sound |
SSE event |
Proposed Design#
Core Concept: tool.confirm Hook#
Introduce a new hook type tool.confirm that handles the confirmation decision:
class HookType(str, Enum):
# Existing hooks
TOOL_EXECUTE_PRE = "tool.execute.pre"
TOOL_EXECUTE_POST = "tool.execute.post"
# New confirmation hook
TOOL_CONFIRM = "tool.confirm"
Confirmation Protocol#
The tool.confirm hook follows a request-response protocol:
Request Phase: System triggers
tool.confirmwith tool detailsDecision Phase: Hook implementation gathers user/client decision
Response Phase: Hook yields a
ConfirmationResult
from dataclasses import dataclass
from typing import Literal
@dataclass
class ConfirmationResult:
"""Result of a tool confirmation request."""
action: Literal["confirm", "skip", "edit"]
edited_content: str | None = None
auto_confirm_remaining: int = 0
Hook Protocol#
from typing import Protocol, Generator
from pathlib import Path
class ToolConfirmHook(Protocol):
"""Hook for tool confirmation decisions."""
def __call__(
self,
tooluse: "ToolUse",
preview: str | None,
workspace: Path | None,
) -> Generator[ConfirmationResult, None, None]:
"""Request confirmation for tool execution.
Args:
tooluse: The tool about to be executed
preview: Optional preview content for display
workspace: Workspace directory path
Yields:
ConfirmationResult with the user's decision
"""
pass
Implementation Architecture#
┌─────────────────────────────────────┐
│ ToolUse.execute() │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ trigger_hook(TOOL_CONFIRM) │
└─────────────────┬───────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CLI Confirm Hook│ │Server Confirm │ │ Auto Confirm │
│ (terminal input)│ │Hook (SSE/HTTP) │ │ Hook (always) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ ConfirmationResult │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Execute or Skip Tool │
└─────────────────────────────────────┘
Hook Implementations#
1. CLI Confirmation Hook#
def cli_confirm_hook(
tooluse: ToolUse,
preview: str | None,
workspace: Path | None,
) -> Generator[ConfirmationResult, None, None]:
"""CLI-based confirmation using terminal input."""
# Show preview if available
if preview:
print_preview(preview, tooluse.tool, copy=True)
# Make content editable
if tooluse.content:
set_editable_text(tooluse.content, get_extension(tooluse))
# Get user decision via terminal prompt
confirmed = ask_execute(f"Execute {tooluse.tool}?")
if confirmed:
edited = get_editable_text() if editable else None
was_edited = edited != tooluse.content if edited else False
yield ConfirmationResult(
action="edit" if was_edited else "confirm",
edited_content=edited if was_edited else None,
)
else:
yield ConfirmationResult(action="skip")
2. Server Confirmation Hook#
def server_confirm_hook(
tooluse: ToolUse,
preview: str | None,
workspace: Path | None,
) -> Generator[ConfirmationResult, None, None]:
"""Server-based confirmation using SSE events."""
session = get_current_session()
# Check auto-confirm
if session.auto_confirm_count > 0:
session.auto_confirm_count -= 1
yield ConfirmationResult(action="confirm")
return
# Create pending tool entry
tool_id = str(uuid.uuid4())
session.pending_tools[tool_id] = ToolExecution(
tooluse=tooluse,
status=ToolStatus.PENDING,
)
# Emit SSE event
emit_event("tool_pending", {
"tool_id": tool_id,
"tool": tooluse.tool,
"content": tooluse.content,
"preview": preview,
})
# Wait for client decision (via HTTP endpoint)
result = wait_for_confirmation(tool_id)
yield result
3. Auto-Confirm Hook (Non-Interactive Mode)#
def auto_confirm_hook(
tooluse: ToolUse,
preview: str | None,
workspace: Path | None,
) -> Generator[ConfirmationResult, None, None]:
"""Always confirms - for non-interactive/autonomous mode."""
yield ConfirmationResult(action="confirm")
Integration Points#
Tool Execution Flow#
# In tools/base.py - ToolUse.execute()
def execute(self, confirm: ConfirmFunc) -> Generator[Message, None, None]:
# Trigger confirmation hook
confirm_results = list(trigger_hook(
HookType.TOOL_CONFIRM,
tooluse=self,
preview=self.get_preview(),
workspace=get_workspace(),
))
if not confirm_results:
# No confirmation hook registered - fall back to confirm function
if not confirm(f"Execute {self.tool}?"):
yield Message("system", "Aborted")
return
result = ConfirmationResult(action="confirm")
else:
result = confirm_results[0]
# Handle result
if result.action == "skip":
yield Message("system", "Operation skipped by user")
return
if result.action == "edit" and result.edited_content:
self.content = result.edited_content
# Proceed with execution
yield from self._do_execute()
Hook Registration#
# In gptme/main.py or gptme/chat.py
def init_confirmation_hooks(interactive: bool, server_mode: bool):
"""Register appropriate confirmation hook based on mode."""
if server_mode:
register_hook(
name="server_confirm",
hook_type=HookType.TOOL_CONFIRM,
func=server_confirm_hook,
priority=100,
)
elif interactive:
register_hook(
name="cli_confirm",
hook_type=HookType.TOOL_CONFIRM,
func=cli_confirm_hook,
priority=100,
)
else:
register_hook(
name="auto_confirm",
hook_type=HookType.TOOL_CONFIRM,
func=auto_confirm_hook,
priority=100,
)
Evaluation Dimensions#
Dimension 1: Code Simplification#
Criteria: Does this reduce complexity in tool implementations?
Score |
Description |
|---|---|
1 |
Increases complexity |
2 |
No change |
3 |
Minor simplification |
4 |
Moderate simplification |
5 |
Major simplification |
Current Assessment: 4/5
Rationale:
Tools no longer need to handle confirmation logic directly
execute_with_confirmation()helper can be simplified or deprecatedSingle point of confirmation logic vs scattered across tools
Minor complexity added in hook registration
Dimension 2: Extensibility#
Criteria: How easy is it to add new confirmation backends?
Score |
Description |
|---|---|
1 |
Requires core changes |
2 |
Complex integration |
3 |
Moderate effort |
4 |
Simple plugin |
5 |
Trivial addition |
Current Assessment: 5/5
Rationale:
New backends just register a hook function
No core code changes needed
Examples: Discord bot, GUI, mobile app, voice confirmation
Clear protocol makes implementation straightforward
Dimension 3: Backward Compatibility#
Criteria: Does this maintain existing behavior and APIs?
Score |
Description |
|---|---|
1 |
Breaking changes, migration required |
2 |
Breaking changes, partial migration |
3 |
Deprecation warnings, works with changes |
4 |
Fully backward compatible with deprecations |
5 |
Fully backward compatible, no changes needed |
Current Assessment: 4/5
Rationale:
ConfirmFunctype can still work (fallback when no hook)ask_execute()still functions (wrapped by CLI hook)Server V2 API unchanged externally
Internal refactoring required for
execute_with_confirmation()
Dimension 4: Testability#
Criteria: How testable is the new design?
Score |
Description |
|---|---|
1 |
Untestable / requires manual testing |
2 |
Difficult to test |
3 |
Moderate test effort |
4 |
Easy to unit test |
5 |
Excellent testability with mocks |
Current Assessment: 5/5
Rationale:
Hooks are pure functions that can be mocked
ConfirmationResultis a simple dataclassCan test each hook implementation independently
Can test tool execution with different hook configurations
Dimension 5: Server Harmonization#
Criteria: Does this improve CLI/Server code sharing?
Score |
Description |
|---|---|
1 |
More divergence |
2 |
No change |
3 |
Minor sharing |
4 |
Significant sharing |
5 |
Full harmonization |
Current Assessment: 4/5
Rationale:
Same protocol for both CLI and Server
Same
ConfirmationResulttypeTool code doesn’t need to know which environment
Server still needs SSE/HTTP infrastructure (inherent)
Dimension 6: Performance Impact#
Criteria: Does this affect performance?
Score |
Description |
|---|---|
1 |
Significant slowdown |
2 |
Noticeable slowdown |
3 |
Minor impact |
4 |
Negligible impact |
5 |
No impact or improvement |
Current Assessment: 5/5
Rationale:
Hook dispatch is O(1) lookup
No additional I/O or computation
Existing confirmation logic just moves to hook
Could potentially improve by reducing redundant preview generation
Overall Evaluation#
Dimension |
Score |
Weight |
Weighted |
|---|---|---|---|
Code Simplification |
4 |
20% |
0.80 |
Extensibility |
5 |
25% |
1.25 |
Backward Compatibility |
4 |
20% |
0.80 |
Testability |
5 |
15% |
0.75 |
Server Harmonization |
4 |
15% |
0.60 |
Performance Impact |
5 |
5% |
0.25 |
Total |
4.45/5 |
Implementation Plan#
Phase 1: Foundation (1-2 days)#
Add
HookType.TOOL_CONFIRMenum valueAdd
ToolConfirmHookprotocolAdd
ConfirmationResultdataclassUpdate hook type overloads
Phase 2: CLI Implementation (2-3 days)#
Create
gptme/hooks/cli_confirm.pyRefactor
ask_execute.pyto be callable by hookRegister CLI hook in interactive mode
Test with existing CLI flows
Phase 3: Server Implementation (2-3 days)#
Create
gptme/hooks/server_confirm.pyIntegrate with
api_v2_sessions.pypending_toolsRegister Server hook in server mode
Test with existing Server V2 flows
Phase 4: Tool Migration (3-5 days)#
Update
ToolUse.execute()to use confirmation hookSimplify
execute_with_confirmation()usageMigrate tools one by one
Add deprecation warnings for direct
ask_executeusage
Phase 5: Documentation & Cleanup (1-2 days)#
Document hook API
Add examples for custom confirmation backends
Remove deprecated code paths
Update tests
Risks and Mitigations#
Risk 1: Async/Sync Mismatch#
Risk: Server needs async, CLI is sync Mitigation: Hook protocol uses generators which work for both; Server hook uses threading.Event for blocking
Risk 2: State Management#
Risk: Auto-confirm count, editable state are currently global Mitigation: Move state into hook context or use ContextVars
Risk 3: Migration Complexity#
Risk: Many tools use execute_with_confirmation
Mitigation: Phased migration with backward compatibility layer
Open Questions#
Priority System: Should multiple confirmation hooks be allowed? (e.g., logging + confirmation)
Timeout: Should there be a configurable timeout for confirmation?
Preview Protocol: Should preview generation be standardized across tools?
State Location: Where should auto-confirm count live in server mode?
Alternatives Considered#
Alternative 1: Use Existing TOOL_EXECUTE_PRE#
Rejected: PRE hook doesn’t have a response mechanism; would need to modify hook system fundamentally.
Alternative 2: Middleware Pattern#
Rejected: More complex than hooks; would require new abstraction layer.
Alternative 3: Event System#
Rejected: Overkill for this use case; hooks are simpler and already exist.
Conclusion#
The hook-based confirmation design provides a clean, extensible solution that:
Harmonizes CLI and Server implementations
Maintains backward compatibility
Enables new confirmation backends
Simplifies tool implementations
Recommendation: Proceed with implementation starting from Phase 1.