The read-observations default can return the caller's own session when the client sends no session id, a cwd that stays blocked under macOS privacy controls can starve the four probe permits for unseen projects, and security-boundaries row 11b now records what #1522 stores for an unknown-family tool event. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
24 KiB
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:
- Scenario D: menu bar app: a self-contained
.appthat bundles the binary, starts the LaunchAgent, and opens/web, status, and config. This is the GUI path if you are building from source. - Scenario A: prebuilt tarball:
no Rust toolchain; run
servein a terminal or install the LaunchAgent by hand. - Scenario B: source build: for developing ai-memory itself.
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, with no WSL-vs-native split.
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 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-hookscommands renderhttp://127.0.0.1:49374, which works from the host agent. -
Hooks are rendered for one of two platforms:
posix-native: a directai-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:shruns the bundled.shscript. This is an explicit compatibility fallback for the Docker wrapper.
Set
AI_MEMORY_HOOK_PLATFORMbefore wiring hooks to override the default. -
The menu bar app still needs
install-mcp/install-hooksfrom a shell; use the bundled binary inside the.appso hook discovery sees the siblinghooks/tree.
Scenario A: Prebuilt Release Binary (Recommended, No Toolchain)
The shortest route is two commands: install.sh downloads and verifies the
tarball for your chip, and ai-memory setup initialises the data dir, installs
and starts the LaunchAgent below for you, and wires your agents:
curl -fsSL https://github.com/akitaonrails/ai-memory/releases/latest/download/install.sh | sh
ai-memory setup
See docs/install.md
for what each step does. The manual steps follow.
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 initonly creates the data dir; it does not start a server.bootstrap,install-hooks,install-mcp, andstatusare all clients that talk to the running server over HTTP, so running them while nothing is serving fails withConnection refused (os error 61)/could not reach http://localhost:49374. Leaveserverunning 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"]
On a machine with an earlier install it could instead silently wire a stale
hooks cache from ~/Library/Application Support/ai-memory, which is worse. If you are on an older release, run install-hooks via the
extracted ./ai-memory path (or upgrade).
Notes:
initon a fresh data dir writes a root bearer token to<data_dir>/auth-tokens/default. The hooks, the CLI andinstall-mcpread it from there, so you never pass--auth-tokenlocally. An install from before 3.0 keeps running without a token untilai-memory auth enable.- Keep the extracted
ai-memoryat a stable path; the hook commands (and the symlink, if you made one) reference it. Re-runinstall-hooksand re-point the symlink if you move it. - Later updates: with the release binary on
PATH(or invoked as./ai-memory), runai-memory upgradeto download the latest matching macOS tarball, verify its.sha256, replace the binary (and siblinghooks/when present), and refresh staged agent hooks. Seedocs/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.
- Install & Start Server runs bundled
ai-memory initif~/Library/Application Support/ai-memory/config.tomlis missing, renderspackaging/launchd/com.github.akitaonrails.ai-memory.plist, and bootstraps the samecom.github.akitaonrails.ai-memorylabel as the hand-installed LaunchAgent below. Do not skip this click; the app does not start the server on first launch by itself. - The status item is green when
GET /admin/statussucceeds, 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. The app sends the token saved in Settings, or, when none is saved, the oneinitwrote toauth-tokens/defaultin its data dir, so an install the app initialized needs no pasting. - 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
Privacy controls and Full Disk Access
The server looks at each hook event's working directory (git discovery) to
decide which project the event belongs to. A LaunchAgent is a background
process under macOS privacy controls (TCC): reading inside ~/Documents,
~/Desktop, ~/Downloads, iCloud Drive, or an external or network volume can
need a permission that a background agent has no way to ask for, and the read
can hang instead of failing. A server started from a terminal or an SSH
session borrows that app's permissions, which is why the same binary can work
there and not under launchd.
The server keeps answering when this happens: it runs those reads off its
request threads with a 2-second limit. Events from a directory it cannot read
are dropped, and stderr.log shows
inspecting cwd "<path>" did not finish within 2s for each one. At most four
of those reads wait at a time, so while a directory stays blocked, events from
a project the server has not seen yet can be dropped too; projects it already
knows keep working. If your
agents work in one of those places, give the binary the plist runs Full Disk
Access, then restart the agent:
# The binary the LaunchAgent runs:
plutil -extract ProgramArguments.0 raw \
~/Library/LaunchAgents/com.github.akitaonrails.ai-memory.plist
# System Settings > Privacy & Security > Full Disk Access: click +, press
# Cmd+Shift+G and paste that path. Then:
launchctl kickstart -k gui/$(id -u)/com.github.akitaonrails.ai-memory
The grant belongs to that file path. If setup later points the plist at a
different binary, grant that one too.
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.
Bearer token
init on a fresh data dir writes the root bearer to
<data_dir>/auth-tokens/default and records [auth] bearer_token_file in
config.toml, so the LaunchAgent needs no token in its plist. To replace that
token with one from the environment, note that launchd has no
EnvironmentFile: add AI_MEMORY_AUTH_TOKEN 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 jobrunningwithlast exit code = (never exited)rather than crash-looping; the launchd child (PPID 1) owning127.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-memoryresolved and logged as the data dir with no--data-dirpassed;405fromGET /mcpon the bound port;KeepAlive(the served process wasSIGKILLed and a replacement was answering about a second later, withrunsincrementing); and a cleanlaunchctl bootout. Start-at-login was configured but not independently exercised, since that needs a logout.ThrottleIntervalis 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
-
Captures from
~/Documents,~/Desktopor an external volume never show up, andstderr.logsaysinspecting cwd ... did not finish: the LaunchAgent binary lacks Full Disk Access. See Privacy controls and Full Disk Access. In 3.1.2 and earlier the same condition made the whole server stop answering (#1514). -
403 forbidden hostfrom Docker-wrapper CLI commands: update the Docker image and wrapper script. Current images allowlisthost.docker.internalfor loopback-published Docker Desktop servers. -
Agent config points at
host.docker.internal: re-runai-memory install-mcp --client <client> --applyandai-memory install-hooks --agent <agent> --applywith the current wrapper. Host-side agent config should usehttp://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-hooksprobes the siblinghooks/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 foundin a new terminal tab: Scenario A/B's./ai-memory/./target/release/ai-memoryis a relative path, so it only resolves from inside the install/build directory. Either keepcd-ing there first, or symlink the binary ontoPATHonce you're done wiring hooks (see the Scenario A/B notes above) so plainai-memoryworks everywhere. -
install-hookswires 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 onPATHbefore runninginstall-hooks), macOS discovery does not resolve the symlink and searches the wrong parent directories; see #546. On a clean machine this fails outright withError: 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. Runinstall-hooksvia 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 runfrom the package does not stageai-memory+hooks/. Use./companions/ai-memory-macos/build.sh. -
Install & Start does not turn the item green: check
~/Library/Logs/ai-memory/stderr.logandlaunchctl print gui/$(id -u)/com.github.akitaonrails.ai-memory. The app never writes into its own bundle; a missingconfig.tomlis created under~/Library/Application Support/ai-memoryby bundledai-memory init. -
Error: binding 127.0.0.1:49374/Address already in use(the server exits immediately and the menu extra stays red): another process already holds the port, for example OpenCode v2's background service (reported in #1044 to default to 49374). Move one side: start ai-memory elsewhere (bind = "127.0.0.1:<free-port>"in~/Library/Application Support/ai-memory/config.toml, then update the app's server URL in Settings to match), or move the other service (opencode service set port <free-port>). Find the current holder withlsof -nP -iTCP:49374 -sTCP:LISTEN. The menu extra now surfaces this error instead of showing only a red status item.
Suggested Test Checklist
ai-memory serve --bind 127.0.0.1:49374starts and logsbind=127.0.0.1:49374(./ai-memory serve …, or./target/release/ai-memory serve …for Scenario B, if you haven't put it onPATHyet).curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:49374/mcpreturns405(reachable; GET not allowed), confirming the loopback server is up.install-hooks --agent claude-code --applywrites hook commands that referencehttp://127.0.0.1:49374and host-side paths.install-mcp --client claude-coderendershttp://127.0.0.1:49374/mcp.- Launch the agent, call
memory_status, send a prompt, then confirm capture (ai-memory statusshows non-zero observations, or query the SQLiteobservationstable). - 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-memoryshowsstate = running; replacing the.appleaves~/Library/Application Support/ai-memoryuntouched.
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.