# AirMemo Manual

The mechanism page explains how a memo reaches an agent. This page is the reference for the three ways it travels: the gateway, which governs an API-routed tool by changing one base URL; the hook, which delivers into a desktop or CLI agent at the moment it starts a session and again at each prompt; and the browser sensor, an add-on for chat windows the other two cannot reach. Then adapters, receipts, enrollment, self-hosting, and the full command and configuration surface.

Every command, flag and key below is read from the shipping build, defaults included. Nothing here is a mockup.

Canonical: https://airmemo.com/docs
Machine-readable: https://airmemo.com/docs.md

---

<!-- section:gateway -->

## The gateway

*One base URL, and the tool is governed.*

The gateway is the rail that asks the least of a tool, so it is the first command on this page. An API-routed tool points its base URL at the gateway and changes nothing else: `/k/<token>` for an Anthropic-shaped client, `/k/<token>/v1` for an OpenAI chat-completions one. The gateway resolves the memos in force for the person behind the token, adds them as a marked system block before the call leaves, and mints the receipt server side. The client’s own provider key is forwarded untouched and never stored or logged.

```
export ANTHROPIC_BASE_URL=https://<gateway>/k/<token>
# an OpenAI-shaped client takes https://<gateway>/k/<token>/v1
docker compose up -d
# infra/selfhost/compose; the gateway listens on 8082
```

It ships. What is not claimed is the vendor: injection and streaming are proven against the vendor SDK and a stand-in endpoint on loopback, never against a live vendor, which needs a vendor API key. Enrolling people is one call, as an owner or an admin — `POST /portal/gateway/enroll`, one token per active person, 30 days by default, at most 1000 per call. An org-wide token is refused on this rail, because a receipt has to name a person.

Two modes. **Alert** is the default: a request whose policy cannot be resolved is forwarded bare, and the response says so in `x-airmemo-policy` (`ok`, `stale`, `unavailable`). **Block** refuses it with `503 airmemo_policy_unavailable` instead; an owner or admin sets it per org, and the change is audited as `org.gateway_mode_configured`. Block stops one outgoing request, never an agent’s turn: a hook pointed at the gateway falls back to its own poll, so the prompt still runs.

Browser chat is not this rail, and no hook reaches it either. The add-on covers it as a sensor (station 03) and reports only the pages it was loaded into, so traffic anywhere else is unobserved rather than counted as covered.

<!-- section:install -->

## Install

*One binary, one command.*

`airmemo-hook install` merges the hook into a platform’s config file and pins the binary’s SHA-256. The installer never overwrites an existing entry: it appends or merges, and a config it cannot parse as JSON is refused, never clobbered. The same promise is a terms clause (see the Terms of Service).

The binary ships through the enrolled customer path, and every download is checked against a published hash list before anything runs. What is not claimed is the end-to-end install proof: a fresh account, the package, and a memo landing in a real tool is unproven until it runs on a clean account, and until then the console is open and a pilot runs there. One command per platform:

```
airmemo-hook install --platform claude
# claude | codex | copilot | cursor | gemini | opencode | windsurf
airmemo-hook uninstall --platform claude
# removes only AirMemo’s entries
```

The installer reports exactly what it did:

```
install: merged into existing config (customer hooks preserved)
uninstall: removed 2 AirMemo entry(es); customer hooks preserved
```

Hash pinning is the fingerprint rule: reinstalling with a different binary is refused until you re-pin with `--force`. `--config`, `--events`, `--timeout` and `--cwd` override the defaults; `--keep-config` on uninstall clears the pin and leaves the config byte-identical for MDM-owned setups.

<!-- section:sensor -->

## The sensor

*An add-on for chat windows. It reports what it saw.*

The third rail is for browser chat, where neither of the others reaches: the gateway cannot route a window, and no command hook fires in one. The add-on reads the prompt on the page before it is submitted, attaches the memo in force for that person, and mints a receipt. That makes it a sensor and not a coverage claim: it reports the pages it was loaded into, and the traffic nobody watched is named as unobserved.

Four host patterns, across three chat products:

- **ChatGPT**: `chatgpt.com` and `chat.openai.com`.
- **Claude**: `claude.ai`.
- **Microsoft Copilot**: `copilot.microsoft.com`.

The memo is not compiled into the extension. The cloud serves it as a signed, expiring document; the add-on caches it, re-checks the expiry when it reads it so a tab left open past the deadline stops carrying the memo without another poll, and posts one receipt per attachment. The cloud re-renders the memo, checks the signature, and only then writes the row, so the receipt is the server’s statement rather than the extension’s.

Status: an add-on we run in test, not a managed install. There is no signed, policy-installed, self-updating build yet, so it is loaded unpacked into a profile we control, and it is never counted as coverage of a fleet.

<!-- section:adapters -->

## Adapters

*One entry per command-hook vendor.*

Five adapters take a command hook, one per vendor. Each entry states the config file the installer writes and the events it wires. The sample documents were rendered by the shipping adapters themselves, not by hand, and they are regenerated from the product on every release, so the demo cannot drift.

### Claude Code

writes ~/.claude/settings.json · wires SessionStart, UserPromptSubmit

Envelope: `hookSpecificOutput` with `hookEventName` and `additionalContext`. The sample memo renders 137 tokens.

### Codex

writes ~/.codex/hooks.json · wires SessionStart, UserPromptSubmit

Same `hookSpecificOutput` envelope. The sample memo renders 137 tokens.

### GitHub Copilot

writes .github/hooks/airmemo.json at the repo root · wires sessionStart

Flat payload, no envelope: `additionalContext` only. `modifiedPrompt` is dropped by the runtime, so the hook never emits it. The sample memo renders 124 tokens.

### Gemini CLI

writes .gemini/settings.json at the working dir · wires SessionStart, BeforeAgent

Same `hookSpecificOutput` envelope. The sample memo renders 137 tokens.

### Cursor

writes ~/.cursor/hooks.json · wires sessionStart

Flat payload, no envelope: `additional_context` only, snake_case. `sessionEnd` output is logged but not used, so the installer wires `sessionStart` only (same discipline as Copilot). The sample memo renders 124 tokens.

Want the full picture? Seven adapters, five hook documents: side by side. Every document there is rendered from the product, not drawn by hand.

Five of the seven need a command hook: Windsurf delivers over MCP, OpenCode through a JS shim.

<!-- section:receipts -->

## Receipts

*Append-only. No UPDATE, no DELETE.*

Every delivery writes a receipt: an append-only record of what was delivered, to which agent, on which platform, and when it expired. A delivered memo carries an authorization line, so an agent or a reviewer can always tell a delivered memo from pasted text:

> [AIRMEMO · authorized · memo_4821] — The authorization line a delivered memo carries

Where the receipts live:

- **On the machine**: `airmemo-hook receipts` prints the local journal as JSON: count, then one row per delivery (memo id, platform, tokens injected, proof, whether it was receipted, queued time).
- **In the portal**: `/app/#/receipts` drills from memo to receipts to agents, so a receipt never disappears when its memo ages out of the newest list.
- **Server-side**: append-only. Receipts and audit have no UPDATE and no DELETE, so the receipted count can only go up.

<!-- section:enroll -->

## Enrollment

*An org, an org key, one command.*

Enrolling an agent needs an org and an org key. Open one at the console: signup is self-serve, no approval step. (A pilot can also start with a person opening the org for you.) With the org active, mint the enrollment key in the portal (Org admin > Rollout > Enrollment key). Enroll needs the org slug and that key; nothing else. The agent identity lands in a 0600-permission config file, and a second enroll is refused rather than overwritten.

```
airmemo-hook enroll --org YOUR_ORG --platform claude --org-key <from-portal>
```

The hook then rides the lifecycle events your agent already exposes (session start and prompt submit), checks one thing: does an authorized memo exist for this agent? If it does, the memo is delivered into the session context, prompt-visible on the next turn. Fail-open: the hook never blocks an agent turn; a platform or network failure degrades to memo delayed, never agent blocked.

<!-- section:self-host -->

## Self-hosting

*The same rate as cloud: $12 a person, a month.*

Self-hosting costs what the cloud costs: $12.00 a person a month, the same number at one person and at a hundred thousand, on a month-to-month plan or a multi-year contract. There is no self-hosted premium and no term discount. The same codebase, deployed where your fleet lives.

What self-hosting includes, and what it does not:

- **No uptime or availability SLA during the pilot** (see the Terms of Service).
- **SSO ships at every tier**, configured by your org owner in the console; MFA and SCIM ship alongside it. Seats are the only unit billed. SOC 2 is not held yet: we will publish the report when it exists, and we do not claim it before then.
- **EU data processing residency is recorded, not performed**: setting `region: eu` records the commitment, and it does not move a byte today.
- **Self-host is a deployment kit.** One image, run twice: an api process and a worker process from the same tagged build, with migrations run once as a one-shot job before the api. Compose and Helm both ship, and both are stood up end to end.
- **Boot refuses rather than degrades.** `DATABASE_URL` missing stops the process with exit 2, and `PORTAL_BASE_URL` unset or non-https stops it with exit 4, because an install that boots looking healthy with dead mailed links and a replayable cookie is worse than one that does not boot. Loopback http and `NODE_ENV=development` are still allowed.
- **The licence is a file, not a call.** `AIRMEMO_LICENSE_FILE` names it; the default is the install path in the image. `AIRMEMO_TENANCY=single` forces single-tenant tenancy. `AIRMEMO_LICENSE_PUBKEYS` is refused at boot with exit 5: the trust anchors are compiled into the build, so no runtime variable can mint a licence.

<!-- section:commands -->

## Commands

*Twelve subcommands, every flag.*

This is the whole CLI surface. Each row lists the flags a command adds to seven common ones — `--base-url`, `--device`, `--hmac-key`, `--platform`, `--agent`, `--config`, `--state-dir` — accepted by every command that loads config. An unknown subcommand prints usage and exits 2.

- `version` — Prints the build as JSON on stdout. CI greps it, so the shape is a contract.
- `config` — Prints the effective config with secrets redacted; the resolved file path goes to stderr.
- `enroll` — Writes the agent identity and license to the config file at 0600. A second enroll is refused, never overwritten.
  --org, --org-key (both required; the key comes from Org admin > Rollout)
- `poll` — One poll cycle: queue, then inject, then receipt. Fail-open; a network failure delays a memo and never blocks a turn.
  --once --simulate-inject
- `inject` — The hook entrypoint itself: reads the platform payload on stdin, prints the vendor JSON on stdout. Always exits 0.
  --event --session-id --text-out
- `install` — Merges the hook into a platform config and pins the binary SHA-256. Appends or merges; a file it cannot parse is refused, never clobbered.
  --bin --events --timeout --cwd --force --mcp-url --mcp-token-env
- `uninstall` — Removes AirMemo entries and clears the pin. Other hooks in the same file are untouched.
  --cwd --keep-config
- `installer-mdm` — Prints the managed-config templates a fleet tool consumes. Writes nothing.
  --platform codex-requirements|claude|gemini (the template) --managed-dir --bin --bin-path
- `managed-install` — Enroll, install and doctor in one non-interactive run. Zero prompts, stdout empty, exit code is the contract.
  --org --org-key --bin --cwd --install-config --force --mcp-url --mcp-token-env
- `receipts` — Prints the local journal as JSON: a count, then one row per delivery — memo id, injected and receipt-sent flags, tokens injected, platform, session id, injection point, proof, dropped reason, queued time.
- `doctor` — Checks version range, config, cache and cloud reachability, and prints a verdict. --bundle writes a redacted support tarball at 0600.
  --show --bundle --bundle-proofs --license-file
- `help` — Usage.

<!-- section:config -->

## Configuration

*One JSON file, thirteen keys.*

The config file is `%LOCALAPPDATA%\airmemo\hook.json` on Windows and `~/.airmemo/hook.json` elsewhere, unless `AIRMEMO_CONFIG` or `--config` names another path. Resolution is defaults, then the file, then the environment, then flags. `airmemo-hook config` prints the result with secrets redacted.

- `base_url` — Cloud endpoint the hook calls.
  default https://api.airmemo.com
- `device_id` — Agent identity.
  written by enroll
- `org_id` — Org the agent belongs to.
  written by enroll
- `hmac_key` — Request signing key. Redacted by config and doctor.
  written by enroll
- `platform` — Adapter name: claude, codex, copilot, cursor, gemini, opencode, windsurf.
  written by install
- `agent` — Agent id, or any.
  default any
- `poll_every_ms` — Poll interval.
  default 10000
- `http_timeout_ms` — Request timeout.
  default 5000
- `cache_max_age_h` — How long a queued memo stays usable.
  default 168
- `cache_max_entries` — Cap on the local cache journal.
  default 1024
- `public_key` — Org key that verifies this install’s license.
  written by license
- `learn` — Opt-in correction capture. Off sends nothing.
  default false
- `force_continuation` — An urgent memo may continue a turn at Stop. Non-urgent never does.
  default false

Seventeen environment variables are read. The ones that name a config key win over the file; the rest choose a path, a template or a credential source:

```
AIRMEMO_CONFIG
AIRMEMO_BASE_URL
AIRMEMO_DEVICE_ID
AIRMEMO_ORG_ID
AIRMEMO_HMAC_KEY
AIRMEMO_PLATFORM
AIRMEMO_AGENT
AIRMEMO_POLL_EVERY_MS
AIRMEMO_HTTP_TIMEOUT_MS
AIRMEMO_CACHE_MAX_AGE_H
AIRMEMO_CACHE_MAX_ENTS
AIRMEMO_LEARN
AIRMEMO_FORCE_CONTINUATION
AIRMEMO_ORG
AIRMEMO_ORG_KEY
AIRMEMO_LICENSE_FILE
AIRMEMO_API_TOKEN
```

`managed-install` returns a code a fleet tool can switch on:

- `0` — Installed and verified, or already installed.
- `1` — Internal error: config parse, key generation, persist, doctor problems.
- `2` — Usage: --org, --org-key or --platform missing.
- `3` — Enrollment key rejected: bad signature, clock skew, unknown agent.
- `4` — Cloud unreachable or a retryable failure; the deployment is not verified.
- `5` — Unsupported platform.
- `6` — Binary hash changed since the last pin. --force re-pins.
- `7` — Agent pending approval: installed, but no owner has approved it yet. Re-run after approval exits 0.

State lives in one directory (`--state-dir`, default the user cache dir): `delivery-cache.json` for queued memos, `queue-etag.json`, `telemetry-failures.jsonl` when telemetry is on, and `rejections.jsonl` for refusals the org has not been told about yet. Deleting the directory resets the agent.

<!-- section:contracts -->

## Contracts

*The documents that govern the service.*

- [Terms of Service](https://airmemo.com/terms) pilot terms, fees, the merge-safe installer clause
- [Privacy Policy](https://airmemo.com/privacy) storage and residency facts
- [DPA](https://airmemo.com/dpa) processing terms and breach notification
- [Acceptable Use Policy](https://airmemo.com/aup) prohibited content and conduct
- [Security and trust](/security) controls, receipts, and what we do not claim
