How ai-memory Captures Automatic Lifecycle Hooks from Agent CLIs
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). For example, running ai-memory install-hooks --agent open-code produces a bash wrapper that posts to http://127.0.0.1:49374/hook:
#!/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) exposes the POST /hook and POST /hook/batch endpoints. Upon receiving a payload, the server:
- 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)) to identify the specificHookEventvariant. - Passes the raw observation through the
Sanitizer(ai_memory_core::Sanitizer), which enforces a typed privacy boundary by transforming the data intoSanitized<NewObservation>. This step guarantees that raw secrets are redacted before reaching storage. - Guards against overload by checking
DEFAULT_HOOK_INGEST_MAX_IN_FLIGHT; if exceeded, the router returns HTTP 429 (Too Many Requests).
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). This function aggregates session observations into a concise markdown summary:
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) 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) – 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) – Implements thePOST /hookendpoint, 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) – DefinesHookEnvelope,HookEventenum 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) – Generates markdown wiki pages from completed sessions uponSessionEnd. - [
crates/ai-memory-cli/src/main.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs) – Contains theinstall-hookssubcommand 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) – 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
Sanitizertype guarantees that allSanitized<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 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 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/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.
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 →