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:
- [
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) – 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) –HookEnvelopestructure 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.
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, andtool_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) –inspect_capture_envelopefunction 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) –CapturePolicy::inspectand 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) –Sanitizerimplementation 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) –process_envelopeand persistence logic
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_routerinrouter.rsreceives events viaPOST /hookand immediately strips assistant messages usingassistant_capture::strip_assistant_message_raw. - Policy Enforcement:
inspect_capture_envelopeapplies 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
Sanitizerfromai_memory_coreredacts 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →