Ctrl K
ComunidadGuías…

docs/remote-quickstart.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!-- authority:non-specification -->

# exomem — remote connector quickstart

**What you get:** exomem on your phone and on claude.ai's web app, via a
remote MCP connector — the same vault you already use locally, reachable from
anywhere.

For the assistant-side behavior after the connector is added, see
[ai-assistant-guide.md](ai-assistant-guide.md). Remote clients cannot use local
hooks, so they should rely on custom instructions plus `read_instructions()`.

**What you need:** a machine that stays on (desktop, home server, or a cheap
VPS) and about **15 minutes**.

This replaces the old `docs/remote-checklist.md`. It's the fast path; the full
walkthrough, architecture diagram, GPU/CUDA notes, and complete
troubleshooting table live in [deployment.md](deployment.md) — this doc links
back to it rather than duplicating it.

## Pick an ingress profile

Your machine needs a public HTTPS URL that claude.ai's cloud infrastructure
can reach — not just your phone or tailnet (see
[deployment.md § Architecture](deployment.md#architecture) for why a purely
internal hostname doesn't work). Pick one:

| Profile | Best for | Setup |
| --- | --- | --- |
| **Cloudflare Tunnel** (recommended) | You own a domain | Scripted: [`scripts/setup-cloudflared.ps1`](../scripts/setup-cloudflared.ps1) + [`deploy/cloudflared/RUNBOOK.md`](../deploy/cloudflared/RUNBOOK.md) |
| **ngrok** | No domain | Every account gets one free static dev domain. Scripted on Windows: [`scripts/setup-ngrok.ps1`](../scripts/setup-ngrok.ps1); two commands on macOS/Linux |
| **SSH reverse tunnel** | Fallback — bring your own VPS | One `ssh -R` command against a VPS you already control |

> **Why not Tailscale Funnel?** It used to be the default no-domain
> recommendation here. It's still beta, has non-configurable bandwidth
> limits, and — the part that actually bites — its shared relay throttles the
> request bursts claude.ai's connector makes during registration and
> reconnects. That looks like "the server is down" on claude.ai's side while
> exomem itself is completely healthy. If you already run Funnel
> successfully, nothing forces you to move; new setups should start with
> ngrok or Cloudflare instead.

## Steps

### 1. Create a GitHub OAuth App

At <https://github.com/settings/developers> → **OAuth Apps** → **New OAuth
App**. `<host>` below is whichever domain / ngrok dev domain / VPS hostname
you land on in step 4:

| Field | Value |
| --- | --- |
| Application name | `exomem` |
| Homepage URL | `https://<host>` |
| Authorization callback URL | `https://<host>/auth/callback` |

The callback path is exactly `/auth/callback` — see
[deployment.md § 3](deployment.md#3-create-a-github-oauth-app-one-time-3-min).
Save the generated **Client ID** and **Client Secret**.

Validate — GitHub 404s the authorize endpoint for a mistyped client id, so a
non-404 here confirms it was copied correctly:

```bash
curl -s -o /dev/null -w "%{http_code}\n" "https://github.com/login/oauth/authorize?client_id=<client-id-from-step-1>"
```

### 2. Populate `.env`

Create `.env` in the repo root:

```text
EXOMEM_BASE_URL=https://<host>
GITHUB_CLIENT_ID=<from step 1>
GITHUB_CLIENT_SECRET=<from step 1>
EXOMEM_GITHUB_USERNAME=<your-github-login>
EXOMEM_GITHUB_USER_ID=<positive-numeric-id-from-GitHub>
EXOMEM_JWT_SIGNING_KEY=<long-random-string>
EXOMEM_VAULT_PATH=<your-Obsidian-vault-root>
```

Generate `EXOMEM_JWT_SIGNING_KEY`:

```bash
python -c "import secrets;print(secrets.token_urlsafe(48))"
```

Resolve the immutable account ID with `GET https://api.github.com/users/<login>`
and copy its numeric `id`, or let `exomem setup --remote` resolve it. For an
offline/headless run, pass `--github-user-id <id>`. The login comparison is
case-insensitive; the numeric ID is the durable trust anchor.

`EXOMEM_BASE_URL` has no trailing slash and no `/mcp` suffix. `EXOMEM_VAULT_PATH`
is required: claude.ai connects over HTTP and passes no environment of its
own, so the service resolves the vault solely from `.env` at startup.

#### Request deadlines

The origin budget must leave time before either the client or the ingress gives up:

| Bound | Where it is set | Default |
| --- | --- | --- |
| Origin request budget | `EXOMEM_MCP_REQUEST_BUDGET_SECONDS` in `.env` | 50 s |
| Edge tool-call timeout | `MCP_TOOL_TIMEOUT_MS` on the Cloudflare worker | 60000 ms |
| Client tool timeout | the connector's own, not yours to set | ~60 s |

Keep the origin budget below both transport timeouts. The reference worker's
60-second timeout is separate from the Cloudflare Tunnel's roughly 100-second
cap; with a roughly 60-second client timeout, the client may give up first.
Where the worker timeout is configurable, keep it below the client's timeout
as well so its timeout response can arrive. The origin stops
optional work early — a reranker pass, a deep context pack, graph enrichment —
so the answer leaves before the edge and the client give up. A response that had
to leave something out carries a `budget` block naming exactly which stages; it
is a success, not an error, and the agent may re-ask with a narrower option set.
Raising the origin budget above either transport timeout risks finishing work
after its response can be delivered. Lower it if your edge or client is stricter.
Local CLI calls and reconcile-class maintenance carry no budget at all.

Validate:

```bash
exomem doctor --profile remote
```

### 3. Start the server

For always-on service install (auto-start on boot), pick your OS — see
[deployment.md § 6](deployment.md#6-install-as-a-service-auto-start-on-boot)
for the full commands. The blessed paths are macOS/Linux
(`bash scripts/install-service.sh --release`) and Windows
(`pwsh -File scripts/install-service.ps1 -Release`). Each path defaults to the
standard multimodal profile and
installs the profile extras into a PyPI-backed service venv, loads `.env`, runs
doctor, starts the native service, and verifies `/mcp` before reporting success.
Or run it in the foreground for a first test:

```bash
exomem --transport http
```

Validate — expect `401`, not a connection error:

```bash
curl -i http://127.0.0.1:8765/mcp
```

### 4. Bring the tunnel up

**Cloudflare Tunnel** (after a one-time, browser-interactive
`cloudflared tunnel login`):

```bash
pwsh -File scripts/setup-cloudflared.ps1 -Hostname kb.example.com -TunnelName exomem-host
```

**ngrok** — your free static dev domain is auto-assigned to your account
(find it at <https://dashboard.ngrok.com> under Universal Gateway →
Domains), then configure the authtoken once:

```bash
ngrok config add-authtoken <token>
```

Then either run it in the foreground for a first test:

```bash
ngrok http --url https://<you>.ngrok-free.dev 8765
```

or install it as an auto-starting Windows service:

```bash
pwsh -File scripts/setup-ngrok.ps1 -Domain <you>.ngrok-free.dev
```

macOS/Linux: `brew install ngrok`, then the same foreground command above —
no script needed for two commands.

**SSH reverse tunnel** — bring your own VPS with a TLS-terminating reverse
proxy already pointed at `127.0.0.1:8765` (Caddy's automatic HTTPS is the
simplest: `your-domain.example.com { reverse_proxy 127.0.0.1:8765 }`):

```bash
ssh -N -R 127.0.0.1:8765:127.0.0.1:8765 you@your-vps
```

Validate — from outside your network, confirm the public host answers:

```bash
curl -I https://<host>/mcp
```

### 5. Verify the triple

This is the primary check — run it once the tunnel is up, so a broken
connector path shows up now instead of when claude.ai's connector add fails:

```bash
exomem doctor --profile remote --probe
```

The curl equivalents it runs under the hood, for anyone who wants to see the
raw requests:

```bash
curl -i http://127.0.0.1:8765/mcp
```

→ `401` (server up, auth enforced).

```bash
curl -i https://<host>/.well-known/oauth-authorization-server
```

→ `200` JSON (OAuth discovery resolves through the tunnel).

```bash
curl -i https://<host>/.well-known/oauth-protected-resource
```

→ `200` JSON with `"resource":"https://<host>/mcp"`. This is the **bare**
path — claude.ai's gateway probes it before following the standard
`resource_metadata` pointer, and a `404` here aborts connector registration
with `mcp_registration_failed`. exomem ships a workaround route for exactly
this; this check proves the route is live through your actual tunnel, not
just importable.

### 6. Add the connector

claude.ai → **Settings** → **Connectors** → **Add custom connector** →
`https://<host>/mcp` → log in with GitHub as the account named in
`EXOMEM_GITHUB_USERNAME`. A newly upgraded installation asks each existing
client to do this once. Keep the connector definition and URL unchanged: after
that final login, Codex/Claude reuse the Exomem-owned session across fresh chats
and processes without depending on the temporary GitHub token.

Then add Exomem behavior to the hosted client:

1. Paste this learning instruction into the account-level or project-level custom instructions.
2. Paste the instruction block from [ai-assistant-guide.md](ai-assistant-guide.md) after it.

```text
Use Exomem as the memory for my work. Before you start a task, recall what Exomem holds about the systems, projects or topics it touches. As you work, save each reusable fact you establish (how something is structured, a pitfall, a method that worked) as one concise observation, and update an existing note when one covers it, so a later session does not have to find it again.
```

The agent then recalls what Exomem holds before a task and saves what it learns.
The two texts together are longer than a 1,500-character field, so where the
client caps a field, give each its own field. If you cannot paste the block, at
minimum start new chats by asking the agent to call `read_instructions(profile="compact")`
once before using the KB.

### Act as the owner from your remote connectors (optional)

By default a remote sign-in is a separate, non-owner principal: governance
treats your phone or web connector as someone other than you. If the GitHub
account in `EXOMEM_GITHUB_USER_ID` is your own, add one line to `.env` (or the
managed install's `service.env`) and restart the service:

```text
EXOMEM_OWNER_OAUTH_SUBJECT=github:<the value of EXOMEM_GITHUB_USER_ID>
```

`exomem setup --remote` asks this once and writes the line for you; with
`--yes` it writes it only when you pass `--remote-owner` (`--no-remote-owner`
removes it).

Remote sign-ins by that account then act as the owner: default-deny scopes,
owner-only governance and your per-owner state (episode ledgers, prominence)
are the same as on your local doors. They stay labelled remote: ledger rows say
`principal_kind: owner-oauth` with the remote caller hash, and a governance
session opened locally cannot be resumed remotely, or the reverse.

- The value is only ever read from the service environment, never from the
  vault. A malformed value is treated as unset and never stops the service.
- `exomem doctor --profile remote` reports `env.EXOMEM_OWNER_OAUTH_SUBJECT` as
  unset, active, mismatch or malformed. Before enabling it, read
  `governance.remote_owner_former_audience`: rules or grants that name your
  remote connector's former `principal:` audience stop applying to it once
  bound. Nothing is migrated; removing the line restores them.
- `exomem auth sessions` marks the sessions that act as the owner.
- On an HA pair, set the line on every replica, or the audience flips between
  replicas.

### Reuse this service from Claude Code and Codex

Do not leave desktop clients on separate full stdio processes after installing
the service. From the directory containing `.env`, rerun local setup; it reads
`EXOMEM_BASE_URL` automatically. Or name the endpoint explicitly:

```bash
exomem setup --vault "/path/to/vault" \
  --mcp-url https://<host>/mcp \
  --replace-client-registration
```

Then use `/mcp` in Claude Code and `codex mcp login exomem` for native OAuth.
After updating an older Exomem Claude plugin, run `/reload-plugins` or restart
Claude Code so its former stdio process does not survive in the live session.

### ChatGPT Personal Plugin (OAuth only)

Create the Personal Plugin with MCP Server URL `https://<host>/mcp` and
Authentication set to **OAuth**. Keep the discovered authorization, token,
registration, authorization-server, resource, CIMD metadata, and callback
values. Leave **Default scopes** blank, set **Base scopes** to
`offline_access`, and turn **OIDC enabled** off. That base scope lets Exomem's
one-hour access tokens renew through durable rotating refresh tokens without
another GitHub login. Exomem deliberately provides OAuth authorization-code flow, not
OpenID identity claims; the compatibility discovery alias is not permission to
invent a `userinfo` endpoint. In particular, never accept a placeholder such as
`https://example.com` for that field.

Tool-surface changes use a truthful two-phase rollout. Before release, record
the new packaged digest as `pending_tool_surface_sha256` with
`refresh_required: true`; do not claim it is already registered. After the new
service is deployed, confirm `/health/ready.mcp_tool_surface_sha256` matches the
pending digest, refresh or recreate the Personal Plugin, and verify from a fresh
conversation that `read_instructions` and `search_memory` both invoke. If that verified
digest is still the current packaged surface, promote it to
`registered_tool_surface_sha256`, clear the pending value, and set
`refresh_required: false`. If a newer surface landed meanwhile, promote only
the verified digest as the last registered snapshot while preserving the newest
pending digest and `refresh_required: true`. The release gate accepts an
explicit pending rollout but blocks an unacknowledged schema change. The
post-deploy promotion is a manual attestation because ChatGPT exposes no API
for reading its registered plugin snapshot.

Disconnecting and reconnecting the app only renews its OAuth connection; it does
not rescan MCP action schemas. Use the app's action refresh/rescan control when
available, or delete and recreate the app definition, then verify the callable
schema from a fresh conversation.

`bootstrap.server.published_mcp_tool_surface_sha256` identifies the packaged
canonical surface; `/health/ready` fingerprints the tools actually registered
by that process, including configuration differences. Tool discovery alone is
not proof that calls are routable. If an existing conversation says it is
restricted to developer MCPs, advertises a missing resource, or keeps the
previous schema after refresh, that conversation's host policy is poisoned: use
a fresh conversation. Server or plugin changes cannot repair policy state
already frozen into that thread.

## ngrok free-tier limits

- **120 requests/minute**, **~20,000 requests/month** per endpoint.
- A **one-time browser interstitial** the first time anything hits a fresh
  ngrok session — click through it once. If the GitHub OAuth redirect stalls
  during connector setup, this is usually why.
- Fine for one person's connector traffic. If setup stalls mid-burst, check
  the [ngrok dashboard](https://dashboard.ngrok.com/) for `429`s on the
  endpoint. Cloudflare Tunnel or a paid ngrok plan removes the cap.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| claude.ai connector add fails with `mcp_registration_failed` | Bare `/.well-known/oauth-protected-resource` 404s through the tunnel | Re-run step 5's probe; if it fails, the tunnel isn't forwarding to the right port, or exomem needs updating. |
| ngrok setup stalls, then a `429` shows in the ngrok dashboard | Free-tier burst limit (120 req/min) hit during registration | Wait a minute and retry, or switch to Cloudflare Tunnel. |
| GitHub OAuth redirect never completes on first ngrok use | The one-time ngrok browser interstitial intercepted the redirect | Open the ngrok URL directly once, click through the interstitial, then retry the connector add. |
| "Couldn't reach the MCP server" during connector add | OAuth discovery failed | See [deployment.md's troubleshooting table](deployment.md#troubleshooting) — the rest of the fixes there apply regardless of ingress profile. |
| An existing connector gets one `401 invalid_token` immediately after the durable-session cutover | Its legacy FastMCP reference token is intentionally no longer accepted | Complete one final browser login. Do not delete or recreate the connector. |
| Tools return `503` without an OAuth challenge | The authoritative session store is unavailable, not the login | Repair coordinator/network/storage health and retry. Re-authorizing cannot fix a 503. |
| ChatGPT enables OIDC or proposes `https://example.com` as `userinfo` | The Personal Plugin UI inferred OpenID from Exomem's OAuth compatibility alias | Turn OIDC off, leave scopes blank, and keep the discovered OAuth/CIMD endpoints. |
| ChatGPT discovers tools but invocation says `Resource not found` | Its registered tool snapshot is older than the deployed MCP surface | Refresh or recreate the Personal Plugin, verify the tool-surface fingerprint, then test from a fresh conversation. |

For everything else — service management, GPU/CUDA, revoking access,
restarting, logs, multi-host setups — see [deployment.md](deployment.md).