Files
ai-memory/docs/macos.md
T

20 KiB
Raw Blame History

macOS Support

macOS is a supported platform: the workspace test suite runs on macOS CI and tagged releases publish native ai-memory-macos-aarch64.tar.gz (Apple Silicon) and ai-memory-macos-x86_64.tar.gz (Intel) binaries.

On macOS the native binary is the recommended way to run ai-memory. Three native installs exist:

It binds the server on 127.0.0.1:49374, and both the MCP endpoint and the lifecycle hooks talk to that loopback address — which the native agent can reach and which is already in the default Host-header allowlist. The Docker wrapper (Scenario C) is also supported when you prefer a containerised server.

Unlike Windows there is only one "path world" on macOS: POSIX paths and POSIX .sh hooks throughout. There is no WSL-vs-native split to get wrong.

Rule Of Thumb

Run install-mcp / install-hooks from the same shell that launches Claude Code, Codex, Cursor, Gemini CLI, or another agent — on macOS that is just your normal Terminal.

  • The agent runs as a native macOS process, so its config must point at a host-reachable server URL. Native installs and Docker-wrapper install-mcp / install-hooks commands render http://127.0.0.1:49374, which works from the host agent.

  • Hooks are rendered for one of two platforms:

    • posix-native — a direct ai-memory hook --event … call. The default for native macOS/Linux Claude Code installs (cargo / release binary) and the Docker wrapper's checksum-verified host client; it uses the local event spool + OIDC-token fallback and enforces capture policy v1.
    • posix — sh runs the bundled .sh script. This is an explicit compatibility fallback for the Docker wrapper.

    Set AI_MEMORY_HOOK_PLATFORM before wiring hooks to override the default.

  • The menu bar app still needs install-mcp / install-hooks from a shell; use the bundled binary inside the .app so hook discovery sees the sibling hooks/ tree.

Use this when you want a local server plus native hooks without a Rust toolchain or Docker. Each tagged release publishes a macOS tarball per architecture.

# 1. Download the archive for your chip and extract it to a stable location.
#    aarch64 = Apple Silicon (M-series); x86_64 = Intel.
mkdir -p ~/Applications/ai-memory && cd ~/Applications/ai-memory
curl -fsSL -O https://github.com/akitaonrails/ai-memory/releases/latest/download/ai-memory-macos-aarch64.tar.gz
tar -xzf ai-memory-macos-aarch64.tar.gz
# `curl` downloads are not Gatekeeper-quarantined, so the binary runs as-is.
# If you downloaded via a browser instead, clear the quarantine flag once:
#   xattr -d com.apple.quarantine ./ai-memory

# 2. Initialise the data dir (defaults to
#    ~/Library/Application Support/ai-memory; override with AI_MEMORY_DATA_DIR).
./ai-memory init

# 3. Start the server (loopback only).
./ai-memory serve --transport http --bind 127.0.0.1:49374

The server from step 3 must stay running for every other command. ai-memory init only creates the data dir — it does not start a server. bootstrap, install-hooks, install-mcp, and status are all clients that talk to the running server over HTTP, so running them while nothing is serving fails with Connection refused (os error 61) / could not reach http://localhost:49374. Leave serve running in its own terminal (or set it up as a login service, below), then run the rest in a second terminal.

In a second terminal, wire the agent:

cd ~/Applications/ai-memory
# `install-hooks` auto-discovers the bundled hooks/ directory beside the binary.
./ai-memory install-hooks --agent claude-code --apply
./ai-memory install-mcp --client claude-code --apply

Optionally, once hooks are wired, put the binary on PATH so later commands (ai-memory status, a fresh terminal tab, the checklist below) don't need cd/./:

sudo ln -sf ~/Applications/ai-memory/ai-memory /usr/local/bin/ai-memory

As of v1.39.0, running install-hooks through the symlink works: hook discovery canonicalises the running binary's path before walking up to the sibling hooks/ directory (#546, fixed in v1.39.0). On v1.38.x or older, the walk did not resolve through a symlink — running install-hooks via /usr/local/bin/ai-memory sent discovery to the wrong parent directories, failing outright on a clean machine:

Error: could not locate hooks directory. Tried: ["/…/hooks/claude-code",
"/usr/local/share/ai-memory/hooks/claude-code", "/usr/share/ai-memory/hooks/claude-code",
"…/Library/Application Support/ai-memory/hooks/claude-code"]

— or, worse, silently wiring a stale hooks cache from ~/Library/Application Support/ai-memory on a machine with an earlier install. If you are on an older release, run install-hooks via the extracted ./ai-memory path (or upgrade).

Notes:

  • The MCP endpoint, capture hooks, and ai-memory status work without a token in this single-user loopback setup. If you explicitly configure AI_MEMORY_AUTH_TOKEN for the server, pass the same token with --auth-token or export it for CLI commands.
  • Keep the extracted ai-memory at a stable path; the hook commands (and the symlink, if you made one) reference it. Re-run install-hooks and re-point the symlink if you move it.
  • Later updates: with the release binary on PATH (or invoked as ./ai-memory), run ai-memory upgrade to download the latest matching macOS tarball, verify its .sha256, replace the binary (and sibling hooks/ when present), and refresh staged agent hooks. See docs/install.md#keeping-ai-memory-up-to-date.

Scenario B: Source Build

Use this when developing ai-memory itself. Requires Rust 1.95 (rust-toolchain.toml) plus the Xcode Command Line Tools (xcode-select --install); SQLite is bundled and libgit2 is vendored, so no extra system libraries are needed.

git clone https://github.com/akitaonrails/ai-memory
cd ai-memory
cargo build --release --workspace
./target/release/ai-memory init
./target/release/ai-memory serve --transport http --bind 127.0.0.1:49374

From another shell in the repo, install-hooks finds the bundled hooks/ automatically (no --source needed from the repo root):

./target/release/ai-memory install-hooks --agent claude-code --apply
./target/release/ai-memory install-mcp   --client claude-code --apply

If you symlink the built binary onto PATH for convenience (e.g. ln -sf "$(pwd)/target/release/ai-memory" ~/.local/bin/ai-memory), do it after the install-hooks call above, not before — see the install-hooks symlink caution in Scenario A (#546); it applies here too and is the exact layout that bug was filed against.

Scenario C: Docker Wrapper

Use this when you want the server data in a Docker volume while the agent still runs as a native macOS process. The wrapper renders host-side agent config with http://127.0.0.1:49374, but its own thin-client commands reach the server from inside a helper container via Docker Desktop's host.docker.internal alias.

This assumes the ai-memory thin-client wrapper is already on PATH; if ai-memory --version doesn't resolve yet, install it first via the README Docker quick-start (downloads a small shell script to ~/.local/bin/ai-memory). On a stock macOS Terminal ~/.local/bin is not on PATH by default — add export PATH="$HOME/.local/bin:$PATH" to ~/.zshrc if which ai-memory comes up empty after installing the wrapper.

# Start the server. The image default allowlist includes host.docker.internal so
# wrapper thin-client commands (status, search, …) are not rejected with 403.
docker run -d --name ai-memory --restart unless-stopped \
    -p 127.0.0.1:49374:49374 -v ai-memory-data:/data \
    akitaonrails/ai-memory:latest

# Wire the native host agent. The wrapper keeps these rendered URLs on loopback.
ai-memory install-mcp   --client claude-code --apply
ai-memory install-hooks --agent  claude-code --apply

The wrapper is a shell script, not the native binary, so the install-hooks symlink caution above (#546) does not apply here.

The published Docker image includes both linux/amd64 and linux/arm64, so Apple Silicon pulls the native arm64 image without --platform linux/amd64.

Scenario D: Menu bar app

Use this when you want a self-contained macOS install: one .app that contains the ai-memory binary and hooks/, starts the existing LaunchAgent, and opens the surfaces the tool already has (/web, ai-memory status, config.toml, logs). It does not replace those tools with a second dashboard.

From a source checkout (Rust 1.95 + Xcode / Swift 6):

./companions/ai-memory-macos/build.sh
open "companions/ai-memory-macos/dist/AI Memory.app"

Drag AI Memory.app to /Applications so the LaunchAgent path stays stable across rebuilds. The menu extra has no Dock icon.

  1. Install & Start Server — runs bundled ai-memory init if ~/Library/Application Support/ai-memory/config.toml is missing, renders packaging/launchd/com.github.akitaonrails.ai-memory.plist, and bootstraps the same com.github.akitaonrails.ai-memory label as the hand-installed LaunchAgent below. Do not skip this click; the app does not start the server on first launch by itself.
  2. The status item is green when GET /admin/status succeeds, yellow when the server is up but the LLM/embedding role is in error or the write queue is non-empty, red when the server is unreachable.
  3. Open Web UI, Show Status…, Open Config, Open Data Directory, and Open Logs call the existing browser UI, the bundled CLI, and Finder.

Wire an agent with the bundled binary so install-hooks finds the sibling hooks/ tree (#546):

BIN="/Applications/AI Memory.app/Contents/Resources/runtime/ai-memory"
"$BIN" install-mcp --client claude-code --apply
"$BIN" install-hooks --agent claude-code --apply

Memory, config, models, and logs stay outside the bundle (~/Library/Application Support/ai-memory and ~/Library/Logs/ai-memory). Replacing the .app is an update; it does not rewrite that tree. Details: companions/ai-memory-macos/README.md.

Notarization and a Homebrew cask are not part of this companion yet.

Run as a Login Service (launchd)

Scenarios A–C leave the server in the foreground: close that terminal and capture stops. Scenario D already installs this LaunchAgent from Install & Start Server. The macOS counterpart of a systemd user unit is a LaunchAgent — a plist in ~/Library/LaunchAgents/ that the per-user launchd domain starts at login and restarts on failure. The repo ships one at packaging/launchd/com.github.akitaonrails.ai-memory.plist, and the macOS release tarballs include it.

launchd expands nothing. A plist has no home specifier and no EnvironmentFile, so every path in it is a literal and the template carries two placeholders you substitute at install time. Run this from the extracted tarball (Scenario A) or the repo root (Scenario B):

# launchd creates the log files but not their parent directory, and a missing
# one is a silent redirect failure.
mkdir -p ~/Library/Logs/ai-memory

# Wherever you keep the binary: ~/Applications/ai-memory/ai-memory for a
# release tarball, ./target/release/ai-memory for a source build.
AI_MEMORY_BIN=~/Applications/ai-memory/ai-memory

sed -e "s|__AI_MEMORY_BIN__|$AI_MEMORY_BIN|" \
    -e "s|__HOME__|$HOME|" \
    packaging/launchd/com.github.akitaonrails.ai-memory.plist \
    > ~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist

launchctl bootstrap gui/$(id -u) \
    ~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist

The agent runs ai-memory serve --transport http --enable-web and passes neither --data-dir nor --config: on macOS the binary already defaults to ~/Library/Application Support/ai-memory with the config file inside it, so naming them would only add two more paths to substitute. bind comes from that config, defaulting to 127.0.0.1:49374. Re-render and reload the plist if you move the binary.

Verify it came up:

launchctl print gui/$(id -u)/com.github.akitaonrails.ai-memory | grep state
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:49374/mcp   # 405
tail -f ~/Library/Logs/ai-memory/stderr.log

Coming from systemd

systemd --user launchd (per-user domain)
systemctl --user enable --now ai-memory launchctl bootstrap gui/$(id -u) <plist>
systemctl --user disable --now ai-memory launchctl bootout gui/$(id -u)/<label>
systemctl --user status ai-memory launchctl print gui/$(id -u)/<label>
systemctl --user restart ai-memory launchctl kickstart -k gui/$(id -u)/<label>
journalctl --user -u ai-memory -f tail -f ~/Library/Logs/ai-memory/stderr.log
loginctl enable-linger $USER no equivalent — a LaunchAgent stops at logout
EnvironmentFile= no equivalent — see the token note below

<label> is com.github.akitaonrails.ai-memory. After editing the plist, bootout then bootstrap again; kickstart -k only restarts the process and does not re-read the definition.

If you configure a bearer token

AI_MEMORY_AUTH_TOKEN is read from the process environment only — it is not a config.toml key, and launchd has no EnvironmentFile. A single-user loopback setup needs no token at all. If you do set one, add it to your rendered plist and tighten the file, because ~/Library/LaunchAgents is not private:

  <key>EnvironmentVariables</key>
  <dict>
    <key>AI_MEMORY_AUTH_TOKEN</key>
    <string>…</string>
  </dict>
chmod 600 ~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist

Removing the agent

launchctl bootout gui/$(id -u)/com.github.akitaonrails.ai-memory
rm ~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist

Nothing rotates the two log files; they grow without bound. Add a newsyslog.d entry or truncate them periodically if that matters to you.

Validated on macOS 26.6.2 (build 25G83, Apple Silicon) with ai-memory v1.38.0 installed per Scenario A, and separately with v1.21.0 to confirm the agent does not depend on a recently added flag. Confirmed: launchctl bootstrap; the job running with last exit code = (never exited) rather than crash-looping; the launchd child (PPID 1) owning 127.0.0.1:49374, so the reply came from the agent rather than a foreground server left over on the same port; ~/Library/Application Support/ai-memory resolved and logged as the data dir with no --data-dir passed; 405 from GET /mcp on the bound port; KeepAlive — the served process was SIGKILLed and a replacement was answering about a second later, with runs incrementing; and a clean launchctl bootout. Start-at-login was configured but not independently exercised, since that needs a logout. ThrottleInterval is left at its 10s default, so a crash within 10s of startup is respawned after that delay rather than immediately. Corrections from anyone on a different macOS version are welcome.

Hook Platform on macOS

AI_MEMORY_HOOK_PLATFORM selects how hook commands are rendered. On macOS the two relevant values are posix-native (direct binary call; the native default) and posix (the bundled .sh scripts; the Docker-wrapper default). Set it before running install-hooks so the choice is baked into the rendered commands. The native hook spools events locally, does short session-start cleanup, and starts a detached session-end hook-drain helper; the whole-minute spool-timing overrides are shared with Windows and documented in docs/windows.md.

Native posix-native ai-memory hook commands enforce the nearest-marker [capture] ignore_paths policy before spool or network delivery. The Docker wrapper's posix shell-script path does not. Re-run install-hooks --agent <agent> --apply after upgrading to refresh an existing native install; see Capture exclusions.

Troubleshooting on macOS

  • 403 forbidden host from Docker-wrapper CLI commands: update the Docker image and wrapper script. Current images allowlist host.docker.internal for loopback-published Docker Desktop servers.
  • Agent config points at host.docker.internal: re-run ai-memory install-mcp --client <client> --apply and ai-memory install-hooks --agent <agent> --apply with the current wrapper. Host-side agent config should use http://127.0.0.1:49374.
  • Hooks bundle not found from a release archive: ensure you extracted the whole tarball, not just the binary. Current install-hooks probes the sibling hooks/ directory automatically.
  • Platform-mismatch warning on Apple Silicon: update to a current Docker tag. Tagged releases publish a multi-arch manifest with linux/arm64.
  • ai-memory: command not found in a new terminal tab: Scenario A/B's ./ai-memory/./target/release/ai-memory is a relative path, so it only resolves from inside the install/build directory. Either keep cd-ing there first, or symlink the binary onto PATH once you're done wiring hooks (see the Scenario A/B notes above) so plain ai-memory works everywhere.
  • install-hooks wires the wrong (or no) hooks/ bundle even though the tarball was extracted whole: if the binary is reached through a symlink (e.g. you put it on PATH before running install-hooks), macOS discovery does not resolve the symlink and searches the wrong parent directories — see #546. On a clean machine this fails outright with Error: could not locate hooks directory. Tried: [...]; if a hooks cache from an earlier install already exists under ~/Library/Application Support/ai-memory, it can silently reuse that stale copy instead and report success. Run install-hooks via the real extracted/built path instead of the symlink until that's fixed.
  • No Dock icon after opening AI Memory.app: that is the menu extra. Look in the menu bar (brain / status symbol), not the Dock.
  • Menu extra says "Runtime not bundled": swift run from the package does not stage ai-memory + hooks/. Use ./companions/ai-memory-macos/build.sh.
  • Install & Start does not turn the item green: check ~/Library/Logs/ai-memory/stderr.log and launchctl print gui/$(id -u)/com.github.akitaonrails.ai-memory. The app never writes into its own bundle; a missing config.toml is created under ~/Library/Application Support/ai-memory by bundled ai-memory init.

Suggested Test Checklist

  1. ai-memory serve --bind 127.0.0.1:49374 starts and logs bind=127.0.0.1:49374 (./ai-memory serve …, or ./target/release/ai-memory serve … for Scenario B, if you haven't put it on PATH yet).
  2. curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:49374/mcp returns 405 (reachable; GET not allowed), confirming the loopback server is up.
  3. install-hooks --agent claude-code --apply writes hook commands that reference http://127.0.0.1:49374 and host-side paths.
  4. install-mcp --client claude-code renders http://127.0.0.1:49374/mcp.
  5. Launch the agent, call memory_status, send a prompt, then confirm capture (ai-memory status shows non-zero observations, or query the SQLite observations table).
  6. Scenario D: after Install & Start Server, the menu extra is green; Open Web UI loads http://127.0.0.1:49374/web; launchctl print gui/$(id -u)/com.github.akitaonrails.ai-memory shows state = running; replacing the .app leaves ~/Library/Application Support/ai-memory untouched.

Report which scenario you used, your chip (Apple Silicon / Intel), the agent and version, and whether hooks executed or failed with a connect/resolve error.