Configurable hooks.json vs In-Process Hooks in OpenHuman: Architecture and Usage
OpenHuman provides two distinct hook systems: 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 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 files across four distinct trust layers, merging them with priority given to the most trusted layer. As defined in src/openhuman/hooks/config.rs, the discovery order is:
- System-level (most trusted)
- Global user directory (
~/.openhuman/) - Workspace directory
- Action/project directory (least trusted)
The core uses the HookOutput::merge method found in 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.
/// 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 is transformed into a runtime hook via the bridge system located in 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.
//! 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 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 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 telemetryToolHook: Intercepts and potentially blocks or modifies tool callsStopHook: Handles graceful shutdown signals
/// 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 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.
/// 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:
{
"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 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:
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:
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.jsonprovides a user-extensible, out-of-process hook system discovered from layered configuration files (system →~/.openhuman/→ workspace → project) and executed via the bridge insrc/openhuman/hooks/bridge.rs.- In-process hooks are Rust traits (
PostTurnHook,ToolHook,StopHook) defined insrc/openhuman/agent/hooks.rsthat 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 layers) during the agent lifecycle. Typically, in-process hooks handle built-in behaviors like tool gating and cleanup, while 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, 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), they must be compiled into the binary. Users requiring Python or JavaScript logic should use the 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 processes layers in the order: system, global (~/.openhuman/), workspace, and project. Enable debug-level logging in the OpenHuman core to see which 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.
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 →