# How ai-memory Captures Agent Lifecycle Events from Shell Hooks

> Learn how ai-memory captures agent lifecycle events using shell hooks. Discover its three-layer pipeline for deterministic event recording without impacting agent output.

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

---

**ai-memory captures agent lifecycle events through a three-layer pipeline involving shell/PowerShell hook scripts, a native Rust capture helper in [`hook_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/hook_capture.rs), and HTTP posting to a local server, ensuring deterministic event recording without disturbing agent output.**

The `akitaonrails/ai-memory` repository implements a robust mechanism for **capturing agent lifecycle events from shell hooks** across different AI coding agents. By intercepting events like `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` at the shell level, ai-memory creates a complete audit trail of agent interactions without modifying the agent's core behavior or polluting stdout.

## The Three-Layer Capture Architecture

The **capture pipeline** operates through three tightly coupled layers that ensure consistent behavior across platforms:

- **Shell/PowerShell Hook Scripts** – Agent-specific scripts located in directories like `hooks/pool/` or `hooks/opencode/` that detect the current context and trigger capture
- **Native Capture Helper ([`hook_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/hook_capture.rs))** – Rust implementation in [`crates/ai-memory-cli/src/commands/hook_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook_capture.rs) that mirrors script logic for performance-critical paths
- **HTTP Posting Layer** – Delivery mechanism using `reqwest` to transmit events to the local ai-memory server at `127.0.0.1:49374`

Each layer maintains **capture-only semantics**, meaning they write nothing to stdout and cannot interfere with the agent's normal output stream.

## Shell and PowerShell Hook Scripts

Agent-specific hooks reside in organized directories such as [`hooks/pool/session-start.sh`](https://github.com/akitaonrails/ai-memory/blob/main/hooks/pool/session-start.sh) or [`hooks/opencode/user-prompt-submit.sh`](https://github.com/akitaonrails/ai-memory/blob/main/hooks/opencode/user-prompt-submit.sh). These scripts perform three critical operations:

1. **Locate the nearest [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) marker** by walking up from the current working directory
2. **Construct a query-string suffix** encoding `cwd`, `workspace`, `project`, and other marker-declared fields
3. **Invoke the native `ai-memory hook` subcommand** or fall back to direct HTTP requests to post the event payload

The shared logic lives in [`hooks/_lib.sh`](https://github.com/akitaonrails/ai-memory/blob/main/hooks/_lib.sh), which provides helper functions like `ai_memory_url_encode` for RFC-3986 compliant encoding. As noted in the source at [lines 97-104](https://github.com/akitaonrails/ai-memory/blob/main/hooks/_lib.sh#L97-L104), this script maintains strict parity with the Rust implementation in [`hook_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/hook_capture.rs) to prevent behavioral drift between shell and native execution paths.

## Native Capture Implementation in hook_capture.rs

The Rust implementation in [`crates/ai-memory-cli/src/commands/hook_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook_capture.rs) provides the authoritative logic for **capture policy resolution**, **query-string construction**, and **HTTP delivery**.

### Resolving Capture Policies

The `capture_policy` function (lines 21-45) walks up from the supplied `cwd` to locate the nearest [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) marker and parses a strict `[capture]` section. Only the `ignore_paths` key is permitted in this section; unknown keys trigger errors. If the marker is missing or malformed, the function returns a default `CapturePolicy`.

The `read_capture_config` implementation (lines 47-75) enforces safety limits by reading a maximum of `MAX_MARKER_BYTES` from the marker file. It parses the TOML content and extracts the optional `ignore_paths` array, propagating errors for oversized files or invalid configuration keys.

### Building the Query-String Context

The `marker_query_suffix` function (lines 14-30 and the core loop at lines 54-98) constructs the URL parameters appended to hook POST requests. It builds strings like `&cwd=…&workspace=…&project=…&project_strategy=…` by consulting the nearest *settings marker*—a marker that declares scope information beyond the `[capture]` section.

Critically, the implementation enforces **separation of scope and capture**. As implemented in `marker_query_suffix_impl` (lines 63-71), a nested capture-only marker containing `[capture]` and `ignore_paths` does **not** shadow outer markers that declare workspace or project settings. This ensures that scope information (workspace, project) comes from the outermost relevant marker, while capture configuration (ignore paths) comes from the innermost capture-only marker.

The `url_encode` function (lines 90-108) guarantees safe transmission by percent-encoding non-RFC-3986 characters, including Windows backslashes, preventing HTTP-layer rejections due to malformed URLs.

### Posting Events via HTTP

The delivery layer uses `build_client` (lines 32-41) to construct a one-shot `reqwest` client with appropriate timeouts. The `post_hook` function (lines 75-104) transmits JSON payloads to the server, returning a delivery outcome that informs the drain loop's retry logic. Possible outcomes include:

- **Delivered** – Successful reception by the server
- **Saturated** – Server is overloaded; back off and retry
- **Failed** – Permanent error; drop the event
- **Unreachable** – Network failure; queue for retry
- **Unauthorized** – Authentication failure; requires user intervention

## Configuration Resolution and Marker Inheritance

When agents omit the `cwd` parameter (such as Cursor), the system employs `resolve_cwd_with_fallbacks` (lines 68-88) to maintain capture continuity. This function implements a robust fallback chain:

1. Prefer the payload's explicit `cwd` field
2. Check the `DEVIN_PROJECT_DIR` environment variable
3. Default to the process's current working directory

For **project strategy detection**, the system consults functions in [`crates/ai-memory-cli/src/marker.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/marker.rs) such as `find_marker` and `find_settings_marker`. These utilities determine whether a directory represents a "repo-root" project or follows another organizational strategy, ensuring that events are correctly bucketed for later indexing and replay.

## Practical Implementation Examples

The following examples demonstrate how to implement capture hooks using both shell scripts and the native Rust interface.

### Example 1: Shell Hook Implementation

A bash hook forwarding a `UserPromptSubmit` event to the ai-memory server:

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

# hooks/pool/user-prompt-submit.sh

set -euo pipefail

# 1️⃣ Determine cwd and locate nearest .ai-memory.toml

CWD="$(pwd)"
MARKER="$(ai-memory find-marker "$CWD")"

# 2️⃣ Build query string (adds workspace/project when present)

QS="$(ai-memory marker-query-suffix "$CWD" "repo-root")"

# 3️⃣ POST JSON payload to the server

curl -sS -X POST "http://127.0.0.1:49374/hook?event=user-prompt-submit$QS" \
     -H "Content-Type: application/json" \
     -d "$(cat <<EOF
{
  "hook_event_name": "userPromptSubmit",
  "session_id": "$AI_MEMORY_SESSION_ID",
  "cwd": "$CWD",
  "user_prompt": "$1"
}
EOF
)"

```

### Example 2: Native Rust Integration

Using the capture helper directly in a custom binary:

```rust
use ai_memory_cli::commands::hook_capture::{marker_query_suffix, build_client, post_hook};
use std::time::Duration;

fn main() {
    // Resolve cwd (fallbacks are built‑in)
    let cwd = std::env::current_dir().unwrap().to_string_lossy().into_owned();

    // Build query string with a default project strategy
    let qs = marker_query_suffix(&cwd, Some("repo-root"));
    let url = format!("http://127.0.0.1:49374/hook?event=session-start{qs}");

    // Example payload
    let payload = serde_json::json!({
        "hook_event_name": "sessionStart",
        "session_id": "123e4567-e89b-12d3-a456-426614174000",
        "cwd": cwd,
    })
    .to_string();

    // Send it
    let client = build_client();
    let outcome = futures::executor::block_on(post_hook(
        &client,
        &url,
        &payload,
        None,
        Duration::from_secs(2),
    ));

    println!("Hook POST outcome: {:?}", outcome);
}

```

### Example 3: Inspecting Capture Policies

Checking the active capture policy for a directory:

```rust
use ai_memory_cli::commands::hook_capture::capture_policy;

fn main() {
    let cwd = "/path/to/project";
    let policy = capture_policy(cwd);
    // The policy knows the ignore list and can decide whether an event should be dropped.
    println!("Capture policy for {}: {:?}", cwd, policy);
}

```

## Summary

- **ai-memory** implements a deterministic, platform-agnostic pipeline for **capturing agent lifecycle events from shell hooks** through three coordinated layers: shell scripts, native Rust helpers, and HTTP delivery.
- The **capture policy** system enforces strict configuration boundaries, allowing only `ignore_paths` in the `[capture]` section while inheriting scope settings (workspace, project) from outer markers.
- **Robust fallback mechanisms** ensure continuous operation even when agents omit critical context like the current working directory.
- All event transmission uses **percent-encoded query strings** and structured JSON payloads posted to `127.0.0.1:49374`, with clear delivery outcomes driving retry and backoff logic.

## Frequently Asked Questions

### What lifecycle events does ai-memory capture?

ai-memory captures a comprehensive set of **agent lifecycle events** including `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop`. Each event type corresponds to a specific hook script or native invocation that records the state transition without interfering with the agent's primary output stream.

### How does ai-memory handle agents that don't provide a cwd?

When an agent omits the `cwd` field (such as Cursor), the `resolve_cwd_with_fallbacks` function in [`hook_capture.rs`](https://github.com/akitaonrails/ai-memory/blob/main/hook_capture.rs) (lines 68-88) implements a three-tier fallback chain: it first checks the payload's `cwd` field, then the `DEVIN_PROJECT_DIR` environment variable, and finally falls back to the process's current working directory. This ensures that events are always associated with a meaningful directory context for proper indexing.

### Can I customize which files are ignored during capture?

Yes, customization is available through the **capture policy** system. By creating an [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) marker file with a `[capture]` section containing an `ignore_paths` array, you can specify which files or patterns should be excluded from capture. The `read_capture_config` function (lines 47-75) enforces that only `ignore_paths` is permitted in this section, rejecting unknown keys to prevent configuration errors.

### What happens if the ai-memory server is unreachable?

The system categorizes connection failures under the **Unreachable** delivery outcome. When `post_hook` detects that the server at `127.0.0.1:49374` is unreachable, it returns this status to the drain loop, which queues the event for retry with exponential backoff. Events are never silently dropped due to transient network failures; only permanent errors (such as authentication failures) result in event rejection.