Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Xenocept is a desktop screen capture and annotation tool, built for the AI era.

The pitch is simple: stop describing what’s on your screen and start showing it — to your AI agent, your QA tracker, your support tool, your teammates, or any workflow you can configure.

What Xenocept Does

You press a global hotkey. Your screen freezes. An annotation overlay drops in. You click-drag to mark regions of interest, draw on them, attach written descriptions, and stack as many comments as you need. When you’re done, you hit Submit and Xenocept bundles everything — pixels, markup, descriptions, metadata — into a structured snapshot and delivers it to whatever consumer you’ve configured.

It’s the missing tool between “I see a problem” and “the right system has the context to act on it.”

What Makes It Different

  • Snappy. The Tauri overlay window is created at startup and kept hidden; pressing the hotkey captures a fresh screenshot and unhides the existing window.
  • Pluggable delivery. Submitted sessions are dispatched to destinations you configure — each destination wraps a plugin that knows how to ship the data (an HTTP endpoint, an MCP channel, a file, an email, your own JavaScript). See Consumers & Delivery.
  • Local-first. Every submitted session is persisted in an AeorDB-backed store on your machine. Browseable, searchable, yours. Delivery to anywhere else is a side effect.
  • Native screen capture. Through the xcap crate — pixel-buffer captures on whichever native API your platform exposes (PipeWire/X11 on Linux, the equivalent on macOS/Windows).
  • No frameworks, no build step. Tauri shell, Rust backend, vanilla WebComponents on the frontend. A binary you can audit.

Who It’s For

  • Developers working with AI agents — Drive Claude, Codex, or any MCP-compatible agent with visual context, not just words.
  • QA engineers and bug reporters — Stop losing context to flat screenshots. Every report carries the annotation and description as a single payload.
  • Customer support — Walk through issues with screen-share sessions and deliver structured, redacted reports into your help-desk pipeline.
  • Designers and reviewers — Comments stay anchored to the regions they describe. Reviewers see exactly which pixel a note refers to.
  • Tool builders — Wire snapshots into anything via the stable JSON schema and the plugin system.

Reading This Documentation

The docs are organized for two kinds of readers:

Status

Xenocept is under active development. The desktop binary, capture pipeline, annotation overlay, comment workflow, and AeorDB-backed storage are all in place. Pluggable delivery and the plugin system are landing in stages. Where this documentation describes behavior that’s still in flux, you’ll see a note saying so.

The vision document lives in the bot-docs/docs/vision.md file inside the repository — it’s the master plan and is updated as the project evolves.

Installation

Xenocept is a desktop application. It runs as a singleton background process with an icon in the system tray and a global hotkey that summons its annotation overlay.

Prerequisites

Pre-built binaries for Linux, macOS, and Windows are coming soon. Until then, building from source is the supported install path.

Linux (Ubuntu / Debian)

These system libraries are required to compile and run Xenocept:

# Tauri / GTK / WebKit
sudo apt-get install -y \
  libgtk-3-dev \
  libwebkit2gtk-4.1-dev \
  libjavascriptcoregtk-4.1-dev \
  libsoup-3.0-dev \
  libglib2.0-dev \
  libcairo2-dev \
  libpango1.0-dev \
  libatk1.0-dev \
  libgdk-pixbuf2.0-dev

# Screen capture (xcap crate — PipeWire/Wayland headers needed even on X11)
sudo apt-get install -y \
  libpipewire-0.3-dev \
  libgbm-dev

# Bindgen (FFI bindings)
sudo apt-get install -y \
  libclang-dev

# rsync (shared web components sync)
sudo apt-get install -y \
  rsync

You’ll also need a Rust toolchain (the project pins the edition; rustup will pick up the project’s pinned channel automatically):

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

macOS

A Rust toolchain via rustup plus Xcode command-line tools is enough. Screen capture goes through the xcap crate, which uses whichever native macOS capture API is appropriate for the system version.

xcode-select --install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows

Install rustup for Windows plus the Visual Studio Build Tools (the C++ workload, including the MSVC toolchain and the Windows 10/11 SDK).

Build & Run

Clone the repository, then:

cargo build
cargo run

The first build pulls a fair number of crates; expect a few minutes the first time and seconds on incremental rebuilds.

When Xenocept starts, it:

  1. Spawns an HTTP server on http://127.0.0.1:9500 (hardcoded — the GUI and external consumers both reach Xenocept through this port). See HTTP Delivery and MCP.
  2. Opens its Tauri window.
  3. Registers the global hotkey (default Ctrl+Backquote).
  4. Drops into the system tray.

Closing the window does not exit — the app stays alive in the tray so the hotkey keeps working. The tray icon’s double-click action opens the main window back up.

Verifying the Install

After launching:

  1. Hover over the system tray. You should see the Xenocept icon.
  2. Press the hotkey (Ctrl+Backquote by default). A fresh screenshot of the monitor under your cursor is taken and the annotation overlay appears.
  3. Press the hotkey again (or Esc) to hide the overlay.

If anything goes wrong, see Troubleshooting.

Where to Next

First Capture

Walk through your first end-to-end capture session. Assume Xenocept is installed and running in the system tray.

1. Summon the Overlay

Press the global hotkey:

Ctrl + Backquote

(Same default on every platform. You can change it in the Settings UI.)

Xenocept captures a fresh screenshot of the monitor under your cursor and shows the annotation overlay on top of it.

The overlay’s background is the captured image. The real screen behind continues to live, but you’re now working on a frozen snapshot.

2. Pick a Tool

Hold down the radial-menu trigger (default mouse / touchpad gesture configured in the desktop app, see the in-app help) to bring up the radial menu. The slices show all the available tools:

  • Drawing slice: Pointer/Select, Pencil, Brush, Rectangle / Rectangle-filled, Circle / Circle-filled, Line, Arrow
  • Pen size slice (interactive)
  • Text & Comments slice: Text, Note (comment-area), Bubble (comment-bubble)
  • Utilities slice: Area (session-area), Blur, Emoji
  • History slice: Undo, Redo

Release on a tool to select it. The current selection becomes active for the next interaction.

3. Make a Comment

A Note (a.k.a. comment-area) is Xenocept’s main feedback unit. To make one:

  1. With the Note tool selected, click-drag a rectangle around the region you want to talk about. That rectangle is the focus area.
  2. After releasing, the overlay enters a side-pick phase. Click on one of the four sides (top / bottom / left / right) of the focus area — that’s where the text box will sit.
  3. A textarea appears on the chosen side. Type your description.
  4. Press Esc to commit the text — or click the Confirm button hovering near the textarea. (Pressing Enter inserts a newline; it does not commit.)

The note is now on the canvas. Click somewhere else and you can make another, or switch to a different tool.

A Bubble (comment-bubble) works similarly but is anchored to a single point rather than to a rectangle. Pick a target with a click instead of drag-selecting a region.

4. Other Tools

  • Pencil / Brush / Line / Arrow / Rectangle / Circle — click-drag to draw.
  • Text — click to place a textarea, type, Esc/Confirm to commit (same pattern as comments).
  • Blur — click-drag a rectangle; Xenocept replaces those pixels (on the overlay only) with a blurred copy. Useful for redacting secrets.
  • Emoji — pick from recents or search, then click to place the floating glyph.
  • Pointer / Select — click an existing object to select it; drag selected objects to move; Delete/Backspace to remove.
  • Area (session-area) — defines the crop region for the final submitted screenshot. Drag a rectangle; only what’s inside that rectangle ends up in screenshot.png at Submit time.

5. Undo As Needed

Ctrl/Cmd + Z undoes the last edit. Ctrl/Cmd + Shift + Z redoes. The undo stack is per-overlay-instance and lives in memory only — it survives Esc (the window is just hidden), but a fresh hotkey press resets the canvas.

6. Submit

When you’re satisfied, Submit through the in-app affordance (a button in the overlay UI). What happens then:

  1. Xenocept POSTs the session data to /api/v1/sessions.
  2. The backend stores screenshot.png, optionally screenshot-clean.png, and one focus-{i}.png per Note under /sessions/{session-id}/ in the AeorDB store. The session metadata is written to session.json.
  3. The session ID is a timestamp string of the form session-YYYY-MM-DD-HH-MM-SS.mmm.
  4. The Sessions list view (subscribed via SSE to /api/v1/sessions/events) is notified of the new session.
  5. If you have auto-send turned on, the frontend’s plugin loader then calls /api/v1/sessions/{id}/dispatch to fan out to every enabled destination.

The overlay closes. Your work is now a permanent session in the local store, browseable from the main Xenocept window.

Things to Know

  • Pressing the hotkey again while the overlay is open hides it; it does not stack screenshots.
  • In-progress canvas state does not persist across overlay close/reopen. Esc hides the overlay; the next hotkey press resets the canvas. If you want your work saved, Submit it.
  • Capture covers one monitor — the one under your cursor at hotkey press time.

Configuration Overview

Xenocept stores all of its state — sessions, screenshots, settings, destinations — inside a single AeorDB-backed file (xenocept.aeordb). The paths described below are paths inside that file, not paths on your real filesystem.

Edit through the Settings UI in the desktop app, or via the HTTP endpoints listed below.

Where the Database Lives

PlatformPath
Linux(depends on Tauri’s resolved app-data dir, typically under ~/.local/share/xenocept/)
macOS~/Library/Application Support/<Xenocept-app-id>/
Windows%APPDATA%\<Xenocept-app-id>\

The exact directory is resolved by Tauri’s app_data_dir at startup ( around the state_store initialization). The Settings UI in the running app is the authoritative source of the actual path on your system.

The Three Config Documents

Inside the AeorDB store:

PathConcern
/config/settings.jsonUser-facing settings (hotkey, autostart, screenshot export path/template). See Settings.
/config/destinations.jsonConfigured destinations (recipients of submitted sessions). See Destinations.
/config/auto-send-destinations.jsonWhich destinations auto-send on Submit, plus a master enable flag.

HTTP Endpoints For Config

EndpointMethodPurpose
/api/v1/settingsGET / POSTRead or overwrite settings.json
/api/v1/destinationsGET / POSTList or create destinations
/api/v1/destinations/{id}PUT / DELETEUpdate or delete a destination
/api/v1/auto-sendGET / PUTRead or update the auto-send config

The HTTP Server Itself

When Xenocept starts, it spawns its HTTP server on:

http://127.0.0.1:9500

Both the host and the port are hardcoded. There is no server configuration block in settings.json. The MCP channel-mode CLI accepts a --port flag, but that flag tells the channel server which port the running Xenocept GUI listens on (defaulting to 9500) — it does not change the listening port.

Capture Area

When you press the hotkey, Xenocept captures a screenshot of the monitor under your cursor — and only that monitor. The overlay opens on top of that monitor’s freeze-frame.

That’s it for the capture step. There is no multi-monitor coordination and no persistent capture rectangle that selects which monitors to freeze.

The Session-Area Tool

The closest thing to “a saved capture region” inside the overlay is the Session-Area tool (Area in the radial menu — ).

When you draw a session-area rectangle on the overlay, you’re defining a crop region for the final submitted screenshot. Everything inside that rectangle is what gets saved to screenshot.png on Submit. The rest of the captured monitor’s pixels are discarded for the submitted artifact (but screenshot-clean.png, if generated, may include the full background — see Snapshot).

The session-area:

  • Is a single rectangle per overlay session
  • Has a minimum size of 20×20 pixels
  • Is drawn the same way as any other rectangle (click-drag), and resized/moved the same way
  • Resets along with the rest of the canvas state on the next hotkey press

What’s Captured vs What’s Submitted

Two different things:

StageWhat it is
Capture (on hotkey)Full screenshot of the monitor under the cursor, used as the overlay background.
SubmitThe session-area’s crop becomes screenshot.png. Each Note captures a focus-{i}.png from its focus area. The screenshot-clean.png is the background-only image (no markup) if the frontend generated one.

Notes are independent of the session-area: a Note can be made anywhere on the overlay, even outside the session-area rectangle.

Note (Comment-Area)

A Note (comment-area in the radial menu, ) is Xenocept’s primary feedback unit. It’s a rectangular focus region with a written description anchored to one of its sides.

(The frontend calls this a comment in code and stores it under comments[] in session.json. The radial menu labels it “Note”. This page uses “Note” to match the user-facing term.)

What a Note Carries

In the submitted session.json, each Note is one entry in the top-level comments array:

{
  "index": 0,
  "text": "the dropdown is cut off",
  "side": "top", // top | bottom | left | right
  "focus_x": 240,
  "focus_y": 180,
  "focus_width": 320,
  "focus_height": 180,
  "focus_image": "focus-0.png"
}

The focus_image field references a per-comment PNG that the backend stores alongside session.json — at /sessions/{session-id}/focus-0.png, focus-1.png, and so on.

The frontend renders the focus crop with annotations baked in and uploads that single PNG. There is no separate “original capture” vs “annotated capture” pair per Note.

Making a Note

  1. With the Note tool selected, click-drag a rectangle around the region you care about. That rectangle is the focus area.
  2. Release. The overlay enters a side-pick phase.
  3. Click on one of the four sides of the focus area (top / bottom / left / right) — that’s where the description textarea will sit.
  4. Type your description.
  5. Press Esc to commit the text or click the Confirm button hovering near the textarea. Enter inserts a newline; it does not commit.

The Note becomes a canvas object you can later select, move, edit, or delete with the Pointer tool.

Pixel Capture Timing

The focus crop is not captured at the moment you release the focus rectangle. It is captured at Submit time. Submit walks the Notes, renders each focus crop with its annotations baked in, and uploads the result.

The practical consequence: if you change the canvas background (e.g. take a fresh screenshot via a different flow before Submit), what gets saved into focus-{i}.png reflects the current background plus the Note’s annotations — not the background that was visible when you drew the focus rectangle.

In the normal flow this rarely matters (one capture per session, no background swaps), but it’s worth knowing if you’re poking at the system.

Editing a Note

While the overlay is open, double-clicking a Note’s focus rectangle re-opens its textarea for editing. You can also drag the focus rectangle to move it or use the Pointer tool to select and delete it.

After Submit, the session is written and immutable. The submitted Note doesn’t change unless you explicitly open the session again in edit-mode (via the Sessions list UI), which creates a new session on next Submit.

Session

A session in Xenocept is the persisted artifact created when you press Submit. It is the immutable record of one capture + annotation + comment cycle. The in-progress canvas state inside the overlay is not itself persisted — only Submit writes a session to disk.

The Two States

StateWhat it isWhere it lives
Live canvasThe active drawing/annotation state inside the overlay.In-memory only, held by the <xenocept-canvas> web component.
Submitted sessionThe persisted result of pressing Submit./sessions/{session-id}/ inside the AeorDB store.

The transition between them is one user action — pressing the Submit button — and is one-way.

What’s In a Submitted Session

/sessions/{session-id}/ contains:

session.json # the structured metadata
screenshot.png # the (possibly session-area-cropped) submitted screenshot
screenshot-clean.png # optional — background-only, no markup (frontend may or may not send)
focus-0.png # one per Note
focus-1.png
...

session.json carries:

{
  "id": "session-2026-05-15-18-30-00.123",
  "created_at": "2026-05-15T18:30:00.123Z",
  "screenshot": "screenshot.png",
  "comments": [ /* Note entries — see Note page */ ],
  "bubbles": [ /* Bubble anchors */ ],
  "texts": [ /* freestanding text objects */ ],
  "emojis": [ /* emoji glyph annotations */ ],
  "canvas_objects": [ /* verbatim canvas objects, used to reconstruct for edit mode */ ]
}

Notably not in the schema:

  • schema_version
  • snapshot_id (the id is just id, and it’s a timestamp string, not a ULID)
  • A workspace block
  • A metadata block with platform / display server / version
  • A monitors array
  • bounds / capture / annotated per comment

The id is generated as chrono::Utc::now.format("session-%Y-%m-%d-%H-%M-%S%.3f").

The Live Canvas, Briefly

While the overlay is open, the live canvas accumulates objects (strokes, shapes, Notes, bubbles, text, blur, emoji). The canvas state is mutable. Ctrl+Z and Ctrl+Shift+Z walk the undo/redo stacks. None of that state is persisted to disk while in progress.

When the overlay is hidden (by Esc or by another hotkey press that toggles it off), the Tauri window survives but the canvas state still lives in memory in that window. The user just can’t see it.

When the overlay is shown again by a fresh hotkey press, the canvas state is reset before the new screenshot is installed — the object list, undo stack, and redo stack are all cleared. The previous live canvas is gone. There is no auto-resume.

If you want to keep your work, Submit it. Submit is the only path that persists the work; everything else is throwaway.

Multiple Sessions

There is one Xenocept process at a time — the app is single-instance. Within that process there is also at most one live canvas (there’s only one overlay window).

Submitted sessions accumulate in /sessions/ and don’t conflict — each gets its own timestamp-derived id.

Editing a Past Session

The main Xenocept window’s Sessions list lets you open a past session in edit mode. The frontend reconstructs the canvas from session.json and the stored PNGs, and Submit will create a new session — the original stays as it was. There is no in-place mutation of submitted sessions.

Snapshot

A note on terminology: “snapshot” was the older intended name for what the code now stores under the session concept. The code consistently uses “session” — session.json, /sessions/{id}/, submit_session, session_events. This page is kept for now as a cross-reference for anyone arriving from the old vocabulary. For the authoritative description of the submitted artifact, see Session.

The Submitted Artifact

The submitted artifact in Xenocept is called a session. On Submit, Xenocept persists /sessions/{session-id}/ containing session.json, screenshot.png, optionally screenshot-clean.png, and one focus-{i}.png per Note. See Session for the full layout and Session JSON Schema for the JSON shape.

Why the Name Change

The early design vocabulary had a clean trio: Workspace → Comment → Snapshot. The implementation converged on a slightly different model: capture is single-monitor (no workspace concept), comments are Notes, and the submitted artifact is a session. The shape of that artifact is also simpler than the earlier design imagined — no schema_version, no workspace block, no metadata block, no ULID. Just a timestamp-keyed directory with images and a flat session.json.

This page exists to point you at Session when you arrive looking for “snapshot” terminology. The substance is the same; the name is what changed.

What’s Immutable

Once persisted, the files under /sessions/{session-id}/ are not overwritten by anything except an explicit delete (DELETE /api/v1/sessions/{id}, ). The frontend’s “edit mode” doesn’t mutate the original — it loads a session into the canvas as starting state and creates a new session on the next Submit.

Cross-References

The UX Philosophy

Xenocept lives in your way by definition — it overlays your screen, holds a freeze-frame for you to mark up, and demands your attention while you’re using it. The whole product is built around keeping that interruption short and the interaction obvious.

1. Snappy

The Tauri overlay window is created at app startup and kept hidden. Pressing the hotkey:

  1. Takes a fresh screenshot of the monitor under the cursor
  2. Shows the pre-existing overlay window with that screenshot as its background

There is no app-launch step. The window is already alive — pressing the hotkey just makes it visible and gives it a fresh frame. A “Capturing Screen…” loading state shows briefly during the actual capture, but the overlay window itself doesn’t need to start up.

Capture happens on-demand at hotkey press — the overlay does not stream the screen continuously to a warm window.

The window is pre-warmed as a window; the image inside it is fresh on every capture, not continuously streamed.

2. Esc Drains a Stack of Behaviors

Esc cascades through a priority list:

  1. If the radial menu is open → close the menu
  2. Else if a text editor is open → commit the text
  3. Else if a tool operation is in progress → cancel it
  4. Else if objects are selected → deselect
  5. Else → hide the overlay

Each step consumes the event. The user never has to think about which Esc does what; the right thing happens.

3. State Lives Until the Next Capture

A precise statement of what persists when:

  • Live canvas state (drawn objects, undo stack, redo stack, selections) lives in the overlay window’s memory.
  • Hiding the overlay (Esc with no other state to drain, or pressing the hotkey while visible) just hides the Tauri window. The state stays put in memory.
  • A fresh hotkey press while the overlay is hidden captures a new screenshot, shows the window, and resets the canvas state. The previous live work is gone.

So Esc is safe in the sense that you don’t lose anything immediately. But the next hotkey-driven capture wipes the canvas. If you want your work persisted, press Submit. Submit is the only path that writes the session to disk.

4. Submit Is the Persistence Boundary

POST /api/v1/sessions is what writes a session to the AeorDB store. It’s the only thing that does. Everything else is non-persistent.

After Submit:

  • The session is in /sessions/{session-id}/ and is content-addressed/deduplicated by AeorDB.
  • A created event is broadcast on /api/v1/sessions/events so the Sessions list updates without polling.
  • If auto-send is on, the frontend then calls /api/v1/sessions/{id}/dispatch to fan out to configured destinations.

5. Quiet When Not Called On

Closing the main Xenocept window does not exit the app — it stays in the system tray with the hotkey registered. Double-clicking the tray icon brings the main window back. A single click is currently not bound to “open main window”.

The hotkey is the primary input. Tray and main-window UI are for browsing past sessions, editing destinations, and changing settings.

6. Forgiving by Default

Undo handles drawing. Esc cascades through tool/menu/selection state safely. The undo stack is unbounded in memory (no caps), so any number of operations within a single overlay session can be walked backward. Quitting the app entirely drops the in-memory undo history — there is no persisted undo log.

Global Hotkey

The global hotkey is the front door to Xenocept. From anywhere on your system — your editor, your browser, a video call — pressing it grabs a screenshot of the monitor under your cursor and shows the annotation overlay.

Default Binding

Ctrl + Backquote

Same default on every platform. There is no separate macOS variant.

The current binding lives in /config/settings.json under the hotkey key. Change it from the Settings UI; that writes the new value back to settings.json and re-registers the global shortcut immediately via POST /api/v1/hotkey.

What It Does (Both Directions)

The hotkey toggles the overlay:

StateHotkey press
Overlay hiddenCapture a fresh screenshot of the monitor under the cursor, then show the overlay. The canvas state is reset — the previous drawing, undo history, and pending comments are dropped.
Overlay visibleHide the overlay. No new capture, no state changes.

Crucially: pressing the hotkey while the overlay is open is not a “capture again on top of my current work” command. It hides the overlay.

If you want to take a fresh screenshot mid-session, you have to:

  1. Hide the overlay (press the hotkey, or Esc through any in-progress state).
  2. Set up the screen the way you want it.
  3. Press the hotkey again.

But that flow also wipes the current canvas state (because the show cycle resets it). If you wanted to keep what you’d already drawn, your option is to Submit first.

After You Press It

When the overlay shows with a fresh screenshot:

  1. The screenshot becomes the canvas background.
  2. The screenshot is also saved to /screenshots/{filename}.png in the AeorDB store as an always-on photo-roll, independent of whether you ever Submit. You can find these in the Screenshots list.
  3. The overlay re-pulls user preferences so the active tool, menu opacity, and similar settings reflect any changes you made in the Settings UI since the last capture.
  4. The canvas plays a brief zoom-in reveal animation.

Cooldown

After the overlay shows, the canvas suppresses input for ~300 ms. The cooldown prevents an immediate click that would happen to coincide with the hotkey press from registering as a drawing action.

Suspending the Hotkey

There’s a POST /api/v1/hotkey/suspend endpoint the frontend uses to temporarily disable the hotkey — for example, while an in-overlay text editor is active and the user might be typing the same key combo. External tooling generally doesn’t need to interact with this.

Conflicts

If another app already owns the binding, Tauri’s global_shortcut.register returns an error and the hotkey is not active. Xenocept logs a warning at startup. The Settings UI surfaces unbound hotkeys so you can pick another combination.

On Wayland, the desktop portal restricts which keys can be globally bound on some compositors. If registration fails repeatedly, try a less-common combination.

Freeze-Frame Capture

When you press the hotkey, the screen appears to freeze. What actually happens:

  1. Xenocept asks the OS for a fresh screenshot of the monitor under the cursor.
  2. The overlay window — already created at startup but kept hidden — is shown on top of that monitor.
  3. The overlay’s canvas displays the captured image as its background.

The real desktop keeps running underneath the overlay; you’re annotating a static image on top of it. That’s what makes the “freeze” instant — there’s no real screen pause, just a perfectly-aligned image overlay.

Single Monitor, Cursor’s Monitor

Capture is single-monitor. Xenocept asks xcap for the monitor the cursor is currently on and captures only that one. The overlay opens on that monitor.

If you have multiple monitors and the work you want to annotate is on a different one, move your cursor there before pressing the hotkey.

Through the xcap Crate

The capture itself goes through the xcap crate (xenocept-client/Cargo.toml:43). xcap is a cross-platform screen-capture library that picks the native API for each OS:

PlatformWhat xcap uses
Linux (Wayland)PipeWire via the XDG Desktop Portal
Linux (X11)XGetImage / XShm via libxcb
macOSThe system’s native capture API (ScreenCaptureKit or its predecessor depending on version)
WindowsDXGI Desktop Duplication / Windows.Graphics.Capture

Xenocept does not call any of those APIs directly. It calls xcap::Monitor::capture_image and gets a pixel buffer back. The platform specifics are xcap’s concern.

Permissions

Most platforms require explicit permission before an app can capture the screen:

  • macOS. First capture triggers the system Screen Recording prompt. Approve Xenocept under System Settings → Privacy & Security → Screen Recording, then restart Xenocept.
  • Linux (Wayland). First capture triggers the desktop portal prompt asking which screen(s) Xenocept may capture. Choose your default monitor (or “All screens”).
  • Linux (X11). No explicit prompt — X11 allows any app on the display to grab screen pixels.
  • Windows. No prompt for local capture.

Some endpoint-security tools flag screen-capture APIs as suspicious; if captures consistently return black or fail, check your antivirus / EDR for quarantined Xenocept activity.

Latency

Pressing the hotkey to overlay-visible is typically tens of milliseconds plus the time xcap takes to grab the frame. There’s a brief ~300 ms input cooldown after the canvas finishes loading the captured image, during which keyboard / pointer input on the canvas is ignored to avoid spurious clicks.

DRM-Protected Content

Game capture and DRM-protected video (Netflix DRM-protected windows, etc.) may render as black on some platforms — that’s per-OS policy, not a Xenocept choice.

Annotation Tools

The overlay’s tools live in the radial menu. Hold the menu trigger to summon it, then release on a slice to pick a tool.

The menu is grouped into five slices:

SliceTools
DrawingPointer (Select), Pencil, Brush, Rectangle / Rectangle-filled, Circle / Circle-filled, Line, Arrow
Pen SizeInteractive size picker (1 px – 24 px)
Text & CommentsText, Note (comment-area), Bubble (comment-bubble)
UtilitiesArea (session-area), Blur, Emoji
HistoryUndo, Redo

Tool Details

ToolWhat it does
Pointer (Select)Click an object to select; drag to move; Delete/Backspace removes; PageUp/PageDown reorder z-index.
PencilFreehand stroke.
BrushThicker / softer freehand.
RectangleOutline-only rectangle.
Rectangle-filledSolid-fill rectangle.
CircleOutline-only ellipse.
Circle-filledSolid-fill ellipse.
LineStraight line between two points.
ArrowStraight arrow with a head.
TextPlace a free-floating text annotation. Click to start; type into the textarea; Esc commits, Enter inserts a newline.
Note (comment-area)The primary feedback tool — see Comment Workflow.
Bubble (comment-bubble)Speech-bubble text anchored to a single point rather than a rectangle.
Area (session-area)Crop region for the final submitted screenshot.png. Single rectangle per overlay; minimum 20×20 px.
BlurClick-drag a rectangle; Xenocept replaces those pixels (in the overlay) with a blurred copy of the underlying background. Stored as an ordinary canvas object with a pre-rendered blurCanvas — fully undoable like any other tool.
EmojiPick from recents or search, resize with the mouse wheel / pen size, then click to place the glyph.
Undo / RedoWalk the undo/redo stacks. Equivalent to Ctrl+Z / Ctrl+Shift+Z.

The Markup Layer

The overlay uses a four-canvas architecture:

  • canvas-background — the captured screenshot
  • canvas-drawing — committed annotations (the markup layer)
  • canvas-preview — the live in-progress draw
  • canvas-overlay — handles, selection highlights, hover affordances

The markup layer is not baked into the background image until Submit. Selecting and moving an object, or swapping the background, doesn’t lose your markup — it lives independently.

Submit Composites the Layers

When you Submit, the frontend composites the markup layer onto the background and uploads the result as screenshot.png. For each Note, it renders the focus area (with its annotations baked in) and uploads focus-{i}.png. The clean (background-only, no markup) version uploads as screenshot-clean.png if your build’s frontend opts to send it.

Blur is just an ordinary canvas object that happens to render as a blurred patch — fully undoable like any other tool. If you draw a blur and Submit, the blur is in the final PNG; if you draw a blur, undo it, then Submit, the blur is not in the final PNG.

Keyboard Shortcuts

Only a small set of keys are bound:

KeyAction
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
EscapeCommits an in-progress text editor; otherwise cascades through cancel/deselect/hide (see UX Philosophy)
Delete / BackspaceDelete the current selection
PageUp / PageDownMove selection up/down in z-order

Single-letter tool shortcuts are not bound. Use the radial menu to switch tools.

To remove an object, select it with the Pointer and press Delete.

Comment (Note) Workflow

The Note tool — comment-area in the radial menu — is the primary feedback mechanism. Each Note is a rectangular focus region paired with a written description anchored to one of its sides.

This page walks through exactly what happens, click by click.

Pick the Tool

Hold the radial menu trigger; release on Note (the slice in the “Text & Comments” group). The active tool is now Note.

Drag the Focus Area

Click and drag a rectangle around the region of the screen you want to talk about. Release the mouse — the rectangle is committed as the focus area.

The focus area is the region whose pixels (with any annotations you draw inside it later) get captured into focus-{i}.png on Submit.

The Side-Pick Phase

After release, the overlay enters a side-pick phase. Hover near any of the four sides of the focus rectangle — top, bottom, left, right — and you’ll see the side-pick affordance.

Click one of the sides. That’s where the description textarea will sit.

The side-pick is mandatory; the Note isn’t created until you commit a side.

Type the Description

A textarea appears on the chosen side. Type your description.

Key behavior:

  • Esc commits the text and closes the textarea
  • Enter inserts a newline
  • The Confirm button hovering near the textarea also commits; its tooltip is “Confirm (or press Esc)”

Yes — this is the opposite of what most editors do with Enter and Esc. The choice exists because comments often want multi-line descriptions (“the dropdown is cut off — see the second screenshot too — also the spacing on the right…”), so Enter is reserved for line breaks. Esc is the commit so the keyboard never gets a second meaning for the same key.

What Just Got Created

The Note is now a canvas object. It has:

  • A focus rectangle (the focus_x / focus_y / focus_width / focus_height bounds you dragged)
  • A side (the one you clicked)
  • A text (what you typed)

That’s the in-memory state. The PNG of the focus crop is not captured here — it’s captured at Submit time, when the frontend renders the focus area with its annotations and uploads it.

Editing After Creation

Switch to the Pointer tool (or hit the menu and switch). Click the Note’s focus rectangle to select it. From there:

  • Drag the rectangle to move it
  • Double-click the focus rectangle to re-open the textarea for editing
  • Delete / Backspace removes the Note

While editing the text, Esc commits / Confirm commits (same keys as creating).

Multiple Notes

There’s no limit. Make as many as you want; each one captures its own focus-{i}.png on Submit. Notes can overlap; they live independently.

What Gets Submitted

When you press Submit, the frontend:

  1. Renders the canvas background + markup + each Note’s focus crop separately.
  2. Uploads screenshot.png (the full annotated frame, optionally cropped to the session-area), screenshot-clean.png (background only, no markup), and one focus-{i}.png per Note.
  3. Posts session.json with the structured comments array — see Session JSON Schema.

Undo / Redo

Every operation in the overlay is undoable.

ShortcutAction
Ctrl/Cmd + ZUndo the last operation
Ctrl/Cmd + Shift + ZRedo

There are also Undo / Redo slices in the History group of the radial menu.

What Goes on the Stack

The undo stack records every change to the canvas’s object list:

  • Drawing operations (strokes, shapes, lines, arrows, text)
  • Note (comment-area) creation and edits
  • Bubble and free-text placement
  • Blur application
  • Emoji placement
  • Object deletion and z-order changes
  • Session-area (the crop region) resize/move
  • Background swap when you click a thumbnail in the screenshot history widget

If you can do it on the canvas, undo will reverse it.

How the Stack Stores State

Each entry on the stack is a deep-cloned snapshot of the canvas’s object list — the strokes, shapes, comments, and other items currently on the layer. Undoing pops a previous list off the stack and makes it the current scene; redoing reverses that.

The stack is in-memory only. No images are persisted, no disk cache, no eviction.

Practical consequence: undo/redo are bounded by your process’s memory, but in normal sessions the cost is small (object descriptions, not pixel buffers). There are no configurable size or count caps in the current build.

Blur Is Not Special

The blur tool is an ordinary canvas object that renders as a blurred patch. Undo treats it the same as any other object — there is no separate “unblurred pixel buffer” for blur, no Submit-time baking that differs from other tools, no “blur becomes permanent on Submit” mechanic.

The composite that gets written to screenshot.png on Submit includes whatever objects are currently in the canvas’s object list — including blur objects. If you draw a blur, undo it, then Submit, the blur is not in the submitted PNG. Same as any other tool.

Esc Preserves the Stack — But Briefly

The undo and redo stacks live in the overlay window’s memory. Hitting Esc to dismiss the overlay does not clear them — the window stays alive in the background, holding the stacks.

But the next hotkey press wipes them. A fresh capture resets the canvas, the undo stack, and the redo stack.

So:

  • Esc → state survives in memory, invisible.
  • Hotkey → fresh capture → state wiped.

If you wanted to keep your work, the right move is to Submit before the next hotkey press. Submit is what writes the state to disk.

Quitting Xenocept entirely also drops the in-memory undo history. The undo log is never persisted.

Screenshot History

The overlay includes a history widget of past captured screenshots that you can quickly swap in as the canvas background.

The History Widget

The widget shows up to 5 screenshots from your capture history, newest first.

Its source of truth is /api/v1/screenshots/list — the always-on screenshot photo-roll that’s populated every time the hotkey fires a capture. Sessions are separate; the screenshot history is “every capture ever,” whether or not you ended up Submitting them.

Visibility

The widget is hidden by default and only appears while the radial menu is open. It floats in a position on the overlay (top-right by default in current builds) and is draggable.

If you don’t see it, summon the radial menu — it’ll appear alongside.

Clicking a Thumbnail

Click any thumbnail to swap the canvas background to that screenshot. Your existing annotations stay on the canvas — only the background changes.

This means you can:

  • Realize the UI you wanted to annotate looked different earlier
  • Click a past screenshot from before the change
  • Keep your in-progress markup intact

(The reverse also works — click back to a more recent thumbnail.)

What Gets Submitted

The active background at Submit time is what gets baked into screenshot.png. Other thumbnails on the stack are unrelated — they’re past captures sitting in your /screenshots/ library independently.

If you’ve drawn annotations with screenshot A as the background, then swapped to screenshot B and Submitted, the result is screenshot B + your annotations.

The /screenshots/ Tree

Each capture lives at /screenshots/{filename}.png in the AeorDB store. The widget pulls the latest 5; you can browse the full list via:

  • GET /api/v1/screenshots/list
  • GET /api/v1/screenshots/{filename}
  • GET /api/v1/screenshots/{filename}/thumbnail

The Xenocept main window’s Screenshots view uses the same endpoints to show every capture, not just the most recent 5.

Rectangle Manipulation

Notes, bubbles, session-area, blur rectangles, and other rectangular objects share a common interaction model on the canvas. This page describes what’s actually wired up today.

Selecting

With the Pointer tool active, click an object to select it. Selected objects show their grab handles.

The Eight Grab Handles

Selected rectangles show eight handles:

●─────●─────●
│ │
● ●
│ │
●─────●─────●
  • Four corner handles — resize two edges at once. Cursor: nw-resize, ne-resize, sw-resize, se-resize.
  • Four edge-midpoint handles — resize a single edge. Cursor: n-resize, s-resize, e-resize, w-resize.

Click and drag a handle to resize.

The Move Icon

The move icon appears in the top-left area of a selected rectangle. Hover over it and the cursor becomes grab; while dragging, grabbing.

Click and drag the move icon to reposition the entire rectangle. Size stays the same.

Cursors Are the Affordance

Handles and the move icon become visible on hover/selection — they don’t clutter the canvas the rest of the time. The cursor change is what tells you a drag will resize-vs-move.

Minimum Sizes

There’s no universal minimum across all rectangles, but a couple of specific tools clamp:

  • Session-area (session-area) — clamps to a minimum of 20×20 px.
  • General annotation rectangles — clamp at 2 px during resize, effectively just preventing collapse to nothing.

Deleting

With a rectangle selected, Delete or Backspace removes it. The deletion is undoable like any other operation.

Reordering (Z-Order)

With a rectangle selected:

  • PageUp — move forward (toward the viewer) in z-order
  • PageDown — move backward

The change is undoable.

Notes (Comments) Specifically

Notes have an additional interaction beyond the generic rectangle model: double-clicking the focus rectangle re-opens the description textarea for editing. See Comment Workflow for the full Note lifecycle.

Consumers & Delivery

Xenocept’s principle here is: the user decides what happens with their sessions.

When you press Submit, Xenocept does two things:

  1. Persists the session locally in its AeorDB-backed store.
  2. Optionally dispatches the session to one or more configured destinations.

Local storage is the source of truth. Delivery is a side effect: if every destination fails, the session is still safe locally and can be re-dispatched later.

How Delivery Actually Works

Delivery happens through plugins. Every destination wraps a plugin (identified by a reverse-DNS pluginID) plus a pluginConfig blob the plugin understands. There are a handful of built-in plugins that ship with Xenocept, plus a system for installing or writing your own.

The flow:

  1. User configures one or more destinations through the Settings UI (or directly via the Destinations API).
  2. Optionally, the user enables auto-send on some subset of destinations.
  3. After Submit, the frontend’s plugin loader runs each destination’s plugin, passing the session.
  4. The plugin uses Xenocept’s low-level delivery primitives — POST /api/v1/deliver/http, POST /api/v1/deliver/file, POST /api/v1/deliver/command — to do the actual work (call an external service, write a file, run a command).

Built-In Plugins

Plugin IDWhat it does
org.aeor.xenocept.fileWrites the session’s screenshot (and optional metadata) to a configured filesystem path.
org.aeor.xenocept.emailEmails the session out via a configured SMTP setup.
org.aeor.xenocept.aeordbPushes the session into an external AeorDB instance.
org.aeor.xenocept.claude (separate repo)Delivers to Claude Code via the MCP channel server. See MCP.

Additional community plugins can be installed via the Plugins API (/api/v1/plugins/install, ).

Sub-Pages

HTTP

Xenocept exposes a low-level HTTP proxy for plugins that want to call external services:

POST /api/v1/deliver/http

This is not a “destination type” the user picks from a dropdown. It’s a primitive for plugin code — when a plugin wants to ship a session to a webhook, it formats the body itself and calls this endpoint.

Request / Response

The request body is application/json:

{
  "url": "https://example.com/intake",
  "method": "POST",
  "headers": {
    "Authorization": "Bearer...",
    "Content-Type": "application/json"
  },
  "body": "..." // verbatim string body
}

Supported methods: GET, POST, PUT, PATCH, DELETE.

The response:

{
  "status": 200,
  "body": "..."
}

The proxy returns the upstream’s status code and response body to the plugin. The plugin decides what counts as success.

What the Proxy Does (and Doesn’t Do)

The proxy is intentionally thin:

  • Sends one HTTP request
  • Returns the response
  • Does SSRF defense: rejects URLs that resolve to loopback, link-local, or unspecified ranges, or have a localhost-looking hostname
  • Returns 502 on transport failure

What it does not do:

  • No retries. One shot. If the plugin needs retries, it loops.
  • No multipart construction. Whatever string the plugin passes as body is sent verbatim. Plugins that need multipart build it themselves.
  • No ${VAR} env-var substitution in URL or headers. Plugins assemble the final strings before calling.
  • No HMAC signing of outgoing requests. If your downstream service requires a signature, the plugin computes it.
  • No streaming for huge payloads. The whole response is loaded into memory.

Why a Proxy?

Two reasons:

  1. SSRF safety. The frontend is a webview; if a plugin’s JavaScript could fetch arbitrary URLs from inside Xenocept, that would be an SSRF risk against any local services on the user’s machine. Forcing outbound HTTP through this proxy gives us one place to enforce a denylist.
  2. Auth wraps consistently. The deliver routes go through the same dev-unsafe-or-token middleware as the rest of the dangerous endpoints.

Plugin Usage Sketch

A destination plugin’s deliver typically looks like:

'use strict';

export async function deliver(session, attachments, config) {
  const body = JSON.stringify({
    session_id: session.id,
    text: session.comments.map((c) => c.text).join('\n'),
  });

  const r = await fetch('/api/v1/deliver/http', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: config.endpoint,
      method: 'POST',
      headers: { 'Authorization': `Bearer ${config.apiKey}` },
      body
    })
  });

  const result = await r.json;
  if (result.status >= 400)
    throw new Error(`destination returned ${result.status}: ${result.body}`);
}

See Plugin Delivery for the full plugin contract.

Model Context Protocol (MCP)

Xenocept ships a first-class MCP delivery mechanism for AI agents. The flagship target is Claude Code, but the same machinery works for any MCP-compatible agent.

There are two modes: channel mode (push) and polling mode (pull). Pick the one that matches your environment.

Channel Mode (Push)

In channel mode, the xenocept binary runs in a special MCP channel server mode as a subprocess of your AI agent. SSE events from the running Xenocept GUI flow through the channel and become inline notifications in your agent’s session.

Architecture

┌───────────────────────────────────────────────┐
│ Xenocept GUI (xenocept, port 9500) │
│ - SSE server: /api/v1/channels/events │
│ - HTTP send: /api/v1/channels/send │
│ - Channel registry: name → SSE connection │
└───────────────┬───────────────────────────────┘
                │ SSE (filtered by channel name)
                ▼
┌───────────────────────────────────────────────┐
│ xenocept --mcp <ChannelName> │
│ - SSE client (subscribes by name) │
│ - MCP stdio server (channel capability) │
└───────────────┬───────────────────────────────┘
                │ stdio MCP (JSON-RPC)
                ▼
┌───────────────────────────────────────────────┐
│ Claude Code session (user's terminal) │
└───────────────────────────────────────────────┘

Launching the Channel Server

The same xenocept binary handles both the GUI and the MCP channel server. To run it in channel mode, invoke it with --mcp:

xenocept --mcp MyChannelName

When invoked this way, Xenocept does not open a window or sit in the tray. It runs as a stdio MCP server, subscribes to the Xenocept GUI over SSE filtered by MyChannelName, and forwards events into the MCP session as inline notifications.

The channel name is any UTF-8 string you choose. It identifies this MCP session in the Xenocept GUI — the GUI’s “Send to Channel” UI lets you pick which channel a snapshot goes to.

Wiring It Into Claude Code

Add Xenocept as an MCP server in your Claude Code project’s .mcp.json:

{
  "mcpServers": {
    "xenocept": {
      "command": "/path/to/xenocept",
      "args": ["--mcp", "MyChannelName"]
    }
  }
}

Restart Claude Code. The next time you press Submit in the Xenocept GUI and choose “MyChannelName” as the destination, the snapshot arrives inline in your active Claude session.

Multiple Channels

You can run multiple --mcp instances at once, each with a different channel name. Each Claude Code project (or window, or session) gets its own channel. The Xenocept GUI’s “Send to Channel” dropdown lists every currently-active channel.

Other Options

FlagPurpose
--mcp <name>Channel mode, identified by <name>
--port <n>Target Xenocept GUI HTTP port (default 9500)

The channel name accepts any UTF-8 string that round-trips through percent-encoding (emoji and spaces are fine). Leading/trailing whitespace is trimmed. Empty names are rejected with a clear error.

Polling Mode (Pull)

If you don’t want to install a channel server — or your agent doesn’t support an external MCP server — you can use polling mode instead.

In polling mode, your agent pulls snapshots from the Xenocept HTTP API on a schedule (typically every few seconds, or whenever the user asks it to check). No subprocess, no SSE, no channel — just HTTP.

Endpoints

EndpointPurpose
GET /api/v1/sessions/listList recent submitted sessions
GET /api/v1/sessions/{id}/metaFetch a single session’s session.json
GET /api/v1/sessions/{id}/files/{*path}Fetch any image attachment by relative path (e.g. screenshot.png, focus-0.png)
GET /api/v1/sessions/search?...Trigram-fuzzy search across session content (comment text, bubble text, free text, OCR)
GET /api/v1/sessions/eventsServer-Sent Events stream of session events (created / dispatched)

The session JSON shape is documented in Session JSON Schema.

When Polling Is the Right Choice

  • You don’t want to install or run a separate --mcp subprocess.
  • Your agent already polls for state on a regular cadence.
  • You’re integrating Xenocept into a tool that doesn’t speak MCP at all.

When Channel Mode Is Better

  • You want zero-latency delivery (push, not pull).
  • The agent is already running long-lived and can host an MCP server.
  • You want the snapshot to appear inline in the agent’s session without the agent having to ask.

The Session Inside the MCP Session

When an MCP-delivered session lands, the agent sees the session JSON plus the image attachments. How it’s surfaced depends on the agent — Claude Code, for example, can render the focus images directly into the conversation with the comment text as commentary.

The structured nature of the session (Notes with focus rectangles and side-anchored text) is what makes this useful: the agent isn’t squinting at a flat screenshot and guessing what the user meant. The annotation is the structure.

File

The File destination ships sessions to a configured filesystem path. It’s a built-in plugin identified by:

pluginID: org.aeor.xenocept.file

You configure it from the Settings UI like any other destination (see Destinations). The plugin’s pluginConfig carries the target directory path.

How Plugins Do “File Delivery”

There’s a low-level primitive plugins can use to write files:

POST /api/v1/deliver/file

The built-in File destination plugin uses this. Custom plugins can use it too if they want to write files to disk as part of their delivery flow.

What the File Destination Writes

When auto-send (or an explicit “Send” action) dispatches to the File destination, the plugin reads the session from the AeorDB store and writes the relevant files into the configured path. Manual and automatic dispatches go through the same write path, so behavior is identical between them.

If your use case is “drop sessions in a watched directory,” the File destination is the right starting point. Set it up, point it at the directory, and watch what shows up.

Plugin Delivery

Every destination in Xenocept is backed by a plugin. The built-ins (File, Email, AeorDB, Claude/MCP — see Overview) are plugins that ship with Xenocept. If you want to ship sessions somewhere not covered by a built-in, write your own plugin.

This page describes the plugin model at the level of “what gets installed and what runs at dispatch time.” For the deeper plugin authoring guide — APIs, lifecycle, event subscriptions — see the Plugins section.

What “Plugin Delivery” Means

When a destination is dispatched, the frontend’s plugin loader runs the destination’s plugin and passes it the session. The plugin is a JavaScript ES module shipping with a package.json that includes a Xenocept-specific block.

A minimal sketch (the precise API surface lives in the frontend code; verify against static/plugins/ or wherever your build keeps them):

'use strict';

export async function deliver(session, attachments, config) {
  // session   — the parsed session.json
  // attachments — fetch URLs / Uint8Arrays for screenshot.png, focus-i.png, etc.
  // config    — the destination's pluginConfig from destinations.json

  // Use Xenocept's delivery primitives — these run server-side:
  //   POST /api/v1/deliver/http     (HTTP proxy with SSRF guard)
  //   POST /api/v1/deliver/file     (write to a filesystem path)
  //   POST /api/v1/deliver/command  (run a local command)
  //
  // Or do entirely in-page work (transform, upload via `fetch`, etc.).

  // Throw / reject on failure; return / resolve on success.
}

The exact entry-point name, the shape of attachments, and how config is delivered are frontend-side concerns. Treat the above as illustrative; the authoritative reference is the frontend plugin code in the running build.

Backend Hooks Plugins Rely On

Plugins use a handful of backend HTTP endpoints to do their work. None of these are plugin-specific — they’re general-purpose primitives:

EndpointPurpose
POST /api/v1/deliver/httpOutbound HTTP from a plugin (SSRF-guarded, see HTTP).
POST /api/v1/deliver/fileWrite a file to disk.
POST /api/v1/deliver/commandRun a local command with stdin / stdout / stderr captured.
GET /api/v1/sessions/{id}/metaFetch a session’s JSON.
GET /api/v1/sessions/{id}/files/{*path}Fetch a session’s image attachments.

(src/api.rs:305-309, 397-399)

Installation

Plugins can be installed via the Plugins API:

  • POST /api/v1/plugins/install — install a published plugin (npm package) (src/api.rs:388)
  • POST /api/v1/plugins/install-from-source — install from a local source directory (src/api.rs:389)
  • POST /api/v1/plugins/uninstall — remove an installed plugin (src/api.rs:390)

GET /api/v1/plugins/list (src/api.rs:314) enumerates installed plugins. GET /api/v1/plugins/search (:315) searches npm.

Plugin Signing

The backend includes a signature verifier (src/security/sig.rs). The package.json field it expects is pluginId (camelCase, top-level) when verifying — not the nested xenocept.plugin_id shape that some older docs described. If you’re publishing a plugin you want to be installable in a hardened deployment, that’s the field to set.

Versus the Built-Ins

If your need fits a built-in (write to a file, send an email, push to AeorDB, deliver to Claude via MCP), use the built-in. Plugins are for the cases the built-ins don’t cover.

If you’re tempted to write a plugin that just calls POST /api/v1/deliver/http once, consider whether the user can already do that by configuring the built-in HTTP-like destination (if any exists in the build) with pluginConfig.endpoint. The plugin path is the right one when you need real logic in between.

Session JSON Schema

The submitted session’s JSON document is what a plugin (and any downstream consumer) sees. This page documents the shape as the backend writes it today.

This is not a stable, versioned schema. Fields can be added in any release; downstream consumers should ignore unknown keys.

Top-Level Shape

{
  "id": "session-2026-05-15-18-30-00.123",
  "created_at": "2026-05-15T18:30:00.123Z",
  "screenshot": "screenshot.png",
  "comments": [... ],
  "bubbles": [... ],
  "texts": [... ],
  "emojis": [... ],
  "canvas_objects": [... ]
}
FieldTypeDescription
idstringGenerated as chrono::Utc::now.format("session-%Y-%m-%d-%H-%M-%S%.3f"). Lexicographically sortable by creation time.
created_atstringRFC 3339 / ISO 8601 UTC timestamp.
screenshotstringAlways "screenshot.png". The primary screenshot image is at /sessions/{id}/screenshot.png.
commentsarrayNotes (rectangular focus + side + description + crop image).
bubblesarraySpeech-bubble-style anchors (text targeted at a point).
textsarrayFree-floating text objects placed on the canvas.
emojisarrayEmoji glyphs placed on the canvas.
canvas_objectsarrayVerbatim canvas object array, used to reconstruct the scene for edit mode. Opaque to most consumers.

A Comment Entry

Each entry of comments:

{
  "index": 0,
  "text": "the dropdown is cut off on narrow viewports",
  "side": "top", // top | bottom | left | right
  "focus_x": 240,
  "focus_y": 180,
  "focus_width": 320,
  "focus_height": 180,
  "focus_image": "focus-0.png"
}

focus_image is a sibling-relative path; the file is at /sessions/{id}/focus-{index}.png inside the AeorDB store.

A Bubble Entry

Each entry of bubbles:

{
  "text": "this scrolls weirdly",
  "target_x": 540,
  "target_y": 220
}

A bubble is anchored to a point on the screenshot rather than to a rectangle.

A Text Entry

Each entry of texts:

{
  "text": "see ticket #4123",
  "x": 400,
  "y": 100
}

A free-floating text annotation placed on the canvas.

An Emoji Entry

Each entry of emojis:

{
  "emoji": "🧊",
  "x": 400,
  "y": 100,
  "font_size": 48
}

An emoji annotation placed on the canvas. font_size is the rendered glyph size in canvas pixels.

Attachments

A session’s image attachments live alongside session.json inside the AeorDB store:

/sessions/{id}/
  session.json
  screenshot.png # the submitted screenshot
  screenshot-clean.png # optional — background only, no markup
  focus-0.png # one per comment
  focus-1.png
 ...

Plugins reach these through HTTP:

GET /api/v1/sessions/{id}/files/{path}

For example, GET /api/v1/sessions/{id}/files/screenshot.png or GET /api/v1/sessions/{id}/files/focus-0.png.

Forward Compatibility

Don’t assume:

  • That this shape is stable. There is no version gate; fields may be added or renamed in any release.
  • That comments is ordered by anything other than insertion order during the session.
  • That the id parses as anything other than a string (it’s session--prefixed and includes literal hyphens — don’t try to parse it as a single timestamp without the prefix).

Do:

  • Ignore unknown keys.
  • Treat focus_image as a path you fetch from /api/v1/sessions/{id}/files/{...}, not as inline base64.
  • Use the id as an opaque identifier.

Why AeorDB

Xenocept is built on top of AeorDB — a content-addressed, single-file embedded database. AeorDB runs in-process inside the Xenocept binary; there is no separate server or client lib involved.

What Xenocept Needs From Storage

Xenocept’s storage workload has a specific shape:

  1. Many PNG images. Every comment captures one focus PNG (its annotated crop). Every Submit also writes a full-frame screenshot, and optionally a clean (background-only) copy. A long-running install accumulates many of these.
  2. High redundancy. Two captures of the same UI overlap on most pixels. Most of the bytes are identical.
  3. Searchable over text. Users want to fuzzy-search past sessions by comment text, bubble text, OCR’d UI text, AI-generated descriptions, etc.
  4. Reactive UI. When a session is submitted or dispatched, the Sessions list should update without polling.
  5. No external dependencies. Xenocept is a desktop app. Spinning up Postgres or Redis to power it would be absurd.

What AeorDB Provides

NeedAeorDB Feature
Many PNGsSingle-file, embedded, no setup.
High redundancyContent-addressed storage with BLAKE3 hashing — identical chunks are stored once. Captures with overlapping pixels dedupe automatically.
Searchable textTrigram fuzzy-search indexes over comment / bubble / text / OCR / description fields. See Indexes.
Reactive UIAn internal event bus and SSE machinery. Xenocept builds its own session-event broadcast on top — see Events & Reactivity.
No external depsOne .aeordb file. Compressed (zstd) on disk. Crash-recoverable via WAL.

AeorDB’s query operators are eq, gt, lt, between, in, contains, similar, fuzzy, phonetic, and match, combined via and / or / not. The authoritative list lives in the AeorDB querying docs.

The Net Effect

The same in-process .aeordb file powers:

  • The submitted session history (/sessions/)
  • The screenshot capture history (/screenshots/)
  • Settings, destinations, and auto-send config (/config/)
  • The session-content fuzzy-search index (/sessions/.aeordb-config/indexes.json)

One file. One library link. No deployment story.

Storage Layout

Xenocept’s AeorDB store organizes data into a deliberate path tree. Treat these paths as stable but private — consumers should use the Xenocept HTTP API, not the underlying store paths.

The On-Disk File

A single file:

xenocept.aeordb

The directory it lives in is resolved by Tauri’s app_data_dir at startup and depends on platform conventions (typically under ~/.local/share/, ~/Library/Application Support/, or %APPDATA%). The Settings UI surfaces the active path.

This file contains the entire on-disk Xenocept state: submitted sessions, captured screenshots, settings, destinations, and the index config. Undo/redo state is not in here — undo is in-memory only.

Path Tree

Inside the AeorDB file, paths look like:

/sessions/
  {session-id}/
    session.json # the structured session metadata
    screenshot.png # the submitted (possibly cropped) screenshot
    screenshot-clean.png # optional — background only, no markup
    focus-0.png # one PNG per Note
    focus-1.png
   ...
 .aeordb-config/
    indexes.json # session content index config (see Indexes)

/screenshots/
  {screenshot-id}.png # raw captures saved by the always-on screenshot
                             # export feature (independent of sessions)

/config/
  settings.json # user-facing settings (hotkey, autostart, etc.)
  destinations.json # configured destinations
  auto-send-destinations.json # which destinations auto-send + master enable

/processes/
  active.json # bookkeeping for active subprocess plugins

Sessions

/sessions/{session-id}/session.json is the authoritative artifact of a Submit. See Session for the shape and Session JSON Schema for the schema details.

The session-id is a timestamp string like session-2026-05-15-18-30-00.123. Sorting session IDs lexicographically gives chronological order.

A Submit operation roughly:

  1. Generates a new session-id from the current UTC timestamp.
  2. Decodes and stores screenshot.png (and optionally screenshot-clean.png).
  3. For each Note in the request, decodes and stores focus-{i}.png.
  4. Builds session.json and stores it.
  5. Broadcasts a created event on the internal session-events channel. The Sessions list UI is subscribed via /api/v1/sessions/events.
  6. Auto-send (if enabled) then triggers via the frontend’s plugin loader, which calls POST /api/v1/sessions/{id}/dispatch.

The store write itself produces an entries_created event on AeorDB’s internal event bus — but Xenocept does not currently re-broadcast those events to HTTP consumers. The session-event created broadcast at step 5 is the consumer-visible signal.

Content-Addressed Deduplication

AeorDB stores file content addressed by BLAKE3 hash; identical content chunks are stored once on disk. Two screenshots that share most pixels share most bytes. Two focus-{i}.png files derived from the same screenshot share their backgrounds at the chunk level.

In practice:

  • A long-running install with many similar captures inflates the database surprisingly little.
  • Backup is cp xenocept.aeordb /backup/path/.

The HTTP Surface

Use the HTTP API rather than reading the store directly:

EndpointPurpose
GET /api/v1/sessions/listList submitted sessions
GET /api/v1/sessions/search?q=...Fuzzy-search submitted sessions
GET /api/v1/sessions/{id}/metaFetch a session’s session.json
GET /api/v1/sessions/{id}/files/{*path}Fetch a session file (e.g. screenshot.png, focus-0.png)
GET /api/v1/screenshots/listList raw captured screenshots
GET /api/v1/screenshots/{filename}Fetch a raw capture
GET /api/v1/sessions/eventsSSE stream of session events (created / dispatched)

Do Not Manually Edit the File

Tooling that edits the .aeordb file outside Xenocept will:

  • Cause running readers (the Xenocept process, any SSE-subscribed consumer) to misbehave.
  • Invalidate every content-hash reference for any chunk you touch.

If you need to programmatically modify state, do it through the HTTP API. If the API doesn’t expose what you need, file an issue.

Indexes

Xenocept configures one AeorDB index group on first boot: a set of indexes over the /sessions/ tree. They power the Sessions list view’s fuzzy-search box. Outside of that group, there are no other Xenocept-managed indexes today.

The Real Index Config

The config lives at /sessions/.aeordb-config/indexes.json:

{
  "glob": "*/session.json",
  "indexes": [
    { "name": "id", "type": ["string", "trigram"] },
    { "name": "created_at", "type": "string" },
    { "name": "comment_text", "type": "trigram", "source": ["comments", "", "text"] },
    { "name": "bubble_text", "type": "trigram", "source": ["bubbles", "", "text"] },
    { "name": "text_text", "type": "trigram", "source": ["texts", "", "text"] },
    { "name": "ocr_text", "type": "trigram" },
    { "name": "alternative_description", "type": "trigram" }
  ]
}

glob: "*/session.json" tells AeorDB to apply the indexes to every session.json file under /sessions/.

Each "source": [field, "", subfield] entry is a fan-out path: one indexed entry per array element. For comment_text, that means every comment’s text is indexed individually rather than as a single concatenated blob.

Xenocept writes this config on startup and only re-writes it if the contents differ from the desired shape. AeorDB picks up changes and rebuilds the affected indexes in the background.

Notes on the Schema

  • id has both "string" and "trigram" index types — supports both exact lookup and trigram fuzzy-search on the timestamp string.
  • created_at is a "string" index, not a typed timestamp index. AeorDB supports range queries on string indexes lexicographically — and since created_at is ISO 8601 UTC, lexicographic order is also chronological order.
  • The OCR / AI enrichment fields (ocr_text, alternative_description) are populated asynchronously by plugins after Submit (e.g., a Gemini OCR plugin via POST /api/v1/sessions/{id}/ocr-text and /enrich). If you submit a session and search immediately, those fields may not be populated yet.

The Sessions search UI uses GET /api/v1/sessions/search?q=..., which delegates to AeorDB’s global_search across the /sessions/ tree.

AeorDB’s trigram index extracts every contiguous 3-character substring from indexed text. A query string is decomposed the same way; matches are scored by trigram overlap. Practical consequences:

  • Substring matches work ("drop" matches "dropdown").
  • Typo tolerance works to a degree ("dropdwn" matches "dropdown" with reduced score).
  • Very short queries (1-2 characters) won’t produce trigrams and so won’t match anything via the trigram path.

For the exact AeorDB query JSON shape (operators like eq, contains, similar, fuzzy, phonetic, match, plus the and/or/not combinators), see the AeorDB querying docs. Xenocept’s HTTP surface doesn’t expose raw AeorDB queries to callers — the search endpoint takes a single q string and runs it through global_search.

Events & Reactivity

Xenocept’s UI updates without polling. This page describes the actual event plumbing.

The Layers

There are two distinct event systems at play:

  1. AeorDB’s engine event bus (internal to AeorDB). Emits things like entries_created, entries_deleted, versions_created when the underlying store changes. This bus is fed by every write to the store.
  2. Xenocept’s session-event broadcast (Xenocept-specific). An internal channel emits created and dispatched events when a session is submitted or successfully dispatched.

The Xenocept frontend subscribes to layer 2 (/api/v1/sessions/events).

/api/v1/sessions/events

GET /api/v1/sessions/events

A Server-Sent Events stream. Each event is a JSON envelope with kind set to either created or dispatched and a sessionID field identifying the session.

The Sessions list UI subscribes at app startup and refreshes the list on every event.

/api/v1/channels/events

GET /api/v1/channels/events?name=<channel>

A separate SSE stream for the MCP channel-server bridge. xenocept --mcp <name> subprocesses subscribe to a single named channel; the frontend’s “Send to Channel” UI fires a POST /api/v1/channels/send that emits an event for the named channel only. The filtering is done in Xenocept itself via an mpsc registry keyed by channel name — it is not AeorDB’s path-prefix SSE filter, even though the surface effect is similar.

See MCP for the full channel-server flow.

If a consumer needs richer event metadata than kind + sessionID, the workaround is: subscribe to /api/v1/sessions/events, get notified, then call /api/v1/sessions/{id}/meta for the full session JSON.

Versioning, Strictly Speaking

AeorDB supports named snapshots and forks at the storage-engine level. Xenocept does not currently use those features for user-facing functionality. Submitted sessions are stored as a flat append-only collection under /sessions/{id}/ — there’s no “version this session” or “fork this session” operation surfaced to the UI.

The versions/snapshots AeorDB API is reachable from Xenocept’s backend code path, but no HTTP route exposes it to consumers. If Xenocept ever grows a “name a snapshot” or “fork a session” feature, this page is where it would land.

Settings

Xenocept’s user-facing configuration lives in /config/settings.json inside the Xenocept AeorDB store. The Settings UI inside the desktop app is the supported way to edit it.

Where It Lives

/config/settings.json is a path inside the AeorDB-backed store file (xenocept.aeordb). It is not a regular file you can cat on disk.

You read and write it through the HTTP API:

EndpointPurpose
GET /api/v1/settingsFetch the current settings JSON
POST /api/v1/settingsOverwrite the settings JSON

Keys That Code Actually Reads

There is no enforced schema. Settings JSON is treated as a free-form document; individual keys are looked up on demand. The keys the app currently honors:

KeyTypePurpose
hotkeystringGlobal hotkey binding (default Ctrl+Backquote). See Hotkeys.
auto_startboolWhether Xenocept launches at OS login.
screenshot_pathstringFilesystem directory where always-on screenshot export saves copies.
screenshot_templatestringFilename template for screenshot export.

The frontend may write additional UI-state keys here (e.g. last-used pen color or radial-menu opacity). Those are read by the frontend, not the backend, and aren’t part of any stable schema. Treat them as undocumented internals that can shift between releases.

Editing By Hand

If you have a reason to edit settings outside the Settings UI:

# Read
curl http://127.0.0.1:9500/api/v1/settings

# Write (overwrites the whole document)
curl -X POST http://127.0.0.1:9500/api/v1/settings \
  -H 'Content-Type: application/json' \
  -d '{"hotkey":"Ctrl+Shift+Backquote","auto_start":true}'

POST /api/v1/settings overwrites the entire document. If you only mean to change one key, first GET, merge in your change, then POST the merged object.

What’s Not Configurable

A few things are not currently exposed as settings:

  • HTTP bind address / port. Xenocept’s HTTP server is hardcoded to 127.0.0.1:9500. The --port flag on the xenocept --mcp <name> CLI is the target port to connect to, not a way to change Xenocept’s listening port.
  • Tool defaults / theme / motion. The frontend may persist some UI state in settings.json opaquely, but these aren’t part of a documented schema.
  • Hot-reload of settings. Changes apply when the relevant code path next reads the file — the hotkey re-registers immediately on change, while other keys are read at startup.

Backups

The AeorDB store file as a whole is your backup unit — copying the .aeordb file copies everything (settings, sessions, destinations, indexes). There is no dedicated settings backup or quarantine path.

Hotkeys

Xenocept registers one global hotkey: the binding that summons (or hides) the capture overlay. Tool selection and editing actions inside the overlay are reached through the radial menu and a small set of fixed keyboard shortcuts.

The Global Hotkey

Default: Ctrl+Backquote on every platform — the same string is used on Linux, macOS, and Windows.

The binding is stored in /config/settings.json under the hotkey key (see Settings). It is editable from the Settings UI; setting it there persists to settings.json and re-registers the global shortcut immediately.

The Hotkey’s Behavior

Pressing the hotkey toggles the overlay:

  • If the overlay is hidden: capture a fresh screenshot of the monitor under the cursor and show the overlay.
  • If the overlay is visible: hide it.

Note that this is a toggle, not a “capture again” affordance. The overlay does not stack screenshots within a single session.

Modifier Syntax

The hotkey value is a Tauri-style shortcut string.

Examples that parse correctly:

Ctrl+Backquote
Ctrl+Shift+Backquote
Alt+F12
Super+Space

The exact token grammar is whatever the Tauri global-shortcut plugin accepts. If you set something that fails to register (key already taken, plugin rejects the syntax), Xenocept logs a warning at startup and falls back to the previous binding.

Fixed Editor Shortcuts (Inside the Overlay)

Once the overlay is open, the canvas listens for these keys:

KeyAction
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
EscapeCommit the in-progress text editor (if any), otherwise dismiss the overlay.
Delete / BackspaceDelete the current selection.
PageUp / PageDownMove selection up/down in z-order.

The radial menu (held to summon, or its persistent variant) is how you switch tools. There are no alphabetic single-key shortcuts for tools.

Conflicts

If another app already owns your chosen binding, Tauri’s global_shortcut.register returns an error. Xenocept logs a warning to its diagnostics log and the hotkey is not active — you’ll need to pick a different combination via the Settings UI.

On Wayland, the desktop compositor decides which keys can be globally bound. Some compositors restrict bindings to a specific set; if registration keeps failing, try a function-key combination or an unusual modifier set.

Suspending the Hotkey

POST /api/v1/hotkey/suspend lets the frontend temporarily disable the hotkey. This is intended for in-app flows where the hotkey would interfere with what the user is doing (for example, when an inline text editor is open inside the overlay). The hotkey resumes when the suspend flag is cleared.

External tooling does not need to interact with this endpoint.

Destinations

A destination is a configured target that can receive a submitted session. Xenocept stores destinations in /config/destinations.json and exposes them through the HTTP API.

Shape

Each destination is a JSON record with the following shape:

{
  "id": "01HF...", // identifier, generated on create
  "name": "Send to Claude", // human-readable label
  "pluginID": "org.aeor.xenocept.claude",
  "pluginConfig": { /* arbitrary JSON the plugin understands */ },
  "enabled": true
}

Notes on the shape:

  • The JSON keys are pluginID and pluginConfig — camelCase, capital ID (those are serde rename targets, see ).
  • There is no type field and no top-level targets wrapper. The file holds a JSON array of destination records.
  • pluginConfig is whatever the named plugin understands. Xenocept itself doesn’t parse it; it hands the value through to the plugin at dispatch time.

HTTP Endpoints

Method + PathPurpose
GET /api/v1/destinationsList destinations
POST /api/v1/destinationsCreate a destination
PUT /api/v1/destinations/{id}Update a destination
DELETE /api/v1/destinations/{id}Delete a destination
POST /api/v1/destinations/{id}/sendDispatch a specific session to this destination

Auto-Send

A second config file, /config/auto-send-destinations.json, holds the user’s auto-send preference — which destinations a freshly-submitted session should automatically be sent to.

Shape:

{
  "destinationIDs": ["01HF...", "01HG..."],
  "enabled": true
}

The enabled flag is a master switch. When false, auto-dispatch is paused even if destinationIDs is non-empty — useful when the user wants to keep their destination selection but stop firing it for a while.

The dispatch logic itself lives in the frontend’s plugin loader and post-Submit dispatch call (/api/v1/sessions/{id}/dispatch, ). The frontend invokes each enabled destination’s plugin via the relevant /api/v1/deliver/* proxy.

Per-Destination Status

  • session.json carries a dispatched_at timestamp once at least one destination has been dispatched to.
  • Plugin-emitted toasts in the overlay UI surface per-destination success/failure during dispatch.

There is no persisted per-destination/per-session result table today.

Editing By Hand

# List
curl http://127.0.0.1:9500/api/v1/destinations

# Create
curl -X POST http://127.0.0.1:9500/api/v1/destinations \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "My destination",
        "pluginID": "com.example.my-plugin",
        "pluginConfig": { "endpoint": "https://example.com/intake" },
        "enabled": true
      }'

POST /api/v1/destinations returns the assigned id. Use it for subsequent PUT/DELETE/send calls.

Setting Up Email

The Email destination sends a submitted Xenocept session as an email message — with the annotated screenshot inline or attached, and the comment / bubble / note text rendered into the body. It’s a built-in destination plugin identified by pluginID: org.aeor.xenocept.email.

This section walks you through configuring that destination for the email providers most users have. The mechanics are the same across providers — Xenocept connects to the provider’s SMTP server, authenticates, and submits a message. What changes is how you authenticate.

The Big Picture

Every major mail provider has retired the use of a regular account password for SMTP. Today you need one of the following:

Auth styleUsed byHow it works
App Password (a 16-character one-off password generated in your account)Gmail, Outlook.com (consumer), iCloud, Yahoo, FastmailEnable 2FA on your account, generate an app password, use that as your SMTP password. Plain-text-looking but scoped and revocable.
OAuth 2.0Microsoft 365 (organizational), advanced GmailRegister an OAuth application in the provider’s developer console, grant it the right scopes, and the email plugin uses a bearer token. More work to set up but the right answer for hardened environments.
Bridge / Local RelayProton MailRun a small companion app on your machine that exposes a local SMTP endpoint. Xenocept talks to localhost; the bridge handles encryption and submission to the provider.
Local Relay (Postfix)Anyone wanting a “send via my Mac” workflowYour Mac’s built-in Postfix relays messages out to whatever real SMTP provider you choose.

The Email destination’s pluginConfig always asks for the same set of fields — host, port, security mode, username, password — and the only thing that varies is what value you put in each field.

Common Fields

When you add an Email destination in the Xenocept Settings UI, you’ll be asked for:

FieldWhat it is
SMTP HostThe provider’s outgoing mail server (e.g. smtp.gmail.com)
SMTP PortAlmost always 587 (STARTTLS) or 465 (implicit TLS)
SecuritySTARTTLS for port 587, SSL/TLS for port 465
UsernameYour full email address
PasswordThe app password generated in your account (or your Bridge password for Proton, or your OAuth token for the OAuth flow)
FromThe address that appears in the message’s From: header. Usually matches your username.
ToThe recipient — yourself, a teammate, a shared inbox, etc.

Pick Your Provider

ProviderPageNotes
Gmail / Google WorkspaceGmailApp Password (consumer) or OAuth2 (advanced)
Microsoft 365 / OutlookMicrosoftConsumer Outlook.com uses App Password; organizational M365 requires OAuth2 (basic auth is being retired in 2026)
iCloud / AppleiCloudApp-specific password required
Yahoo MailYahooApp password required
Proton MailProton MailRequires running Proton Mail Bridge locally
FastmailFastmailApp password required
Self-hosted on a MacmacOS Postfix RelayUse the Postfix that ships with macOS to relay messages through any of the above

A Note on App Passwords

Across every provider, the same general advice applies:

  • Always enable 2-Step Verification first. App passwords are usually only available once 2FA is on.
  • The app password is shown once. Copy it the moment it appears; you can’t re-display it.
  • Name each app password. “Xenocept” or “Xenocept Desktop” is fine. The label helps you revoke just that one later.
  • Revoke when you stop using it. If you uninstall Xenocept or change machines, head back to your account’s app-password page and delete the entry.

The pages that follow are concrete walkthroughs — open the one for your provider.

Gmail / Google Workspace

Use this guide for a personal @gmail.com address or a Google Workspace account at your own domain. There are two paths:

  • App Password (recommended for most users) — simpler; works for personal accounts and most Workspace accounts.
  • OAuth 2.0 via Google Cloud — more setup; appropriate when your Workspace administrator has disabled App Passwords or you need finer-grained scoping.

The App Password path is documented here in full. OAuth 2.0 is summarized at the bottom for advanced users.

1. Turn on 2-Step Verification

App Passwords only exist on accounts that have 2-Step Verification enabled.

  1. Sign in at myaccount.google.com.
  2. Open Security in the left navigation.
  3. Find 2-Step Verification and turn it on if it isn’t already. Follow Google’s prompts (phone number, authenticator app, security key — your choice).

2. Generate an App Password

  1. With 2FA on, go to myaccount.google.com/apppasswords.
  2. Type a name for the app password — Xenocept is fine. The label is just for your reference later.
  3. Click Create. A 16-character password appears in a popup, displayed in groups of four like abcd efgh ijkl mnop.
  4. Copy it now. The popup is the only time Google shows you this value. Spaces in the displayed value are decorative — the actual password is the 16 alphanumeric characters with no spaces.
  5. Click Done.

3. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew Destination → pick Email. Fill in:

FieldValue
SMTP Hostsmtp.gmail.com
SMTP Port587
SecuritySTARTTLS
UsernameYour full Gmail address (e.g. [email protected] or [email protected] for Workspace)
PasswordThe 16-character App Password from step 2 (no spaces)
FromSame as Username
ToWhere you want sessions delivered

Save the destination. Hit Submit on a Xenocept session and you should see the message arrive in the inbox you chose.

Note on port 465. Gmail also supports smtp.gmail.com:465 with implicit SSL/TLS. Either port works; 587 with STARTTLS is the more common modern choice.

Common Issues

  • “Username and Password not accepted.” Your account doesn’t have 2FA enabled, or you pasted your regular password instead of the App Password. Double-check both.
  • “Less secure app access” message. That setting is retired — App Passwords are the supported path now. If you see that phrase, your guide is out of date.
  • Workspace admin blocked App Passwords. Some Workspace orgs disable App Passwords entirely. Ask your admin, or fall through to Path B below.

Path B — OAuth 2.0 via Google Cloud (Advanced)

OAuth 2.0 with Google requires:

  1. A Google Cloud project. Create one at console.cloud.google.com.
  2. The Gmail API enabled. In your project, open APIs & Services → Library and enable Gmail API.
  3. An OAuth consent screen configured. Under APIs & Services → OAuth consent screen, set the user type, app name, support email, and add the https://mail.google.com/ scope (or the more restrictive https://www.googleapis.com/auth/gmail.send).
  4. OAuth credentials (Client ID + Client Secret). Under APIs & Services → Credentials, create an OAuth client ID of type Desktop app or Web application depending on how you intend to obtain refresh tokens.
  5. An initial refresh token. Run an OAuth flow once to obtain a refresh token tied to your sending account; the SMTP auth then uses that token via XOAUTH2.

In practical terms, OAuth 2.0 is several orders of magnitude more setup than the App Password path, and is only worth it if your environment forbids App Passwords. If you need to go down this road, the Google “Sign in with OAuth via IMAP and SMTP” docs are the authoritative reference.

Final Checks

  • The recipient inbox is checked — sessions arrive there, not in the Sent folder of your Gmail account (the From and To can be the same address, though, if you just want to email yourself).
  • Heavy attachments stay under Gmail’s 25 MB per-message limit. Xenocept sessions with many comments may approach that — if you regularly exceed it, point the Email destination at a non-Gmail recipient that accepts larger messages.

Microsoft 365 / Outlook

Microsoft accounts come in two flavors:

  • Personal Outlook.com / Hotmail / Live — the consumer accounts you sign up for at outlook.com.
  • Microsoft 365 (Exchange Online) — organizational accounts tied to a tenant (@yourcompany.com), managed in the Microsoft 365 admin center.

The SMTP setup differs significantly between the two. Read the section that matches your account type.

Important: Basic Auth Deprecation Timeline

Microsoft has announced the permanent removal of Basic Authentication for SMTP submission in Exchange Online (organizational Microsoft 365) through 2026. Beginning March 1, 2026, Microsoft begins rejecting Basic Auth SMTP submissions; by April 30, 2026, rejection reaches 100%. App Passwords for organizational accounts also rely on Basic Auth and stop working at the same time.

What this means:

  • Organizational M365 accounts must move to OAuth 2.0 for SMTP if they want to keep sending via SMTP at all.
  • Consumer Outlook.com accounts are not affected by the Exchange Online deprecation; the App Password path continues to work for personal accounts.

If you’re on a personal account, follow Path A. If you’re on a Microsoft 365 organizational account, you’ll need Path B.

Path A — Personal Outlook.com / Hotmail / Live

1. Turn on Two-Step Verification

  1. Sign in at account.microsoft.com.
  2. Open SecurityTwo-step verification and turn it on. Follow the prompts.

2. Generate an App Password

  1. With 2FA enabled, in the same Security area, open App passwords (under “Advanced security options” or “Manage how I sign in”).
  2. Click Create a new app password.
  3. Copy the generated value immediately.

3. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew DestinationEmail. Fill in:

FieldValue
SMTP Hostsmtp-mail.outlook.com
SMTP Port587
SecuritySTARTTLS
UsernameYour full Outlook.com / Hotmail / Live address
PasswordThe App Password from step 2
FromSame as Username
ToWhere you want sessions delivered

Save the destination and submit a test session.

Path B — Microsoft 365 (Exchange Online, organizational)

For organizational Microsoft 365, SMTP authentication requires OAuth 2.0 via an Azure-registered application. This is non-trivial; budget an hour for first-time setup.

What You’ll Need

  • Tenant Admin access (or admin help) to consent to API permissions.
  • An Azure App Registration with the right permissions.
  • A Client ID, Client Secret, and Tenant ID to give to Xenocept’s Email destination — assuming Xenocept’s Email plugin supports OAuth 2.0. If it doesn’t, consider either (a) running a local relay with macOS Postfix configured for OAuth, or (b) using Microsoft’s High Volume Email or Azure Communication Services Email instead of standard SMTP.

Steps

  1. Register an app in Microsoft Entra.

    • Sign in at portal.azure.com.
    • Navigate to Microsoft Entra IDApp registrationsNew registration.
    • Name it “Xenocept Email” (or similar).
    • Supported account types: Accounts in this organizational directory only (single tenant) is fine for internal use.
    • Click Register. Note the Application (client) ID and Directory (tenant) ID that appear.
  2. Create a client secret.

    • In your new app’s Certificates & secrets blade, click New client secret, set an expiry, and copy the secret value immediately — Azure only shows it once.
  3. Add API permissions.

    • In API permissions, click Add a permissionAPIs my organization uses → search Office 365 Exchange Online.
    • Choose Application permissions (for client-credentials flow) and select SMTP.SendAsApp.
    • Back on the API permissions list, click Grant admin consent (a tenant admin must do this).
  4. Service-principal mailbox permission (Exchange admin step).

  5. Token endpoint and scope.

    • Token endpoint: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
    • Scope: https://outlook.office365.com/.default
    • SMTP server: smtp.office365.com:587, STARTTLS, XOAUTH2 mechanism.
  6. Configure Xenocept.

    • Open Settings → Destinations → New Email destination.
    • Enter SMTP host smtp.office365.com, port 587, security STARTTLS.
    • Authentication: provide the client ID, client secret, tenant ID, and target mailbox address — if the Email plugin’s UI exposes OAuth fields. If it only takes a plain SMTP password, this path will not work and you’ll need a local relay or one of Microsoft’s alternative APIs.

Common Issues

  • “5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled.” Your tenant has SMTP AUTH disabled tenant-wide. Re-enable it in the Exchange admin center, or on a per-mailbox basis with Set-CASMailbox.
  • App Passwords disappear after 2026. If you previously used an App Password against Office 365 SMTP, that path is being removed; OAuth 2.0 is the replacement.
  • Service principal not provisioned. Step 4 is the one most people skip — without it, OAuth permissions look right in Azure but the SMTP server rejects the token.

When in Doubt

If your needs are “I want to email myself / a teammate occasionally” and your organization permits it, the easiest practical path on M365 is to set up a personal Outlook.com account or a free Gmail account dedicated to Xenocept and use Path A from that account or Gmail instead. The OAuth path is real but heavy for casual use.

iCloud / Apple Mail

Apple requires an app-specific password for any third-party app that needs to send mail through iCloud. Your regular Apple ID password will not work, even if you don’t have 2FA on consciously — Apple effectively requires 2FA across the board now.

1. Make Sure Two-Factor Authentication Is On

iCloud doesn’t allow App-Specific Passwords unless Two-Factor Authentication is on for your Apple Account.

If you haven’t already, on any signed-in Apple device:

  • iOS / iPadOS: Settings → tap your name → Sign-In & SecurityTwo-Factor Authentication → turn it on.
  • macOS: System Settings → tap your name → Sign-In & SecurityTwo-Factor Authentication → turn it on.

Don’t disable 2FA after you’ve generated App-Specific Passwords. Turning 2FA off revokes every App-Specific Password you’ve ever created. Existing SMTP connections will start failing.

2. Generate an App-Specific Password

  1. Sign in at account.apple.com.
  2. In the left sidebar (or Sign-In and Security section), select App-Specific Passwords.
  3. Click Generate an app-specific password (or the “+” icon).
  4. Enter a label such as Xenocept or Xenocept Desktop. Apple shows this label in the future so you can revoke just this password without affecting other apps.
  5. Click Create. Apple shows the generated password — a 16-character value with hyphens like abcd-efgh-ijkl-mnop.
  6. Copy it immediately. Apple won’t show it again.

3. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew DestinationEmail. Fill in:

FieldValue
SMTP Hostsmtp.mail.me.com
SMTP Port587
SecuritySTARTTLS
UsernameYour full iCloud Mail address (e.g. [email protected]). Don’t strip the domain — you alone won’t authenticate.
PasswordThe App-Specific Password from step 2. Include the hyphens; Apple’s prompt shows it that way.
FromSame as Username
ToWhere you want sessions delivered

Save the destination and submit a test session.

Custom domains via iCloud Mail. If you use iCloud’s custom-domain feature, the Username is still your full iCloud-managed address. The From can be your custom-domain address if you’ve verified the domain in iCloud — but authentication is always against your iCloud login.

Revoking Access

If you ever want to disconnect Xenocept from your iCloud account:

  1. Return to account.apple.comApp-Specific Passwords.
  2. Find the entry labeled Xenocept (or whatever you named it).
  3. Click the Revoke or trash icon next to that entry.

That single password becomes invalid immediately; your other app-specific passwords keep working.

Common Issues

  • “Authentication failed.” You used your regular Apple ID password instead of the app-specific password. Generate one and use it.
  • “User has no permission to send mail through this server.” Some new iCloud accounts have a brief warm-up period during which SMTP sending is throttled or blocked. Wait a few hours and try again. If it persists, contact Apple Support.
  • Two-factor was turned off. Re-enable 2FA and generate a fresh app-specific password.

Yahoo Mail

Yahoo Mail requires an app password for any third-party SMTP client. Your regular Yahoo password will not authenticate over SMTP.

1. Enable Two-Step Verification

  1. Sign in at login.yahoo.com.
  2. Click your profile icon (top right) → Account info.
  3. Open the Account Security tab.
  4. Find Two-step verification and turn it on. Follow the prompts (phone number or authenticator app).

2. Generate an App Password

  1. In the same Account Security area, look for Generate and manage app passwords (sometimes labeled Manage app passwords).
  2. Click Generate app password (or the “+” icon).
  3. Pick Other app and enter a name — Xenocept is fine.
  4. Click Generate password.
  5. Yahoo shows a 16-character password. Copy it now — you cannot view it again.

3. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew DestinationEmail. Fill in:

FieldValue
SMTP Hostsmtp.mail.yahoo.com
SMTP Port587 (STARTTLS, recommended) or 465 (implicit SSL/TLS)
SecuritySTARTTLS if port 587; SSL/TLS if port 465
UsernameYour full Yahoo address (e.g. [email protected])
PasswordThe 16-character App Password from step 2
FromSame as Username
ToWhere you want sessions delivered

Save the destination and submit a test session.

Common Issues

  • “Invalid username or password.” You used your regular Yahoo password. Yahoo requires the App Password specifically; the regular password is rejected for SMTP.
  • 2-step verification was off. Yahoo only exposes the Generate app password option after 2-step is turned on. Verify under Account Security.
  • Throttling on new accounts. Brand-new Yahoo accounts are sometimes rate-limited on outbound SMTP for a brief period. If sending fails on a fresh account, wait a few hours.

Revoking Access

To disconnect Xenocept from Yahoo:

  1. Return to Account SecurityManage app passwords.
  2. Find the Xenocept entry and click Remove or the trash icon next to it.

The password becomes invalid immediately.

Proton Mail

Proton Mail is end-to-end encrypted and does not expose a public SMTP server in the usual way. Instead, you run a small companion app called Proton Mail Bridge on your computer. The Bridge exposes a local SMTP endpoint on 127.0.0.1; you point Xenocept at that local endpoint, and the Bridge handles encryption and submission to Proton’s servers.

This means: the Bridge must be running on the same machine as Xenocept any time you want the Email destination to work.

1. Confirm Your Account Is Eligible

Proton Mail Bridge is included with Mail Plus, Proton Unlimited, Visionary, and Mail Essentials/Professional/Enterprise plans for Proton Business. Free Proton Mail accounts cannot use the Bridge.

If you’re on a free Proton account, you’ll need to either upgrade or pick a different email provider for the Xenocept Email destination.

2. Install Proton Mail Bridge

  1. Visit proton.me/mail/bridge and download the Bridge for your operating system (macOS, Windows, Linux).
  2. Install it the usual way for your OS.
  3. Launch Bridge and sign in with your Proton Mail credentials. If you have 2FA on your Proton account, you’ll be prompted for the second factor here as well.

The Bridge runs in the background and presents a small status icon in your menu bar / system tray. It needs to stay running for the SMTP endpoint to be reachable.

3. Find Your Local SMTP Credentials

Bridge generates a new, separate password for your local SMTP endpoint. This is not your Proton Mail account password — never use your account password with third-party clients.

  1. Open the Bridge interface (click the status icon → Open Proton Mail Bridge).
  2. Click on your account in the Bridge.
  3. Find the Mailbox configuration or SMTP section. Bridge displays:
    • The SMTP server: 127.0.0.1
    • The SMTP port (typically 1025, but Bridge picks an available port at install time — check what yours says)
    • The Username (your Proton email address)
    • A generated SMTP password (a long random string)
  4. Copy the SMTP password from the Bridge UI. You can re-display it any time from Bridge if you forget it.

4. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew DestinationEmail. Fill in:

FieldValue
SMTP Host127.0.0.1
SMTP PortWhatever Bridge displays (often 1025)
SecuritySTARTTLS (Bridge requires it locally — the connection is to localhost, but Bridge enforces TLS)
UsernameYour Proton email address
PasswordThe generated Bridge SMTP password from step 3
FromYour Proton email address
ToWhere you want sessions delivered

Save the destination and submit a test session. Bridge handles encryption and transit to Proton’s servers transparently.

Common Issues

  • “Connection refused.” Bridge isn’t running, or it’s running but the port differs from what you typed. Open Bridge and confirm the port number.
  • “Authentication failed.” You used your Proton account password instead of the Bridge SMTP password. They’re different by design. Copy the Bridge-generated value.
  • Port already in use. If another app on your machine claims the same port (some media-streaming apps grab 1025), Bridge lets you change to a different port from its settings. Then update Xenocept to match.
  • Certificate warnings. Bridge uses a self-signed certificate for the local TLS connection. Xenocept’s Email plugin should accept the Bridge’s certificate automatically; if it doesn’t, see the Proton Bridge documentation for adding the Bridge CA to your system’s trust store.

When Bridge Isn’t an Option

If you can’t run Bridge (free Proton account, restricted environment), you have a couple of workarounds:

  • Use a non-Proton email account just for Xenocept. The other guides in this section cover Gmail, iCloud, Yahoo, Fastmail, and Outlook.com — any of those work as a Xenocept Email destination.
  • Add a forwarding rule in Proton that drops Xenocept-tagged messages into a Proton inbox, but send them from a different provider.

Fastmail

Fastmail uses app passwords for every SMTP connection. Your regular Fastmail login password will not authenticate over SMTP — that’s true even if you don’t have 2-step verification on, because Fastmail requires per-app credentials for all IMAP / POP / SMTP / CardDAV / CalDAV access.

1. Generate an App Password

  1. Sign in at fastmail.com.
  2. Open SettingsPrivacy & Security.
  3. Find the Connected apps & API tokens section.
  4. Click Manage app passwords and accessNew app password.
  5. Give it a name — Xenocept works. The label appears in the list later so you can revoke just this one.
  6. Access: the default setting Mail, Contacts & Calendars is fine. (Or pick Mail alone if you’d rather scope it.)
  7. Click Generate password.
  8. Fastmail shows the generated password — a long random string. Copy it now. You can re-display it from the same screen later if needed.

2. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew DestinationEmail. Fill in:

FieldValue
SMTP Hostsmtp.fastmail.com
SMTP Port465 (Fastmail’s recommended port — implicit TLS)
SecuritySSL/TLS
UsernameYour full Fastmail address (e.g. [email protected], or your custom domain if you use one)
PasswordThe app password from step 1
FromSame as Username
ToWhere you want sessions delivered

Save the destination and submit a test session.

Port 587 also works. Fastmail accepts STARTTLS on port 587 if 465 is blocked for you, but 465 with implicit TLS is their stated preference.

Custom Domains

If you’ve added a custom domain to Fastmail, the Username for SMTP is still the Fastmail-managed primary address — but the From address can be any verified alias on your custom domain. Xenocept’s Email destination accepts independent From and Username values, so put your custom-domain address in From and your Fastmail login address in Username.

Revoking Access

  1. Return to SettingsPrivacy & SecurityConnected apps & API tokensManage app passwords and access.
  2. Find the Xenocept entry.
  3. Click Revoke or the trash icon.

The app password becomes invalid immediately.

Common Issues

  • “Authentication failed.” You used your Fastmail login password instead of the app password. Fastmail rejects the login password for SMTP — generate an app password.
  • “Connection refused” on port 465. Some networks block port 465. Try port 587 with STARTTLS.
  • Recipient policy. Fastmail enforces sending limits to protect against abuse. Heavy automated traffic from a new account may get throttled briefly. For typical Xenocept use (sessions on demand, not bulk mail), you’ll never hit the limit.

macOS Postfix Relay

macOS ships with Postfix, a battle-tested mail transfer agent, pre-installed but disabled by default. You can configure Postfix as a smart-host relay: Xenocept (and any other app on your Mac) hands a message to Postfix on localhost, Postfix authenticates against your real email provider, and delivers it.

This is useful when:

  • You want a single SMTP endpoint to point all apps at, regardless of which real provider relays the mail.
  • You want to keep your email-provider app password out of every application’s config (Postfix is the only thing that knows it).
  • Your real provider is being awkward about direct SMTP from an app and you’d rather authenticate from a tool like Postfix that’s been around forever.

Heads Up

You’ll be editing system config files in /etc/postfix/ and using sudo. You don’t strictly need to be a sysadmin to do this, but if sudo, vi, and editing config files aren’t comfortable territory, the per-provider guides in this section are the easier path.

1. Pick a Relay Provider

Postfix doesn’t send mail directly to recipients — it hands off to an upstream SMTP server (a “smart host”). Any of the providers in this section works. The most common choices:

ProviderHost & portSee
Gmailsmtp.gmail.com:587Gmail for App Password setup
iCloudsmtp.mail.me.com:587iCloud for App-Specific Password setup
Fastmailsmtp.fastmail.com:465Fastmail
Outlook.comsmtp-mail.outlook.com:587Microsoft

Get an app password from your provider following its guide. You’ll plug that password into Postfix in step 3.

2. Edit /etc/postfix/main.cf

Open a Terminal and edit the main Postfix config:

sudo vi /etc/postfix/main.cf

Add (or update) these lines at the bottom — adjust the relay host/port for your provider:

# Use your provider as the smart host.
relayhost = [smtp.gmail.com]:587

# Enable SASL auth and TLS for the outbound relay.
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/cert.pem

The square brackets around the hostname tell Postfix not to perform MX lookups — important when relaying through a smart host.

iCloud / Fastmail port 465. If your provider uses implicit-TLS port 465 instead of STARTTLS port 587, set smtp_tls_wrappermode = yes alongside the other TLS lines.

3. Add the Relay Credentials

Create the password map:

sudo vi /etc/postfix/sasl_passwd

Add one line in this format — replace the values with yours:

[smtp.gmail.com]:587 [email protected]:YOUR-APP-PASSWORD

(Use your provider’s host, port, username, and the app password you generated. Not your regular account password.)

Save and exit. Then build the hash database Postfix uses for lookups:

sudo postmap /etc/postfix/sasl_passwd

You should see a new /etc/postfix/sasl_passwd.db appear.

For safety, tighten the permissions so the password file is only readable by root:

sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db

4. (Optional) Rewrite the From Address

Many providers will reject mail whose From address doesn’t match the authenticated user. If your local Mac account is wyatt, mail sent from the Mac defaults to [email protected] — which Gmail/iCloud/etc. will refuse to relay.

Map the local user to your real email address. Edit a generic table:

sudo vi /etc/postfix/generic

Add a line like:

[email protected] [email protected]

(Replace wyatt and your-mac-hostname.local with your real local account / hostname, and the right-hand side with your relay username.)

Build the hash and tell Postfix to use it:

sudo postmap /etc/postfix/generic

Then, back in /etc/postfix/main.cf, add:

smtp_generic_maps = hash:/etc/postfix/generic

5. Start / Reload Postfix

Start the Postfix service:

sudo postfix start

(If it’s already running from a previous setup, use sudo postfix reload to pick up the new config.)

To check Postfix is listening:

sudo lsof -iTCP -sTCP:LISTEN -P | grep master

You should see Postfix’s master daemon listening on port 25 (the local SMTP port).

6. Test From the Command Line

Before pointing Xenocept at the local relay, confirm end-to-end:

echo "Test message body" | mail -s "Postfix test" [email protected]

If everything is wired up, the message arrives in your inbox. If it doesn’t, check Postfix’s queue and logs:

mailq
sudo tail -F /var/log/mail.log

The log makes it pretty clear when authentication fails or TLS is misconfigured.

7. Configure the Email Destination in Xenocept

Open the Xenocept Settings UI → DestinationsNew DestinationEmail. Fill in:

FieldValue
SMTP Host127.0.0.1
SMTP Port25
SecurityNone (or STARTTLS if you’ve configured Postfix to require it locally)
Usernameleave blank (Postfix accepts local mail without auth on 127.0.0.1 by default)
Passwordleave blank
FromThe email address you mapped in step 4 (so the provider accepts the From header)
ToWhere you want sessions delivered

Save the destination and submit a test session. Xenocept hands the message to Postfix on localhost; Postfix relays it through your provider; your provider delivers it.

When This Is Worth It

Skip Postfix unless one of these applies:

  • You have multiple apps on your Mac that all need to send mail and you’d rather configure provider credentials in one place.
  • You want sending centralized so you can change providers without touching every app.
  • You’re already comfortable in /etc/postfix/ and want the flexibility.

For the single-app case (“just get Xenocept to email me”), it’s faster and simpler to configure the Email destination directly against your provider. The per-provider guides cover that path.

Plugin System Overview

Xenocept’s frontend loads plugins as JavaScript ES modules. A plugin can register a destination, hook into session events, or add UI affordances. The bundled destinations (File, Email, AeorDB, Claude/MCP) are themselves plugins.

The plugin API is largely a frontend concern. The authoritative contract — entry-point names, the API object passed to setup(), the event catalog — lives in the bundled plugins in static/plugins/ and the plugin loader code in the running build.

What the Backend Knows About Plugins

EndpointPurpose
GET /api/v1/plugins/listEnumerate installed plugins.
GET /api/v1/plugins/searchSearch npm for installable plugins.
POST /api/v1/plugins/installInstall a plugin from npm.
POST /api/v1/plugins/install-from-sourceInstall a plugin from a local source directory.
POST /api/v1/plugins/uninstallRemove an installed plugin.
GET /api/v1/plugins/{scope}/{plugin_id}/{*file_path}Read a plugin file (read-only HTTP exposure of the plugin’s static assets).
PUT /api/v1/plugins/{scope}/{plugin_id}/{*file_path}Write a plugin file.
GET / PUT /api/v1/plugins/{scope}/{plugin_id}/configRead or update plugin-scoped config.

The backend identifies plugins by a pluginID (camelCase, capital ID) — for example, org.aeor.xenocept.file. The signature verifier reads a top-level pluginId field from the plugin’s package.json when verifying signed builds.

Where Plugins Live

Built-in plugins ship with Xenocept. Installed plugins live in a directory the backend manages (read it via /api/v1/plugins/list to see paths on your specific install). The plugin entry point is loaded by the frontend at app startup or after install.

Plugin Authoring

For the actual write-a-plugin walkthrough, see:

  • The bundled built-ins in static/plugins/ of the running build — they are the canonical examples.
  • The Claude destination plugin’s repo (xenocept-plugin-aeor-claude/) for a standalone-plugin example.
  • The frontend plugin loader code (search static/ for plugin_id / pluginID / deliver) for the exact contract.

Writing a Plugin

The precise plugin authoring API is a frontend concern; the bundled plugins in static/plugins/ of the running build are the authoritative reference for entry-point shape, lifecycle hooks, and the API object surfaced to plugins.

The Minimum You Need

A plugin is a JavaScript ES module with a package.json. The package.json carries identity (plugin id, version) and references the entry-point module. The entry-point module exports a deliver function (for destination plugins) and may also export lifecycle / event hooks if the frontend plugin host supports them.

The frontend plugin host is the source of truth for:

  • The exact name and shape of the entry-point export(s)
  • What’s passed to the deliver function
  • Which lifecycle hooks (if any) are called and in what order
  • The shape of any API object surfaced to the plugin
  • Configuration schemas / Settings UI integration

To author a plugin today, the practical workflow is:

  1. Copy one of the built-in plugins as a starting point (read what it exports, mimic the pattern).
  2. Use the delivery primitives (/api/v1/deliver/http, /api/v1/deliver/file, /api/v1/deliver/command) to do the actual work.
  3. Install from source via POST /api/v1/plugins/install-from-source for fast iteration.

Distribution

There is no centralized plugin registry yet. You can:

  • Publish to npm and install via POST /api/v1/plugins/install.
  • Distribute a tarball / git URL for install-from-source.

If your plugin is going to be installed in hardened deployments, signing it (with the field the backend’s verifier reads — top-level pluginId in package.json) lets it pass the signature check.

Events

The Real Event Stream: /api/v1/sessions/events

Xenocept exposes one Server-Sent Events stream:

GET /api/v1/sessions/events

(src/api.rs:306, 1199-1228)

The payload is a JSON envelope with kind set to either created or dispatched. The Sessions list UI subscribes to this stream so the list refreshes the moment a session is submitted or successfully dispatched.

Plugins running in the frontend can also subscribe to this stream if they want to react to session lifecycle. The exact JavaScript API for “as a plugin, observe submit events” is frontend code — read the frontend plugin host for the canonical pattern.

The /api/v1/channels/events Stream

A second SSE stream:

GET /api/v1/channels/events

(src/api.rs:318)

This is the channel-server bridge for MCP — see MCP. It’s filtered server-side by channel name (src/channels.rs). It’s intended for xenocept --mcp <name> subprocesses, not for general plugin consumption.

AeorDB’s Native Events

AeorDB itself emits engine-level events (entries_created, entries_deleted, versions_created) on its internal event bus. Xenocept does not currently expose AeorDB’s event bus to plugins or external consumers. The Xenocept-level SSE streams above are the supported surface.

Frontend Event Bus (Maybe)

The frontend may have its own internal event bus that plugins can subscribe to (for in-page events like “the radial menu opened” or “a comment was committed”). If so, the contract for it lives in the frontend plugin host. This page won’t enumerate event names that aren’t visible from the Rust source, because there’s no way for this page to keep that list honest as the frontend evolves.

If you need to react to a specific UI event from a plugin, check the bundled plugins for examples or read static/components/ directly.

Common Issues

The grab-bag of “why isn’t this working?” questions, with the answers.

The hotkey doesn’t summon the overlay

Most common cause: another app has registered the same combination first.

Xenocept logs a warning at startup if it can’t register the global hotkey. Check the logs (Xenocept main window → Settings → Diagnostics → View Log) for messages like:

WARN: failed to register global hotkey Ctrl+Backquote (already in use)

Pick a different binding in Hotkeys. The new value is applied immediately — no restart needed.

Other causes:

  • On macOS, Screen Recording permission hasn’t been granted yet (see Platform Notes).
  • On Linux/Wayland, the desktop portal denied the binding — try a less-common combination.
  • The Xenocept process isn’t running. Check the tray.

The overlay appears but the screen behind it isn’t frozen

This usually means the capture pipeline failed but the overlay rendered anyway. Symptoms: the overlay shows a transparent or stale image, and you can see the live screen through it.

  • On macOS: confirm Screen Recording permission is enabled and that you’ve restarted Xenocept since granting it.
  • On Linux/Wayland: confirm the desktop portal selection chose “All screens” (not “None” or a single window) the first time you pressed the hotkey.
  • On Linux/X11: rare; usually means another XShm consumer is holding the buffer. Try restarting Xenocept.

Annotations don’t show up

If you’re drawing and nothing appears:

  1. Confirm you’re inside an active comment region (click-drag to make one).
  2. Check the active tool’s color isn’t matching the background — pure white strokes on a white background are invisible.
  3. Look at the diagnostics panel for any frontend errors. A plugin error can in rare cases break the rendering pipeline.

Submit hangs or never completes

Submit fans out to every configured delivery target in parallel. The overall Submit operation waits for all targets to either succeed or exhaust retries.

If one target is misconfigured (a URL with no listener, an unreachable IPC socket), Submit will appear to hang while that target backs off. Eventually (around 30 seconds for HTTP) the target fails and Submit completes.

Workarounds:

  • Disable the broken target in Delivery Targets and re-Submit.
  • Reduce the retry count for known-flaky targets.

The snapshot is already safely stored locally by the time delivery starts. You can re-deliver later.

A previous session resumed when I didn’t want it to

Xenocept does not persist in-progress session state across app restarts — every fresh hotkey press resets the canvas. If you’re seeing stale state, possibilities are:

  • The overlay window had been hidden but never relaunched between sessions; in-memory state survives an Esc-hide. Press the hotkey to fully reset.
  • You opened a submitted session in edit mode (from the Sessions list). Submitting from edit mode creates a new session, leaving the original intact.

I can’t find my old snapshots

Snapshots are stored locally in the AeorDB-backed store. They appear in the main UI’s snapshot browser. If the browser is empty:

  • Check that you’re running the same Xenocept install (same user, same OS). The store is per-user.
  • Confirm the data directory hasn’t been wiped — see Storage Layout for the path.
  • Look at the diagnostics panel for any errors loading the store. A corrupted store would surface here.

The app starts but the Tauri window doesn’t open

The Tauri window opens on first launch but is intentionally closeable without quitting the app. If the window doesn’t appear:

  • Click the tray icon to reopen the main window.
  • If the tray icon isn’t visible either, Xenocept may have failed to start. Run from a terminal (./xenocept) and look at stdout/stderr for errors.

How do I uninstall?

There’s no separate uninstaller right now:

  1. Delete the Xenocept binary.
  2. (Optional) Delete the data directory — see Configuration Overview for paths. This removes your snapshot history and settings.

A package-manager-friendly distribution is planned.

Where do I report bugs?

See the project README in the xenocept-client repository for issue tracking and contact information.

Platform Notes

Per-platform quirks, gotchas, and setup steps beyond what the Installation page covers.

Linux

Wayland

Screen capture under Wayland goes through the xcap crate, which uses PipeWire and the XDG Desktop Portal.

  • First-capture permission prompt. The first hotkey press triggers a portal dialog asking which screen(s) Xenocept can capture. Choose your default monitor (or “All screens”). The selection is remembered.
  • Compositor support. GNOME, KDE Plasma, Sway, Hyprland, and other major compositors implement the relevant portal interfaces. Older or fringe compositors may not — check that xdg-desktop-portal is installed and running.
  • Global hotkeys. Wayland’s hotkey support varies by compositor. If Ctrl+Backquote fails to bind, try alternatives in Hotkeys.

X11

xcap falls back to XGetImage / XShm on X11.

  • No permission prompt. X11 doesn’t gate capture per-app; whatever app can read the root window can capture it.
  • DPMS / screen blanking. Capturing a blanked screen returns black pixels — that’s by design.
  • HiDPI. Captured at native pixel resolution. The overlay scales appropriately.

System Tray Icon

Most modern Linux desktops support the StatusNotifier (Ayatana) protocol, which Xenocept uses for its tray icon. If your environment doesn’t:

  • GNOME by default does not show a tray. Install the appindicator extension.
  • Plain X11 with no panel: there’s nothing to show the icon in — Xenocept will still run and the hotkey still works.

The tray icon is a convenience, not a requirement. The hotkey is the primary entry point.

macOS

Screen Recording Permission

macOS gates screen capture per-app. The first time Xenocept tries to capture:

  1. macOS prompts: “Xenocept would like to record this computer’s screen.”
  2. Click “Open System Settings.”
  3. Add Xenocept to the allowed apps list.
  4. Restart Xenocept — the permission isn’t picked up until the next launch.

Without this permission, capture returns blank/black images.

Capture API

Screen capture uses the xcap crate, which picks the right native API for the running macOS version internally. Those API specifics are xcap’s implementation detail.

macOS Hotkey Defaults

The default global hotkey is Ctrl+Backquote on every platform, including macOS. There is no Cmd-based macOS variant in the current build. The Settings UI shows the current binding.

Tray Icon

The tray icon appears in the menu bar (top-right). Some menu-bar managers (like Bartender) may hide it by default — adjust if needed.

Windows

Capture API

Screen capture uses the xcap crate, which on Windows uses DXGI Desktop Duplication / Windows.Graphics.Capture under the hood. No special permission grants are needed — local capture is allowed by default.

Tray Icon

The tray icon appears in the system tray (bottom-right). Windows hides infrequently-used tray icons by default; pin Xenocept’s icon for easier access via the tray overflow menu.

Defender / SmartScreen Warnings

Unsigned development builds trigger Windows Defender SmartScreen warnings. Click “More info” → “Run anyway” to bypass.

Released builds will be code-signed; SmartScreen will accept them without the warning.

Path Separators

Throughout these docs, paths use forward slashes. On Windows, both \ and / work in most contexts (the Rust file APIs normalize), but if you’re hand-editing config files, prefer escaped backslashes (\\) in JSON strings:

{
  "path": "C:\\Users\\you\\Documents\\xenocept"
}

All Platforms

Firewall / Localhost

Xenocept’s HTTP server is hardcoded to 127.0.0.1:9500. Personal firewalls almost never block localhost-to-localhost traffic, but if you’ve installed something aggressive, double-check it allows 127.0.0.1:9500.

The bind is intentionally localhost-only and is not user-configurable.

Antivirus and Capture

Some endpoint-protection tools flag screen-capture APIs as suspicious. If Xenocept’s capture returns black frames or fails outright, check whether your AV has a quarantine entry for it.