# Admission Webhook System and Content Filtering in ai-memory: A Complete Guide

> Learn about ai-memory's admission webhook system and content filtering. Intercept wiki mutations, filter content, enrich metadata, or reject writes before storage.

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

---

**The admission webhook system in ai-memory is a configurable chain of HTTP callbacks that intercepts every durable wiki mutation, enabling operators to filter content, enrich metadata, or reject writes before they reach the SQLite store or on-disk markdown.**

The `akitaonrails/ai-memory` engine implements an admission webhook system to give operators fine-grained control over what enters the permanent wiki. By invoking a sequential chain of HTTP callbacks immediately before persistence, the system supports content filtering, front-matter enrichment, and policy enforcement without allowing direct access to the underlying storage layers.

## How the Admission Chain Works

### Trigger Points and Supported Operations

The webhook chain fires for specific durable mutation operations. According to the source code, the engine invokes the admission chain for the following `op` values: `write_page`, `consolidate`, `delete`, `purge_project`, `purge_workspace`, `move_project`, `move_session`, `handoff_begin`, `handoff_accept`, and `handoff_cancel`.

These trigger points are wired into the wiki write entry point in [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs). This ensures that every state-changing operation passes through the configured hooks before the engine commits data to disk or updates the SQLite index.

### Execution Order and Blocking Behavior

Webhooks execute **sequentially** in the order they appear in the configuration. Each webhook receives the page payload as it may have been mutated by the previous hook, creating a deterministic pipeline.

The engine supports two execution modes:

- **Blocking** (`blocking = true`, the default): The hook runs synchronously inside the write path. It may mutate the page payload or reject the operation entirely.
- **Non-blocking** (`blocking = false`): The hook is fire-and-forget, dispatched after the durable write completes. It can only observe the final state and cannot mutate or abort.

### Failure Policies and Timeout Limits

When a webhook cannot be reached or returns a non-2xx status, the engine follows the hook's configured `failure_policy`:

- `ignore` — logs a warning and continues processing the chain (the default).
- `reject` — aborts the write immediately and propagates an error to the caller.

Each webhook also respects a per-request `timeout_ms` (default **2000 ms**). Because blocking hooks run sequentially, the worst-case write latency is the sum of all blocking hook timeouts. The system enforces hard limits: the chain length is capped at `MAX_ADMISSION_WEBHOOKS = 16`, and response bodies larger than `MAX_RESPONSE_BYTES = 1 MiB` are discarded.

## Content Filtering Mechanism

### Mutating Page Payloads Across the Chain

For `write_page` and `consolidate` operations, a webhook can return a partial page object to modify the incoming content. The engine applies these changes before passing the payload to the next hook or committing the final atomic write.

The expected response format is:

```json
{
  "page": {
    "frontmatter": { },
    "body": "…"
  }
}

```

Missing fields are left unchanged. This is the core **content filtering** mechanism in `akitaonrails/ai-memory`. A webhook may strip secrets, enforce front-matter schemas, add contributor metadata, or rewrite the page body. The mutated payload flows directly into the next webhook or to the final persistence layer in [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs).

### Content Filtering Example

The following TOML configures two webhooks: one mutating and one observational.

```toml
[[admission_webhooks]]
name = "contributors"
url  = "http://contributors.svc/enrich"
timeout_ms = 2000
failure_policy = "ignore"
events = ["write_page", "consolidate"]
blocking = true

[[admission_webhooks]]
name = "git-mirror"
url  = "http://git-mirror.svc/sync"
timeout_ms = 2000
failure_policy = "ignore"
events = ["write_page", "delete", "purge_project", "move_project"]
blocking = false

```

A mutating webhook can enrich front-matter before persistence. For example, returning this JSON:

```json
{
  "page": {
    "frontmatter": {
      "contributors": [
        {
          "agent": "opencode",
          "user": "alice",
          "first_seen": "2024-01-01T00:00:00Z",
          "writes": 1
        }
      ]
    }
  }
}

```

The engine merges the `contributors` field into the page's front-matter, leaving all other fields intact.

You can trigger the chain through the CLI or MCP:

```bash
ai-memory write-page --workspace default --project notes \
  --path "ideas/new.md" --title "New Idea"

```

## Loop Prevention and Safety Guarantees

### Preventing Infinite Recursion

If a webhook writes back to the engine—for example, by calling `/admin/write-page` to update the same page—it must include the header:

```

X-Memory-Skip-Admission-Chain: <hook-name>

```

This instructs the engine to skip the originating hook on the re-entrant call, preventing infinite recursion through the admission chain. Without this safeguard, a mutating webhook that triggers additional writes could loop indefinitely.

### Storage Isolation

The admission webhook system enforces strict isolation. Hooks can add or modify front-matter, rewrite the page body, mirror writes to external systems, or reject policy violations. However, they **cannot** directly read from or write to the SQLite store or access on-disk markdown outside the single-page payload they receive. This boundary ensures that content filtering happens through the well-defined admission contract rather than ad-hoc storage manipulation.

## Source Code Implementation

### Core Data Structures and Chain Runner

The authoritative definitions for the admission system live in [`crates/ai-memory-wiki/src/admission.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/admission.rs). This file declares the `AdmissionContext` and `ActorContext` structs, which carry the page payload and operation metadata into the chain. The `AdmissionChain::run` method implements the sequential runner that applies timeouts, enforces `MAX_ADMISSION_WEBHOOKS`, and handles blocking versus non-blocking dispatch.

End-to-end tests validating the admission contract are located in [`crates/ai-memory-wiki/tests/admission.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/tests/admission.rs).

### Integration Points

The CLI configuration schema for webhooks is parsed in [`crates/ai-memory-cli/src/config.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/config.rs), where the TOML arrays are deserialized into typed hook definitions. The HTTP server wiring that attaches the admission chain to incoming write requests is implemented in [`crates/ai-memory-cli/src/commands/serve.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/serve.rs). For a human-readable specification of the protocol, see [`docs/admission-webhooks.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/admission-webhooks.md) in the repository root.

## Summary

- The **admission webhook system** in `akitaonrails/ai-memory` intercepts every durable mutation through a sequential chain of HTTP callbacks invoked before persistence.
- **Content filtering** works by returning partial page objects from blocking webhooks, allowing hooks to mutate front-matter or body content while leaving unspecified fields unchanged.
- Hooks run in declared order, support blocking (synchronous, mutating) and non-blocking (asynchronous, observational) modes, and respect per-hook `failure_policy` and `timeout_ms` settings.
- Hard limits cap the chain at `MAX_ADMISSION_WEBHOOKS = 16` and responses at `MAX_RESPONSE_BYTES = 1 MiB`, with a default timeout of **2000 ms**.
- The `X-Memory-Skip-Admission-Chain` header prevents recursive loops when webhooks write back into the engine.

## Frequently Asked Questions

### What operations trigger the admission webhook chain?

The admission chain runs for `write_page`, `consolidate`, `delete`, `purge_project`, `purge_workspace`, `move_project`, `move_session`, `handoff_begin`, `handoff_accept`, and `handoff_cancel`. These trigger points cover every durable mutation path in the wiki engine.

### How does a webhook filter or modify page content?

For `write_page` and `consolidate` operations, a blocking webhook returns a JSON object containing a partial `page` with optional `frontmatter` and `body` fields. The engine merges these fields into the current payload before the next hook or the final atomic write, enabling content filtering, secret stripping, and metadata enrichment.

### What happens if an admission webhook fails or times out?

The engine applies the hook's configured `failure_policy`. If set to `ignore` (the default), the engine logs a warning and continues. If set to `reject`, the engine aborts the entire write and returns an error to the caller. Each hook also has a `timeout_ms` budget (default 2000 ms) that caps its execution time.

### Can a webhook write back to the wiki without causing an infinite loop?

Yes, but the re-entrant request must include the `X-Memory-Skip-Admission-Chain` header set to the originating hook's name. This tells the engine to skip that specific hook for the recursive call, breaking the cycle and preventing infinite recursion through the admission chain.