---
name: loci
description: Run prompts on a local model through the Loci app on this Mac. Use for routine work that does not need frontier reasoning (summaries, extraction, classification, rewriting, bulk triage over many items) or when the person wants to save cloud usage.
---

# Loci: local models for agents

Loci is a Mac app that runs open models on this machine. It serves a loopback MCP server and an OpenAI-compatible endpoint to agents on the same Mac. Nothing sent to it leaves the Mac. It gives an agent the model and model management, not the person's assistant: Loci's own tools and the person's files, mail and memories are never reachable through it. It needs an Apple silicon Mac on macOS 14 or later and the direct build, bundle id `ai.askloci.rust`; the Mac App Store build has no agent API. Windows later; no Linux.

## Quick check

Run this first. `ok` means Loci is running, the token works and a model answers under a schema. Anything else names the step to go to.

```bash
loci_check() {
  local file="$HOME/Library/Application Support/LociRust/agent-api.json" url token out content
  url="$(plutil -extract url raw -o - "$file" 2>/dev/null)" || { echo "no agent-api.json: Loci is not running or not installed (step 1)"; return 1; }
  token="$(plutil -extract token raw -o - "$file")"
  curl -fsS "$url/health" >/dev/null || { echo "no answer at $url/health: Loci is not running; re-read agent-api.json after launching it (step 1)"; return 1; }
  curl -fsS -H "Authorization: Bearer $token" "$url/v1/models" >/dev/null || { echo "token refused at $url/v1/models: re-read agent-api.json and register again (step 3)"; return 1; }
  out="$(mktemp)"; content="$(mktemp)"
  curl -fsS -H "Authorization: Bearer $token" -H "Content-Type: application/json" "$url/v1/chat/completions" -o "$out" \
    -d '{"model":"default","messages":[{"role":"user","content":"Name one color and one fruit."}],"max_tokens":64,"response_format":{"type":"json_schema","json_schema":{"name":"check","schema":{"type":"object","properties":{"color":{"type":"string"},"fruit":{"type":"string"}},"required":["color","fruit"],"additionalProperties":false}}}}' \
    || { echo "no answer from a model: none is installed yet, or the first one is still downloading (step 4)"; rm -f "$out" "$content"; return 1; }
  if plutil -extract choices.0.message.content raw -o "$content" "$out" >/dev/null 2>&1 && plutil -extract fruit raw -o - "$content" >/dev/null 2>&1; then
    echo ok
  else
    echo "a model answered, but not under the schema: see Troubleshooting"
  fi
  rm -f "$out" "$content"
}
loci_check
```

## 1. Is Loci installed and running?

```bash
curl -fsS http://127.0.0.1:5624/health || plutil -extract url raw -o - "$HOME/Library/Application Support/LociRust/agent-api.json"
```

- `/health` answers `{"ok":true,"app":"loci","version":"…"}`: Loci is running. Go to step 3.
- A URL prints but port 5624 does not answer: Loci may be on another port, so try `<url>/health`. If that fails too, the app is not running.
- Neither works: Loci is not running or not installed. Identify the app before launching it, because two builds share the name and only the direct build has the agent API:

```bash
mdls -name kMDItemCFBundleIdentifier -raw /Applications/Loci.app   # or: osascript -e 'id of app "Loci"'
```

- `ai.askloci.rust`: the direct build. The agent API arrived in Loci 0.1.21, so check the version before launching:

  ```bash
  plutil -extract CFBundleShortVersionString raw -o - /Applications/Loci.app/Contents/Info.plist
  ```

  Below 0.1.21, update it first with the person's consent: `brew upgrade --cask --greedy askloci/tap/loci` when Homebrew installed it (`brew list --cask askloci/tap/loci` succeeds), otherwise reinstall it as in step 2. Then run `open -g -a Loci`, wait up to 60 s for `agent-api.json` to appear, and poll `<url>/health`. On 0.1.21 or later, a file that never appears means Agent Access is switched off (see Troubleshooting). The port is 5624 when free and another port otherwise, so re-read the file after every launch.
- `com.mwaldman.loci-ai`: the Mac App Store build, which has no agent API. The person needs the direct build (step 2). Homebrew refuses the cask while an unmanaged `/Applications/Loci.app` exists, so with the person's consent remove or rename that copy first.
- The command fails: Loci is not installed. Go to step 2.

## 2. Install

Install only when the person wants Loci. It needs an Apple silicon Mac on macOS 14 or later: `sysctl -n hw.optional.arm64` prints `1` on Apple silicon, while an error (`unknown oid`) or `0` means Intel (`uname -m` misreports under Rosetta). Intel Macs: use the Mac App Store build; it has no agent API. Homebrew first:

```bash
brew install --cask askloci/tap/loci
```

Use the full name: `brew search` skips taps that are not trusted, so it does not list Loci. Installing by the full name trusts this one cask, and Homebrew checks the disk image's SHA-256 before installing. Loci updates itself, so `brew upgrade` leaves it alone.

Without Homebrew, download the disk image and copy the app out of it. Running the image's `Install Loci.app` instead shows a window the person must approve, so copy the app inside it. If `/Applications/Loci.app` already exists, move it aside first with the person's consent (for example to the Trash with `mv`), because `cp -R` merges into it:

```bash
DMG="${TMPDIR:-/tmp}/Loci.dmg"; MNT="${TMPDIR:-/tmp}/loci-dmg"
curl -fL -o "$DMG" https://askloci.ai/api/mac-download
hdiutil attach -nobrowse -mountpoint "$MNT" "$DMG"
APP="$MNT/Install Loci.app/Contents/Resources/Loci.app"; [ -d "$APP" ] || APP="$MNT/Loci.app"
cp -R "$APP" /Applications/
hdiutil detach "$MNT"
```

Then `open -g -a Loci` and wait for `agent-api.json`, then `<url>/health`. The first launch shows a setup page. The API answers at once, but no model downloads until the person has finished that page once, so ask them to finish it before any pull (step 4).

## 3. Register the MCP server

The endpoint file (mode 0600, readable only by the person) holds `url`, `mcp_url`, `openai_base_url`, `token`, `app_version`, `pid` and `started_at`. Read one field at a time, and never print `token` into a transcript or log:

```bash
FILE="$HOME/Library/Application Support/LociRust/agent-api.json"
plutil -extract mcp_url raw -o - "$FILE"   # also: url, openai_base_url
```

Claude Code (user scope, so every project sees it); the substitutions keep the token out of the output, and the last line checks the endpoint and token right away:

```bash
FILE="$HOME/Library/Application Support/LociRust/agent-api.json"
claude mcp add --transport http --scope user loci "$(plutil -extract mcp_url raw -o - "$FILE")" --header "Authorization: Bearer $(plutil -extract token raw -o - "$FILE")"
curl -sS -H "Authorization: Bearer $(plutil -extract token raw -o - "$FILE")" "$(plutil -extract openai_base_url raw -o - "$FILE")/models"
```

Codex: append the server to `~/.codex/config.toml` unless it is already there, again without printing the token. If the block exists, edit it by hand when the token or port has changed.

```bash
FILE="$HOME/Library/Application Support/LociRust/agent-api.json"; CONFIG="$HOME/.codex/config.toml"
mkdir -p "$HOME/.codex"; touch "$CONFIG"
grep -q '^\[mcp_servers.loci\]' "$CONFIG" || cat >> "$CONFIG" <<EOF

[mcp_servers.loci]
url = "$(plutil -extract mcp_url raw -o - "$FILE")"
http_headers = { Authorization = "Bearer $(plutil -extract token raw -o - "$FILE")" }
tool_timeout_sec = 600
EOF
```

A session that is already running has no `loci_*` tools until it restarts: start a new session and call `loci_status` there. `claude mcp get loci` echoes the token and `codex mcp list` only reads the config without connecting, so neither proves the connection. If you prefer not to store the token in the Codex file, use `bearer_token_env_var = "LOCI_AGENT_TOKEN"` instead of `http_headers` and export that variable in the shell that starts Codex. Codex can speak the newest MCP revision when `[features] mcp_2026_07_28 = true`; Loci answers both, so nothing needs to change.

Cursor reads `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one. Add the server with the two values from the file. Cursor also resolves `${env:LOCI_AGENT_TOKEN}` inside `headers`, for a person who would rather keep the token out of the file:

```json
{
  "mcpServers": {
    "loci": {
      "url": "<mcp_url>",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

Gemini CLI and Copilot CLI take the same one-line registration as Claude Code, again without printing the token. Gemini saves to the project unless `--scope user` is given; Copilot saves to `~/.copilot/mcp-config.json`:

```bash
FILE="$HOME/Library/Application Support/LociRust/agent-api.json"
gemini mcp add --transport http --scope user --header "Authorization: Bearer $(plutil -extract token raw -o - "$FILE")" loci "$(plutil -extract mcp_url raw -o - "$FILE")"
copilot mcp add --transport http --header "Authorization: Bearer $(plutil -extract token raw -o - "$FILE")" loci "$(plutil -extract mcp_url raw -o - "$FILE")"
```

Any OpenAI-compatible client: base URL `<openai_base_url>`, API key `<token>`, model `default` or an id from `GET /v1/models`. `stream: true` and `response_format` (`json_object`, `json_schema`) work.

## 4. Get a model ready

Call `loci_status`. If `ready` is `false`, the app is still starting: wait a few seconds and call again. Then:

- `models.installed` has an entry: go to step 5; `loci_warm` it if its `state` is `cold`.
- `models.installed` is empty: a Loci with no model starts downloading its own new-chat default about three minutes after its window is up, and that download replaces a pull of any other model and deletes its bytes. So pull the id in `models.downloading` if there is one, otherwise the `models.installable` entry marked `recommended`, and expect a pull of any other model to answer `queued` with the note "Loci is still downloading its first model" until that one is installed. Pull only ids the list shows: large-memory Macs are not offered the 2B models, and any other id answers `failed` or `unknown_model`. Each `installable` entry carries `engine` (`gguf` or `mlx`). On a busy Mac prefer the recommended model or one with a modest context window: a GGUF load is charged by its context window, so a small model with a 128K window can be refused where a larger one with a 64K window passes.
- A pull that stays `queued` with no `error` and `bytes` 0 for more than a minute is Loci's problem, not yours: first-run setup has not been finished once, or the Loci window is blank. Ask the person to open Loci and finish its setup page, or relaunch it, then pull again.

```
loci_pull {"model":"<id>","wait_seconds":30}
loci_warm {"model":"<id>","wait_seconds":30}
```

`wait_seconds` is 0 to 60; use 30, because Codex ends a tool call after 60 s by default and these calls answer at about `wait_seconds`. Repeat `loci_pull` until `state` is `installed`; in between it reports `queued` or `downloading` (`bytes` of `total_bytes`; `bytes` can stay 0 for several seconds while setup starts, and `download_bytes` in `loci_status` excludes a vision model's projector). Stop on `failed` or `unknown_model` and report the `error`. Downloads run one at a time. Then `loci_warm` until `state` is `loaded`, so the first answer is fast; it reports `loading` in between, and `failed` or `not_installed` end it. If `loci_pull` ends `failed` with "There isn't enough available memory to load this model", Loci's memory policy refused the load after the download and the API cannot retry it: ask the person to free memory and load the model from Loci's model list, or relaunch Loci, which retries the setup.

## 5. Route work to Loci

Send to Loci: summarizing files or logs, extracting fields into JSON, classifying or tagging items, rewriting prose, first-pass triage of search results, anything repeated over many items (`loci_map`). Keep on your own model: multi-step reasoning about unfamiliar code, decisions the person will act on without review, anything where a wrong answer is expensive.

- `loci_status {}` lists each installed model's `state`: `warm` (loaded this session), `loading`, or `cold`. Prefer a warm model: a cold one loads first, which can take tens of seconds. It also reports `measured_output_tokens_per_second`; at 30 tokens per second, 1,000 tokens take about 33 s, so set `max_tokens` to what the answer needs.
- `loci_generate {"model":"default","messages":[{"role":"user","content":"…"}],"schema":{…},"max_tokens":1024,"temperature":0.2}` takes `messages` (roles `system`, `user`, `assistant`) or a `prompt`. `max_tokens` is 1 to 32768 (default 1024); `temperature` 0 to 1.2. It returns `text`, `json` when a schema was given, `model`, `engine` (`gguf` or `mlx`), `finish_reason` (`stop`, or `length` when cut off) and `usage` with `tokens_per_second`. A busy Mac can refuse one call with `isError` "There isn't enough available memory to start this model. Close other apps, or choose a smaller model."; wait a minute and retry once before reporting it.
- `loci_map {"instruction":"…","items":[…],"schema":{…},"max_tokens":512}` runs one instruction over 1 to 200 items (strings or objects, 4 MiB in total; `max_tokens` defaults to 512 per item) and returns `results`, one entry per item in order (`index`, `text`, `json` when a schema was given, `error` when the item failed). A failed item does not stop the rest. A large map can run for minutes: raise Claude Code's `MCP_TOOL_TIMEOUT` (milliseconds, in the environment that starts it) first, or send batches the harness can finish inside its timeout.
- `loci_pull` and `loci_warm`: step 4.

The `schema` is a JSON Schema for an object, and the answer is validated against it after decoding. Supported: `type`, `enum`, `const`, `properties`, `required`, `additionalProperties`, `items`, `minItems`/`maxItems`, `minLength`/`maxLength`, `anyOf`, `minimum`/`maximum`, `exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`. Refused before any model loads: `oneOf`, `allOf`, `$ref`, `pattern`, `format`, any length or item bound of 2000 or more, and schemas over 64 KiB. Use `anyOf`, never `oneOf`; omit `maxLength` for free text.

`default` is the model the person chats with in Loci right now: prefer it. Naming another installed model loads that one in place of the chat's, and the chat reloads its own on its next turn.

## 6. Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| connection refused | app not running | `open -g -a Loci`, re-read agent-api.json, wait for `/health` |
| no agent-api.json 60 s after launch, and Loci is older than 0.1.21 | that version has no agent API | update it (step 1) |
| no agent-api.json 60 s after launch on 0.1.21 or later | Agent Access is switched off in Settings → Advanced → Agent Access | tell the person; ask before turning it back on |
| Loci starts but there is no agent-api.json, and the bundle id is `com.mwaldman.loci-ai` | the Mac App Store build is installed; it has no agent API | install the direct build (step 2) once the person agrees to remove or rename `/Applications/Loci.app` |
| 401 | token reset (Reset access token) or missing header | re-read agent-api.json and register again |
| 403 | the `Origin` header names a site other than a local page | call from a process, not a web page |
| 421 | Host header is not 127.0.0.1, localhost or [::1] | use the URLs from agent-api.json |
| `isError` "… is not installed" | model id not installed | `loci_status`, then `loci_pull` |
| `isError` "did not satisfy the schema" or "was not valid JSON" | the model drifted from the schema | the raw answer follows "Raw answer:" in `text`; repair it or retry with a tighter schema |
| `isError` naming an unsupported keyword | schema uses a refused keyword | rewrite it with the supported keywords |
| `ready` is `false` | the app is still reading its models | wait a few seconds, call `loci_status` again |
| `agent-api.json` names a pid that is not running | the file went stale after a crash or a forced quit | trust the file only after `/health` answers; relaunch Loci and re-read it |
| the Loci window shows first-run setup, and `loci_pull` stays `queued` with `bytes` 0 and no `error` | no model downloads until the setup page has been finished once | ask the person to finish the setup page, then pull again |
| setup is finished, the window is blank, and `loci_pull` stays `queued` with no `error` | the app's page died under it while the API kept answering | ask the person to relaunch Loci, then pull again |
| `loci_pull` answers `queued` with "Loci is still downloading its first model" in `error` | Loci's own first download is live and outranks yours; nothing of yours started | wait for it (`models.downloading` in `loci_status`), then pull again |
| `loci_pull` answers `queued` with "Loci's first model download failed in the app" in `error` | the first download failed, or was paused or refused, in the window | ask the person to retry it there; the API cannot |
| `loci_pull` ends `failed` with "There isn't enough available memory to load this model" | the memory policy refused the load after the download; `loci_warm` answers `not_installed` | ask the person to free memory and load the model from Loci's model list, or relaunch Loci |
| `isError` "There isn't enough available memory to start this model" | a busy Mac; the next call often passes | wait a minute and retry once, then report it |

## 7. Install this skill for next time

Claude Code: save this file as `~/.claude/skills/loci/SKILL.md`. Codex: `~/.agents/skills/loci/SKILL.md` (older Codex also reads `~/.codex/skills/loci/SKILL.md`). Latest version: https://askloci.ai/agents/SKILL.md
