# Creating Configurable Hooks for OpenHuman Agent Tool Calls

> Learn how to create configurable hooks for OpenHuman agent tool calls. Inject custom logic easily with PostTurnHook and ToolHook interfaces for enhanced agent functionality.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: how-to-guide
- Published: 2026-08-29

---

**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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json). This bridge, implemented in **[`src/openhuman/hooks/bridge.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json)** (schema defined in [`src/openhuman/hooks/schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) specifying the tool pattern and script location:

```json
{
  "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`](https://github.com/tinyhumansai/openhuman/blob/main/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`.

```sh
#!/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`](https://github.com/tinyhumansai/openhuman/blob/main/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:

```rust
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:

```rust
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`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/ops.rs) delegates to the registered bridge, allowing [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) configurations to influence your tool's behavior without hardcoding logic.

## Summary

- **Core traits**: `PostTurnHook` and `ToolHook` in [`src/openhuman/agent/hooks.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/bridge.rs) maps [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/schemas.rs). You can configure a single script entry in [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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.