# How ai-memory Lifecycle Hooks Capture and Sanitize Agent Observations

> Discover how ai-memory lifecycle hooks capture and sanitize agent observations using Axum, pure capture policies, and SQLite persistence. Protect sensitive data efficiently.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-20

---

**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`](https://github.com/akitaonrails/ai-memory/blob/main/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:
- [[`crates/ai-memory-hooks/src/router.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs) – Entry points and orchestration
- [[`crates/ai-memory-hooks/src/assistant_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/assistant_capture.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/assistant_capture.rs) – Assistant message stripping logic
- [[`crates/ai-memory-hooks/src/payload.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/payload.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/payload.rs) – `HookEnvelope` structure definitions

### 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`](https://github.com/akitaonrails/ai-memory/blob/main/capture_policy.rs).

The policy resolver checks the envelope against rules derived from the nearest [`.ai_memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.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:
- [[`crates/ai-memory-hooks/src/router.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs) – `inspect_capture_envelope` function implementation
- [[`crates/ai-memory-hooks/src/capture_policy.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/capture_policy.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/capture_policy.rs) – `CapturePolicy::inspect` and protocol parsing

### 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:
- [[`crates/ai-memory-core/src/sanitize.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/sanitize.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/sanitize.rs) – `Sanitizer` implementation and privacy rules
- [[`crates/ai-memory-hooks/src/router.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs) – `process_envelope` and persistence logic

## Code Examples

### Sending a Hook Event from a Client

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

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/capture_policy.rs) inspects every incoming envelope for `_ai_memory_capture` markers. Based on project-specific [`.ai_memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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.