---
title: Bookmarks (part 2)
---

# Bookmarks (part 2)

## What it is

This is part 2 of the [Bookmarks](bookmarks.md) page. It carries the reference material: the localhost CLI routes with their kill switch, curl examples and response shapes, and the implementation map of the queries, launcher, favicon service, IPC channels and renderer state behind the popover.

## Where to find it

Bookmarks are reached from the bookmark icon in the toolbar pill, which opens the popover holding the search box, the Add button and the hierarchical tree; [Bookmarks](bookmarks.md) walks that surface end to end, including the eight kinds, the cmd confirmation gate, reordering and the Settings toggle. The material on this page is reached either from the local CLI control server on 127.0.0.1:19519 or by reading the code paths named below.

## How it behaves

### CLI control

Bookmarks are exposed through Omniscio's CLI control server on `127.0.0.1:19519` (override with `AMC_CLI_PORT`). All routes require a bearer token in the `Authorization: Bearer <token>` header — see the [cli-control.md](cli-control.md) doc for how the token is delivered and how to fetch it from the DPAPI vault. Six endpoints are provided:

- `GET /bookmarks` — list every bookmark in display order.
- `POST /bookmarks` — create one (URL / Path / Folder / Cmd; **never Exe**).
- `PATCH /bookmarks/:id` — update name, value, parent, or kind.
- `DELETE /bookmarks/:id` — delete (cascades to descendants for folders).
- `POST /bookmarks/reorder` — reorder all rows under one parent in a single all-or-nothing transaction.
- `POST /bookmarks/:id/launch` — launch a URL, Path, or Cmd bookmark. Exe is rejected.

#### Kill switch

Every bookmarks route is gated by the `bookmarksCliEnabled` setting (default `true`, toggle at **Settings → CLI Control → Enable bookmarks CLI**). When the user flips it off, every route returns `403 { ok: false, error: 'Bookmarks CLI disabled in Settings → CLI Control', disabled: true }` before any auth/parse/business work runs. This is the panic-button switch — flip it off and external callers cannot read or mutate bookmarks at all, even with a valid bearer token. The setting is checked on every request, so the switch takes effect immediately without an app restart. This flag is independent from `showBookmarksInToolbar` — the in-app popover keeps working when this is off, and the CLI keeps working when `showBookmarksInToolbar` is off (see "Settings" above).

#### Mutations apply immediately (NOT approval-gated)

Bookmark mutations (POST / PATCH / DELETE / reorder) apply immediately, the same way `/keybindings/*` does — they are personal launcher entries with no destructive cascade and the user-in-the-loop gate doesn't add value. Each successful mutation emits a `bookmarks:changed` push so any open Omniscio popover reloads. The shared rate-limit pool covers them at 10 mutations / minute / token.

This contrasts with `/cron/*`, `/automation/*`, and most `/email-summarizer/*` mutations, which land as `pending` rows in `cli_pending_actions` and require user approval in the inbox before any side effect.

#### Read budget on GET

`GET /bookmarks` is bearer-gated AND read-budgeted at **60 requests / minute per token-hash**. Past the budget, the server returns `429 { ok: false, error: 'read budget exceeded', retryAfter: <seconds> }`. The point is to make cheap polling expensive enough that a leaked token cannot be used as a bookmark exfiltration drip.

#### Security: exe is rejected, cmd first-run is still gated

Two distinct hardening rules apply to launch:

1. **`exe` kind is rejected at the CLI surface.** A `POST /bookmarks/:id/launch` against an Exe bookmark returns `403 { ok: false, error: 'exe-kind bookmarks cannot be launched via CLI; launch from the Omniscio UI instead' }` (no `kind` field). The CLI cannot launch arbitrary executables — full stop. The in-app popover can still launch Exe bookmarks because that path goes through the IPC handler, which sees a real Electron renderer and a user click.

2. **`cmd` first-run still requires user confirmation in the Omniscio popover.** The IPC handler accepts a `confirmed: true` field on its launch input that lets the popover skip the first-run modal. The CLI launch schema is `z.object({}).strict()` — **no fields at all**, with `.strict()` so `additionalProperties` are rejected, not silently stripped. A CLI caller cannot send `confirmed: true` to bypass the gate. When a CLI caller asks to launch a never-confirmed cmd row, the server returns `200 { ok: true, data: { ok: false, requiresConfirm: true } }` to signal "go run this from the Omniscio UI to confirm it the first time, then come back". Note the **two-level `ok`** envelope: the outer `ok: true` only signals "the HTTP route handled the request"; the inner `data.ok: false` is the launcher's verdict. A CLI caller checking only the outer flag will misclassify unconfirmed-cmd as success — always check `response.data.ok` AND `response.data.requiresConfirm`. This is load-bearing security control — never relax the schema to accept additional fields.

> **Why the strict schema matters**: a bearer token leak combined with a permissive launch schema would equal full RCE — any leaked token could fire arbitrary `cmd.exe /c ...` payloads. The `.strict({})` schema is what keeps a leaked token bounded to "things the user has already pre-confirmed in the UI".

#### Working `curl` examples

These assume `TOKEN` is set; the global CLAUDE.md "Omniscio CLI Bearer Token" section shows how to fetch it from the DPAPI vault.

```bash
TOKEN=$(powershell.exe -NoProfile -File "$HOME/.claude/secrets/get-secret.ps1" amc-cli | tr -d '\r\n')

# 1. List all bookmarks
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/bookmarks

# 2. Create a URL bookmark at the root
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"kind":"url","name":"Anthropic","value":"https://www.anthropic.com"}' \
  http://127.0.0.1:19519/bookmarks

# 3. Create a folder
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"kind":"folder","name":"Work"}' \
  http://127.0.0.1:19519/bookmarks

# 4. Move a bookmark into a folder (PATCH parent)
curl -s -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"parentId":"<folder-id>"}' \
  http://127.0.0.1:19519/bookmarks/<bookmark-id>

# 5. Reorder all rows under one parent (all-or-nothing)
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"parentId":null,"orderedIds":["id-a","id-b","id-c"]}' \
  http://127.0.0.1:19519/bookmarks/reorder

# 6. Launch a URL bookmark
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{}' \
  http://127.0.0.1:19519/bookmarks/<bookmark-id>/launch

# 7. Delete a bookmark (cascades for folders)
curl -s -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/bookmarks/<bookmark-id>
```

#### Reorder is all-or-nothing

`POST /bookmarks/reorder` takes the **full list of every row under one parent** as `orderedIds`, in the desired final order. The server validates that:

- Every id in `orderedIds` exists in the database.
- Every id in `orderedIds` currently has the requested `parentId` (or null for root).
- Every existing sibling under that `parentId` appears in `orderedIds` exactly once.

If any of those checks fails, the entire reorder is rejected with `400` and **no rows are mutated**. There is no partial application — you cannot reorder three rows out of five and have the other two end up at random positions. This matches the behaviour the in-app drag handler relies on, so a CLI-driven reorder is indistinguishable from a drag.

#### Response shapes summary

All bookmarks routes wrap their result in the standard `{ ok, data }` envelope; the inner `data` shape is what previous versions of this doc showed at the top level.

| Endpoint                                                            | Success                                                | Common failures                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /bookmarks`                                                    | `200 { ok: true, data: { bookmarks: BookmarkRow[] } }` | `401 unauthorized` (missing/invalid token), `403 forbidden` (feature disabled), `429 read budget exceeded`                                                                                                                                                                                                                                                                                                                                                                     |
| `POST /bookmarks`                                                   | `201 { ok: true, data: { id: string } }`               | `400` (validation, depth cap, cycle), `401 unauthorized` (missing/invalid token), `403 forbidden` (feature disabled), `429` (rate limit)                                                                                                                                                                                                                                                                                                                                       |
| `PATCH /bookmarks/:id`                                              | `200 { ok: true }`                                     | `400` (validation, cycle, depth), `404 not found`, `401 unauthorized` (missing/invalid token), `403 forbidden` (feature disabled), `429`                                                                                                                                                                                                                                                                                                                                       |
| `DELETE /bookmarks/:id`                                             | `200 { ok: true, data: { deletedCount: number } }`     | `404 not found`, `401 unauthorized` (missing/invalid token), `403 forbidden` (feature disabled), `429`                                                                                                                                                                                                                                                                                                                                                                         |
| `POST /bookmarks/reorder`                                           | `200 { ok: true }`                                     | `400` (orderedIds mismatch), `401 unauthorized` (missing/invalid token), `403 forbidden` (feature disabled), `429`                                                                                                                                                                                                                                                                                                                                                             |
| `POST /bookmarks/:id/launch` (url/path/cmd/session/project/virtual) | `200 { ok: true, data: { ok: true } }`                 | `200 { ok: true, data: { ok: false, requiresConfirm: true } }` (never-confirmed cmd — outer `ok` is true but inner is false), `200 { ok: true, data: { ok: false, error: '<kind> not found' } }` (nav kind whose target was deleted), `400` (folder), `403` (exe rejected, with error `'exe-kind bookmarks cannot be launched via CLI; launch from the Omniscio UI instead'`), `404 not found`, `401 unauthorized` (missing/invalid token), `403 forbidden` (feature disabled) |

Every successful mutation emits `bookmarks:changed` so the open Omniscio UI re-renders without polling.

## For agents

### How it works

The data model lives in [src/main/db/bookmarks-queries.ts](../../src/main/db/bookmarks-queries.ts) — `listBookmarks`, `createBookmark`, `updateBookmark`, `deleteBookmark`, plus internal helpers that enforce the cycle and depth rules. The launcher is [src/main/services/bookmarks-launcher.ts](../../src/main/services/bookmarks-launcher.ts) — it owns the per-bookmark debounce, the cmd confirmation gate, the URL/path/exe/cmd dispatch, and the `confirmed = 1` write that turns first-time cmd rows into silent ones. The favicon service is [src/main/services/bookmarks-favicon.ts](../../src/main/services/bookmarks-favicon.ts) — `fetchFavicon`, `invalidateOnHostChange`, `sweepOrphanFavicons`.

The IPC channels are `bookmarks:list`, `bookmarks:create`, `bookmarks:update`, `bookmarks:delete`, `bookmarks:launch`, plus the `bookmarks:changed` push event. Every CRUD handler in [src/main/ipc/bookmarks-handlers.ts](../../src/main/ipc/bookmarks-handlers.ts) emits `BOOKMARKS_CHANGED` after a successful write, so any open popover reloads its tree and stays in sync (a second Omniscio window, the same window after a CLI mutation, etc.).

The renderer side is laid out under [src/renderer/src/features/bookmarks/](../../src/renderer/src/features/bookmarks/). [BookmarksToolbarButton.tsx](../../src/renderer/src/features/bookmarks/BookmarksToolbarButton.tsx) owns the toolbar entry-point — it reads the `showBookmarksInToolbar` setting and renders nothing when it's off — and lazy-loads the three modal-only components (popover, add modal, confirm modal) per CLAUDE.md's "heavy modal-only components must be `React.lazy()` in EVERY caller" rule. [BookmarksPopover.tsx](../../src/renderer/src/features/bookmarks/BookmarksPopover.tsx) handles the portal positioning, the search input, the empty state, and the click-outside / Esc / re-click close logic. [BookmarksTree.tsx](../../src/renderer/src/features/bookmarks/BookmarksTree.tsx) handles the search filter, expand/collapse state, keyboard navigation, the 600ms hover-into-folder auto-expand timer, and the cross-parent drop-into handler; rows render via [BookmarkRow.tsx](../../src/renderer/src/features/bookmarks/BookmarkRow.tsx) which also computes the `paddingLeft = 6 + depth * 12` indent and emits the 3-zone (folder) or 2-zone (leaf) drop-zone metadata that [src/renderer/src/hooks/useDragReorder.ts](../../src/renderer/src/hooks/useDragReorder.ts) consumes. [BookmarkAddModal.tsx](../../src/renderer/src/features/bookmarks/BookmarkAddModal.tsx) handles both add and edit modes; [BookmarkConfirmModal.tsx](../../src/renderer/src/features/bookmarks/BookmarkConfirmModal.tsx) is the cmd first-run gate. The **Enable bookmarks** toggle lives in the Toolbar settings section, [src/renderer/src/features/toolbar/ToolbarSettings.tsx](../../src/renderer/src/features/toolbar/ToolbarSettings.tsx) — a single toggle bound to `showBookmarksInToolbar` (there is no standalone Bookmarks settings section; `'bookmarks'` is an `'active'`-only legacy alias of `'toolbar'`). It does **not** render the tree or any CRUD controls.

The Zustand store is [src/renderer/src/stores/bookmarks-store.ts](../../src/renderer/src/stores/bookmarks-store.ts). It holds the flat row list, exposes `load` / `create` / `update` / `delete` / `launch` / `reorder` actions, and `buildBookmarkTree(rows)` builds the hierarchical tree the popover consumes. The store re-fetches on `BOOKMARKS_CHANGED` push so a CLI mutation or a second-window edit shows up in the open popover live.

## Related

This page is the companion to [Bookmarks](bookmarks.md), which covers the feature itself: what bookmarks are, where to find the popover, and how to add, reorder and rename them. [cli-control.md](cli-control.md) describes the CLI control server those routes live on, [keyboard-shortcuts.md](keyboard-shortcuts.md) holds the global shortcuts table the tree navigates within, and [tray-and-window.md](tray-and-window.md) covers the Electron shell and child-process rules the launcher uses.
