Bookmarks (part 2)
The reference half of the Bookmarks page: the localhost CLI routes with their kill switch, curl examples and response shapes, plus the implementation map of the queries, launcher, favicons, IPC channels and renderer state.
What it is
This is part 2 of the Bookmarks 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 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 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:
exekind is rejected at the CLI surface. APOST /bookmarks/:id/launchagainst an Exe bookmark returns403 { ok: false, error: 'exe-kind bookmarks cannot be launched via CLI; launch from the Omniscio UI instead' }(nokindfield). 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.cmdfirst-run still requires user confirmation in the Omniscio popover. The IPC handler accepts aconfirmed: truefield on its launch input that lets the popover skip the first-run modal. The CLI launch schema isz.object({}).strict()— no fields at all, with.strict()soadditionalPropertiesare rejected, not silently stripped. A CLI caller cannot sendconfirmed: trueto bypass the gate. When a CLI caller asks to launch a never-confirmed cmd row, the server returns200 { 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-levelokenvelope: the outerok: trueonly signals "the HTTP route handled the request"; the innerdata.ok: falseis the launcher's verdict. A CLI caller checking only the outer flag will misclassify unconfirmed-cmd as success — always checkresponse.data.okANDresponse.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.
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
orderedIdsexists in the database. - Every id in
orderedIdscurrently has the requestedparentId(or null for root). - Every existing sibling under that
parentIdappears inorderedIdsexactly 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 — listBookmarks, createBookmark, updateBookmark, deleteBookmark, plus internal helpers that enforce the cycle and depth rules. The launcher is 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 — 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 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/. 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 handles the portal positioning, the search input, the empty state, and the click-outside / Esc / re-click close logic. 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 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 consumes. BookmarkAddModal.tsx handles both add and edit modes; 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 — 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. 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, which covers the feature itself: what bookmarks are, where to find the popover, and how to add, reorder and rename them. cli-control.md describes the CLI control server those routes live on, keyboard-shortcuts.md holds the global shortcuts table the tree navigates within, and tray-and-window.md covers the Electron shell and child-process rules the launcher uses.
Last verified 2026-09-23