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 agent
  • ToolHookDecision::Ask: Pauses execution pending user confirmation
  • ToolHookDecision::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: PostTurnHook and ToolHook in src/openhuman/agent/hooks.rs define the hook interface, receiving TurnContext and ToolHookContext respectively.
  • Global registry: The EMBEDDER_POST_TURN_HOOKS mutex stores active hooks, accessible via register_embedder_post_turn_hook and executed through fire_hooks.
  • Configurable bridge: src/openhuman/hooks/bridge.rs maps hooks.json entries to trait implementations, executing external scripts and translating JSON responses into ToolHookDecision outcomes.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →