Agent Chat & MCP Server
memrynote can run an in-app Agent Chat backed by a provider-neutral agent backend. Claude CLI is the first full backend, local OpenAI-compatible servers can be used for BYO local models, and the Codex CLI backend uses the same contract when enabled. After a vault opens, the desktop app keeps the Agent runtime idle until Agent Chat or Agent MCP is opened. At that point it starts a local MCP endpoint on 127.0.0.1 with a random port, then gives the selected backend only the vault tools for the current conversation.
Opening the Agent MCP settings uses the same lazy start path as opening Agent Chat. If the runtime is still starting, memrynote retries the status check until the vault-scoped endpoint, bearer token, and tool list are ready to copy.
The Agent runtime and MCP endpoint are scoped to the open vault. Closing or switching vaults stops active turns, clears pending tool approvals, and restarts the Agent services for the next vault. The MCP endpoint is stopped with the vault even if the Agent runtime itself failed to start, so the next vault always gets a fresh endpoint and bearer token instead of the previous vault's.
The same MCP endpoint can also be copied into other desktop AI clients for vault read tools.
Open Settings -> AI Assistant -> Agent MCP to copy the current endpoint and bearer token. Open Settings -> AI Assistant -> Agent Permissions to set the default Agent Chat access mode and action-confirmation behavior.
Agent Chat
Open the right sidebar, choose Agent, and pick a provider. The compact Day/Agent switch keeps the active view highlighted at the top of the right sidebar, and switching to Agent takes effect on the first click even when the sidebar opened on the Day view. The assistant backend still starts up on first use rather than at launch, so the panel can show a brief loading state while providers and conversation history are detected. The Agent header includes a new-conversation button, a history menu for switching back to recent conversations, and a pop-out button for moving the current conversation into a workspace tab. Popped-out conversations keep the generated conversation title as the tab name, use the same centered reading column as notes, and leave the right sidebar ready for a new chat. The popped-out tab keeps the scroll bar at the window edge while the chat content stays centered, and the tab name is the only conversation title shown in that workspace view. Assistant responses render as full-width text in both the sidebar and popped-out tabs instead of bordered bubbles, with text aligned to the prompt input. For Claude CLI, memrynote checks that claude is available on PATH, that it reports version 2.1.0 or newer, and that the Agent disclosure has been accepted; the Codex CLI is detected the same way. On macOS and Linux, memrynote resolves your login shell's PATH at startup, so CLIs installed in shell-managed locations (~/.local/bin, Homebrew, nvm, volta) are still found when the app is launched from the Dock or Finder rather than from a terminal. These checks run in the background rather than pausing the app, and a successful check is reused for a few minutes so a burst of turns does not re-run it. A failed check is never remembered: if you install or upgrade a CLI while memrynote is running, the next message or provider check picks it up without a restart. If the CLI is removed, replaced, or loses its executable bit between that check and the moment a turn actually starts, the turn ends right away with a Claude CLI failed to start: ... (or Codex CLI failed to start: ...) error naming the reason, and the conversation is free to accept a new message as soon as the CLI is back. For local models, configure a compatible server in Settings -> AI Assistant -> Agent Permissions first. If the global AI switch is off in Settings -> AI, the Agent tab and Agent MCP current-note bridge are hidden and inactive.
Agent Chat can:
- keep local conversation history in the vault database
- attach the active note as context for a turn
- mention notes, tasks, journals, inbox items, and calendar events inline in a prompt with
@ - stream assistant text back into the sidebar
- link returned or created memrynote items directly in assistant replies, with a collapsible Sources section for the same items
- fold a turn's tool calls, tool results, and optional approvals into one collapsed activity row
- stop an in-flight turn
- compact older conversation history when a prompt grows too large
Naming a new conversation and compacting its history both run the CLI in the background. A CLI that prints a lot of diagnostic output while doing so no longer stalls the turn: memrynote reads that output as it arrives rather than waiting for the CLI to finish. If the CLI then fails, the tail of that output is written to the local log, the conversation falls back to a title derived from your first message, and the turn continues with its history uncompacted — your message and the conversation are never lost.
Conversation history is stored encrypted, so every message you send has to decrypt the transcript before the agent can see it. memrynote reads and decrypts that history once per message and reuses it for the rest of the turn, so sending a message into a long conversation costs the same single pass whether or not the turn also compacts. Long conversations stay responsive instead of getting slower with every reply.
While a turn runs, its tool calls collapse into a single activity row instead of stacking one row per step, so a long turn no longer pushes the answer off screen. The row names the tool the agent is running, counts the steps so far, and shows a spinner until the turn ends. Once the turn is done the row settles to a step count, and selecting it expands the individual tool calls with their parameters and results. A tool that needs your approval keeps the row open, because the Allow and Deny buttons live inside it.
Tool activity is saved with the conversation, encrypted in the vault database like the rest of the transcript. Reopening a conversation later — after a restart, after a sync, or after memrynote has dropped an older transcript from memory — still shows which tools ran, with what parameters, and whether each one succeeded, failed, or was denied. The saved record is deliberately smaller than the live one: it keeps the tool name, its identifying parameters (long values are shortened), and the outcome, but not the tool's returned data. Only tools that finished are recorded, so a turn stopped mid-tool leaves no half-finished row behind. Conversations from before this was added simply show no tool rows; nothing else about them changes. Saved tool rows are a record for you, not context for the assistant — they are never replayed into a later prompt.
When a tool result includes a real memrynote reference, Agent Chat renders that item as a clickable mention instead of plain text. Lists link each returned note, task, inbox item, journal entry, calendar event, project, or folder that has a navigable reference. Create and update confirmations also link the affected item when the tool returns its ID or journal date. The reply footer shows a collapsible Sources section only when the assistant message contains those memrynote item links. Inbox snooze confirmations use the same explicit inbox item reference as other inbox writes.
memrynote only links explicit tool-provided references. Plain titles without an ID or date stay as normal text. Clickable item mentions use memrynote's standard link color, show a dotted underline on hover, and display the matching item icon before the title. Each link shows exactly one icon: the item's own icon when it has one, otherwise the icon for its type. Note icons are read directly from the vault, so a linked note shows the same custom icon it has in the sidebar even while the reply is still streaming, and regardless of which agent backend produced the link. When a backend writes an item's emoji into the link text itself, memrynote moves that emoji into the icon slot rather than showing it next to a generic one. Inbox items use their capture-type icon, and journal, calendar, and task links use the same visual language as the main app surfaces.
Press Enter to send the prompt. Press Shift+Enter to insert a new line in the prompt box.
Type @ in the prompt to open the mention picker. Notes, tasks, journals, inbox items, and calendar events appear with their item icons, and calendar events are read from the local calendar with archived events hidden. Choosing a result replaces the active @ query with one inline tag, so a prompt such as summarize @Star Wars Movies keeps the referenced item visibly attached inside the prompt. Mention tags submit as readable @Title text plus an encrypted structured attachment reference.
The picker waits for a brief pause in typing before it searches, so typing a long mention runs one search rather than one per character. The list always reflects the last thing you typed — a slower search for an earlier query is discarded rather than shown.
The prompt box uses the operating-system text editing menu, so Cut, Copy, Paste, Select All, and native right-click editing work like other text fields.
Dictating a prompt
The microphone in the prompt bar records a prompt and types it for you. Click it once to start recording — the icon turns red and nothing else on the bar moves. Click it again to stop; the icon becomes a spinner while the audio is transcribed, then the text is appended to whatever is already in the prompt box.
Dictation uses the same engine as inbox voice memos, so it needs a transcription backend configured first — see Voice Transcription. Audio recorded here is transcribed and discarded; it does not create an inbox item. If the microphone is blocked at the operating-system level, memrynote points you at the relevant privacy settings.
Connected tools
While Google Calendar is not linked, a tray peeks out below the prompt box offering it. Clicking the Google Calendar icon opens the same consent dialog as the Connect Google button on the calendar page, and the tray disappears once the account is linked — from either place.
Stop requests are scoped to the active conversation across Claude, Codex, and local providers. Automatic title generation and conversation summaries run through the selected backend without exposing memrynote MCP tools.
Stop works from the moment you send a turn, including the pause before the backend starts answering — while a local provider is being checked for streaming and tool support, or while Claude or Codex is launching. Pressing stop in that window ends the turn there instead of letting it run to completion in the background.
Stopping a turn — and quitting memrynote while one is running — asks the chat backend to shut down first, and forces it to close if it has not stopped shortly afterwards. Quitting waits for that to finish, so a backend is never left running in the background after memrynote is gone. A backend that has to be forced closed does not get to finish writing its own scratch files, so the unfinished reply is lost; your message and the rest of the conversation are kept.
The prompt bar holds four controls. The + button opens the mention picker and the per-turn permissions menu; a dot on the button means at least one permission differs from your defaults. The first chip shows the active provider and model, and its menu switches either. The second chip sets reasoning effort. The microphone dictates a prompt by voice, and the arrow sends the turn.
The provider is pinned per conversation; changing it after messages exist updates the conversation and records the switch in the chat history. Claude and Codex expose prompt-time reasoning effort settings, with provider-specific settings shown only for the active provider. The permissions menu starts from your default Agent Permissions setting, then lets you send a single turn as Vault only or Computer access, and optionally allow web search for that turn.
Vault only keeps the CLI backend constrained to memrynote vault tools. Computer access gives the backend broader local CLI access for that turn. Web search is passed through only when the selected backend supports it.
Claude and Codex conversations also have a per-conversation model selector. memrynote starts Claude on opus and Codex on the highest suggested GPT version, then passes the selected model through to the CLI for each turn. The built-in model list is only a shortcut for common CLI aliases; type a custom model ID when you want to pin another CLI-supported model.
Local model support uses OpenAI-compatible HTTP APIs. memrynote ships presets for Ollama, LM Studio, and llama.cpp server, plus a Custom endpoint. Local tool access is gated by a capability probe. If the model can emit tool calls and continue after a tool result, memrynote enables the full vault tool set. If the probe fails, local chat can still answer from attached context, but vault tool calls stay disabled.
The probe costs a couple of model generations, so memrynote runs it once and reuses the verdict for up to ten minutes instead of repeating it on every message. Changing the preset, base URL, model, or API key re-checks immediately, and an unreachable provider is never remembered — start your local server and the next message picks it up. If you swap the model behind an unchanged configuration, press Probe Tools in Settings to force a fresh check.
If the configured local provider is not running, the model picker returns no discovered models instead of treating the settings page as an Agent runtime error. Start the provider, then load models or test the connection again.
When no conversation is selected yet, the prompt box stays available. Sending the first prompt creates a new Agent Chat conversation, attaches the active note when one is open, and streams the reply into the sidebar.
New conversations start with a temporary title. When you send the first prompt, the selected chat backend also generates a short conversation title. memrynote stores that title on the encrypted conversation row and refreshes the sidebar title without giving the title-generation subprocess access to memrynote MCP tools.
If a turn fails partway through — a window closing while the reply is still streaming, for example — memrynote stops the chat backend it started for that turn instead of leaving it running in the background. The unfinished reply is lost, but your message and the rest of the conversation are kept, so you can send the prompt again. A turn that is simply taking a long time is never stopped this way. The same applies when a turn fails before the reply even starts streaming, such as a disk or database error while saving the empty reply.
Conversation rows, message bodies, and message attachments are encrypted at rest before they are written to SQLite. Free accounts keep agent chat history local-only. Paid accounts can sync finalized conversations and terminal messages through memrynote Sync; in-progress streaming messages are not enqueued until the turn finishes.
If memrynote cannot verify the local vault key for the current database, Agent Chat stays unavailable instead of opening unreadable conversation history. A fresh local vault can initialize a new local key without sign-in; an existing vault with a mismatched or missing keychain key must be recovered through the normal vault recovery flow.
Connection
The endpoint is:
http://127.0.0.1:<port>/mcpExternal MCP clients must send:
Authorization: Bearer <token>The token is generated in memory for the current app launch. It is not saved to disk, changes when memrynote restarts, and can be rotated manually from settings. Missing or stale tokens receive 401.
The localhost MCP endpoint can serve overlapping Agent Chat turns and external read requests. memrynote keeps the URL/token stable for the app session, but handles each MCP request with an isolated transport so one client connection does not block another.
If the operating system refuses a connection to the endpoint — for example when the app has run out of file handles — memrynote records the failure in the app log as an AgentMcpServer error and keeps the endpoint listening on the same URL and token. Individual client connections can be dropped, but Agent Chat and external clients can reconnect without restarting the app or reopening the vault.
Client-specific config keys vary. Use the copied URL as the MCP server URL and the copied token as a Bearer authorization header. Plain external clients can use read tools, but they do not get the in-app conversation/window context that approved writes require.
A write is only approved for a conversation that still exists. A conversation that has been deleted — including one deleted on another device and carried here by sync — no longer counts as an existing conversation, so its write tools are refused rather than writing into a record that is gone from your chat history everywhere.
Tools
Read tools are available to Agent Chat and external MCP clients:
vault_search_notesvault_read_notevault_list_foldervault_get_current_notevault_list_tasksvault_get_taskvault_list_projectsvault_get_projectvault_list_statusesvault_get_journal_entryvault_list_journal_entriesvault_list_inbox_itemsvault_get_inbox_itemvault_get_tagsvault_list_canvasesvault_read_canvasvault_read_canvas_elementsvault_desktop_read
Notes and filed files
Filing a PDF, image, audio file, or video into the vault indexes it alongside your markdown notes, so it can turn up in vault_search_notes. Every search hit therefore carries a file_type:
markdown— a real note.vault_read_notereturns its content.pdf,image,audio,video— a filed file. There is no markdown to read, sovault_read_noterefuses it with aVALIDATIONerror naming the file type instead of returning bytes for the client to treat as text.vault_update_noterefuses it the same way, so an agent cannot overwrite a filed document with markdown.
Pass file_types to narrow the search up front — ["markdown"] for notes only, or ["pdf", "image"] to look for filed documents. The filter runs inside the search query, so limit counts only matching rows. Omit file_types to search every file type.
Notes indexed by older memrynote versions have no recorded file type; those are always treated as markdown, so upgrading never hides existing notes.
Create, update, delete, archive, move, and reorder tools require Agent Chat context. They can be auto-accepted or shown for inline approval depending on the Agent Permissions setting:
vault_create_notevault_rename_notevault_delete_notevault_create_foldervault_rename_foldervault_delete_foldervault_create_taskvault_delete_taskvault_complete_taskvault_uncomplete_taskvault_archive_taskvault_unarchive_taskvault_move_taskvault_reorder_tasksvault_duplicate_taskvault_convert_task_to_subtaskvault_convert_subtask_to_taskvault_create_projectvault_update_projectvault_delete_projectvault_archive_projectvault_reorder_projectsvault_create_statusvault_update_statusvault_delete_statusvault_reorder_statusesvault_create_journal_entryvault_update_journal_entryvault_delete_journal_entryvault_add_to_inboxvault_update_inbox_itemvault_snooze_inbox_itemvault_archive_inbox_itemvault_unarchive_inbox_itemvault_delete_inbox_itemvault_add_inbox_tagvault_remove_inbox_tagvault_update_notevault_update_taskvault_add_tagvault_remove_tagvault_move_to_foldervault_add_canvas_itemvault_remove_canvas_itemvault_create_canvasvault_draw_on_canvasvault_edit_canvas_elementsvault_desktop_write
Canvas
| Tool | What it does |
|---|---|
vault_list_canvases | Every canvas, with how many items sit on each |
vault_read_canvas | One canvas: the notes/tasks/events on it (with titles) and any text written on it |
vault_read_canvas_elements | The shapes on one canvas: ids, position, size, text, colors, arrow bindings |
vault_create_canvas | Create an empty canvas |
vault_add_canvas_item | Put existing notes/tasks/events on a canvas as cards |
vault_remove_canvas_item | Take a card off a canvas |
vault_draw_on_canvas | Draw shapes, text, arrows, lines, freedraw, frames, images and embeds |
vault_edit_canvas_elements | Move, restyle, retext or delete elements already on a canvas |
Canvas tools need the Spatial Canvas feature turned on (Settings → Features). With it off they return a message saying so rather than failing silently.
There are two ways to read a canvas, for two different questions. vault_read_canvas answers "what is on this canvas" — the notes, tasks and events, with their titles, and any text written on it. It returns no geometry, because a scene is mostly coordinates and style properties and dumping that into an agent's context crowds out everything useful. Long canvases cap the amount of text returned and say so with texts_truncated. vault_read_canvas_elements answers the other question — "where is everything" — and is the read to use before drawing: an element id from it is what an arrow binds to and what an edit changes.
Drawing
vault_draw_on_canvas takes Excalidraw's own element skeletons, in camelCase, so anything you can draw by hand you can ask for: rectangles, ellipses, diamonds, text, arrows, lines, freedraw, frames, images and embeds, with the full style vocabulary (strokeColor, backgroundColor, fillStyle, strokeWidth, strokeStyle, roughness, opacity, roundness, fontSize, fontFamily).
Two things make it a diagram rather than a pile of shapes:
- Refs. Give an element a
refand other elements in the same call can point at it. The response maps every ref to the id it became, so the next call can keep building on it. - Real bindings. An arrow with
startandend— either{"ref": "..."}for something in the same call or{"elementId": "..."}for something already on the canvas — is bound on both sides. It follows the shapes when you drag them, exactly like an arrow you drew yourself. Frames work the same way: children can be new elements or ones already there.
An agent cannot mint entity cards this way. A card is a rectangle carrying the entity reference that canvas_entity_refs is rebuilt from, so cards go through vault_add_canvas_item, which checks that the note/task/event actually exists. vault_edit_canvas_elements will move and restyle a card, but not rewrite its text — that text lives in the note.
Images need a fileId already attached to the canvas; agents cannot upload binaries.
Adding an item applies to the open editor when you have that canvas open, so the card appears while you watch instead of being overwritten by the next autosave. A canvas nobody has open is updated directly, and the write is rejected if it changed in the meantime. If the result is too large to sync, the tool response says so.
Some canvas operations are deliberately unavailable through vault_desktop_read / vault_desktop_write:
canvas.get— returns the whole scene; usevault_read_canvasorvault_read_canvas_elementscanvas.update— replaces the entire scene with no version check, which would overwrite whatever you have open; use the item and drawing toolscanvas.librarySave— saves the shape library as one whole list, so a partial one deletes shapescanvas.uploadAsset— binary image upload, no agent path yet
An arrow an agent draws is a picture, not a stored relationship. It is bound, it moves with the shapes, and it is genuinely useful on a diagram — but nothing queries it. When you want a connection the rest of the app can follow, use wiki links between notes or the project link layer; the arrow is the drawing of that connection, not the connection.
vault_desktop_read and vault_desktop_write cover the remaining desktop CRUD surface through an allowlisted desktop API operation name plus an args array. They are used for desktop domains such as templates, saved filters, bookmarks, reminders, calendar events, folder views, properties, tags, search reasons, inbox conversions, project links, and settings. The write bridge uses the same in-app approval flow as named write tools. Security-sensitive and system operations stay outside the allowlist, including account/auth flows, provider connect/disconnect/refresh actions, app updater actions, external open/reveal actions, import dialogs, OS settings panes, telemetry, feedback and diagnostics reporting, and raw secret writes. Unsupported or unavailable desktop API operations return a structured MCP error instead of falling back to an arbitrary desktop call.
Inbox items convert through the bridge into any of the four targets the app itself offers: inbox.convertToNote, inbox.convertToTask, inbox.convertToEvent, and inbox.convertToReminder. notes.applyTemplate applies a template the agent can already read through templates.get to an existing note.
settings.getFeaturesSettings reports which surfaces are turned on — home, inbox, journal, tasks, calendar, graph, and spatial canvas — so an agent can tell whether an action is even available before suggesting it, and settings.setFeaturesSettings toggles them. settings.getInboxSettings and settings.setInboxSettings cover the daily inbox review reminder.
Tag categories are reachable through the same bridge. tags.listCategories is a read operation that returns {"success": true, "categories": [...]}, where each entry carries its id, name, sort order, and tag count. tags.createCategory, tags.renameCategory, tags.deleteCategory, and tags.reorder are write operations behind the usual approval flow; tags.createCategory returns {"success": true, "category": {...}} and the other three return {"success": true}. Like the rest of the desktop bridge, these operations report their own failures as {"success": false, "error": "..."} rather than raising an MCP error, so check success before reading the payload. tags.reorder applies a drag result — tag-to-category assignments, category ordering, or both in one transaction — and is the only way to move a tag into a category. Deleting a category keeps its tags and makes them uncategorized.
vault_get_tags returns each tag with its color, icon, sort_order, category_id, and category_name. Both category fields are null for an uncategorized tag.
Calendar desktop reads accept the same single-object shape as the renderer bridge. For example: calendar.listEvents accepts args: [{}], and calendar.getRange accepts either args: ["2026-05-14", "2026-06-14"] or args: [{"startAt": "2026-05-14T00:00:00.000Z", "endAt": "2026-06-15T00:00:00.000Z"}].
Google-integration operations — calendar sources, provider status, Google calendar lists, promoting external events, and Google calendar settings — are excluded from the agent allowlists outright.
Google-synced events themselves are gated on explicit user consent. calendar.getRange resolves includeExternal from the stored answer to the Let AI read Google Calendar events setting, never from the caller: an agent that passes includeExternal: true still gets native-only results unless the user granted access. Not asked yet, declined, or a settings read that failed all resolve to native-only. See Calendar → Google Data and AI Features.
Google user data is never used to train or improve AI models, in line with the Google API Services User Data Policy (Limited Use).
By default, Agent Chat accepts these tool calls automatically. The chat still shows each requested tool as compact, subdued text with a readable label such as Reading note or Creating task. Click the label to open or close the details area with the raw MCP tool name, parameters, and results.
If you switch tool confirmations to Ask first in settings, memrynote pauses the turn and shows inline approval controls inside the tool row. You can allow the request once, allow create tools always for that conversation, deny it, or edit the arguments before allowing. Note updates load a before/after diff before the write is applied. Unauthenticated or context-free write requests continue to be denied.
Stopping the turn while an approval is waiting counts as a denial: the pending request is refused, the tool never runs, and the approval controls disappear. Nothing is written to your vault.
An approval you never answer expires after 30 minutes. The controls disappear, the tool never runs, and the tool row is marked as expired rather than denied — the agent is told the request timed out, not that you refused it. Take as long as you need to read a request or its diff; half an hour is there so a prompt left open overnight does not keep the request waiting forever, not to hurry you. If a request expires and you still want it, ask the agent again.
Project Links
vault_list_projects returns each project's icon, home_note_id, and linked_counts (notes, files, events) alongside task_count. Nonzero linked_counts are the signal that a project has a hub link layer worth reading; task_count alone says nothing about it.
The hub's link layer is reachable through the desktop bridge. Reads, via vault_desktop_read:
| Operation | Args | Answers |
|---|---|---|
tasks.listProjectContents | [projectId] | Which notes, files, and events a project holds |
tasks.listProjectLinks | [projectId] | The raw link rows, including pin state |
tasks.listForItem | [itemType, itemId] | Which projects a note, file, or event belongs to |
itemType is one of note, file, or calendar_event.
Writes, via vault_desktop_write, behind the same approval flow as other writes:
| Operation | Args |
|---|---|
tasks.linkProjectItem | [{ projectId, itemType, itemId }] |
tasks.unlinkProjectItem | [{ projectId, itemType, itemId }] |
tasks.setProjectLinkPinned | [{ projectId, itemId, pinned }] |
tasks.setProjectHomeNote | [{ projectId, noteId }] |
tasks.captureUrlToProject | [{ projectId, url }] |
tasks.importFilesToProject | [{ projectId, sourcePaths }] |
setProjectHomeNote takes noteId: null to clear the project's home note, the same way the hub's own overview rail does.
captureUrlToProject fetches the page title over the network, and importFilesToProject copies files from paths you supply into the vault — both on caller-supplied input, like the already available inbox.captureLink and notes.importFiles. Operations that open native UI or hand an item to the OS stay out of the allowlist.
importFilesToProject waits for the indexer to assign an id to each imported file, so importing several large files at once can exceed the bridge's ten-second window. The import still completes; the tool call reports a timeout. Import in smaller batches to see the result.
Current Note
vault_get_current_note can snapshot the active note when the request is associated with a memrynote window. Plain external clients do not have that window context, so the tool returns null instead of guessing.
Privacy
The MCP server binds only to localhost. Read tools still expose the content they return to the client you configure, so only paste the token into clients you trust on this machine. Write tools still require an active Agent Chat conversation context, and the in-app tool confirmation setting decides whether that conversation pauses for approval or accepts the call automatically.
Provider privacy depends on the selected backend:
- Claude and Codex providers may send prompts, attachments, and tool results through that provider.
- Local loopback providers keep model prompts on this machine, except for the actual tool effects memrynote applies inside the vault after approval.
- Custom non-loopback endpoints send data to the configured endpoint and must be explicitly enabled with the not-fully-local warning.