How ai-memory Captures Agent Lifecycle Events from Shell Hooks
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, 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/orhooks/opencode/that detect the current context and trigger capture - Native Capture Helper (
hook_capture.rs) – Rust implementation incrates/ai-memory-cli/src/commands/hook_capture.rsthat mirrors script logic for performance-critical paths - HTTP Posting Layer – Delivery mechanism using
reqwestto transmit events to the local ai-memory server at127.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 or hooks/opencode/user-prompt-submit.sh. These scripts perform three critical operations:
- Locate the nearest
.ai-memory.tomlmarker by walking up from the current working directory - Construct a query-string suffix encoding
cwd,workspace,project, and other marker-declared fields - Invoke the native
ai-memory hooksubcommand or fall back to direct HTTP requests to post the event payload
The shared logic lives in 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, this script maintains strict parity with the Rust implementation in 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 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 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:
- Prefer the payload's explicit
cwdfield - Check the
DEVIN_PROJECT_DIRenvironment variable - Default to the process's current working directory
For project strategy detection, the system consults functions in 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:
#!/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:
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:
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_pathsin 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 (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 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.
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 →