How ai-memory Lifecycle Hooks Capture and Sanitize Agent Observations

ai-memory lifecycle hooks intercept agent tool-use events via an Axum HTTP router, evaluate them against pure capture policies to filter or redact sensitive data, then sanitize observations through a central Sanitizer before atomic SQLite persistence.

The akitaonrails/ai-memory repository implements a privacy-first observation pipeline that processes every LLM agent interaction through a strict three-stage lifecycle. This architecture ensures that raw tool arguments, file paths, and assistant messages never persist in storage without explicit policy approval and comprehensive sanitization.

The Three-Stage Hook Pipeline

The hook server (crates/ai-memory-hooks) processes observations through distinct phases that separate ingestion, policy enforcement, and persistence concerns.

Stage 1: HTTP Ingestion and Assistant Message Stripping

When an agent emits a tool-use event, the Axum router receives a POST /hook request (or POST /hook/batch for bulk operations). The hook_router function in router.rs immediately applies defensive stripping via assistant_capture::strip_assistant_message_raw, which removes any raw assistant_message fields from the JSON payload before construction of the internal HookEnvelope.

This backstop ensures that assistant message content never enters the processing pipeline, regardless of client configuration. The router then constructs the envelope through HookEnvelope::from_query_and_body, merging URL query parameters (event, agent, etc.) with the sanitized JSON body.

Relevant source files:

Stage 2: Capture Policy Evaluation

Before any storage occurs, the system evaluates the optional _ai_memory_capture marker through inspect_capture_envelope. This function parses the marker using CaptureProtocol::parse and executes a pure, IO-free capture policy via CapturePolicy::inspect defined in capture_policy.rs.

The policy resolver checks the envelope against rules derived from the nearest .ai_memory.toml marker and returns one of three dispositions:

  • Keep – Persist the full observation
  • Drop – Silently discard the event
  • Metadata-only – Replace the payload body with a safe allow-list containing only session_id, cwd, tool_family, tool_name, and tool_call_id, stripping all tool arguments and file paths

This evaluation operates under strict bounded guards: MAX_IGNORE_PATTERNS, MAX_CANDIDATE_PATH_CHARS, and MAX_MATCH_WORK prevent pathological glob matching from causing denial-of-service.

Source references:

Stage 3: Sanitization and Atomic Persistence

Observations passing the capture filter enter process_envelope, where the HookState applies its Sanitizer instance from the core library. The state.sanitizer.sanitize(&env.raw) call redacts PII, environment variables, and authentication tokens according to globally-configured privacy rules.

After sanitization, the system creates a NewObservation record and dispatches it to the WriterHandle—a single-writer SQLite actor that guarantees indexes and data commit within the same transaction. This single-writer invariant eliminates race conditions and ensures consistency across the observation store.

Key components:

Code Examples

Sending a Hook Event from a Client

use reqwest::Client;
use serde_json::json;

#[tokio::main]
async fn main() {
    let client = Client::new();
    let body = json!({
        "tool_name": "edit_file",
        "tool_input": { 
            "file_path": "secret/token.txt", 
            "old_value": "...", 
            "new_value": "..." 
        },
        "session_id": "sess-123",
        "cwd": "/repo"
    });

    let url = "http://127.0.0.1:49374/hook?event=pre_tool_use&agent=claude-code";
    
    let resp = client.post(url).json(&body).send().await.unwrap();
    assert_eq!(resp.status(), 202); // Accepted for processing
}

Internal Server Processing Flow

// Simplified excerpt from router.rs
async fn handle_hook(
    State(state): State<std::sync::Arc<HookState>>,
    Json(mut raw): Json<serde_json::Value>,
) -> impl axum::response::IntoResponse {
    use axum::http::StatusCode;
    
    // 1. Strip assistant messages immediately
    assistant_capture::strip_assistant_message_raw(&mut raw);
    
    // 2. Build envelope and apply optional backstop
    let mut env = HookEnvelope::from_query_and_body(query, raw);
    assistant_capture::apply_assistant_backstop(&mut env, state.capture_assistant_enabled);
    
    // 3. Evaluate capture policy
    let Some(env) = inspect_capture_envelope(env) else {
        return (StatusCode::ACCEPTED, "capture policy dropped");
    };
    
    // 4. Sanitize privacy-sensitive fields
    let sanitized = state.sanitizer.sanitize(&env.raw);
    
    // 5. Persist via single-writer actor
    let observation = NewObservation::from_envelope(env, sanitized);
    state.writer.ingest(observation).await.unwrap();
    
    (StatusCode::ACCEPTED, "queued")
}

Security and Performance Safeguards

The ai-memory hook pipeline implements defense-in-depth through several architectural constraints:

Zero-Knowledge Ingestion: Even if a malicious client embeds secrets in tool arguments, the capture policy either drops the event or rewrites it to metadata-only form before storage. Raw sensitive data never touches the SQLite database without explicit configuration.

Bounded Computation: All glob matching, candidate path extraction, and pattern normalization operate under strict limits defined in capture_policy.rs. The system caps MAX_IGNORE_PATTERNS, MAX_CANDIDATE_PATH_CHARS, and MAX_MATCH_WORK to prevent resource exhaustion attacks via deeply nested paths or complex ignore patterns.

Single-Writer Consistency: By funneling every write through the WriterHandle actor, the system maintains the invariant that indexes and data always commit atomically. This eliminates background-task race conditions and ensures that observation queries reflect consistent states.

Summary

  • HTTP Ingestion: The hook_router in router.rs receives events via POST /hook and immediately strips assistant messages using assistant_capture::strip_assistant_message_raw.
  • Policy Enforcement: inspect_capture_envelope applies a pure capture policy that can keep, drop, or replace events with metadata-only payloads, preventing sensitive file paths and tool arguments from storage.
  • Privacy Sanitization: The Sanitizer from ai_memory_core redacts PII and secrets before persistence, serving as the single source of truth for privacy rules across the system.
  • Atomic Persistence: A single-writer SQLite actor (WriterHandle) guarantees transactional consistency between observation data and search indexes.

Frequently Asked Questions

How does ai-memory prevent sensitive tool arguments from being stored?

The capture policy evaluator in capture_policy.rs inspects every incoming envelope for _ai_memory_capture markers. Based on project-specific .ai_memory.toml configuration, it can replace the entire tool argument payload with a metadata-only body containing only safe fields like session_id, cwd, and tool_name. This occurs before the sanitization stage, ensuring sensitive paths and parameters never reach the database.

What happens when a capture policy decides to drop an observation?

When CapturePolicy::inspect returns a Drop disposition, the inspect_capture_envelope function returns None, causing the router to skip all further processing. The client receives an HTTP 202 Accepted response (maintaining compatibility with fire-and-forget instrumentation), but the observation is never sanitized, persisted, or written to the SQLite store.

Why does ai-memory use a single-writer actor for SQLite operations?

The WriterHandle actor serializes all database writes to enforce the invariant that indexes and observation data commit within the same transaction. This architectural choice eliminates race conditions between background indexing tasks and foreground ingestion, preventing index corruption and ensuring query consistency without complex locking mechanisms.

Where is the sanitization logic defined in the codebase?

The Sanitizer struct and its sanitization rules reside in crates/ai-memory-core/src/sanitize.rs. This crate provides the central sanitize method used by the hook router in crates/ai-memory-hooks/src/router.rs, ensuring uniform privacy protection across both the HTTP hook interface and any other observation ingestion points in the system.

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 →