Creating Configurable Hooks for OpenHuman Agent Tool Calls
OpenHuman provides a trait-based hook system that allows developers to inject custom logic at specific lifecycle points, including post-turn completion and individual tool call execution, through the PostTurnHook and ToolHook interfaces defined in src/openhuman/agent/hooks.rs.
Creating configurable hooks for OpenHuman agent tool calls enables you to audit, modify, or block tool executions without modifying core agent logic. The tinyhumansai/openhuman repository implements this through a minimal, security-conscious contract that runs after sandboxing and policy checks. This architecture ensures that custom behavior remains auditable while preventing privilege escalation.
Understanding the OpenHuman Hook Architecture
The hook system in OpenHuman centers on two primary traits that define extension points for agent behavior.
PostTurnHook and ToolHook Traits
The core abstractions reside in src/openhuman/agent/hooks.rs. The PostTurnHook trait defines a single method, on_turn_complete, which receives a TurnContext struct containing the assistant's response, tool call records, and thread metadata. This allows hooks to inspect or log completed turns.
The ToolHook trait operates at a finer granularity, wrapping individual tool calls. It can veto executions, modify arguments, or supply custom results before the core tool implementation runs. When a tool executes, the runtime calls fire_tool_hooks, passing a ToolHookContext that the hook uses to make decisions.
Global Hook Registry
Hooks are stored in a global mutex called EMBEDDER_POST_TURN_HOOKS. The public API exposes register_embedder_post_turn_hook for runtime registration, while fire_hooks iterates over registered instances, invoking each with the current TurnContext. If any hook returns an error, fire_hooks aborts immediately, preventing subsequent hooks from executing.
Building the Configurable Hook Bridge
The configurable hook bridge connects the generic trait system to user-supplied scripts defined in hooks.json. This bridge, implemented in src/openhuman/hooks/bridge.rs, parses JSON-RPC-style payloads and executes external scripts, converting their output into ToolHookDecision variants.
Hooks.json Schema and Bridge Registration
The bridge reads hooks.json (schema defined in src/openhuman/hooks/schemas.rs) at startup, creating a ConfiguredHookBridge instance that registers itself as both a PostTurnHook and ToolHook via register_embedder_post_turn_hook. Each entry in the configuration specifies the event type (tool_call), tool name patterns, script paths, and optional environment variables.
When the bridge intercepts a tool call, it spawns the configured script, passes the JSON payload via stdin, and expects a structured response on stdout. The supported decisions are Deny, Ask, or ProceedWith, allowing scripts to block dangerous operations or modify execution context.
Implementing Tool Call Hooks
Tool call hooks follow a strict lifecycle that preserves OpenHuman's security guarantees. Because all policy checks—including sandboxing, budget enforcement, and credential gating—complete before hooks run, implementations must remain purely side-effect-free with respect to the core's security policy.
ToolHookDecision Outcomes
When fire_tool_hooks delegates to the bridge, the script's response determines the execution path:
ToolHookDecision::Deny: Immediately aborts the tool call and returns an error to the agentToolHookDecision::Ask: Pauses execution pending user confirmationToolHookDecision::ProceedWith: Continues with potentially modified arguments
The bridge handles JSON serialization in src/openhuman/hooks/ops.rs, which provides runtime entry points like tool_call and prompt_submitted that the bridge invokes.
Step-by-Step Implementation Guide
Follow these steps to create a configurable hook that intercepts agent tool calls.
Step 1: Define the Hook Configuration
Add an entry to hooks.json specifying the tool pattern and script location:
{
"hooks": [
{
"name": "block-dangerous-tool",
"event": "tool_call",
"tool_name": "dangerous_action",
"script": "scripts/block_dangerous.sh",
"env": {
"ALLOWED_USERS": "alice,bob"
}
}
]
}
According to the schema in src/openhuman/hooks/schemas.rs, the tool_name field supports pattern matching, and the env object injects variables into the script's environment.
Step 2: Implement the External Script
Create the script that processes JSON payloads from stdin. The expected input follows the Cursor hook contract with fields like hook_event, tool_name, args, and user_id.
#!/usr/bin/env bash
# Read the JSON payload from stdin
read -r payload
# Extract the user ID from the payload
user=$(echo "$payload" | jq -r .user_id)
if [[ ",alice,bob," != *",$user,"* ]]; then
# Deny the tool call for unauthorized users
echo '{"decision":"deny","reason":"user not authorized"}'
exit 0
fi
# Allow the call to proceed unchanged
echo '{"decision":"proceed"}'
The bridge in src/openhuman/hooks/bridge.rs parses this output and maps "decision":"deny" to ToolHookDecision::Deny, causing the core to abort the tool call and complete the turn with an error message.
Step 3: Programmatic Hook Registration
For Rust-based hooks that don't use the external script bridge, implement the trait directly and register at startup:
use openhuman_core::openhuman::agent::hooks::{PostTurnHook, TurnContext};
use std::sync::Arc;
// Simple hook that logs the assistant's final reply
struct LoggingHook;
impl PostTurnHook for LoggingHook {
fn on_turn_complete(&self, ctx: TurnContext) {
println!("Turn finished – assistant said: {}", ctx.assistant_response);
}
}
// Register the hook at startup
openhuman_core::openhuman::agent::hooks::register_embedder_post_turn_hook(
Arc::new(LoggingHook) as Arc<dyn PostTurnHook>,
);
This registers the hook in EMBEDDER_POST_TURN_HOOKS, ensuring fire_hooks includes it on every turn completion.
Step 4: Consume Hooks in Tool Implementations
When implementing a custom tool, forward the call to the hook system:
use openhuman_core::openhuman::agent::hooks::{ToolHook, ToolHookContext, ToolHookDecision};
pub struct MyTool;
impl Tool for MyTool {
fn call(&self, ctx: &ToolHookContext) -> ToolHookDecision {
// Forward to the hook bridge; it may modify args or deny execution
openhuman_core::openhuman::hooks::ops::tool_call(ctx)
}
}
The tool_call function in src/openhuman/hooks/ops.rs delegates to the registered bridge, allowing hooks.json configurations to influence your tool's behavior without hardcoding logic.
Summary
- Core traits:
PostTurnHookandToolHookinsrc/openhuman/agent/hooks.rsdefine the hook interface, receivingTurnContextandToolHookContextrespectively. - Global registry: The
EMBEDDER_POST_TURN_HOOKSmutex stores active hooks, accessible viaregister_embedder_post_turn_hookand executed throughfire_hooks. - Configurable bridge:
src/openhuman/hooks/bridge.rsmapshooks.jsonentries to trait implementations, executing external scripts and translating JSON responses intoToolHookDecisionoutcomes. - Security model: Hooks run after sandboxing and policy enforcement; they can observe and log but cannot bypass security controls or elevate privileges.
- Implementation path: Define configurations in
hooks.json, implement scripts processing stdin/stdout JSON, or register Rust traits directly for native performance.
Frequently Asked Questions
How do I register a hook programmatically without using hooks.json?
You can register hooks directly in Rust by implementing the PostTurnHook trait and calling register_embedder_post_turn_hook with an Arc<dyn PostTurnHook>. This bypasses the configurable bridge entirely and executes your logic natively within the agent process. The registration persists for the lifetime of the program in the global EMBEDDER_POST_TURN_HOOKS registry.
What security guarantees apply to configurable hooks?
All configurable hooks run after OpenHuman's core security checks complete, including sandbox validation, budget enforcement, and credential gating. Hooks operate under a read-only or append-only contract—they cannot modify security policies or escape sandboxes. The bridge ensures this by parsing script output into constrained ToolHookDecision variants that the core interprets, rather than executing arbitrary code within the privileged agent context.
Can a single hook handle multiple tool calls or events?
Yes, the bridge supports pattern matching on tool names and event types as defined in src/openhuman/hooks/schemas.rs. You can configure a single script entry in hooks.json with wildcard patterns or register a single Rust struct implementing both PostTurnHook and ToolHook. When fire_hooks or fire_tool_hooks executes, it invokes all matching hooks in the order they were registered.
Where are hook contexts attached to agent sessions?
The post_turn_hooks field is attached to harness sessions in src/openhuman/agent/harness/session/types.rs. This connects the hook registry to specific agent instances, ensuring that fire_hooks receives the correct TurnContext containing thread-specific metadata like the assistant's response and tool call history for that particular session.
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 →