# Configurable hooks.json vs In-Process Hooks in OpenHuman: Architecture and Usage

> Understand configurable hooks.json and in-process hooks in OpenHuman. Customize behavior with out-of-process scripts or gain compiled-in control with Rust traits.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: architecture
- Published: 2026-08-30

---

**OpenHuman provides two distinct hook systems: [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) for user-authored, out-of-process scripts that customize behavior without recompilation, and in-process Rust traits for embedders needing compiled-in control over core events.**

The OpenHuman agent framework separates extensibility into two complementary hook architectures. The **configurable hooks** system allows end users and organizations to inject policy and automation via external scripts, while **in-process hooks** provide the embedder with direct, native integration into the agent's execution lifecycle. Understanding when to use each system is critical for both system administrators customizing agent behavior and developers embedding the core.

## What Are Configurable Hooks (hooks.json)?

The [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) system provides a declarative, file-based mechanism for extending agent behavior without modifying source code. According to the OpenHuman source code, this system follows the **Cursor hooks JSON contract** and supports multi-layer configuration discovery.

### Discovery and Layering

At startup, the core searches for [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) files across four distinct trust layers, merging them with priority given to the most trusted layer. As defined in [`src/openhuman/hooks/config.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/config.rs), the discovery order is:

1. System-level (most trusted)
2. Global user directory (`~/.openhuman/`)
3. Workspace directory
4. Action/project directory (least trusted)

The core uses the `HookOutput::merge` method found in [`src/openhuman/hooks/types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/types.rs) (line 166) to combine layers, ensuring that a less-trusted layer cannot override security-critical configurations from a more-trusted layer.

```rust
/// The configuration file name – `hooks.json` is looked up in multiple
/// locations and merged in a trusted‑to‑untrusted order.
/// (src/openhuman/hooks/config.rs, line 81)
pub const HOOKS_FILE_NAME: &str = "hooks.json";

```

### Execution Model and Bridge Architecture

Each entry in [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) is transformed into a runtime hook via the **bridge** system located in [`src/openhuman/hooks/bridge.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/bridge.rs) (lines 8-10). The function `build_bridge()` converts JSON definitions into executable hook objects that spawn child processes.

Hook scripts run **out-of-process** and communicate via STDIN and STDOUT using JSON envelopes. The core enforces timeouts and sandboxing on these child processes, making this suitable for untrusted or polyglot scripts.

```rust
//! At bootstrap, turn every configured `hooks.json` entry into behaviour.
/// (src/openhuman/hooks/bridge.rs, line 8‑10)
pub fn build_bridge(...) -> HookBridge { ... }

```

### Security and Trust Boundaries

The [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) system implements a robust security model where the **layer ordering** prevents privilege escalation. The host verifies that declared script binaries are executable and respect configured sandbox settings before invoking them. This allows organizations to enforce policies—such as requiring manager approval for file-write operations—without recompiling the core.

## What Are In-Process Hooks?

In-process hooks provide the embedder (the host application) with direct, compiled-in control over core agent events. These are implemented as Rust **trait objects** defined in [`src/openhuman/agent/hooks.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/hooks.rs) and registered at bootstrap time.

### Core Trait Definitions

The primary hook traits established in the OpenHuman source code include:

- **`PostTurnHook`**: Executes after each turn completion for cleanup or telemetry
- **`ToolHook`**: Intercepts and potentially blocks or modifies tool calls
- **`StopHook`**: Handles graceful shutdown signals

```rust
/// Called after each turn – embedder can clean up or emit telemetry.
/// (src/openhuman/agent/hooks.rs, line 12‑15)
pub trait PostTurnHook: Send + Sync {
    fn post_turn(&self, ctx: &TurnContext) -> HookResult;
}

/// Decides whether a tool call may proceed, be denied, or require a prompt.
/// (src/openhuman/agent/hooks.rs, line 28‑33)
pub trait ToolHook: Send + Sync {
    fn decide(&self, call: &ToolCall) -> ToolHookDecision;
}

```

### Execution Model

Unlike configurable hooks, in-process hooks run **directly within the same Rust thread or async task** as the core. No serialization or process spawning occurs, resulting in minimal latency. The core calls trait methods synchronously when events occur, such as when [`src/openhuman/agent/dispatcher.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/dispatcher.rs) consults the `ToolHook` before executing a tool call.

### Registration via CoreBuilder

Embedders supply concrete implementations when constructing the core using `CoreBuilder`. For example, `core::builder::CoreBuilder::post_turn_hook` accepts a boxed trait object that the core retains for the agent's lifetime.

```rust
/// Graceful stop hook used by the test harness.
/// (src/openhuman/agent/stop_hooks.rs, line 5‑9)
pub struct TestStopHook;
impl StopHook for TestStopHook { ... }

```

## Key Differences Between Hook Systems

| Aspect | Configurable Hooks (hooks.json) | In-Process Hooks (Rust traits) |
|--------|--------------------------------|--------------------------------|
| **Author** | End users and system administrators | Core developers and embedders |
| **Language** | Any executable (shell, Python, etc.) | Rust only |
| **Process Model** | Out-of-process child process | In-process function call |
| **Communication** | JSON via STDIN/STDOUT | Native Rust values |
| **Performance** | Higher latency (process spawn) | Zero-copy, immediate execution |
| **Discovery** | File-system layers at startup | Compile-time registration via `CoreBuilder` |
| **Security** | Sandboxed, layer-based trust | Compile-time trust model |

## Practical Implementation Examples

### Creating a hooks.json Audit Policy

Place the following in `~/.openhuman/hooks.json` to audit every file-write tool before execution:

```json
{
  "audit_file_writes": {
    "event": "pre_tool_use",
    "command": "/usr/local/bin/audit-write.sh",
    "args": ["{{tool_name}}", "{{args}}"]
  }
}

```

The core loads and merges this configuration at startup. The bridge invokes [`audit-write.sh`](https://github.com/tinyhumansai/openhuman/blob/main/audit-write.sh) with the tool details; if the script exits non-zero, the core denies the tool call.

### Implementing a Custom ToolHook in Rust

For embedders requiring complex gating logic, implement the `ToolHook` trait to force manager approval for specific operations:

```rust
use openhuman::agent::hooks::{ToolHook, ToolHookDecision};

struct ManagerApprovalHook;

impl ToolHook for ManagerApprovalHook {
    fn decide(&self, call: &ToolCall) -> ToolHookDecision {
        if call.name == "dangerous_file_write" {
            // Ask the UI for manager approval; `Ask` will pause the turn.
            ToolHookDecision::Ask("manager_approval".into())
        } else {
            ToolHookDecision::Proceed
        }
    }
}

// Register the hook when building the core:
let core = CoreBuilder::new()
    .tool_hook(Box::new(ManagerApprovalHook))
    .build()
    .await?;

```

### Adding a PostTurnHook for Cleanup

Use in-process hooks to manage resources that persist across turns:

```rust
use openhuman::agent::hooks::PostTurnHook;

struct CleanupHook;

impl PostTurnHook for CleanupHook {
    fn post_turn(&self, ctx: &TurnContext) -> HookResult {
        std::fs::remove_dir_all("/tmp/openhuman_tmp")
            .ok(); // ignore errors
        HookResult::Ok
    }
}

```

This executes immediately after each turn completes, ensuring temporary files are removed before the next iteration begins.

## Summary

- **[`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json)** provides a user-extensible, out-of-process hook system discovered from layered configuration files (system → `~/.openhuman/` → workspace → project) and executed via the bridge in [`src/openhuman/hooks/bridge.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/bridge.rs).
- **In-process hooks** are Rust traits (`PostTurnHook`, `ToolHook`, `StopHook`) defined in [`src/openhuman/agent/hooks.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/hooks.rs) that give embedders direct, zero-overhead control over agent events.
- Configurable hooks prioritize flexibility and polyglot support at the cost of process-spawn overhead, while in-process hooks prioritize performance and deep integration.
- Security for configurable hooks relies on **layer ordering** and sandboxing, whereas in-process hooks rely on the Rust compile-time trust boundary.

## Frequently Asked Questions

### Can I use both hook systems simultaneously in the same OpenHuman deployment?

Yes. The two systems are complementary and designed to coexist. The core consults both in-process hooks (registered via `CoreBuilder`) and configurable hooks (loaded from [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) layers) during the agent lifecycle. Typically, in-process hooks handle built-in behaviors like tool gating and cleanup, while [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) handles organization-specific policies and external integrations.

### What happens if a hooks.json script fails or times out?

According to the bridge implementation in [`src/openhuman/hooks/bridge.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/bridge.rs), the core enforces strict timeouts on out-of-process hook execution. If a script fails to respond or exits with a non-zero status code, the core treats this as a denial signal. For `pre_tool_use` events, this results in the tool call being blocked; for other events, the failure is logged but may not halt execution depending on the hook's criticality.

### Why can't I write in-process hooks in Python or JavaScript?

In-process hooks are implemented as Rust trait objects that must satisfy `Send + Sync` bounds and integrate directly with the core's async runtime. Because they execute within the same memory space and call stack as the agent dispatcher (particularly in [`src/openhuman/agent/dispatcher.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/dispatcher.rs)), they must be compiled into the binary. Users requiring Python or JavaScript logic should use the [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) system, which spawns external processes and communicates via JSON.

### How do I debug which hooks.json files are being loaded?

The discovery logic in [`src/openhuman/hooks/config.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/hooks/config.rs) processes layers in the order: system, global (`~/.openhuman/`), workspace, and project. Enable debug-level logging in the OpenHuman core to see which [`hooks.json`](https://github.com/tinyhumansai/openhuman/blob/main/hooks.json) files are discovered and merged via the `HookOutput::merge` operation. The core logs the absolute path of each file processed and any merge conflicts that occur between layers.