Files
ai-memory/docs/install.md
T
Talysson de Oliveira Cassiano cf21fcb351 docs: document per-repository server profiles (#992)
marker-file.md gains a section on the `server` key: registering
profiles, the fail-closed rules, inheritance and the walk boundary with
the allowlist-mode caveat that comes with it, check-capture output,
which integrations route or drop, what `uninstall` removes, that two
profiles may share a URL with different tokens (one server, several
identities under #708), and two known limits (older binaries draining a
shared spool; MCP and `ai-memory run` still using the install server).

install.md, cookbook.md, security.md and the README point to it,
security-boundaries.md adds row 11d, and CHANGELOG records the feature
and the backfill fix; the Kimi `{}` fix reached release/2.5 through the
cherry-pick to main (#996), so its commit is no longer part of this branch.
2026-09-30 18:19:10 +00:00

2672 lines
131 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Installation cookbook
The [README quick-start](../README.md#quick-start) covers the happy
paths (Docker + Claude Code, Arch AUR, Fedora RPM, macOS menu bar app). This
page covers everything else:
- [Server on a different machine](#server-on-a-different-machine)
(homelab, LAN box, remote server)
- [Configuring the CLI URL and auth](#configuring-the-cli-url-and-auth)
- [Fedora native package (RPM)](#fedora-rpm)
(systemd system service or user service)
- [Arch Linux native packages (AUR)](#arch-linux-native-packages-aur)
(systemd system service or user service)
- [macOS menu bar app](#macos-menu-bar-app)
(self-contained `.app` + LaunchAgent)
- [Configuring other agent CLIs](#configuring-other-agent-clis)
(Codex, Command Code, Devin CLI, OpenCode, OMP, Pi, Cursor, Claude Desktop, Gemini CLI, Antigravity CLI, Grok Build CLI, Zero, ZCode, Kimi Code, Kiro CLI, Pool, OpenClaw, VS Code Copilot, Zed)
- [Installing hooks without docker](#installing-hooks-without-docker)
(curl-based installer)
- [Running ai-memory without docker](#running-ai-memory-without-docker)
(mise, building from source)
- [Managed cross-harness workstreams](managed-workstreams.md)
(`ai-memory run`, transparent native resume, and argument forwarding)
- [LLM provider tiers + self-hosted Ollama](#llm-provider-tiers)
- [Common subcommands](#common-subcommands)
- [Managed routing snippets and Agent Skills](#managed-routing-snippets-and-agent-skills)
- [Human password bootstrap and recovery](#human-password-bootstrap-and-recovery)
- [Operating without auth](#operating-without-auth) (local-only)
- [Keeping ai-memory up to date](#keeping-ai-memory-up-to-date)
> **Shorthand.** Most snippets use `$TOKEN` and `homelab:49374`. If
> you're following along verbatim:
> ```bash
> export TOKEN=$(docker run --rm akitaonrails/ai-memory:latest generate-auth-token)
> ```
> and replace `homelab` with `localhost` if the server runs on the
> same machine as the agent CLI.
The Docker image is published for `linux/amd64` and `linux/arm64`; Apple
Silicon Macs and ARM64 Linux hosts should not need `--platform linux/amd64`.
> **Podman.** The `bin/ai-memory` wrapper automatically uses rootless Podman
> when Docker is not installed. It also works through the `podman-docker`
> `docker` shim; set `AI_MEMORY_DOCKER=podman` to force Podman when both engines
> are installed. The default image name is fully qualified for non-interactive
> Podman short-name resolution. See
> [SELinux-enforcing hosts](#selinux-enforcing-hosts) for how it detects the
> engine's rootless and SELinux state.
---
## Server on a different machine
When the ai-memory server runs on a LAN box (homelab, headless server)
and you use Claude Code / Codex / etc. on a laptop:
### Server side (the homelab host)
```bash
docker run -d --name ai-memory \
--restart unless-stopped \
-p 0.0.0.0:49374:49374 \
-v ai-memory-data:/data \
-e AI_MEMORY_AUTH_TOKEN="$TOKEN" \
-e AI_MEMORY_ALLOWED_HOSTS="<server-ip>,localhost,127.0.0.1" \
-e AI_MEMORY_LLM_PROVIDER=anthropic \
-e ANTHROPIC_API_KEY=sk-ant-... \
akitaonrails/ai-memory:latest
```
See [Security](../README.md#security) in the README for why
`AI_MEMORY_AUTH_TOKEN` and `AI_MEMORY_ALLOWED_HOSTS` are both required for
normal non-loopback binds. Bearer auth does not encrypt traffic: use the ready
[Caddy](../docker/compose.tls.caddy.yml) or
[Cloudflare Tunnel](../docker/compose.tls.cloudflared.yml) templates from the
[HTTPS reverse-proxy guide](https-via-proxy.md) for LAN or remote access.
When the proxy serves `/web` over HTTPS, also set
`AI_MEMORY_AUTH__SECURE_COOKIE=true` in the server environment and close or
redirect direct HTTP access to that hostname. Do not set it for direct HTTP:
browsers then correctly withhold the session cookie.
### Client side (the laptop)
```bash
export AI_MEMORY_SERVER_URL="http://<server-ip>:49374"
export AI_MEMORY_AUTH_TOKEN="$TOKEN"
ai-memory install-mcp --client claude-code --apply
ai-memory install-hooks --agent claude-code --apply
```
`--session-aware` is an optional Claude Code MCP mode:
```bash
ai-memory install-mcp --client claude-code --session-aware --apply
```
It replaces the static HTTP MCP entry with a local ai-memory stdio bridge that
still connects to the configured remote server and bearer token, while
forwarding Claude's lifecycle session id. Pair it with
`[auto_scope] mode = "per_session"` when the same operator runs concurrent
Claude Code sessions in different projects. The default static HTTP
registration remains appropriate for one active project at a time.
If `CLAUDE_CONFIG_DIR` is set, the claude-code installers match Claude Code's
own config resolution: `install-mcp` writes the MCP registration to
`$CLAUDE_CONFIG_DIR/.claude.json` (instead of `~/.claude.json`),
`install-hooks` / `setup-agent` target `$CLAUDE_CONFIG_DIR/settings.json`
(instead of `~/.claude/settings.json`), and `install-skills --scope global`
uses `$CLAUDE_CONFIG_DIR/skills` (instead of `~/.claude/skills`). `uninstall`
sweeps the active relocated paths alongside the home defaults. It cannot
discover an older arbitrary `CLAUDE_CONFIG_DIR` that is no longer set. The
Docker wrapper forwards the variable for config roots under its existing
`$HOME` bind mount; use the native binary when the relocated root is outside
`$HOME`.
The CLI commands (`bootstrap`, `status`, `search`, `lint`, `auto-improve`,
`curator`, `pending-writes`, etc.) inherit the two env vars automatically. So do
`install-mcp`, `install-hooks`, and
`setup-agent`: with `AI_MEMORY_SERVER_URL` set, `install-mcp` derives the
`/mcp` endpoint and `install-hooks` uses the bare server origin.
After upgrading ai-memory, refresh the managed routing package in existing
projects so Claude Code/OpenCode/Codex/Gemini pick up new tool guidance and
proactive retrieval rules. From an agent, ask "refresh the ai-memory routing in
this project"; from the terminal, run `ai-memory install-instructions` (or pass
`--target AGENTS.md` for non-Claude prompt files). The update is idempotent:
legacy long snippets between `<!-- ai-memory:start -->` /
`<!-- ai-memory:end -->` are replaced in place with the slim snippet, and
managed Agent Skills are installed or updated alongside it.
If you install into `AGENTS.md` and the project is also used from Claude Code,
make `CLAUDE.md` import it with a bare `@AGENTS.md` first line. Claude Code
loads `CLAUDE.md` and does not read `AGENTS.md`, so without that import the
installed block is absent from context at session start. See
[Claude Code memory](https://code.claude.com/docs/en/memory#agents-md).
---
## Configuring the CLI URL and auth
The `ai-memory` binary is a thin HTTP client. It never opens the wiki
or SQLite directly; state-touching commands go through the running
server, which is the sole writer.
Configuration is two optional environment variables:
| Variable | Default | When to set it |
|---|---|---|
| `AI_MEMORY_SERVER_URL` | `http://127.0.0.1:49374` | When the server runs somewhere other than the same machine, such as `http://192.168.0.90:49374`. |
| `AI_MEMORY_AUTH_TOKEN` | unset | When the server has bearer auth enabled. |
For a single-laptop loopback server, set neither variable. For a
remote or homelab server, put both in your shell rc or direnv file:
```bash
export AI_MEMORY_SERVER_URL="http://192.168.0.90:49374"
export AI_MEMORY_AUTH_TOKEN="<token>"
```
Explicit `--server-url` and `--auth-token` flags on `install-mcp`,
`install-hooks`, and `setup-agent` override the environment. That is
useful when you are generating config for a client that talks to a
different server than your default CLI target.
If you run `install-mcp --apply` first and later run `install-hooks --apply`
without env vars or flags, hooks reuse the existing ai-memory MCP entry for
that agent when possible. This keeps remote MCP config and lifecycle capture
pointed at the same server instead of falling back to loopback.
All installer `--apply` modes preserve symlinked configuration files: the
atomic update is written to the symlink target, including a missing final
target, while the timestamped backup stays next to the user-facing config
path. This keeps stow, chezmoi, and similar dotfile-managed installs linked.
`init`, `serve`, and `generate-auth-token` do not need these env vars because
they either create local files or start the server itself.
### Default project resolution (`--project-strategy`)
By default each session files memory under `basename(cwd)`. Because an agent
shell keeps its working directory between tool calls, a single
`mkdir sub && cd sub` reparents the rest of the session into a phantom project
named `sub`. To make every session for an install resolve its project from the
git repo root instead — collapsing subdirectories and worktrees — bake the
strategy into the hooks:
```bash
ai-memory install-hooks --apply --agent claude-code --project-strategy repo-root
```
`--project-strategy` accepts `basename` (the new-install default; bakes nothing)
or `repo-root`. Omitting it during a later `--apply` preserves the strategy
already baked into that agent's ai-memory hooks, including during the wrapper's
automatic post-upgrade refresh. Pass `basename` explicitly to remove an
existing `repo-root` default. This works for every agent and delivery path. A
per-repo `.ai-memory.toml` marker's own `project_strategy` / `project` still
take precedence — see
[the marker-file reference](marker-file.md#install-wide-default-no-marker).
---
## Fedora (RPM)
Download the `x86_64` or `aarch64` RPM from the
[latest GitHub release](https://github.com/akitaonrails/ai-memory/releases/latest)
and install it with `sudo dnf install ./ai-memory-*.rpm`. The package includes
the binary, hook sources, system and user systemd units, and system service
configuration. Follow the [user-level service](#user-level-service) or
[system service](#system-service) steps below to initialize and start it.
## Arch Linux native packages (AUR)
Use the native packages when you want `/usr/bin/ai-memory` plus systemd units
instead of the Docker wrapper. The package installs the binary and hook sources
once; each user still stages their agent hook scripts into their own home dir
with `install-hooks --apply`.
### Package choice
```bash
yay -S ai-memory-bin # prebuilt Linux x86_64/aarch64 binary, fastest install
yay -S ai-memory # builds from source, works on x86_64 and aarch64
```
Both packages install the same runtime layout:
| Path | Purpose |
|---|---|
| `/usr/bin/ai-memory` | Native CLI/server binary. |
| `/usr/share/ai-memory/hooks/` | Packaged hook source bundle used by `install-hooks`. |
| `/usr/lib/systemd/system/ai-memory.service` | System-wide service unit. |
| `/usr/lib/systemd/user/ai-memory.service` | Per-user service unit. |
| `/usr/lib/sysusers.d/ai-memory.conf` | Creates the `ai-memory` system user. |
| `/usr/lib/tmpfiles.d/ai-memory.conf` | Creates `/var/lib/ai-memory` for the system service. |
| `/etc/ai-memory/config.toml` | System-service config file, tracked as a pacman backup file. |
| `/etc/ai-memory/env` | System-service environment/secrets file, tracked as a pacman backup file. |
The binary itself does not guess between system and user mode. The unit file
chooses explicitly:
| Mode | Data dir | Config | Env/secrets | Requires sudo? |
|---|---|---|---|---|
| User service | `~/.local/share/ai-memory` | `~/.config/ai-memory/config.toml` | `~/.config/ai-memory/env` | No |
| System service | `/var/lib/ai-memory` | `/etc/ai-memory/config.toml` | `/etc/ai-memory/env` | Yes |
Do not run both services on the same bind address. They can coexist on disk, but
only one can listen on `127.0.0.1:49374` unless you change `bind` in one config.
### User-level service
Use this on a single-user workstation. It needs no sudo after package install and
keeps all state in your home directory.
```bash
mkdir -p ~/.config/ai-memory ~/.local/share/ai-memory
ai-memory \
--data-dir ~/.local/share/ai-memory \
--config ~/.config/ai-memory/config.toml \
init
```
Edit provider/auth settings if you want LLM consolidation or bearer auth:
```bash
$EDITOR ~/.config/ai-memory/config.toml
$EDITOR ~/.config/ai-memory/env
```
For a loopback-only local service, bearer auth is optional. If you want one:
```bash
TOKEN=$(ai-memory generate-auth-token)
printf 'AI_MEMORY_AUTH_TOKEN=%s\n' "$TOKEN" >> ~/.config/ai-memory/env
```
Start and inspect the service:
```bash
systemctl --user daemon-reload
systemctl --user enable --now ai-memory.service
systemctl --user status ai-memory.service
journalctl --user -u ai-memory.service -f
```
If the service should keep running after you log out:
```bash
loginctl enable-linger "$USER"
```
Verify the HTTP server:
```bash
curl http://127.0.0.1:49374/mcp
# Expect a JSON-RPC error, which means the server is reachable.
```
### System-level service
Use this for a shared workstation, LAN box, or homelab-style host where the
server should run independently of any logged-in user.
Make sure the package-created user and state directory exist, then initialize
the data layout as that service user:
```bash
sudo systemd-sysusers /usr/lib/sysusers.d/ai-memory.conf
sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/ai-memory.conf
sudo -u ai-memory ai-memory \
--data-dir /var/lib/ai-memory \
--config /etc/ai-memory/config.toml \
init
```
Edit system config and secrets:
```bash
sudoedit /etc/ai-memory/config.toml
sudoedit /etc/ai-memory/env
```
The package installs `/etc/ai-memory/env` as root-readable only because it may
hold API keys. Keep that file out of backups or logs that other users can read.
For LAN exposure, set a non-loopback bind and allowed hosts in
`/etc/ai-memory/config.toml`, and set a bearer token in `/etc/ai-memory/env`:
```toml
bind = "0.0.0.0:49374"
allowed_hosts = ["homelab", "192.168.0.90", "localhost", "127.0.0.1"]
```
```bash
TOKEN=$(ai-memory generate-auth-token)
printf 'AI_MEMORY_AUTH_TOKEN=%s\n' "$TOKEN" | sudo tee -a /etc/ai-memory/env
```
Start and inspect the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now ai-memory.service
sudo systemctl status ai-memory.service
journalctl -u ai-memory.service -f
```
Verify from the host:
```bash
curl -sI http://127.0.0.1:49374/handoff
# 401 Unauthorized when AI_MEMORY_AUTH_TOKEN is set.
```
### LLM provider login with native services
> **You do not need a paid platform API key.** ai-memory's LLM features
> (consolidation, lint, auto-improve) are opt-in, and when you enable them you
> can authenticate with a **subscription you already pay for** instead of a
> metered API key: a Claude Pro/Max plan via `anthropic-oauth`
> (`claude setup-token`), a ChatGPT Plus/Pro/Codex plan via `openai-oauth`
> (`ai-memory auth login openai-oauth`), or a GitHub Copilot plan via `copilot`
> (`ai-memory auth login copilot`). See
> [`docs/llm-providers.md`](llm-providers.md) for the full table. And you can
> skip an LLM entirely: the default zero-LLM path still captures, searches
> (FTS), and writes rule-based summaries with no provider at all —
> [`docs/local-embeddings.md`](local-embeddings.md) makes vector search
> keyless too.
API-key providers go in the relevant env file:
```bash
# User service
printf 'AI_MEMORY_LLM_PROVIDER=anthropic\nANTHROPIC_API_KEY=sk-ant-...\n' >> ~/.config/ai-memory/env
systemctl --user restart ai-memory.service
# System service
sudoedit /etc/ai-memory/env
sudo systemctl restart ai-memory.service
```
OAuth-style providers write tokens into the selected data dir. Run the login
with the same `--data-dir` and `--config` pair as the service:
```bash
# User service
ai-memory \
--data-dir ~/.local/share/ai-memory \
--config ~/.config/ai-memory/config.toml \
auth login openai-oauth
# System service
sudo -u ai-memory ai-memory \
--data-dir /var/lib/ai-memory \
--config /etc/ai-memory/config.toml \
auth login openai-oauth
```
Use `auth login copilot` the same way for GitHub Copilot. For per-developer
native hook auth against an OIDC issuer, run `auth login oidc-device` in the
developer's selected data dir instead:
```bash
ai-memory auth login oidc-device \
--issuer "https://issuer.example.com/realms/team" \
--client-id "ai-memory-cli"
```
The stored OIDC access token is also used by thin-client HTTP commands
(`status`, `search`, `read-page`, `write-page`, `backup`, `embed`, and
similar) when no static `AI_MEMORY_AUTH_TOKEN` / `[auth].bearer_token` is
configured. Static bearer auth still has precedence. This is for external
OIDC-aware gateways/bridges; native ai-memory server auth still uses static root
bearer / DB-user tokens, and `/admin/*` remains root-only unless a gateway
translates accepted OIDC auth into upstream auth that ai-memory accepts.
OIDC/Keycloak `sid` claims describe the login provider's session, not the
coding-agent session ai-memory uses for `[auto_scope]` isolation. Gateways may
propagate the authenticated user/client/agent headers, but
`X-Memory-Actor-Session-Id` should only contain a real lifecycle-hook session id
from a session-aware bridge.
Restart the service after changing provider settings:
```bash
systemctl --user restart ai-memory.service # user mode
sudo systemctl restart ai-memory.service # system mode
```
### Wire agent CLIs after native install
For a local loopback server with no bearer token:
```bash
ai-memory install-mcp --client claude-code --apply
ai-memory install-hooks --agent claude-code --apply
```
For concurrent Claude Code sessions, set `[auto_scope] mode = "per_session"` in
the server config and add `--session-aware` to the `install-mcp` command. This
works for local and LAN servers; the generated stdio bridge keeps using
`AI_MEMORY_SERVER_URL` / `--server-url` and `AI_MEMORY_AUTH_TOKEN`.
For a bearer-protected local or LAN server, export the endpoint first. The MCP
URL includes `/mcp`; the hook URL is the bare origin.
```bash
export AI_MEMORY_SERVER_URL="http://127.0.0.1:49374"
export AI_MEMORY_AUTH_TOKEN="$TOKEN"
ai-memory install-mcp --client claude-code --apply
ai-memory install-hooks --agent claude-code --apply
```
`install-hooks` finds packaged hook sources under `/usr/share/ai-memory/hooks`,
then stages runnable copies under `~/.local/share/ai-memory/hooks/<agent>/` so
the agent can execute files owned by your user. Re-run `install-hooks --apply`
after package upgrades to refresh those staged copies.
### Hook latency expectations
Hook-capable agents launch one short-lived hook process per lifecycle event. A
successful tool call normally fires both a pre-tool and a post-tool event, so
per-invocation overhead is paid twice. As an order-of-magnitude reference, one
independent v1.29.0 evaluation on native macOS aarch64 measured about 145 ms per
`posix-native` invocation (about 290 ms per completed tool call) and about 172
ms per legacy `.sh` invocation.
Those figures are one host's measurements, not a benchmark or performance
guarantee. Process startup, filesystem and security software, the selected
data directory, authentication, and host load can all change the result. The
native hook fast path skips full config loading and tracing, and normally
spools locally instead of waiting for the server, so keep the installer's
native default where supported. Latency-sensitive deployments should measure
their own agent host before opting into additional captured events or choosing
a script fallback.
### Capture-policy capability and refresh
`[capture] ignore_paths` is enforced only by native `ai-memory hook` commands
and generated OpenCode/OMP/Pi/OpenClaw integrations. Local installers select
native commands where supported; legacy `.sh`/`.ps1` hooks and remote-only or
Docker script bundles do not enforce it. Re-run `install-hooks --agent <agent>
--apply` or refresh/reinstall generated plugins after upgrading; installer
capability output reflects the selected integration. See the canonical
[capture exclusions reference](marker-file.md#capture-exclusions).
Lifecycle observation bodies are bounded separately from the 10 MiB HTTP
request limit. User prompts and post-compaction summaries retain up to 16 KiB;
notifications and tool excerpts retain up to 2 KB. Native `ai-memory hook`
commands truncate those fields UTF-8-safely before they enter the local spool
or wire. The server repeats the event-specific caps for every integration,
including script and generated clients, then applies a 16 KiB backstop after
sanitization before any observation reaches SQLite or FTS. Native hook commands
invoke the installed binary directly, so upgrading that binary is enough to
receive the client-side cap.
**Disable Claude Code prompt capture.** Regulated or mixed-trust machines can
leave the rest of Claude Code's lifecycle capture enabled while preventing
`UserPromptSubmit` text from entering the local spool or wire:
```bash
ai-memory install-hooks --agent claude-code --no-capture-prompts --apply
```
The installer removes only ai-memory's prompt hook and preserves third-party
hooks registered under the same event. A later bare `install-hooks --apply`
(including an upgrade refresh) inherits the disabled state. Re-enable prompt
capture explicitly with `--capture-prompts`. These options are Claude Code-only:
some other agents use their prompt hook to inject handoff context, so removing
it would break continuity. Disabling prompt capture reduces session summaries
and recall quality because user intent is no longer part of the observation
stream; tool and session-boundary capture continues unchanged.
**Capture only repositories that opt in.** The controls above narrow *what* is
captured; this one narrows *where*. By default a repository with no
`.ai-memory.toml` marker is still captured, so a machine that works across many
checkouts captures every new one automatically — forgetting a marker means
capturing more, not less. Allowlist mode inverts that:
```bash
ai-memory install-hooks --apply --capture-mode allowlist
```
A repository without a marker then emits **no lifecycle event at all** — not a
trimmed one. The event is dropped in the hook process before it can reach the
local spool or the wire, so nothing is written to disk for a repository that
never opted in. Opting a repository in is just placing a `.ai-memory.toml`
marker in it, which is the same file that already configures routing and
`ignore_paths`.
**It is enforced by native `ai-memory hook` commands only** — the same
boundary that already applies to `[capture] ignore_paths`, and for the same
reason: the gate runs inside the hook binary, immediately before it spools.
That is what a normal `install-hooks --apply` writes on Linux, macOS and
Windows, so the usual install is covered. It is the *script* installs that are
not: the bundled shell/PowerShell hooks POST to the server directly and never
execute the binary, so nothing reads the mode. In practice that means the
legacy `posix`/`windows` platform override (`AI_MEMORY_HOOK_PLATFORM`), the
Docker host wrapper, and `setup-agent` snippets, which emit script commands by
design. `install-hooks --apply` prints the mode and warns when the install it
is writing cannot enforce it.
Within that boundary the mode is not per-agent: it is stored once in the data
directory and every native hook command reads it, whichever agent invoked it.
That also means a later bare
`install-hooks --apply` — including the auto-refresh inside `ai-memory upgrade`
— leaves it alone by construction rather than by re-detecting it. Every
`--apply` prints the mode in force. Return to the default with
`--capture-mode denylist`.
Verify it on any repository without changing anything:
```bash
printf '{"cwd":"%s"}' "$PWD" | ai-memory hook --event user-prompt-submit \
--agent claude-code --server-url "$AI_MEMORY_SERVER_URL" --check-capture
```
`--check-capture` inspects policy without spooling, draining, or contacting the
server, and needs a JSON payload naming the directory to test. In the output,
`"admits_capture": false` means that repository captures nothing;
`"marker_present"` shows whether a marker was found by the upward walk.
Note the trade: recall is lost for every repository you do not mark, and a
repository you *intended* to capture stays silent until you add its marker.
**Send different repositories to different servers.** One install can deliver
capture to several ai-memory servers, for example one per organisation. Register
each server once with `ai-memory server add <name> --url <url> --root <dir>
--auth-token-stdin`, then set `server = "<name>"` in each repository's
`.ai-memory.toml`. Repositories without the key keep using the server
`install-hooks` configured. A profile that does not resolve drops the event
instead of falling back. Native hook commands route profiles; the generated
TypeScript integrations and the script hooks drop routed repositories. See
[`marker-file.md`](marker-file.md#routing-capture-to-another-server-server).
Some agent harnesses attach the assistant's final turn to their `Stop` event —
Claude Code sends it as a raw `last_assistant_message`. By default that text is
never persisted: the native hook binary strips the raw field before it can reach
the local spool or the wire, and the server strips it defensively on arrival.
**Opt-in capture (#196).** You can opt in to storing a sanitized, 2 KB-capped
excerpt of the assistant's final turn as the Stop body. It is a **double
opt-in** — enable the server first, then the client:
1. **Server:** set `capture_assistant = true` in the live
`<data_dir>/config.toml` (or the service's configured TOML file), or set
`AI_MEMORY_CAPTURE_ASSISTANT=true`, then restart `ai-memory serve`.
2. **Client:** re-install the Claude Code (or Codex) hooks with the flag:
```bash
ai-memory install-hooks --agent claude-code --capture-assistant --apply
# Codex is supported too — its Stop payload carries last_assistant_message:
ai-memory install-hooks --agent codex --capture-assistant --apply
```
The client sanitizes (built-in patterns) and truncates the excerpt before it
touches the spool or wire; the server re-scrubs with its `[sanitize]` patterns
before storing. If either side is off — or the marker is malformed — the Stop
stays empty. Re-running `install-hooks` without `--capture-assistant` removes
the flag (idempotent). `--capture-assistant` is Claude Code, Codex and
OpenCode 2 (`--agent opencode2`) on a native hook platform only; on any other agent or the script fallback the
installer refuses it rather than enabling something that cannot take effect. Assistant text is
privacy-sensitive — read the `SECURITY.md` notes on what it can contain and where
it flows (consolidation/reviewer prompts, and out to a cloud LLM provider if one
is configured) before enabling it.
Upgrading the binary is sufficient for native Claude Code installs, and pending
spooled events drain with the raw field stripped as well. Installs that run the
`.sh`/`.ps1` script fallback (the Docker script bundle or an explicit
`AI_MEMORY_HOOK_PLATFORM=posix`) cannot sanitize the assistant text, so a `Stop`
payload still carrying the raw field is dropped whole by the script rather than
POSTed verbatim. The Docker wrapper deliberately keeps script commands because a
binary path inside its helper container is not valid on the host; running
`install-hooks` through that wrapper refreshes the scripts but does not convert
them. To capture assistant text safely, install a native ai-memory client on the
agent host, then use that native executable to run
`install-hooks --agent claude-code --apply`. Even if the script fallback is
retained, the server still strips any raw field on receipt before persistence.
Native `ai-memory hook --event ...` commands spool events locally. The POSIX
shell bundle spools too, but only on failure: it POSTs first and writes the
event to the same `<data_dir>/hook-spool/` contract when the server is
unreachable or answers 5xx, then flushes the backlog behind the next delivery
that succeeds. A 4xx is a permanent rejection and is not retried. (The
PowerShell bundle still drops an undelivered event.) Session start
does a short bounded cleanup drain before fetching a handoff; cancellation-prone
boundary events (`stop`, `pre-compact`, and `session-end`) start a detached
`hook-drain` helper so delivery does not depend on one shutdown hook surviving.
The POSIX bundle assigns one idempotency key before its initial POST and keeps
that key if the event enters the spool. A server that processed an event but
lost the response will not duplicate its observation or completed session-end
effects; if processing stopped after the observation commit, the retry re-runs
downstream work. SessionEnd atomically
commits its end watermark with its automatic handoff; a retry that finds that
transaction complete finishes any interrupted wiki commit, durable provider
enqueue, and ingest-key completion without adding a second handoff. Those
incomplete effects remain at-least-once until the server marks the event
complete.
On Unix, the helper uses a trusted `setsid` launcher when available and falls
back to a separate process group otherwise; Windows uses detached/breakaway
process flags. The spool is capped, so a permanently undrained backlog is
eventually pruned rather than unbounded, but old undelivered events can be lost.
The built-in timings stay short on agent-facing paths, but high-latency or
large-backlog instances can raise them with whole-minute runtime env vars in the
agent's environment; no `install-hooks` rerun is needed:
| Env var | Built-in default | Max override | What it caps |
|---|---:|---:|---|
| `AI_MEMORY_HOOK_DRAIN_TIMEOUT_MINUTES` | 3 seconds | 60 minutes | each event POST during a drain |
| `AI_MEMORY_HOOK_HANDOFF_TIMEOUT_MINUTES` | 3 seconds | 60 minutes | the synchronous `session-start` handoff GET |
| `AI_MEMORY_HOOK_START_BUDGET_MINUTES` | 3 seconds | 60 minutes | total time `session-start` may spend waiting for the drain lock and cleanup draining |
| `AI_MEMORY_HOOK_BACKGROUND_DRAIN_BUDGET_MINUTES` | 5 minutes | 60 minutes | total time the detached `hook-drain` helper may spend after a background-drain boundary |
| `AI_MEMORY_HOOK_INCREMENTAL_THRESHOLD` | 32 events | positive integer | spool backlog size that triggers a 250 ms `post-tool-use` catch-up drain |
Timing values must be positive whole minutes. Missing, empty, non-numeric, or
zero values fall back to the built-in defaults; values above 60 are clamped. The
incremental threshold is a positive event count; invalid values fall back to 32.
Server-side hook ingest also has an optional per-source limiter for shared or
remote installs that need protection from one runaway agent session. Set
`AI_MEMORY_HOOK_RATE_PER_SEC` on the server to the token refill rate per
actor/session source; `0` or unset disables the limiter. Set
`AI_MEMORY_HOOK_RATE_BURST` to override the burst size (defaults to the refill
rate, minimum one token when enabled). The limiter is bounded in both key count
and key bytes, and `/hook/batch` drains can skip over-budget sources while still
accepting later unrelated sources.
### Native service operations
```bash
# User service
systemctl --user restart ai-memory.service
systemctl --user stop ai-memory.service
journalctl --user -u ai-memory.service -n 100
# System service
sudo systemctl restart ai-memory.service
sudo systemctl stop ai-memory.service
journalctl -u ai-memory.service -n 100
```
Backups still use the same CLI, just point it at the service data dir:
```bash
# User service
ai-memory --data-dir ~/.local/share/ai-memory backup --to ~/ai-memory-backup.tar.gz
# System service
sudo -u ai-memory ai-memory --data-dir /var/lib/ai-memory backup --to /var/lib/ai-memory/backup.tar.gz
```
Package removal does not delete data. Stop the service and remove state only
when you intentionally want to erase memory:
```bash
systemctl --user disable --now ai-memory.service
sudo systemctl disable --now ai-memory.service
# Optional destructive cleanup:
rm -rf ~/.local/share/ai-memory ~/.config/ai-memory
sudo rm -rf /var/lib/ai-memory /etc/ai-memory
```
### Maintainer integration test
The normal CI runs `scripts/check-native-packaging.sh`, a host-safe regression
check that uses a temporary alternate root for `systemd-analyze`,
`systemd-sysusers`, and `systemd-tmpfiles`. It verifies unit syntax, expected
paths, sysusers output, tmpfiles rules, env-file mode, and AUR shell syntax
without writing to host `/usr`, `/etc`, `/var`, or touching real services.
The repo also includes a manual Arch integration harness that is intentionally
kept out of routine CI because it creates a disposable distrobox, installs
packages, starts real systemd services, and can take several minutes:
```bash
scripts/test-native-arch-systemd-distrobox.sh
```
It verifies the AUR metadata shape, builds the current working tree, installs
the native layout into the disposable Arch container, starts the system service
with `systemctl`, starts the user-profile command under transient systemd
supervision, and checks that packaged hook sources under
`/usr/share/ai-memory/hooks` can be staged by `install-hooks`.
The destructive part of that script refuses to run unless it detects a
container/distrobox environment.
Useful knobs:
```bash
AI_MEMORY_NATIVE_TEST_BOX=ai-memory-native-test scripts/test-native-arch-systemd-distrobox.sh
AI_MEMORY_NATIVE_TEST_KEEP_BOX=1 scripts/test-native-arch-systemd-distrobox.sh
AI_MEMORY_NATIVE_TEST_IMAGE=quay.io/toolbx/arch-toolbox:latest scripts/test-native-arch-systemd-distrobox.sh
```
---
## macOS menu bar app
On a Mac, the self-contained menu bar app is the GUI install: it bundles the
native `ai-memory` binary and `hooks/` tree, governs the existing LaunchAgent
(`com.github.akitaonrails.ai-memory`), and opens `/web`, `ai-memory status`,
`config.toml`, the data directory, and logs. It does not replace those tools
with a second dashboard.
Needs a Rust toolchain and Xcode / Swift 6 (the same as a source build):
```bash
git clone https://github.com/akitaonrails/ai-memory
cd ai-memory
./companions/ai-memory-macos/build.sh
open "companions/ai-memory-macos/dist/AI Memory.app"
```
Drag **AI Memory.app** to `/Applications`, then **Install & Start Server**
from the menu extra (no Dock icon). When the status item is green, wire an
agent with the bundled binary so `install-hooks` finds the sibling `hooks/`
tree:
```bash
BIN="/Applications/AI Memory.app/Contents/Resources/runtime/ai-memory"
"$BIN" install-mcp --client claude-code --apply
"$BIN" install-hooks --agent claude-code --apply
```
Durable memory stays in `~/Library/Application Support/ai-memory`. Replacing
the `.app` is an update and does not rewrite that tree. Prebuilt tarball,
source-build, Docker-wrapper, and hand-installed launchd paths remain in
[`docs/macos.md`](macos.md). Companion source:
[`companions/ai-memory-macos`](../companions/ai-memory-macos).
---
## Configuring other agent CLIs
> `install-mcp --server-url` accepts either the bare server origin or the full
> MCP endpoint and appends a missing `/mcp` exactly once.
> `install-hooks --server-url` takes the bare server **origin**
> (e.g. `http://homelab:49374`) — hook scripts append `/hook`, `/handoff`,
> etc. themselves.
Each agent CLI needs two things:
1. **MCP registration** - so the agent can call `memory_query`,
`memory_recent`, `memory_handoff_accept`.
2. **Lifecycle hooks** - so the server auto-captures session events.
Without this, the agent can still query memory but capture
becomes manual.
Claude Desktop, VS Code Copilot, and Zed are MCP-only today. The
hook-capable clients in the [README Support Matrix](../README.md#support-matrix),
including Pi and Zero, have lifecycle capture paths through `install-hooks`.
> **Hook install pattern.** Local supported profiles default to host-native
> commands. Claude Code may use its supported Windows exec form (`command` =
> real `ai-memory.exe`, `args` = argv tokens for `hook --event ...`); other
> agents use native single command strings according to their hook schema.
> PowerShell/Git Bash script bundles are compatibility fallbacks and do not
> enforce capture-policy v1. Remote-only/Docker script installs still use the
> two-step path: (1) `docker cp` bundled scripts to your home dir, (2)
> `docker run --rm install-hooks` renders the config snippet.
> OpenClaw, OpenCode, OMP, and Pi are different: they use generated
> TypeScript plugin/extension files, so no shell-script extraction is
> needed for those clients.
### OpenAI Codex
```bash
# MCP snippet (merge into $CODEX_HOME/config.toml, default ~/.codex/config.toml):
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client codex \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
# Hooks — extract scripts + render config:
docker cp ai-memory:/usr/local/share/ai-memory/hooks ~/.ai-memory/
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent codex \
--hooks-dir ~/.ai-memory/hooks \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
Native Codex tool hooks use top-level `tool_name`, `tool_input`, `tool_response`,
and `tool_use_id` fields (verified against CLI 0.154.0). ai-memory records the
tool family and call ID on `PreToolUse` and `PostToolUse`; recognized tools such
as `Bash` and `apply_patch` also retain a sanitized response excerpt on
`PostToolUse`, capped at 2 KB including metadata. Structured JSON responses are
flattened using the same bounded excerpt path. Inputs are not copied into
observations, and unknown tools (including unrecognized MCP names) retain only
metadata. `PostToolUse` alone does not prove success, so Codex outcomes remain
`unknown`.
Capture exclusions still run before native spooling. Codex's `apply_patch`
passes patch text in `tool_input.command`, which does not provide direct file
paths to the capture policy. With active `ignore_paths`, those events retain
only metadata; ai-memory does not parse patch or shell text to infer paths.
Tool events are delivered at the normal 32-event catch-up threshold or a
lifecycle drain boundary, so a small active turn may still have queued events.
Codex CLI 0.145.0 and later expose a native `SessionEnd` hook. `Stop` is captured
as a turn boundary and leaves the session open; a native `SessionEnd` triggers
the final summary, handoff, and auto-improvement eligibility. See the
[Codex hook lifecycle](https://learn.chatgpt.com/docs/hooks#sessionend) for when
Codex ends a session. For older clients or a missed session-end delivery, run:
```bash
ai-memory finalize-session
# add --all to close every matching open Codex session in this workspace/project
# or target one exact concurrent session (mutually exclusive with --all)
ai-memory finalize-session --session-id <uuid>
```
Antigravity CLI also lacks a true session-end event. Its `Stop` hook marks the
end of one execution loop, so ai-memory intentionally records it without
closing the conversation. Its `PreInvocation` hook likewise runs before every
model call; ai-memory treats only the documented `invocationNum = 0` call as
SessionStart. Later invocations return an empty hook result without capturing
another start or fetching the single-use handoff, so a handoff created while
the current conversation winds down remains available to the next session.
After the final turn, finalize the latest matching Antigravity session
explicitly:
```bash
ai-memory finalize-session --agent antigravity-cli
# add --all only to close every matching open Antigravity session in this scope
# or add --session-id <uuid> to close one exact concurrent session
# if the conversation continued after a first finalize, re-close it to cover
# the new observations (a re-run with nothing new is a harmless no-op)
ai-memory finalize-session --agent antigravity-cli --reopen --session-id <uuid>
```
### Devin CLI
Devin uses `~/.devin/config.json` for MCP servers and `~/.devin/hooks.v1.json`
for lifecycle hooks by default. If you prefer one combined Devin config file,
pass `--config-file ~/.devin/config.json` to `install-hooks`; ai-memory then
merges the hook entries under that file's `hooks` key.
```bash
ai-memory install-mcp --client devin --apply \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
ai-memory install-hooks --agent devin --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
ai-memory install-skills --agent devin
```
Devin's hook vocabulary is close to Claude Code's, with two important
differences:
- Devin emits `PostCompaction` after compaction and includes a `summary` field;
ai-memory records it as `post-compaction`.
- Devin does not expose subagent start/stop hooks, so ai-memory cannot capture
nested subagent boundaries for Devin.
The `SessionStart` hook injects pending handoffs through Devin's
`hookSpecificOutput.additionalContext`. Real Devin `SessionStart` and
`PostToolUse` payloads may omit `session_id` and `cwd`; ai-memory now infers cwd
from `DEVIN_PROJECT_DIR` or the hook process working directory when the payload
omits it, and mints/reuses a per-host session id from hook state when necessary,
so those events are still captured. A payload-provided value always wins.
### Kimi Code
Kimi Code keeps MCP servers in `~/.kimi-code/mcp.json` and lifecycle hooks in
`~/.kimi-code/config.toml`; both move together when `$KIMI_CODE_HOME` is set.
The CLI also accepts `--agent kimi` as an alias. `install-mcp` writes the
server URL with a `?flavor=moonshot` query because the Moonshot API rejects
root-level `anyOf`/`oneOf`/`allOf` in tool parameter schemas ("moonshot
flavored json schema") — the ai-memory server answers flavored requests with
flat schemas, and all other clients keep the upstream shape.
```bash
ai-memory install-mcp --client kimi-code --apply \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
ai-memory install-hooks --agent kimi-code --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
`install-hooks` merges `[[hooks]]` entries into `config.toml`, preserving the
provider/model settings the same file holds. Entries cover 10 events —
`SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`,
`PostToolUseFailure` (Kimi Code reports tool failures separately from
successful calls; it reuses the post-tool-use handler), `Stop`,
`SubagentStart`, `SubagentStop`, and `PreCompact` — and default to
native `ai-memory hook --event … --agent kimi-code` commands on local installs
(local spool plus batched delivery, capture-policy v1 enforced); the staged
script bundle under `~/.local/share/ai-memory/hooks/kimi-code/` is the
compatibility fallback (POSTs to `/hook`, spooling a failed delivery for a
later drain, without capture-policy v1 enforcement). A pending handoff
is injected at `UserPromptSubmit` through the hook's stdout, which Kimi Code
appends to the model context as a user message before the turn; Kimi Code
fires `SessionStart` but discards that hook's stdout, so hooks installed by
an older release consumed handoffs without delivering them. Existing native
hook commands invoke the current `ai-memory` binary and pick up the corrected
delivery behavior on upgrade. Re-run
`ai-memory install-hooks --agent kimi-code --apply` only for a
script-fallback installation so its staged scripts are refreshed.
Kimi Code hook entries accept only `event`, `matcher`, `command`, and
`timeout`; extra fields make the whole `config.toml` fail to load, so prefer
`install-hooks --apply` over hand edits.
### Command Code
Command Code keeps user-scope MCP and hook configuration in separate JSON
files under `~/.commandcode/`. Install both integrations with:
```bash
ai-memory install-mcp --client command-code --apply \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
ai-memory install-hooks --agent command-code --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
The aliases `commandcode`, `cmdc`, and `cmd` are accepted. `install-mcp`
merges a native HTTP entry into `~/.commandcode/mcp.json`; `install-hooks`
merges only Command Code's four stable events (`SessionStart`, `PreToolUse`,
`PostToolUse`, and `Stop`) into `~/.commandcode/settings.json`, preserving
other settings and hook handlers. The hook definitions deliberately omit
`matcher`: Command Code documents omission as "all tools", while any matcher
on `SessionStart` or `Stop` prevents that lifecycle hook from firing.
Local installs use the native `ai-memory hook` command, so Command Code's
native `session_id` and `cwd` are attributed directly;
recognized `shell_command`, `read_file`, `write_file`, and `edit_file`
payloads pass through the same bounded capture-exclusion policy as other
native integrations. A pending handoff is injected through
`hookSpecificOutput.additionalContext` at `SessionStart`.
Command Code's stable `Stop` event ends a turn, not a session. Finalize the
open session after the last turn when you need immediate consolidation and a
handoff:
```bash
ai-memory finalize-session --agent command-code
ai-memory finalize-session --agent command-code --session-id <uuid>
```
ai-memory does not install Command Code Mods. Mods run arbitrary unsandboxed
code and are not needed for the stable hook or managed-session paths.
Managed sessions are opt-in:
```bash
ai-memory run command-code
ai-memory run command-code --yolo --model <model-id>
```
The aliases `commandcode`, `cmdc`, and `cmd` select the same adapter. The
default executable is `command-code` on Unix and `cmdc` on native Windows.
Fresh sessions keep Command Code's native UUID; returning sessions use exact
`--session <uuid>`. The read-only adapter accepts only the observed v3 header,
requires its UUID filename and canonical `cwd` to match the checkout, excludes
checkpoint/prompt sidecars, hidden reasoning, images, and provider metadata,
and preserves `parentId` on visible events for branch provenance. An unknown
future transcript version fails closed until its schema is audited. Direct
`cmd`, `cmdc`, or `command-code` launches remain unchanged.
### Kiro CLI
Kiro CLI has one MCP surface and two incompatible lifecycle-hook formats.
ai-memory supports both through explicit installer targets: `kiro-cli` remains
the v2 target, while `kiro-cli-v3` selects the standalone v3 registration. The
global MCP file is `$KIRO_HOME/settings/mcp.json`, defaulting to
`~/.kiro/settings/mcp.json`; pass `--config-file .kiro/settings/mcp.json` for a
project-scoped entry.
```bash
ai-memory install-mcp --client kiro-cli --apply \
--server-url "https://memory.example/mcp" \
--auth-token "$TOKEN"
```
The `kiro` alias is equivalent. The installed URL includes
`?flavor=bedrock` so Kiro's Bedrock backend receives schemas without
root-level `anyOf`, `oneOf`, or `allOf`; nested schemas and runtime validation
remain intact. Kiro requires HTTPS for non-loopback remote servers, so the CLI
rejects a plain-HTTP homelab URL before changing the file. Configure a reverse
proxy as described in [HTTPS via reverse proxy](https-via-proxy.md).
Install v2 hooks with the `kiro-cli` agent value. When `--server-url` is omitted,
`install-hooks` can infer the hook origin and bearer token from the managed MCP
entry above.
```bash
# Default v2 engine: merge hooks into every existing global agent config.
ai-memory install-hooks --agent kiro-cli --apply
# A project-local v2 agent overrides a same-named global agent. Update the
# selected local config explicitly instead of assuming the global copy runs.
ai-memory install-hooks --agent kiro-cli --apply \
--config-file .kiro/agents/<agent-name>.json
```
The v2 engine stores camelCase hooks inside agent JSON files. ai-memory updates
existing `$KIRO_HOME/agents/*.json` files only; it will not fabricate an agent
that Kiro never selects. Create and select an agent first when that directory
is empty. Kiro gives [project-local agents precedence over global agents](https://kiro.dev/docs/cli/custom-agents/configuration-reference/),
so use `--config-file` when the active definition lives under
`.kiro/agents/`. All target files are parsed before any one is changed, and
unrelated agent fields, third-party hooks, and each agent's existing
`--project-strategy` remain intact.
The install registers spawn, user-prompt, pre-tool, post-tool, and stop capture,
remains fail-open when ai-memory is unavailable, and delivers a pending handoff
through successful `agentSpawn` stdout. Verified v2 tool payloads enforce
`[capture] ignore_paths`; an unrecognized payload shape is stored as bounded
metadata rather than exposing file content.
Install v3 hooks with the explicit `kiro-cli-v3` target. This distinction is
intentional: `kiro` and `kiro-cli` continue to mean v2 so an upgrade cannot
silently rewrite an existing installation into an incompatible format. The
standalone registration was acceptance-tested with an interactive Kiro CLI
2.16.2 `--v3` session.
```bash
# Global v3 registration under $KIRO_HOME/hooks (default ~/.kiro/hooks).
ai-memory install-hooks --agent kiro-cli-v3 --apply
# Project-local v3 registration.
ai-memory install-hooks --agent kiro-cli-v3 --apply \
--config-file .kiro/hooks/ai-memory.json
```
The v3 installer writes the documented standalone `version: "v1"` schema with
PascalCase triggers. It preserves third-party entries in a shared file,
refuses an unsupported schema version or a third-party collision with an
ai-memory-reserved hook name, and bounds capture-only commands to one second.
SessionStart gets five seconds so ai-memory's bounded handoff fetch can finish.
Both engines use the same sanitized hook-ingress boundary: documented and live
`tool_name`/`tool_input` file operations honor `[capture] ignore_paths`, while
unknown file-tool payload shapes degrade to metadata-only capture.
Kiro v2's `stop` event ends a turn, not the session. After the final turn, close
the matching session explicitly; use the exact id when several Kiro sessions
are open in the same project:
```bash
ai-memory finalize-session --agent kiro-cli
ai-memory finalize-session --agent kiro-cli --session-id <uuid>
```
`ai-memory uninstall --only hooks --apply --yes` removes only exact ai-memory
entries from global v2 agents, the current project's `.kiro/agents` directory,
and ai-memory's global/current-project v3 registration. A purely generated v3
file is deleted; third-party entries in a shared file remain. `ai-memory run
kiro` (alias `kiro-cli`) manages the default v2 engine and honors `$KIRO_HOME`;
add `--v3`, `--mode`, or `--agent-engine v3` for version-safe v3 resume. Once
linked, a later plain Kiro launch recovers the stored engine transparently, and
bare `ai-memory run` considers checkout-local sessions from both incompatible
stores. See
[managed workstreams](managed-workstreams.md#native-adapter-behavior).
### Pool (Poolside Agent CLI)
Pool reads lifecycle hooks from a project-scoped `.poolside/settings.yaml` at
the root of each repository it runs in — there is no user-global hook file for
ai-memory to merge. `install-hooks --agent pool` (alias `poolside`) therefore
stages the hook scripts to the stable user-global location and prints a
ready-to-paste `hooks:` snippet; ai-memory deliberately does not write files
inside your repositories.
```bash
# Stage the scripts and print the snippet to paste into
# <repo>/.poolside/settings.yaml:
ai-memory install-hooks --agent pool --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
The snippet wires Pool's five documented events — `SessionStart`,
`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` (Claude-shaped
names, snake_case JSON payload on stdin, verified against Poolside CLI
v1.0.16). Local installs use the native `ai-memory hook` command, so Pool's
documented `tool_name`/`tool_input` file operations honor `[capture]
ignore_paths` and unknown file-tool payload shapes degrade to metadata-only
capture.
Pool has no true session-end event; `Stop` is a turn boundary. After the final
turn, close the session explicitly; use the exact id when several Pool
sessions are open in the same project:
```bash
ai-memory finalize-session --agent pool
ai-memory finalize-session --agent pool --session-id <uuid>
```
Pool tolerates hook stdout, but model-visible context injection from
`SessionStart` stdout is not demonstrated, so the session-start hook captures
only and never fetches the (single-use) handoff — recover a prior session's
handoff via the MCP `memory_handoff_accept` tool. No first-party
`install-mcp` client and no managed workstream (`ai-memory run pool`) are
claimed: Pool's native session-store contract is not demonstrated, per
[managed-harness contributions](managed-harness-contributions.md).
### ZCode (z.ai)
ZCode wires lifecycle hooks in the root `hooks` block of
`~/.zcode/cli/config.json` — the same file that holds its other CLI
registration. `install-hooks --agent zcode` (alias `zai`) merges ai-memory's
entries into that block around any third-party hooks you already have, and is
idempotent: re-running strips only ai-memory's own entries (marked by
`statusMessage: "ai-memory capture"`) and rewrites them in place.
```bash
ai-memory install-hooks --agent zcode --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
# Preview the exact hooks block without writing anything:
ai-memory install-hooks --agent zcode \
--server-url "http://homelab:49374"
```
Entries are exec-form `{"type": "process", "command": <ai-memory>, "args":
[…]}` — ZCode spawns them with the event JSON on stdin and no shell, which the
native `ai-memory hook` command reads directly, so the local spool, bearer
auth, and `[capture] ignore_paths` exclusions all apply. Every emitted key is
from ZCode's documented hook schema (`type`, `command`, `args`, `enabled`,
`timeoutMs`, `statusMessage`); ZCode drops entries carrying undocumented keys,
so none are emitted. Six documented triggers are wired: `SessionStart`,
`UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, and
`Stop` (verified live against the embedded engine v0.16.5).
`PostToolUseFailure` fires *instead of* `PostToolUse` when a tool throws and
is forwarded onto the same capture channel with the error preserved as the
observation outcome. `PermissionRequest` is deliberately not installed: its
hook chain races the interactive permission client, and a fast client decision
aborts the losing hook, so passive capture of that event is unreliable by
design.
ZCode injects `SessionStart` stdout as model context
(`hookSpecificOutput.additionalContext`), so unlike Pool, Zero, and Grok the
prior session's handoff is delivered automatically at session start. There is
no true session-end event — `Stop` fires at the end of every turn — so close
finished sessions explicitly; use the exact id when several ZCode sessions are
open in the same project:
```bash
ai-memory finalize-session --agent zcode
ai-memory finalize-session --agent zcode --session-id <uuid>
```
No first-party `install-mcp` client and no managed workstream
(`ai-memory run zcode`) are claimed yet.
### Hermes Agent (Nous Research)
Hermes declares lifecycle hooks in the `hooks:` block of
`~/.hermes/config.yaml`. `install-hooks --agent hermes` (alias `hermes-agent`)
prints the block to paste there:
```bash
ai-memory install-hooks --agent hermes \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
Hermes splits each configured `command` with `shlex.split` and runs it with
**no shell**, passing the event JSON on stdin — so the generated entry invokes
the native `ai-memory hook` command directly (the same exec-form shape Zero and
ZCode use) and there is no script bundle to stage. There is also no `--apply`
that writes the file: `~/.hermes/config.yaml` is YAML you also edit, and Hermes
gates user hooks behind its own acceptance prompt (`hooks_auto_accept`). Paste
the block and let Hermes accept it.
Two events are wired: `pre_tool_call` → `pre-tool-use` and `post_tool_call` →
`post-tool-use`. Their payload carries `tool_name` / `tool_input`, which is what
gives Hermes sessions tool observations, tool-family titles, and
`[capture] ignore_paths` exclusion enforcement.
Session lifecycle is deliberately not wired here: automatic recall, prompt
capture, session-end and the automatic handoff for Hermes belong to the memory
provider, the community-maintained
[`ai-memory-hermes-plugin`](https://github.com/MrLuciano/ai-memory-hermes-plugin).
A second, hook-driven `session-end` would close the same session twice. Hermes
ignores session-start hook stdout, so a pending handoff is recovered through
MCP: `memory_handoff_list` then `memory_handoff_accept`.
No first-party `install-mcp` client and no managed workstream
(`ai-memory run hermes`) are claimed yet.
### OpenCode
```bash
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client opencode \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
# Plugin — write to ~/.config/opencode/plugins/ai-memory.ts.
# If you have the local wrapper installed, prefer `--apply`:
ai-memory install-hooks --agent opencode --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
# Docker-only preview path; redirect only if you want to write the file yourself:
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent opencode \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
Restart OpenCode after installing or changing the plugin; plugins are
loaded at startup.
### OpenCode 2 (beta)
The generated plugin targets the OpenCode 2.0.10+ event API (checked against
2.0.14). OpenCode 2 runs one long-lived service behind every CLI, so closing a
terminal is not a session end: each completed root turn instead writes a
deterministic checkpoint (no LLM call) of `sessions/<id>.md` and refreshes the
session's automatic handoff, keeping one open baton per live session. The next
session claims the latest checkpoint of a session that has captured nothing for
ten minutes. Nothing tells a closed terminal from a parallel session still at
work in the same directory, so this is a heuristic: a session in use keeps its
baton, one session's turn never retires another live session's baton, and a
claim retires only older batons of quiet sessions. A tool or model call that
runs longer than ten minutes without a captured event looks quiet. Startup context is claimed once per root
session and retained on every later model request; child sessions never claim
it or publish a baton.
Assistant text stays opt-in, as for Claude Code and Codex: set
`capture_assistant = true` on the server and install with
`ai-memory install-hooks --agent opencode2 --capture-assistant --apply`. The
plugin hands the last completed text to the native hook, which sanitizes and
caps it before it reaches the spool or the wire; the excerpt then rides in the
next session's automatic handoff. A bare re-apply preserves the opt-in.
For a commented `opencode.jsonc`, preview `install-mcp --client opencode2` and
merge the entry into the existing `mcp.servers` object by hand: the apply path
writes strict JSON.
The 2.0 beta installs side by side as `opencode2` and shares v1's config
dir and session store, but its MCP schema and plugin API changed. Wire it
with the `opencode2` client/agent names:
```bash
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client opencode2 \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
# Plugin — write to ~/.config/opencode/plugins/ai-memory-opencode2.ts.
# If you have the local wrapper installed, prefer `--apply`:
ai-memory install-hooks --agent opencode2 --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
V2 nests servers under `mcp.servers` (no `enabled` field), so the v1
(`mcp`) and v2 (`mcp.servers`) entries coexist in the one
`~/.config/opencode/opencode.json(c)` file — the beta explicitly supports
this mixed nesting, so keep both entries and do not "convert" the file by
removing the v1 one
(see [Migrate from V1](https://opencode.ai/v2/docs/migrate-v1)). `ai-memory run opencode2`
resumes the same native sessions as `ai-memory run opencode` through the
`opencode2` binary. Both plugins share the one auto-loaded dir while the
beta is side-by-side; a host may warn about its sibling's file (the two
plugin APIs are incompatible) — that warning is benign, and `uninstall`
removes each file only on its own ownership markers.
> **Back up `~/.local/share/opencode` before running the beta against
> your real data.** The two binaries share the one `opencode.db` file and
> the beta has migrated its schema in place before, leaving stable
> `opencode` 1.x broken
> ([upstream #42260](https://github.com/anomalyco/opencode/issues/42260)).
> ai-memory only ever opens that database read-only; the migration risk
> comes from launching `opencode2` itself, not from this integration.
>
> **The beta's background service defaults to port 49374 — ai-memory's own
> default.** Running both on defaults crash-loops the opencode2 service
> (`Managed service port 49374 ... is already in use`). Move one side:
> `opencode2 service set port <free-port>`, or start ai-memory with
> `--bind 127.0.0.1:<free-port>` (and matching `--server-url` installs).
**On a Gemini/Vertex model, serve Gemini-safe schemas.** OpenCode forwards MCP
tool schemas to the configured provider verbatim, and Google's `Schema`
(Vertex/Gemini `functionDeclaration.parameters`) accepts only a single `type` per
field. `schemars` renders every optional tool argument as a nullable union
(`"type": ["integer", "null"]`), so the provider rewrites it into `any_of` with
`description` still beside it and Vertex fails the whole session at
`tools/list`:
```
Unable to submit request because `ai-memory_memory_auto_improve` functionDeclaration
`parameters.max_proposals` schema specified other fields alongside any_of.
When using any_of, it must be the only field set.
```
Either set `gemini_safe_schemas = true` in `config.toml`
(`AI_MEMORY_GEMINI_SAFE_SCHEMAS=true`) on the server, which collapses those
unions to `"type": "integer"` plus `nullable: true` for every client, or append
`?flavor=gemini` to just this client's MCP URL. Runtime validation is unchanged
either way. Gemini CLI and Antigravity CLI normalize schemas client-side and
need neither.
### Oh My Pi / OMP
```bash
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client omp \
--server-url "http://homelab:49374/mcp" \
--auth-token "$TOKEN"
# Extension — write to ~/.omp/agent/extensions/ai-memory-omp.ts.
# If you have the local wrapper installed, prefer `--apply`:
ai-memory install-hooks --agent omp --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
Restart OMP after installing or changing the extension; extensions are
loaded at startup. The ai-memory CLI accepts `--client omp` (or
`--client oh-my-pi`) for MCP and `--agent omp` (or `--agent oh-my-pi`)
for hooks; both target OMP's native `.omp` integration surface.
### Pi
Pi does not read a native `mcp.json`. ai-memory supports Pi through one
generated TypeScript extension at `~/.pi/agent/extensions/ai-memory-pi.ts`; the
same file captures lifecycle events and bridges ai-memory's HTTP MCP tools into
Pi with `pi.registerTool`. When `PI_CODING_AGENT_DIR` is set (it relocates
Pi's whole `~/.pi/agent` home), the extension is written to
`$PI_CODING_AGENT_DIR/extensions/ai-memory-pi.ts` instead.
The Pi and OMP extensions use distinct filenames (`ai-memory-pi.ts` and
`ai-memory-omp.ts`) so installing one never overwrites the other. They are
not interchangeable — only Pi's bridges MCP tools.
#### OMP profiles
`omp --profile <name>` relocates OMP's agent home to
`~/.omp/profiles/<name>/agent`. Point the installers at the same profile so
the extension and MCP entry are installed in that directory:
```bash
ai-memory install-hooks --agent omp --profile work --apply
# Or pass OMP_PROFILE to each installer:
OMP_PROFILE=work ai-memory install-hooks --agent omp --apply
OMP_PROFILE=work ai-memory install-mcp --client omp --apply
```
`--profile` takes precedence over `OMP_PROFILE`. The legacy `PI_PROFILE` is
used only when `OMP_PROFILE` is unset. Names are trimmed and validated using
OMP's rules. An empty environment value, a whitespace-only name or `default`
selects the default profile; an explicit `--profile ""` is rejected.
A named profile ignores `PI_CODING_AGENT_DIR`. The default profile uses it
unless it points to the named profile directory exported by a parent OMP
process. `PI_CONFIG_DIR` changes the `.omp` root relative to your home using
OMP's path-joining rules; it does not replace an explicit agent directory.
The extension and `mcp.json` stay in the agent directory even when sessions
move to XDG storage. See [native adapter behavior](managed-workstreams.md#native-adapter-behavior)
for the session paths.
`uninstall --profile <name>` removes the profile's integration files. It also
checks the default profile and the legacy `~/.omp` locations, including when
`PI_CONFIG_DIR` selects a different root.
```bash
ai-memory install-hooks --agent pi --apply \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
# `install-mcp --client pi` prints this guidance instead of writing mcp.json:
ai-memory install-mcp --client pi --server-url "http://homelab:49374/mcp"
```
Restart Pi after installing or changing the extension. OMP / Oh My Pi remains
separate and continues to use `.omp` paths.
### Bind mounts vs docker cp
The `setup-agent` subcommand does the extract + render in one shot
using a bind mount:
```bash
docker run --rm -v "$HOME/.ai-memory:/host" \
akitaonrails/ai-memory:latest \
setup-agent --agent claude-code --to /host/hooks \
--host-prefix "$HOME/.ai-memory/hooks" \
--server-url "http://homelab:49374" --auth-token "$TOKEN"
```
This works cleanly when the container user's UID matches the host
user's UID (e.g. the homelab where both are 1000). It **fails on
rootless Docker** and on hosts with `userns-remap` enabled - the
container can't write to a host directory that belongs to a UID
outside the user-namespace mapping.
The `docker cp` pattern recommended above sidesteps all of that
because `docker cp` is mediated by the docker daemon and outputs
files owned by the user running the command. Prefer it as the
default; reach for `setup-agent` only when your docker setup is
known not to remap UIDs.
### Other MCP clients
See [**`docs/mcp-install.md`**](mcp-install.md) for the per-client MCP
config file path and snippet, or one-shot it via:
```bash
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client cursor --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent cursor --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client claude-desktop --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client gemini-cli --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent gemini-cli --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client antigravity-cli --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent antigravity-cli --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client grok --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent grok --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client openclaw --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent openclaw --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client kiro-cli --auth-token "$TOKEN" \
--server-url "https://memory.example/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent kiro-cli --auth-token "$TOKEN" \
--server-url "https://memory.example"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client command-code --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent command-code --auth-token "$TOKEN" \
--server-url "http://homelab:49374"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client vscode-copilot --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client zed --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
docker run --rm akitaonrails/ai-memory:latest \
install-mcp --client zcode --auth-token "$TOKEN" \
--server-url "http://homelab:49374/mcp"
```
Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, Kiro CLI, Command Code, and OpenClaw support both
`install-mcp` and `install-hooks`. Grok's `install-mcp --client grok` writes
`$GROK_HOME/config.toml` (default `~/.grok/config.toml`); its hooks live under
`$GROK_HOME/hooks` (default `~/.grok/hooks`). `install-hooks --agent grok`
captures lifecycle events.
Grok ignores `SessionStart` stdout and discards an allowing `UserPromptSubmit`,
so those hooks do not accept the handoff. The first `PostToolUse` prints
`hookSpecificOutput.additionalContext` (pending handoff, plus an opted-in
`[briefing]`). The model sees it after that tool result, not before the
first prompt. A session with no tool call leaves the handoff for
`memory_handoff_accept`. Claude Desktop, VS Code Copilot, Zed,
and ZCode
are MCP-only here, so you'll need to nudge the model to call
`memory_query` / `memory_handoff_accept` itself.
For clients with `install-hooks` support, the capture path handles
handoff injection at session start or the client's closest equivalent, except
for Zero's no-stdout SessionStart behavior. Grok delivers on the first
`PostToolUse` instead (Antigravity CLI uses `PreInvocation`).
---
## Installing hooks without docker
If you only need to use ai-memory *from* a machine (i.e. that machine doesn't
run the server), download and verify the release installer. The installer then
downloads and verifies the release's hook archive before writing any scripts:
```bash
installer_base=https://github.com/akitaonrails/ai-memory/releases/latest/download/ai-memory-install-hooks
installer_tmp="$(mktemp -d)"
trap 'rm -rf "$installer_tmp"' EXIT
curl -fsSL "$installer_base" -o "$installer_tmp/ai-memory-install-hooks"
curl -fsSL "$installer_base.sha256" -o "$installer_tmp/ai-memory-install-hooks.sha256"
expected="$(awk 'NR == 1 { print $1 }' "$installer_tmp/ai-memory-install-hooks.sha256")"
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$installer_tmp/ai-memory-install-hooks" | awk '{ print $1 }')"
else
actual="$(shasum -a 256 "$installer_tmp/ai-memory-install-hooks" | awk '{ print $1 }')"
fi
[ -n "$expected" ] && [ "$actual" = "$expected" ] || { echo "installer checksum mismatch" >&2; exit 1; }
chmod +x "$installer_tmp/ai-memory-install-hooks"
"$installer_tmp/ai-memory-install-hooks" --agent claude-code
rm -rf "$installer_tmp"
trap - EXIT
# Then render the JSON config (still wants `ai-memory` somewhere —
# either via docker as a one-shot, or installed locally):
docker run --rm akitaonrails/ai-memory:latest \
install-hooks --agent claude-code \
--hooks-dir "$HOME/.ai-memory/hooks" \
--server-url "http://homelab:49374" \
--auth-token "$TOKEN"
```
The curl script installer supports
`--agent claude-code|codex|cursor|gemini-cli|antigravity-cli|grok|opencode|opencode2|openclaw|omp|oh-my-pi|pi`
and `--to <dir>`; `--help` prints the full flag list. OpenCode,
OpenClaw, OMP / Oh My Pi, and Pi do not need script extraction because
`install-hooks` generates TypeScript plugin/extension files for them
instead. For Pi, the generated extension also provides the MCP bridge.
The generated TypeScript integrations survive an unreachable server the
same way the native hooks do: a delivery that fails at the network level
(or gets a 5xx) is written to `<data_dir>/hook-spool/` in the exact
format `ai-memory hook-drain` reads, and the plugin drains that backlog
itself once the server is reachable again — so a day of laptop work off
the server's network is captured, not silently dropped. Each spooled
entry carries an idempotency key, so the plugin's own drain and a
manual `hook-drain` can race without double-ingesting. Note the spool
lands in the *agent host's* data dir (`AI_MEMORY_DATA_DIR` or the
platform default), which is where a native `ai-memory` install looks.
This path is friction-free when:
- You have curl + bash but not docker
- You don't need to run a local ai-memory server (you're a client of
a homelab/remote ai-memory)
### Hook command paths across a container boundary
`install-hooks --apply` stages the hook scripts into the data dir and
writes their absolute paths into the agent's config. When the CLI runs
inside a container but the agent runs on the host, those staged paths
would be container paths the host can't see. Set
`AI_MEMORY_HOOKS_HOST_ROOT` to the *host* directory that the staged
`hooks/` tree is mounted from and the rendered config uses
`<host-root>/<agent>/…` command paths instead. The bundled docker
wrappers (`bin/ai-memory`, `bin/ai-memory.ps1`) forward this variable
automatically; you only set it by hand for custom container setups.
---
## Running ai-memory without docker
Most users should stick to the docker wrapper from the Quick start. Arch
Linux users have the [AUR packages](#arch-linux-native-packages-aur). For any
other host, `mise` installs a tagged release binary directly from GitHub —
no Rust toolchain needed:
```bash
mise use -g github:akitaonrails/ai-memory
```
This uses [mise's GitHub backend](https://mise.jdx.dev/dev-tools/backends/github.html),
which downloads the release archive matching your OS/arch
(`ai-memory-linux-x86_64.tar.gz`, `ai-memory-macos-aarch64.tar.gz`, etc.),
verifies its checksum against the published `.sha256` sidecar, then extracts
it and puts `ai-memory` on `PATH`. (mise can also check GitHub artifact
attestation and SLSA provenance where a project publishes them; ai-memory's
release workflow does not emit either today, so only the checksum applies.)
No dedicated mise plugin or
registry entry is required — the backend works against any repo whose
release assets follow this naming convention. By default mise also holds back
the very newest release for a short safety window
([`minimum_release_age`](https://mise.jdx.dev/dev-tools/github-backend.html)),
so a fresh tag may resolve to the previous version for a day or so; pin an
exact tag with `mise use -g github:akitaonrails/ai-memory@1.30.0` to bypass
that.
`cargo install ai-memory` is not available: the crate name is already taken
by an unrelated project on crates.io, as is `ai-memory-core` (the workspace's
foundational internal crate). Publishing would require renaming at least
that crate for the registry — a naming decision the project hasn't made yet.
Build from source only when hacking on ai-memory itself or running on a
platform none of the above covers. On macOS, tagged releases also publish
native `ai-memory-macos-aarch64.tar.gz` and `ai-memory-macos-x86_64.tar.gz`
archives when you only need the client CLI.
```bash
git clone https://github.com/akitaonrails/ai-memory ~/.ai-memory
cd ~/.ai-memory
cargo build --release --workspace
./target/release/ai-memory init # one-time
./target/release/ai-memory serve --transport http \
--bind 127.0.0.1:49374 # MCP + hook HTTP server
```
Data dir defaults to `~/.local/share/ai-memory` on Linux,
`~/Library/Application Support/ai-memory` on macOS, and the platform
local-data directory on Windows, typically
`%LOCALAPPDATA%\ai-memory`. Override with `AI_MEMORY_DATA_DIR=/path`.
To require bearer-token auth, set `AI_MEMORY_AUTH_TOKEN` in the
server's environment.
#### Optional serve flags
The `serve` subcommand also accepts:
| Flag | Env var | What it does |
|---|---|---|
| `--enable-web` | `AI_MEMORY_ENABLE_WEB=true` | Mount the read-only web browser + `/api/v1` JSON API. |
| `--base-path /wiki` | `AI_MEMORY_BASE_PATH` | Host the entire HTTP surface (`/mcp`, `/hook`, `/admin/*`, `/api/v1`, `/web`) under a configurable subpath — useful behind a reverse proxy sharing a hostname. `.` and `..` segments are rejected; unsafe chars cause a fallback to root with a warning. See [`docs/https-via-proxy.md`](https-via-proxy.md#hosting-under-a-subpath). |
| `--web-slug /web` | `AI_MEMORY_WEB_SLUG` | Where the web UI mounts within the base-path. Default `/web`; set to `/` to mount the UI at the base-path root. |
| `--web-ui-dir <path>` | `AI_MEMORY_WEB_UI_DIR` | Serve a custom SPA from `<path>` instead of the built-in browser. ai-memory injects `<base href>` and `<meta name="ai-memory-base-path">` so the SPA can build relative URLs and API calls under the configured prefix. |
| `--cors-allow-origin <origin>` | `AI_MEMORY_CORS_ALLOW_ORIGINS` (CSV) | Allow listed origins to call `/api/v1`. Layer is scoped only to that route — `/mcp`, `/hook`, `/admin`, and `/web` remain origin-locked. |
| _(config only)_ | `AI_MEMORY_HOOK_RATE_PER_SEC`, `AI_MEMORY_HOOK_RATE_BURST` | Optional per-actor/session hook ingest token bucket. Unset/`0` rate disables it; burst defaults to the rate (minimum one token when enabled). |
On macOS, see [`docs/macos.md`](macos.md); use the archive matching your
architecture: `aarch64` for Apple Silicon, `x86_64` for Intel. The
[menu bar app](#macos-menu-bar-app) is the self-contained GUI path (bundles
the binary, starts the LaunchAgent, opens `/web` and status). On Windows, see
[`docs/windows.md`](windows.md).
The short version: run the install commands from the same environment that
launches the agent. WSL2-launched agents need WSL paths and POSIX `.sh` hooks.
Native Windows agents can use the tagged `ai-memory-windows-x86_64.zip`, the
Docker Desktop wrapper, or a source build. Native Claude Code uses Claude exec
form with a real `ai-memory.exe` by default; the Windows Docker wrapper renders
other native Windows script-hook agents through encoded PowerShell `.ps1`
fallback commands.
When run from source, `install-hooks` finds the bundled scripts in
the repo's `hooks/` automatically. Extracted release archives also
auto-discover the sibling `hooks/` bundle beside the `ai-memory` binary:
```bash
./target/release/ai-memory install-hooks --agent claude-code --auth-token "$TOKEN"
```
(No need for `setup-agent` in this case - the scripts already live
at the right host path.)
---
## LLM provider tiers
ai-memory works in three intensity tiers:
| Tier | What you get | Env vars | Cost |
|---|---|---|---|
| **Zero-LLM** (default) | FTS5 + manually declared entity + graph search, rule-based session summaries, auto-handoffs from prompt + tool-call history | (none) | $0 |
| **+ LLM consolidation** | LLM rewrites session pages as coherent narratives; PreCompact checkpoints; LLM-driven contradiction lint | `AI_MEMORY_LLM_PROVIDER=anthropic` + `ANTHROPIC_API_KEY` | ~$0.01–0.05 / session |
| **+ Anthropic via subscription** | Same LLM features using a Claude Pro/Max subscription instead of an API key | `AI_MEMORY_LLM_PROVIDER=anthropic-oauth` + `ANTHROPIC_OAUTH_TOKEN` | Uses your Claude subscription |
| **+ ChatGPT/Codex OAuth** | Same LLM features using a ChatGPT Pro/Plus login instead of an OpenAI Platform key | `AI_MEMORY_LLM_PROVIDER=openai-oauth` + `ai-memory auth login openai-oauth` | Uses your ChatGPT subscription |
| **+ Codex credential reuse** | Same LLM features using the Codex CLI-owned login without copying or owning its refresh token | `AI_MEMORY_LLM_PROVIDER=codex` + an authenticated Codex CLI | Uses your ChatGPT subscription |
| **+ GitHub Copilot** | Same LLM features using a GitHub Copilot subscription | `AI_MEMORY_LLM_PROVIDER=copilot` + `ai-memory auth login copilot` or `COPILOT_GITHUB_TOKEN` | Uses your Copilot subscription |
| **+ LLM reranking** | At most one relevance pass over up to 30 bounded project/scopes search candidates; normal order is preserved on invalid, failed, timed-out, or concurrency-saturated responses | `AI_MEMORY_RERANKER=llm` + any configured LLM provider | One LLM call per eligible query, at most four concurrently |
| **+ Hybrid retrieval** | Adds vector cosine similarity to FTS5 + entity + graph RRF. Better recall on paraphrased queries | `AI_MEMORY_EMBEDDING_PROVIDER=openai` + `OPENAI_API_KEY` (or `EMBEDDING_API_KEY`) | ~$0.0001 / page on backfill |
### Recommended models (chosen as defaults)
If you set only the provider, ai-memory picks a sensible default:
| Setting | Default | Why |
|---|---|---|
| `AI_MEMORY_LLM_PROVIDER=anthropic` | `claude-haiku-4-5` | **Recommended default.** Best balance of speed, restraint, and classification quality. Not a reasoning model. Consistently classifies durable project rules as `kind: rule`. |
| `AI_MEMORY_LLM_PROVIDER=anthropic-oauth` | `claude-sonnet-4-6` | Anthropic via Claude subscription. Run `claude setup-token` once; set `ANTHROPIC_OAUTH_TOKEN` (or `CLAUDE_CODE_OAUTH_TOKEN`). No `ANTHROPIC_API_KEY` needed. Same `/v1/messages` endpoint, Bearer token auth. |
| `AI_MEMORY_LLM_PROVIDER=openai` | `gpt-5.4-mini` | Cheaper + faster alternative. Same parse reliability; mild over-classification on thin sessions. |
| `AI_MEMORY_LLM_PROVIDER=openai-oauth` | `gpt-5.5` | ChatGPT/Codex backend. Run `ai-memory auth login openai-oauth` once; ai-memory stores the refresh token in `<data_dir>/auth.json` and refreshes access tokens automatically. Optional `AI_MEMORY_LLM_REASONING_EFFORT` (`none`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`/`ultra`/`persistent`) is mapped to each provider's native reasoning field; omit it to keep the model default. |
| `AI_MEMORY_LLM_PROVIDER=codex` | `gpt-5.6-luna` | Reuses only `access_token` and `account_id` from Codex's `auth.json`; token renewal is delegated to `codex app-server --stdio`. |
| `AI_MEMORY_LLM_PROVIDER=copilot` | `gpt-5.5` | GitHub Copilot Chat backend. ai-memory stores a GitHub user token in `<data_dir>/auth.json`, exchanges it for a short-lived Copilot API token, and refreshes before expiry. |
| `AI_MEMORY_LLM_PROVIDER=gemini` | `gemini-3.5-flash` | Google's hosted option with a generous free tier. ai-memory disables Gemini 3.5 Flash's default dynamic thinking so hidden thought tokens do not truncate strict JSON. Set `GEMINI_API_KEY` (or `GOOGLE_API_KEY`). |
| `AI_MEMORY_LLM_PROVIDER=opencode` | `claude-sonnet-4-6` | [OpenCode](https://opencode.ai) cloud API. Defaults to the **Go** endpoint, `opencode.ai/zen/go/v1` — a cost-optimised model subset. GPT-5.6 Luna uses Go's Responses endpoint; other models use Chat Completions. For **Zen**'s full catalogue, set `AI_MEMORY_LLM_BASE_URL=https://opencode.ai/zen/v1` plus an `AI_MEMORY_LLM_MODEL` from it; the default model id is Go's. Requests identify ai-memory by version and reuse one session header across related attempts. Both endpoints take `OPENCODE_API_KEY` (key from `opencode.ai/auth`). Alias: `opencode-zen` — historical, and it selects Go like the others; the endpoint is chosen by the base URL, not the alias. |
| `AI_MEMORY_EMBEDDING_PROVIDER=openai` | `text-embedding-3-small` (1536-dim) | 5× cheaper than `-3-large` with marginal recall loss. |
| `AI_MEMORY_EMBEDDING_PROVIDER=openai` + `AI_MEMORY_EMBEDDING_BASE_URL=https://openrouter.ai/api/v1` | `openai/text-embedding-3-small` via [OpenRouter](https://openrouter.ai) | Uses `EMBEDDING_API_KEY`, else reuses `LLM_API_KEY` or `OPENAI_API_KEY`, with the OpenAI-compatible embedding client. |
| `AI_MEMORY_EMBEDDING_PROVIDER=openai` + `AI_MEMORY_EMBEDDING_BASE_URL=https://api.orcarouter.ai/v1` | `openai/text-embedding-3-small` via [OrcaRouter](https://www.orcarouter.ai) | Uses `EMBEDDING_API_KEY`, else reuses `LLM_API_KEY`, with the OpenAI-compatible embedding client. |
| `AI_MEMORY_EMBEDDING_PROVIDER=voyage` | `voyage-3` (1024-dim) | Voyage's current general-purpose recommendation. |
| `AI_MEMORY_EMBEDDING_PROVIDER=google` / `gemini` | `gemini-embedding-001` (768-dim) | Google-hosted embeddings via `embedContent`. Set `GEMINI_API_KEY` (or `GOOGLE_API_KEY`). |
| `AI_MEMORY_EMBEDDING_PROVIDER=openai-compat` | no default — set model, dim, and base URL explicitly | Self-hosted engines (Ollama, LM Studio, vLLM). Keyless by default; `EMBEDDING_API_KEY`, else `LLM_API_KEY`, is sent as a bearer token when present (gateways). Example: `AI_MEMORY_EMBEDDING_BASE_URL=http://localhost:11434/v1`, `AI_MEMORY_EMBEDDING_MODEL=nomic-embed-text`, `AI_MEMORY_EMBEDDING_DIM=768`. Switching an existing `openai`+base-URL setup to `openai-compat` changes the stored `{provider, model, dim}` triple — run `ai-memory embed --force` to re-embed. Asymmetric models need `AI_MEMORY_EMBEDDING_QUERY_PREFIX` / `AI_MEMORY_EMBEDDING_DOCUMENT_PREFIX`, e.g. `nvidia/Nemotron-3-Embed-1B-BF16` wants `query: ` / `passage: ` — see [`docs/llm-providers.md`](llm-providers.md) for that and for Qwen3-Embedding/instruction-tuned E5, which need a different (query-only) format. |
| `AI_MEMORY_EMBEDDING_PROVIDER=copilot` | `text-embedding-3-small` (1536-dim) | Reuses the `copilot` LLM provider's OAuth login (`ai-memory auth login copilot`, `COPILOT_GITHUB_TOKEN`, or `GITHUB_COPILOT_API_TOKEN`) — no separate API key. Calls Copilot's `/embeddings` endpoint following the OpenAI-compatible contract Copilot documents for chat; that endpoint's exact shape is not covered by a live test against Copilot here, so treat it as needing a real-Copilot smoke test. |
> **What we don't recommend:** reasoning-mode models (Claude with extended
> thinking, GPT-o3, Gemini "thinking" variants) — they burn token budget on
> internal reasoning and hang or emit empty responses with the strict-JSON
> consolidation prompt. Turn reasoning off if you must use one.
#### Embedding credentials (`EMBEDDING_API_KEY`)
`EMBEDDING_API_KEY` is optional and credentials the embedding role alone. Set
it when the embedder should authenticate against a different provider than the
LLM — the `openai` and `openai-compat` embedders otherwise borrow the chat
model's `OPENAI_API_KEY` or `LLM_API_KEY`, which is then sent to whatever
`AI_MEMORY_EMBEDDING_BASE_URL` points at and rejected with a 401.
Precedence for `AI_MEMORY_EMBEDDING_PROVIDER=openai`:
1. `EMBEDDING_API_KEY`
2. `OPENAI_API_KEY`
3. `LLM_API_KEY`, only when `AI_MEMORY_EMBEDDING_BASE_URL` is set
`openai-compat` checks `EMBEDDING_API_KEY`, then `LLM_API_KEY`, and stays
keyless when neither is set. `voyage` and `google`/`gemini` are unaffected:
they read their own `VOYAGE_API_KEY` and `GEMINI_API_KEY`/`GOOGLE_API_KEY` and
never borrow another role's key. With `EMBEDDING_API_KEY` unset, key
resolution is exactly what it was before the variable existed.
```bash
# Chat on api.openai.com — `openai` is the provider that sends
# `max_completion_tokens`, which gpt-5 / o-series models require and
# `openai-compat` never emits — with embeddings on another endpoint.
export AI_MEMORY_LLM_PROVIDER=openai
export OPENAI_API_KEY=sk-...
export AI_MEMORY_EMBEDDING_PROVIDER=openai
export AI_MEMORY_EMBEDDING_BASE_URL=https://openrouter.ai/api/v1
export EMBEDDING_API_KEY=sk-or-v1-...
```
### Anthropic via Claude subscription (OAuth)
> [!WARNING]
> **Unofficial and against Anthropic's usage policies — use at your own risk.**
> Anthropic provides no public OAuth API for the Claude Pro/Max subscription;
> this reuses the `claude setup-token` credential against `/v1/messages`, which
> is **not a supported or sanctioned integration**. Anthropic's terms reserve
> subscription (Claude Code) access for interactive use, and using it as an
> automated API backend may breach those terms and **could get your account
> rate-limited, flagged, or banned**. The header recipe is also undocumented
> and can change without notice. If you want a supported path, use the
> `anthropic` provider with a real Platform API key. We ship this purely as an
> opt-in convenience and make no guarantees about it.
`anthropic-oauth` is for Claude Pro/Max subscribers who want to use their
existing subscription instead of an Anthropic Platform API key. It hits the
**same** `/v1/messages` endpoint as the `anthropic` provider — only the auth
headers differ (Bearer token + `anthropic-beta: oauth-2025-04-20`).
```bash
# Obtain a token once using the Claude Code CLI:
claude setup-token
# Then export it (the CLI may also write CLAUDE_CODE_OAUTH_TOKEN automatically):
export ANTHROPIC_OAUTH_TOKEN=<paste token here>
export AI_MEMORY_LLM_PROVIDER=anthropic-oauth
ai-memory serve
```
For Docker, pass the token as an env var:
```bash
docker run -d --name ai-memory \
-p 127.0.0.1:49374:49374 \
-v ai-memory-data:/data \
-e AI_MEMORY_LLM_PROVIDER=anthropic-oauth \
-e ANTHROPIC_OAUTH_TOKEN=<token> \
akitaonrails/ai-memory:latest
```
Both `ANTHROPIC_OAUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN` are accepted;
ai-memory checks `ANTHROPIC_OAUTH_TOKEN` first. When either variable is exported
on the host, the POSIX and PowerShell Docker wrappers forward its name to
short-lived helper commands such as `llm-test`; the token value is inherited by
Docker rather than placed in the wrapper's command line. The long-lived server
container still needs the provider and token variables in its own environment,
as in the example above.
For both Anthropic providers, ai-memory omits `temperature` for Claude
4.7 and later models and Claude Mythos Preview because those models reject
non-default sampling parameters. `llm-test` deliberately starts with the same
representative 0.2 value as bootstrap and consolidation, then exercises the
provider's compatibility normalization before sending the request.
> [!TIP]
> **Pick a small, fast model.** ai-memory's LLM work — session
> consolidation, lint, and explore — is summarisation/extraction, not hard
> reasoning, so a Haiku-class model is plenty: faster, cheaper, and far easier
> on subscription rate limits than Sonnet/Opus. Set e.g.
> `AI_MEMORY_LLM_MODEL=claude-haiku-4-5`. Save the high-effort thinking models
> for your actual coding agent.
### OpenAI OAuth / Codex
`openai-oauth` is for ChatGPT Pro/Plus/Codex accounts. It does **not** use
`OPENAI_API_KEY` and it does **not** call `api.openai.com`; requests go to the
ChatGPT/Codex Responses backend with a refreshable OAuth token.
For the Docker quick start wrapper, this writes into the same named volume the
server mounts at `/data`:
```bash
ai-memory auth login openai-oauth
docker run -d --name ai-memory \
-p 127.0.0.1:49374:49374 \
-v ai-memory-data:/data \
-e AI_MEMORY_LLM_PROVIDER=openai-oauth \
akitaonrails/ai-memory:latest
```
For a remote Docker host, run the login on that host against the same container
or data volume:
```bash
docker exec -it ai-memory ai-memory auth login openai-oauth
```
Use `ai-memory auth status` to check whether a token is present and
`ai-memory auth logout openai-oauth` to remove it.
> [!TIP]
> **Leave the model at the provider default (`gpt-5.5`).** The Codex/ChatGPT
> backend behind `openai-oauth` only accepts a small server-defined set of model
> ids and rejects others — including `gpt-5-mini` — with a deterministic 400, so
> do not set `AI_MEMORY_LLM_MODEL` for this backend. Consolidation / lint /
> explore are summarisation tasks, so if the default reasoning is too heavy set
> `AI_MEMORY_LLM_REASONING_EFFORT=none` or `low` instead, so hidden thought
> tokens do not eat the JSON budget. Reserve high-effort reasoning for your
> coding agent.
### Codex credential reuse
The independent `codex` provider reads `$CODEX_HOME/auth.json`, falling back to
the platform home's `.codex/auth.json`. It materializes only
`tokens.access_token` and `tokens.account_id`, reloads them before every call,
and never copies or writes the file. On the first 401, it asks
`codex app-server --stdio` to refresh the Codex-owned credential and retries
the Responses request once.
```bash
export AI_MEMORY_LLM_PROVIDER=codex
export AI_MEMORY_LLM_MODEL=gpt-5.6-luna
export AI_MEMORY_LLM_REASONING_EFFORT=medium
ai-memory llm-test --provider codex --model gpt-5.6-luna --prompt "Reply with OK"
ai-memory llm-test --provider codex --model gpt-5.6-luna --structured --prompt "Return a short answer"
```
`AI_MEMORY_CODEX_EXECUTABLE` optionally selects another Codex binary. File
storage is supported; `auto` is supported when it resolves to the same
`auth.json`. Keyring-only and ephemeral storage are not supported. Docker is
outside the automatic setup path: both the executable and credentials must be
available inside the same container/environment.
### GitHub Copilot
`copilot` uses a GitHub user token, then exchanges it for a short-lived Copilot
API token through `https://api.github.com/copilot_internal/v2/token`. The raw
GitHub token is never sent to `api.githubcopilot.com`.
For the Docker quick start wrapper:
```bash
ai-memory auth login copilot
docker run -d --name ai-memory \
-p 127.0.0.1:49374:49374 \
-v ai-memory-data:/data \
-e AI_MEMORY_LLM_PROVIDER=copilot \
akitaonrails/ai-memory:latest
```
For a remote Docker host, run the login against the same data volume:
```bash
docker exec -it ai-memory ai-memory auth login copilot
```
Non-interactive deploys can set `COPILOT_GITHUB_TOKEN` instead. ai-memory also
accepts `GH_TOKEN` and `GITHUB_TOKEN` when running natively; prefer the explicit
`COPILOT_GITHUB_TOKEN` in Docker so you do not pass a broad token by accident.
Advanced users with a pre-minted Copilot API token can set
`GITHUB_COPILOT_API_TOKEN` and optionally `COPILOT_API_URL`.
`auth login copilot` defaults to GitHub Copilot's public device-flow client id.
Pass `--client-id` or set `AI_MEMORY_COPILOT_CLIENT_ID` if you operate your own
OAuth app.
### OpenAI-compatible providers (Ollama / vLLM / LM Studio / hosted APIs)
```bash
docker run -d --name ai-memory \
-p 49374:49374 \
-v ai-memory-data:/data \
-e AI_MEMORY_AUTH_TOKEN="$TOKEN" \
-e AI_MEMORY_LLM_PROVIDER=openai-compat \
-e AI_MEMORY_LLM_BASE_URL=http://host.docker.internal:11434/v1 \
-e AI_MEMORY_LLM_MODEL=qwen2.5-coder:14b \
akitaonrails/ai-memory:latest
```
There is no safe default model for `openai-compat`; the env var is
required. For OpenRouter (Kimi, DeepSeek, etc.):
```bash
-e AI_MEMORY_LLM_PROVIDER=openai-compat
-e AI_MEMORY_LLM_BASE_URL=https://openrouter.ai/api/v1
-e AI_MEMORY_LLM_MODEL=moonshotai/kimi-k2.6
-e LLM_API_KEY=sk-or-v1-...
```
`AI_MEMORY_LLM_REASONING_EFFORT` is honoured on this path: OpenRouter hosts
send `reasoning: { effort, exclude: true }`; `https://api.x.ai` (Grok)
sends Chat Completions `reasoning_effort` (clamped to `low`/`medium`/`high`/
`xhigh`, because Grok cannot disable reasoning); other compat endpoints send
OpenAI-style `reasoning_effort`. Anthropic and Anthropic-OAuth map the same
key to `output_config.effort` and adaptive/disabled thinking on models that
accept those fields (Haiku 4.5 omits them so the default model does not 400;
Fable 5 / Mythos 5 / Mythos Preview omit `thinking: disabled` because those
models reject it). `ultra` and `persistent` clamp to `max` on OpenAI-style
hosts. Gemini and Copilot ignore the key.
[Atlas Cloud](https://www.atlascloud.ai/models/qwen/qwen3.5-flash) uses the
same provider; no Atlas-specific ai-memory provider is needed. Pass its API key
through the generic compatibility credential:
```bash
-e AI_MEMORY_LLM_PROVIDER=openai-compat
-e AI_MEMORY_LLM_BASE_URL=https://api.atlascloud.ai/v1
-e AI_MEMORY_LLM_MODEL=qwen/qwen3.5-flash
-e LLM_API_KEY="$ATLASCLOUD_API_KEY"
```
Replace the model with another current Atlas model id when needed. ai-memory
does not select a default for hosted compatibility endpoints.
[OrcaRouter](https://www.orcarouter.ai) uses the same provider; no
OrcaRouter-specific ai-memory provider is needed. Pass its API key through the
generic compatibility credential:
```bash
-e AI_MEMORY_LLM_PROVIDER=openai-compat
-e AI_MEMORY_LLM_BASE_URL=https://api.orcarouter.ai/v1
-e AI_MEMORY_LLM_MODEL=openai/gpt-4o
-e LLM_API_KEY=sk-orca-...
```
Replace the model with another current OrcaRouter model id (same
`provider/model` format as OpenRouter, e.g. `anthropic/claude-sonnet-4.6` or
`deepseek/deepseek-v4-flash`) when needed.
[Cheaper Inference](https://cheaperinference.com) is an OpenAI-compatible LLM
gateway that advertises models below each lab's list price. It uses
the same provider; no dedicated ai-memory provider is needed.
Pass its API key through the generic compatibility credential:
```bash
-e AI_MEMORY_LLM_PROVIDER=openai-compat
-e AI_MEMORY_LLM_BASE_URL=https://api.cheaperinference.com/v1
-e AI_MEMORY_LLM_MODEL=gpt-5.4-mini
-e LLM_API_KEY=ci_live_...
```
Model ids are bare, without a `provider/` prefix. Replace the model with
another current Cheaper Inference model id (e.g. `claude-haiku-4.5` or
`deepseek-v4-flash`) when needed. Cheaper Inference serves chat models only
and has no embeddings endpoint; configure embeddings separately.
[API Route](https://www.api-route.com/) also uses the existing
`openai-compat` provider. Supply an API Route key and a model ID from its
[current catalog](https://www.api-route.com/pricing):
```bash
-e AI_MEMORY_LLM_PROVIDER=openai-compat
-e AI_MEMORY_LLM_BASE_URL=https://global.api-route.com/v1
-e AI_MEMORY_LLM_MODEL=gpt-5.5
-e LLM_API_KEY="$API_ROUTE_API_KEY"
```
Replace `gpt-5.5` with the exact model ID you intend to use. As with other
hosted compatibility endpoints, no dedicated ai-memory provider is required.
Configure embeddings separately if your chosen API Route model does not
provide an OpenAI-compatible embeddings endpoint.
OpenAI-compatible structured calls use the operation's JSON Schema by default:
```bash
-e AI_MEMORY_LLM_COMPAT_STRICT=true
```
Modern Ollama, vLLM, LM Studio, llama.cpp, and gateway endpoints honour this
OpenAI-style `response_format=json_schema` request. ai-memory retries with its
tolerant parser when an endpoint explicitly rejects the structured-output field
or returns a malformed response shape. For an incompatible endpoint, opt out:
```bash
-e AI_MEMORY_LLM_COMPAT_STRICT=false
```
Hosted gateways that stream long completions past the default 300-second
per-request ceiling fail with `http: error sending request`; raise the
ceiling to match the gateway's worst-case generation time:
```bash
-e AI_MEMORY_LLM_TIMEOUT_SECS=900
```
#### Send a caller-identifying header to a gateway that requires one
Every chat request already carries `User-Agent: ai-memory/<version>`, so a
gateway can tell what is calling it. Some also require a header of their own
for request correlation, and reject or throttle traffic without it. Declare
those once — they are sent on every chat request, whatever the provider:
```bash
-e AI_MEMORY_LLM_HEADERS=x-opencode-session=prod-01,x-opencode-client=ai-memory
```
Entries are `Name=Value` or `Name: Value`, comma separated. A header *value*
cannot contain a comma through the env var; use `llm_headers = [...]` in
`config.toml` when one must. Headers ai-memory sets itself (`authorization`,
`content-type`, `x-api-key`, `x-goog-api-key`, `anthropic-version`,
`anthropic-beta`, `openai-beta`, `host`, `content-length`) are refused at
startup rather than duplicated onto the request. An entry for `user-agent`
overrides the default. Values are never logged.
The `opencode` provider needs no configuration for this: OpenCode asks
callers for `x-opencode-session`, and that provider already sends one id per
logical operation — stable across retries and the structured-output
fallback, so one consolidation pass reads as one operation in OpenCode's
metrics. Supply the header through `AI_MEMORY_LLM_HEADERS` to override that,
for instance to tell several ai-memory instances apart under one account.
#### Match the consolidation budget to a local model's context window
Consolidation defaults to an approximate 100k-token input target plus a 32k
output limit, sized for a 200k-context provider. A local model with a smaller
window can reject the whole request (`exceed_context_size_error` from
llama.cpp, HTTP 400 from most gateways). Lower both limits so their sum fits
the real context window, with additional headroom for tokenizer variance:
```bash
# e.g. a model loaded with an 8k context window
-e AI_MEMORY_CONSOLIDATION__MAX_INPUT_TOKENS=6500
-e AI_MEMORY_CONSOLIDATION__MAX_OUTPUT_TOKENS=1000
```
The double underscore separates the `[consolidation]` section from each key.
The input target accounts for the rendered observations, current page body,
system prompt, page conventions, bounded slot snapshots, structured-output
schema, and provider-envelope reserve. Tokenizers differ, so this is a
conservative estimate rather than an exact provider token count. An automatic
checkpoint provider failure degrades to a rule-based page rather than losing
the checkpoint, but right-sized limits are what allow LLM consolidation to
succeed. Startup rejects input targets below 6,000 and output limits below
1,000 because the batch schema and a useful response cannot fit reliably below
those floors.
---
## Common subcommands
This is the operational shortlist. Run `ai-memory --help` for the authoritative
full command tree.
Two ways to invoke a subcommand against the docker deploy:
```bash
# A) Against the running container (stateful: status, search, backup,
# checkpoints, restore-page, audit-contamination, forget-sweep, lint, embed).
docker exec ai-memory ai-memory status --json
docker exec ai-memory ai-memory search "karpathy"
docker exec ai-memory ai-memory backup --to /data/snapshot.tar.gz
# B) One-shot, no running container needed for pure-stdout helpers
# (generate-auth-token, completions, install-mcp, install-hooks, setup-agent,
# llm-test).
# Auth login is stateful: use docker exec against the running container or
# the wrapper so it writes into the same data volume as the server.
docker run --rm akitaonrails/ai-memory:latest generate-auth-token
docker run --rm akitaonrails/ai-memory:latest completions zsh
docker run --rm akitaonrails/ai-memory:latest install-mcp --client cursor
docker run --rm akitaonrails/ai-memory:latest --help # full subcommand tree
```
| Subcommand | Pattern | What it does |
|---|---|---|
| `serve` | `docker compose up -d` (already done) | Run the HTTP MCP server |
| `run [harness] [args...]` | host wrapper or native binary | Opt into one managed cross-harness workstream; omit the harness to resume the newest usable local session, or name Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, Command Code, Kiro CLI v2/v3, OMP, Grok Build CLI, or Antigravity CLI explicitly; exact `--yolo` and `--fresh` flags are wrapper-owned and other native arguments pass through |
| `show [--json]` | host wrapper or native binary | Choose a client-local checkout and installed managed harness, or return structured discovery data without launching; remote servers never provide checkout paths |
| `continue [--workspace NAME]` | host wrapper or native binary | From any directory, revalidate and resume the newest client-local managed checkout; accepts `--yolo` and `--fresh` but no harness-native arguments |
| `resume [--workspace NAME] [--limit N]` | host wrapper or native binary | Interactively choose a recent managed workstream from valid client-local checkouts; Up/Down selects the workstream and Left/Right cycles `auto` plus installed harnesses before launch; accepts `--yolo` and `--fresh` |
| `workstreams [--workspace NAME] [--project NAME] [--limit N] [--json]` | host wrapper or native binary | List recent workstreams selectable from the current checkout, including current selection and linked harnesses, without exposing paths or native session ids |
| `rename-workstream (--from NAME \| --workstream-id ID) --to NAME [--workspace NAME] [--project NAME] [--json]` | host wrapper or native binary | Retitle one workstream selectable from the current checkout; metadata only, so the stable id, the ledger, the listing order, and the current selection are unaffected |
| `workstream-search [query]` | managed child or thin HTTP client | Search the complete visible managed-workstream ledger; the managed child receives its workstream id automatically |
| `status` | `docker exec` | Counts, paths, derived-index diagnostics, and passive LLM/embedding provider health |
| `search "<query>"` | `docker exec` | Wiki FTS5 search + bounded source authority; use MCP `memory_query` for entity/graph/vector RRF |
| `write-page` | `docker exec` | Manual page write (atomic + indexed) |
| `backup --to` / `restore --from` | `docker exec` | Snapshot or restore the data dir |
| `checkpoints` / `restore-page` | `docker exec` | List wiki git checkpoints or restore one markdown page and reindex it |
| `audit-contamination` | `docker exec` | Read-only structural audit for likely cross-project contamination |
| `forget-sweep` / `lint` / `embed` | `docker exec` | Manual maintenance; sweep + lint also run on the server schedule by default |
| `commit -m "…"` | `docker exec` | Stage + commit the wiki tree |
| `reset --confirm` | `docker exec` | Wipe data (refuses while siblings alive) |
| `generate-auth-token` | `docker run --rm` | Print a random hex bearer token |
| `user add-human` / `list` / `reset-password` / `disable` / `enable` / `patch` | `docker exec` or native binary | Human identities and temporary passwords; never issues an API key |
| `user add` / `expire` / `revive` / `rotate-token` | `docker exec` or native binary | Deprecated 1.x compatibility-token lifecycle backed by `legacy-user-token` |
| `api-key add` / `list` / `rotate` / `revoke` | `docker exec` or native binary | Native `aim_` machine credentials |
| `auth login openai-oauth` | same data volume as the server | Store a ChatGPT/Codex OAuth refresh token for the optional `openai-oauth` LLM provider |
| `auth login copilot` | same data volume as the server | Store a GitHub token for the optional `copilot` LLM provider |
| `auth login oidc-device` | same developer data dir as native hooks and thin-client CLI commands | Store a per-developer OIDC device token for native hook authentication and HTTP CLI fallback auth |
| `install-mcp --client` | `docker run --rm` | MCP-config snippet per client |
| `install-hooks --agent` | `docker run --rm` | Hook-config snippet for an existing hooks dir |
| `setup-agent --agent --to --host-prefix` | `docker run --rm -v` | Extract bundled scripts + print config (one-shot) |
| `install-instructions [--target] [--print] [--no-skills]` | same host environment used for the agent prompt files | Install or update the slim CLAUDE.md / AGENTS.md routing block and, by default, the managed ai-memory Agent Skills |
| `install-skills [--scope] [--agent]` | same host environment used for the agent skill dirs | Install or update only the managed ai-memory Agent Skills |
| `uninstall --apply` | same host environment used for install | Remove only ai-memory-owned hooks, MCP entries, instruction blocks, managed skill files, and generated plugin files after content/marker validation. Removing hooks or MCP also deletes the `ai-memory run` auto-wire sentinels under `<data_dir>/autowire-state/`, so the next managed launch wires again. Use `--mcp-url` for custom MCP endpoints and `--mcp-name` only to narrow removal. |
| `llm-test --provider …` | `docker run --rm -e …` | Smoke-test an LLM provider |
| `completions <shell>` | `docker run --rm` or native binary | Print a bash/zsh/fish/PowerShell/elvish completion script; see [`shell-completions.md`](shell-completions.md) |
### Managed routing snippets and Agent Skills
ai-memory's routing install is agent-facing prompt packaging. It does not add a
runtime skill router, and `SKILL.md` files are not durable memory pages. The
wiki remains the durable source of truth.
`ai-memory install-instructions` now writes two managed prompt artifacts by
default:
1. A slim instruction block in `CLAUDE.md`, `AGENTS.md`, or the file passed with
`--target`. The block is bounded by `<!-- ai-memory:start -->` and
`<!-- ai-memory:end -->` delimiters that appear alone on their own lines.
2. Managed ai-memory Agent Skills containing the detailed tool-routing guidance.
Re-running the command is safe. If a project still has the old long ai-memory
block between line-anchored markers, the refresh replaces that block in place
with the slim snippet, leaves unrelated instructions before and after it alone,
and writes a timestamped `.bak-*` backup before changing an existing file.
Managed skill files contain an ai-memory ownership marker; same-name user skills
without that marker are preserved unless you explicitly force replacement.
Their embedded payloads use LF line endings on every release platform, so CLI
installs and `memory_install_self_routing` return the same bytes on Windows,
Linux, and macOS. This does not rewrite line endings in user-authored files.
`install-instructions --print` previews only the instruction snippet; run
`install-skills --print` when you want to preview the managed skill payloads.
`install-instructions` flags for skills:
| Flag | Meaning |
|---|---|
| `--no-skills` | Refresh only the markered instruction block. |
| `--skills-scope <scope>` | Choose project-local or user-global skill roots. Values: `project`, `global`. Defaults to `project`. |
| `--skills-agent <agent>` | Choose `.claude/skills`, `.agents/skills`, `.devin/skills`, `.grok/skills`, or both Claude/Agents roots. Values: `claude-code`, `agents`, `devin`, `grok`, `both`. By default, `CLAUDE.md` targets imply `claude-code`, `AGENTS.md` targets imply `agents`, and both instruction files imply `both`. |
| `--skills-target-dir <dir>` | Write managed skill directories below an explicit root instead of inferring from scope and agent. |
| `--skills-force` | Replace unmanaged same-name skills during `install-instructions`; without it, they are left untouched and the command exits with an actionable error. |
Use `install-skills` when the instruction block is already right and only the
Agent Skill files need a refresh:
```bash
ai-memory install-skills
ai-memory install-skills --scope global --agent agents
ai-memory install-skills --scope global --agent devin
ai-memory install-skills --scope global --agent grok
ai-memory install-skills --agent both --print
ai-memory install-skills --target-dir .custom/skills --force
```
`install-skills` flags:
| Flag | Meaning |
|---|---|
| `--scope <scope>` | Install into this project or the current user's global skill roots. Values: `project`, `global`. Defaults to `project`. |
| `--agent <agent>` | Install into Claude Code's skill root, the cross-agent skill root, Devin's skill root, Grok's skill root, or both Claude/Agents roots. Values: `claude-code`, `agents`, `devin`, `grok`, `both`. Defaults to `claude-code`. |
| `--target-dir <dir>` | Write managed skill directories below an explicit root; `--scope` and `--agent` are ignored. |
| `--print` | Print target paths and `SKILL.md` contents without writing files. |
| `--force` | Replace unmanaged same-name skills; without it, user-authored same-name skills are preserved. |
Default skill target roots:
| Scope | `--agent claude-code` | `--agent agents` | `--agent devin` | `--agent grok` |
|---|---|---|---|---|
| `project` | `.claude/skills` | `.agents/skills` | `.devin/skills` | `.grok/skills` |
| `global` | `~/.claude/skills` | `~/.agents/skills` | Windows: `%APPDATA%\devin\skills`; non-Windows: `~/.devin/skills` | `$GROK_HOME/skills` (default `~/.grok/skills`) |
Each managed skill is written as `<root>/<skill-name>/SKILL.md`.
`ai-memory uninstall --only skills --apply` removes managed skill files only
from the default project/global roots shown above, after validating the
ai-memory ownership marker. If you installed with `--target-dir` or
`--skills-target-dir`, clean up that custom root manually.
Data dir inside the container is `/data` (mounted via the compose
volume). Outside docker, override with `AI_MEMORY_DATA_DIR=/path`.
Scheduled maintenance is configured in `[maintenance]` in `config.toml`.
By default, rule-based lint and forget sweep run daily outside hook
latency across every existing workspace/project. Embedding backfill is
supported but defaults to off because it can call a paid provider; if you
enable `embedding_backfill_interval_secs` after configuring an embedder,
each scheduled tick backfills every existing workspace/project and may
increase provider usage accordingly.
Forget sweep and rule-based lint persist their last successful completion. On
restart, a job that is not due waits only its remaining interval; a never-run
or overdue job runs once after a bounded startup delay. Failed runs are not
recorded as successful and retry after that bounded delay. Embedding backfill
remains opt-in and keeps its interval-only behavior (no startup catch-up).
`reconcile_tombstones_deleted_pages` (default `false`, also under
`[maintenance]`) is a separate, experimental opt-in: it lets the watcher's
own 30s reconcile pass (not the scheduled jobs above) tombstone an
OKF-imported content page (session summary pages are excluded — see
`docs/okf.md`) whose file has disappeared from disk, after it has been
missing on two consecutive passes and survived a circuit breaker that
refuses to act when more than `max(3, 50%)` of a scope's candidate pages
look missing at once, or when a walk finds nothing at all. The tombstone is
soft (`is_latest = 0` + `superseded_at`, the same shape decay eviction uses)
and is picked up by the same aged-tombstone hard-delete sweep — it is not
exempt from it. What actually protects it: a reconcile tombstone is never
itself destroyed while its chain has no successor; if the file returns, the
new version re-links to the tombstoned chain instead of starting fresh, so
nothing is orphaned for that sweep to destroy. It never runs the blocking
admission gate (nothing can refuse it), but does fire-and-forget any
non-blocking observer/mirror webhook. With it off (the default), reconcile's
behavior is unchanged: a deleted file still requires `ai-memory delete-page`.
See
`docs/okf.md` for the full design.
---
## Bootstrap mid-project
When you adopt ai-memory in a project that's already been around for
a while, the wiki starts empty. `ai-memory bootstrap` ingests the
project's existing history into seed pages so the first session has
warm context.
```bash
cd /path/to/project
ai-memory bootstrap
```
If you installed the Docker wrapper from the quick start and started the
server on `127.0.0.1:49374`, the wrapper automatically reaches that host
loopback server from its short-lived helper container. Set
`AI_MEMORY_SERVER_URL=http://<server>:49374` only when the server is
remote or uses a custom host/port.
**What gets ingested by default:**
| Source | Priority (dropped first when over budget) |
|---|---|
| `CLAUDE.md` / `AGENTS.md` (project rules) | never dropped |
| `README.md` at the repo root | very-late |
| `docs/**/*.md` | late |
| Substantive git commits (body >120 chars OR conventional-commit prefix) | mid |
| Module-level `//!` doc-comments in `**/*.rs` | first to drop |
**Flags:**
```
--repo-path <PATH> (default: git rev-parse --show-toplevel)
--workspace <NAME> (default: the nearest `.ai-memory.toml` marker's
`workspace`, else "default")
--project <NAME> (default: the marker's `project` when pinned,
else derived from cwd — main repo root's
basename via `git rev-parse --show-toplevel`,
or basename(cwd) when no repo is found.
"scratch" only as a defensive fallback for
hook events with no usable cwd.)
--max-input-tokens N (default: 150000; total source budget after prune)
--chunk-input-tokens N (default: 24000; per LLM call, each asking for up
to 16K output tokens; 0 = single call asking for
up to 64K, so input + 64K must fit the context)
--since "30 days ago" (git log filter; supports "N days/months/years ago" + YYYY-MM-DD)
--exclude-git (skip commit history)
--exclude-readme (skip README)
--exclude-docs (skip docs/**/*.md)
--exclude-code (skip Rust module headers)
--dry-run (collect + estimate but don't call LLM or write)
--force (re-bootstrap, overwrites the prior manifest)
```
**Budgets and the estimate.** Bootstrap estimates tokens as bytes ÷ 4, which
undercounts non-English text and code, so it fills only 80% of
`--max-input-tokens` and of each `--chunk-input-tokens` by that estimate. The
dry run still reports tokens by the same estimate; the sources it keeps and the
chunks it plans reflect the margin.
**Cost.** With Kimi 2.6 via OpenRouter ($0.73/$3.49 per M):
- 50k input tokens cap → ~$0.04 worst case input
- 1-2k generated tokens → ~$0.007 output
- Total: well under $0.20 per run.
**Idempotency.** The first run produces a per-project `bootstrap.md`
manifest (at `<wiki>/<workspace>/<project>/bootstrap.md`) listing every
page generated + a one-paragraph rationale. Re-running without `--force`
errors out. Delete the manifest (and the generated pages) if you want a
clean re-bootstrap.
**Dry-run first.** Always worth doing before the real call to see
which sources would actually be sent + how many tokens that
represents. Output is JSON to stdout.
```bash
ai-memory bootstrap --dry-run
{
"sources_collected": 117,
"sources_sent": 22,
"sources_dropped": 95,
"estimated_input_tokens": 48760,
"pages_written": [],
"rationale": "(dry-run; LLM not invoked)",
"dry_run": true,
"llm_chunks": 1
}
```
Large repos (e.g. years of git history) are pruned client-side before
POST, then processed in sequential LLM chunks so provider context limits
are not exceeded. The CLI logs `llm_chunks` in dry-run and the final
outcome.
**Caveat: LLM-fabricated detail.** A bootstrap run can produce
plausible-but-wrong pages (the LLM doesn't know your project, it's
inferring from git history). The wiki is git-versioned precisely so
this is recoverable: review what landed, `docker exec ai-memory git
-C /data/wiki diff HEAD~1`, and revert if it's off.
## Logs and read-only sandboxes
The CLI and server write daily-rolling logs to `<data_dir>/logs/`
(`~/.local/share/ai-memory/logs/` by default). When that location is not
writable — sandboxes like [ai-jail](https://github.com/akitaonrails/ai-jail)
mount `$HOME` read-only or as throwaway tmpfs — ai-memory degrades instead
of failing: it falls back to the OS temp dir, then to stderr-only logging,
printing the exact path that failed at each step. Commands keep working
either way. To keep durable file logs (and durable hook spooling) inside a
sandbox, map the data dir read-write, e.g. `ai-jail --rw-map
~/.local/share/ai-memory …`.
## Human password bootstrap and recovery
Human console login is username/password, not a Bearer pasted into the
browser. Machine APIs keep using `Authorization: Bearer` (`AI_MEMORY_AUTH_TOKEN`
or a native `aim_` key). Before human auth activates, deprecated GET-only
browser compatibility may exchange the root bearer through HTTP Basic for an
HttpOnly `ai_memory_auth` cookie; activation disables that path immediately.
The active classes stay isolated: a password only issues a session; a session
never authenticates `/mcp`/hooks/workstreams; recovery never issues a session;
an API key never logs into `/auth/login`.
### First-time root (greenfield)
Set a one-shot password, then start `serve`. The engine creates the configured
`root_username` (default `root`) with `must_change_password=true` and marks
bootstrap complete. Later restarts ignore the value even if it stays in the
environment — unset it so the plaintext is not kept in process env.
```bash
# At least 12 characters. Must not equal the root bearer, actor-proxy bearer,
# or recovery token, and must not use an `ams_`/`aim_`/`amk_` prefix.
export AI_MEMORY_AUTH__INITIAL_ROOT_PASSWORD='choose-a-long-password'
docker compose up -d # or: ai-memory serve
unset AI_MEMORY_AUTH__INITIAL_ROOT_PASSWORD
```
The first login must change that password (`POST /auth/password` with CSRF).
If the password collides with an existing native API-credential hash, startup
fails closed and bootstrap is not marked complete.
### Break-glass recovery
If the last human root is lost, set a high-entropy recovery secret (at least
32 characters) and `POST /auth/recovery` with a new password. Success returns
204, expires the legacy `ai_memory_auth` cookie, revokes that user's sessions,
and does **not** set `ai_memory_session`. Sign in again with the new password.
```bash
export AI_MEMORY_AUTH__RECOVERY_TOKEN='at-least-32-characters-of-entropy-here'
# restart serve so the process picks up the new value
curl -sS -D - -o /dev/null -X POST http://127.0.0.1:49374/auth/recovery \
-H 'content-type: application/json' \
-d '{"recovery_token":"'"$AI_MEMORY_AUTH__RECOVERY_TOKEN"'","new_password":"brand-new-pass!!","new_password_confirmation":"brand-new-pass!!"}'
```
Rotate by changing the env var and restarting; the previous token stops
working immediately. Public failures (wrong token, recovery unset, password
policy) share one 401 body. The recovery token is not a Bearer, cookie, or
login password.
When human mode is on (bootstrap completed, any password hash, or
initial/recovery secrets configured), `serve` refuses to start unless a
recoverable root exists (`role=root` with a password and not disabled) or
recovery is configured. Explicit machine-only remote deploys with
`AI_MEMORY_AUTH_TOKEN` and no human secrets remain valid. Loopback with
neither Bearer nor human auth stays anonymous.
CIDRs in `AI_MEMORY_AUTH__TRUSTED_PROXY_CIDRS` may supply `X-Forwarded-For`
for login rate limits; untrusted peers' XFF is ignored. Behind HTTPS, set
`AI_MEMORY_AUTH__SECURE_COOKIE=true` as noted above.
## Operating without auth
For local-only / single-machine deploys you can skip the bearer
token:
```bash
docker run -d --name ai-memory \
-p 127.0.0.1:49374:49374 \
-v ai-memory-data:/data \
akitaonrails/ai-memory:latest
```
Notice the bind: `127.0.0.1:49374`, not `0.0.0.0:49374`. This is the
critical pairing - **no bearer token AND loopback only** is the only
safe combination. The server refuses an unauthenticated LAN bind before it
accepts requests. `--allow-insecure-no-auth` can override that refusal only
for an intentional dangerous plain-HTTP deployment; prefer
`AI_MEMORY_AUTH_TOKEN` or loopback instead.
In a *container* that refusal becomes a warning, because the container must
bind `0.0.0.0` internally for `-p` to work at all and cannot see which host
address you published to. The `-p` above is therefore doing the real work: it
is what keeps this container loopback-only. If you change it to publish on a
LAN address, set `AI_MEMORY_AUTH_TOKEN` as well.
Then wire up the agent CLI. Both commands default to no auth and
`http://127.0.0.1:49374` - no extra flags needed for the local case:
```bash
ai-memory install-mcp --client claude-code --apply
ai-memory install-hooks --agent claude-code --apply
```
The installed Docker wrapper runs CLI commands inside a short-lived
helper container. For local loopback servers, it automatically bridges
that helper back to the host's `127.0.0.1:49374`, so `ai-memory status`,
`ai-memory search`, and `ai-memory bootstrap` work with the same default
URL as the generated agent config.
#### SELinux-enforcing hosts
On SELinux-enforcing Linux systems such as Fedora, RHEL, and openSUSE, normal
home-directory labels can prevent the helper container from reaching agent
config even when its UID and GID match the host user. The wrapper checks both
the host enforcement mode and the engine's advertised security options. For the
short-lived helper commands that touch host files (`install-*`, `setup-agent`,
`uninstall`, `backup`, `restore`, and `bootstrap`), it adds `--security-opt
label=disable`; thin-client commands remain confined when they use the named
data volume and implicit configuration. An explicit `--config` path or a valid
host-backed `AI_MEMORY_DATA_DIR` also activates the host-file treatment. This
relaxes SELinux label confinement only for that trusted helper invocation. It
does not modify the long-lived ai-memory server, which uses an engine-managed
named volume.
`bootstrap` is in that list even though it only *reads* host files: an
unmapped UID and a confined label block reads just as hard, and the failure is
misleading — it degrades silently to `no .git found at /work; bootstrapping
from README/docs/rules only` before dying with `Permission denied (os error
13)`.
The two engines report these facts under different keys. Docker answers
`docker info --format '{{.SecurityOptions}}'`; podman has no such field and
fails that template, so the wrapper falls back to podman's
`{{.Host.Security.Rootless}}` and `{{.Host.Security.SELinuxEnabled}}` when the
Docker probe comes back empty. Rootless engines additionally need `-u 0:0`,
because only container UID 0 maps back to the invoking host user — on rootless
podman with SELinux enforcing, both adjustments are required and neither alone
lets the write land.
Do not add `:z` or `:Z` to the wrapper's whole `$HOME` bind. Docker's
[bind-mount documentation](https://docs.docker.com/engine/storage/bind-mounts/#configure-the-selinux-label)
warns that relabeling system directories such as `/home` can make the host
inoperable. Docker documents `label=disable` in the
[`docker run` security options](https://docs.docker.com/reference/cli/docker/container/run/#security-opt).
`ai-memory run`, `ai-memory show`, `ai-memory continue`, `ai-memory resume`,
`ai-memory workstreams`, and `ai-memory rename-workstream` are the exceptions:
the current wrapper intercepts them and starts a cached checksum-verified native
client on the host, where local checkouts, harness executables, and session
stores exist. It preserves an explicit remote `AI_MEMORY_SERVER_URL`. If one of
these commands logs `data_dir=/data`, cannot find a checkout, or cannot find
`codex`, `claude`, or another host executable, refresh the stale wrapper with
`ai-memory upgrade` on that client machine.
### Docker compose alternative
If you prefer compose, clone the repo and run:
```bash
docker compose -f docker/docker-compose.yml up -d
```
The bundled compose file already has `restart: unless-stopped`, a
healthcheck, and the named volume wired up. Agent setup is the same as
the regular Docker path.
---
## Keeping ai-memory up to date
### Docker wrapper
The wrapper checks Docker Hub at most once every 24 hours and prints a
one-line warning when a newer image is available. Upgrade with:
```bash
ai-memory upgrade
```
The command downloads the wrapper and its SHA-256 checksum from the latest
GitHub Release, refuses an unverified update, pulls the latest Docker
image, re-stages hook scripts under
`~/.local/share/ai-memory/hooks/<agent>/` for configured agents, and
prints how to restart the server container so the new binary is used.
Re-running `install-hooks --apply` remains idempotent: ai-memory
replaces only the hook entries it owns and leaves unrelated hooks alone.
When a Compose file is found, the wrapper first verifies that its project owns
the running `ai-memory` container. A standalone container is never handed to an
unrelated Compose project just because its file occupies a conventional path;
the wrapper instead writes the inspected standalone recreation script for
review, preserving the existing `/data` mount and other runtime options.
The macOS menu bar app is not covered by `ai-memory upgrade`. Rebuild with
`./companions/ai-memory-macos/build.sh` (or replace `/Applications/AI Memory.app`
with a newer staged bundle). Wiki, SQLite, config, and models stay in
`~/Library/Application Support/ai-memory`.
Set `AI_MEMORY_NO_VERSION_CHECK=1` to silence the daily check. To pin wrapper
self-upgrades to a fork or tagged release, set `AI_MEMORY_WRAPPER_URL=<url>`;
the wrapper requires `<url>.sha256` unless
`AI_MEMORY_WRAPPER_SHA256_URL=<checksum-url>` is also set.
### Native release binary (Linux / macOS / Windows x86_64)
When `PATH` points at a GitHub-release `ai-memory` binary under a writable
user prefix (for example `~/.local/bin`, or `%LOCALAPPDATA%\ai-memory` on
Windows), the same command upgrades the binary itself:
```bash
ai-memory upgrade
# optional: pin a tag, or force a re-download of the current tag
ai-memory upgrade --version v2.3.2
ai-memory upgrade --force
```
The native path downloads the matching release archive
(`ai-memory-<os>-<arch>.tar.gz` on Unix, `ai-memory-windows-x86_64.zip` on
Windows) and its `.sha256` sidecar from GitHub Releases, verifies the
checksum, replaces the on-disk binary (and a sibling `hooks/` directory when
present), then re-stages hooks for agents already under the data-dir hooks
tree. Windows uses rename-aside (`.exe` → `.old`, then promote `.new`) because
a running image cannot be overwritten in place. It refuses Homebrew/AUR/`/usr`
installs (use the package manager), unwritable prefixes (for example Program
Files — download the zip manually), and in-container binaries (upgrade the
host wrapper/image instead). Windows aarch64 has no release asset yet. For
mirrors or hermetic tests, set `AI_MEMORY_RELEASE_BASE_URL` (or
`release_base_url` in config.toml) to a Releases-compatible base that serves
`{base}/latest/tag` and `{base}/download/<tag>/<asset>` (+ `.sha256`). That
override is a trust boundary: archive and checksum are fetched from the same
base, so the `.sha256` only proves the base served a consistent pair, not that
the binary is genuine — point it only at origins you control. Prefer an
`https://` base; a plain-`http://` base has no transit protection, so an
on-path attacker can substitute both the archive and its matching checksum.
Each response body is capped at 128 MiB.
### Shared notes
When the upgraded server starts, it applies SQLite schema migrations and
pending wiki-structure migrations automatically. No manual database
reset or wiki rewrite is required for normal upgrades. Migrations are
forward-only: after a newer version has applied its schema, an older binary
will refuse to open that data dir (it fails closed rather than risk
corruption), so take a `ai-memory backup` before upgrading if you might need
to roll back to the previous version.
### Upgrading to 2.3.0
2.3.0 is a normal forward upgrade (the only new migration, `V64`, just adds the
cross-project `agent_messages` table — nothing existing is altered or removed).
Two capture/UX conveniences are **on by default**; both are additive and
non-destructive, but worth knowing about for your first session after upgrading:
- **First `ai-memory run <harness>` auto-installs that harness's hooks + MCP** if
they were not already wired (idempotent, one-time per harness; it preserves
your existing hook config, including a `--capture-assistant` opt-in). Disable
with `ai-memory run --no-autowire` or `AI_MEMORY_RUN_AUTOWIRE=false`.
- **The first session in a brand-new (empty) project imports that project's
existing local harness history once**, so installing ai-memory mid-project is
not amnesiac. It only ever runs on an empty project (never touches one that
already has captured memory) and is hard-capped. Disable with
`AI_MEMORY_BACKFILL_ON_START=false`; run it by hand with `ai-memory backfill`.
If the server runs on another host, `ai-memory upgrade` refreshes only
the local client (wrapper/image or native binary) and local hook scripts.
Redeploy the remote server separately with `bin/deploy` or
`docker compose pull && docker compose up -d` in that deploy directory.
Inside ai-jail or another bwrap sandbox, the wrapper is usable from the
sandbox, but run `install-*` commands outside the sandbox because they
write to `~/.local/share/ai-memory/hooks/`.
---
## See also
- [`docs/macos.md`](macos.md) - macOS install paths: menu bar app, native
release tarball, source build, Docker wrapper, and launchd
- [`docs/deploy.md`](deploy.md) - homelab deploy walkthrough
(`bin/deploy`, cloudflared TLS, env-file management)
- [`docs/usage.md`](usage.md) - handoffs, proactive querying, web UI, slim
routing snippet + managed Agent Skills, migration from other memory tools, and raw-wiki inspection
- [`docs/mcp-install.md`](mcp-install.md) - per-client MCP config reference for
every client in the [README Support Matrix](../README.md#support-matrix)
- [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) - what's actually
running inside ai-memory