# How ai-memory Captures Automatic Lifecycle Hooks from Agent CLIs

> Discover how ai-memory captures automatic lifecycle hooks from agent CLIs. Learn about its fire-and-forget pipeline, typed privacy boundary, and atomic persistence for efficient AI-coding session recording.

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

---

**ai-memory records AI-coding sessions automatically through a fire-and-forget pipeline where agent CLIs emit JSON events via generated shell scripts, the server sanitizes payloads through a typed privacy boundary, and a single-writer SQLite actor persists observations atomically without blocking the CLI.**

The ai-memory project eliminates manual instrumentation by implementing a zero-configuration lifecycle capture system for agent CLIs including Claude Code, Codex, and OpenCode. By intercepting events such as `SessionStart`, `UserPrompt`, and `ToolUse` through thin shell wrappers, the system builds a complete, searchable history of development sessions while enforcing strict data privacy guarantees.

## The Three-Stage Capture Architecture

The automatic capture mechanism operates as a tightly-coupled pipeline across three distinct stages, ensuring that no user intervention is required after the initial hook installation.

### Stage 1: CLI Emission via Generated Shell Hooks

Each supported agent CLI ships with a thin shell script that the CLI invokes on specific lifecycle milestones. When events like `SessionStart`, `UserPrompt`, `ToolUse`, or `SessionEnd` occur, the script reads a JSON payload from **stdin** and forwards it via a fire-and-forget HTTP request.

The scripts are generated by the `install-hooks` command implemented in [[`crates/ai-memory-cli/src/main.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs). For example, running `ai-memory install-hooks --agent open-code` produces a bash wrapper that posts to `http://127.0.0.1:49374/hook`:

```bash
#!/usr/bin/env bash

# Auto-generated by `ai-memory install-hooks --agent open-code --apply`.

# Sends a SessionStart event to the local ai-memory server.

AI_MEMORY_HOOK_URL=http://127.0.0.1:49374/hook
exec curl -sS -X POST "$AI_MEMORY_HOOK_URL" \
  -H "Content-Type: application/json" \
  -d "$(cat -)"   # reads the JSON payload from stdin

```

These requests use a short client-side timeout, ensuring the agent CLI never blocks on network I/O.

### Stage 2: Server-Side Routing and Sanitization

The `hook_router` function defined in [[`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) exposes the `POST /hook` and `POST /hook/batch` endpoints. Upon receiving a payload, the server:

1. Parses the `HookEnvelope` (defined in [[`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)) to identify the specific `HookEvent` variant.
2. Passes the raw observation through the **`Sanitizer`** (`ai_memory_core::Sanitizer`), which enforces a *typed privacy boundary* by transforming the data into `Sanitized<NewObservation>`. This step guarantees that raw secrets are redacted before reaching storage.
3. Guards against overload by checking `DEFAULT_HOOK_INGEST_MAX_IN_FLIGHT`; if exceeded, the router returns HTTP 429 (Too Many Requests).

```rust
pub async fn hook_router(
    State(state): State<AppState>,
    Json(envelopes): Json<Vec<HookEnvelope>>,
) -> impl IntoResponse {
    // 1️⃣  Guard against overload
    if state.ingest_gates.current_load() > DEFAULT_HOOK_INGEST_MAX_IN_FLIGHT {
        return (StatusCode::TOO_MANY_REQUESTS, "overloaded");
    }

    // 2️⃣  Process each envelope
    for env in envelopes {
        let (event, raw_body) = (env.event, env.body);
        let mut observation = NewObservation::from_event(event, raw_body);
        // 3️⃣  Sanitise – typed privacy boundary
        let sanitized = Sanitizer::new(&state.sanitize_cfg)
            .sanitize(&mut observation)
            .expect("sanitisation must succeed");
        // 4️⃣  Write to the single‑writer store
        state.writer_handle.submit(sanitized).await?;
    }

    // 5️⃣  Immediately respond (fire‑and‑forget)
    (StatusCode::ACCEPTED, "queued")
}

```

### Stage 3: Atomic Persistence and Async Processing

Once sanitized, observations are handed to the **`WriterHandle`**, a single-writer SQLite actor that ensures all writes occur in a single database transaction. This satisfies the invariant that "indexes commit in the same transaction as the data," preventing inconsistent states.

For `SessionEnd` events, the system triggers **wiki synthesis** via `synthesize_session_page` in [[`crates/ai-memory-hooks/src/synth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/synth.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/synth.rs). This function aggregates session observations into a concise markdown summary:

```rust
pub async fn synthesize_session_page(
    writer: &WriterHandle,
    session_id: SessionId,
) -> Result<(), StoreError> {
    // Gather all observations for the session, build a markdown summary,
    // then write the page atomically via the Wiki component.
    let page = WikiPage::from_observations(writer.fetch_session(session_id).await?);
    writer.wiki.write_page(page).await?;
    Ok(())
}

```

Heavy downstream work—such as indexing, handoff updates, and optional LLM-driven consolidation—executes asynchronously after the server returns HTTP 202 to the CLI, preserving the non-blocking contract.

## Privacy-First Data Flow

The sanitization layer in [[`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) guarantees that every observation crossing into storage is wrapped in the `Sanitized<T>` type. This architectural enforcement ensures that private credentials, environment variables, and secrets are redacted at the boundary, never persisting to disk. The `Sanitizer` configuration is shared across the `hook_router` and core storage layers, maintaining consistent privacy rules throughout the pipeline.

## Key Implementation Files

The automatic lifecycle capture system spans several critical components in the ai-memory repository:

- **[[`crates/ai-memory-hooks/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/lib.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/lib.rs)** – Top-level orchestration of the hook pipeline and re-exports of core sanitizer types.
- **[[`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)** – Implements the `POST /hook` endpoint, rate-limiting logic, and dispatch to the writer.
- **[[`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)** – Defines `HookEnvelope`, `HookEvent` enum variants, and payload size limits.
- **[[`crates/ai-memory-hooks/src/synth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/synth.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/synth.rs)** – Generates markdown wiki pages from completed sessions upon `SessionEnd`.
- **[[`crates/ai-memory-cli/src/main.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs)** – Contains the `install-hooks` subcommand for generating agent-specific shell scripts.
- **[[`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)** – Implements the privacy boundary ensuring redaction before storage.

## Summary

- **ai-memory** captures agent CLI lifecycle events through auto-generated shell scripts that POST JSON payloads to a local server endpoint.
- The **fire-and-forget** design uses short timeouts and immediate HTTP 202 responses, ensuring agent CLIs never block on telemetry collection.
- A **typed privacy boundary** enforced by the `Sanitizer` type guarantees that all `Sanitized<NewObservation>` instances are redacted before persistence.
- **Atomic transactions** in the single-writer SQLite actor ensure that data and indexes commit together, maintaining consistency.
- **Wiki synthesis** triggers automatically on `SessionEnd`, generating markdown summaries without manual intervention.

## Frequently Asked Questions

### Which agent CLIs are compatible with ai-memory lifecycle hooks?

ai-memory supports any CLI that can invoke external scripts on lifecycle events, including Claude Code, Codex, and OpenCode. The [`install-hooks`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs) command generates agent-specific shell wrappers for each supported platform, ensuring the correct JSON schema and endpoint configuration for that particular tool.

### How does ai-memory prevent the CLI from hanging during capture?

The system implements a **fire-and-forget** protocol where generated shell scripts use `curl` with short timeouts to POST data to `http://127.0.0.1:49374/hook`. The server immediately returns HTTP 202 after queueing the observation, while heavy processing occurs asynchronously. If the server is overloaded beyond `DEFAULT_HOOK_INGEST_MAX_IN_FLIGHT`, it returns HTTP 429, instructing the CLI to skip that event rather than blocking.

### What ensures that sensitive data like API keys aren't stored in ai-memory?

Every observation passes through the **`Sanitizer`** implemented in [`ai-memory-core`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/sanitize.rs) before reaching storage. This component enforces a typed privacy boundary by transforming raw input into `Sanitized<NewObservation>`, redacting secrets, environment variables, and personally identifiable information. Raw payloads never touch the database; only sanitized versions persist.

### When does ai-memory generate the session wiki summary?

Wiki synthesis occurs automatically when the system receives a `SessionEnd` hook event. The `synthesize_session_page` function in [[`synth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/synth.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/synth.rs) queries all observations for that session ID, compiles them into a markdown document via the `WikiPage` builder, and atomically writes the summary to storage using the same `WriterHandle` transaction used for the final observation.