---
title: ContextDock Integration (link bundles and lists to project knowledge)
---

# ContextDock Integration (link bundles and lists to project knowledge)

## What it is

[ContextDock](https://contextdock.web.app) is a separate web app where you collect markdown notes — research, customer transcripts, product specs, anything — into named **bundles** (hand-curated collections of docs) and **lists** (tag-driven views over your docs). Omniscio's ContextDock integration lets you link any bundle or list into an Omniscio project so every Claude session in that project automatically sees the contents alongside its own work.

Concretely: linking a bundle or list takes a one-time **snapshot** of the rendered markdown and saves it as a regular file at `<project>/.claude/docs/contextdock-<id>.md`. From that point on, Omniscio inlines the file's contents into the **first user message** of every new Claude session in that project. The agent treats it the same as any other doc you've dropped in `.claude/docs/` (see [project-docs-auto-injection.md](project-docs-auto-injection.md)) — it's just a regular `.md` file with a sentinel header marking Omniscio ownership.

There are three user-visible surfaces:

1. **Settings → ContextDock** — masked input where you paste an API key, a "Validate & Save" button, and (on success) a green-check status with an identity label derived from your key (your account `email` if present, otherwise the key's `keyName`, otherwise the raw `userId`). A **Background refresh (every 5 minutes)** toggle (default **on**) sits below the key card — when on, Omniscio keeps the picker's bundles/lists/list-detail SQLite cache warm against upstream every 5 minutes so the Add Project Doc → ContextDock tab opens instantly with a fresh roster; flip off if you'd rather refresh only when you open a tab or click 🔄. Footer shows `ContextDock CLI v0.2.0 (vendored 2026-05-07)` so you know exactly which CLI version Omniscio is running.
2. **Add Project Doc dialog → ContextDock tab** — fourth tab next to Upload / Paste / Google Doc. Picker is split into three inner sub-tabs — **Bundles** (📦), **Lists** (🏷️), and **Library** — with a per-tab client-side search filter (Bundles/Lists filter their own rows; the Library tab carries its own search box). Bundles and Lists each show a count badge. The **Library** sub-tab is a full-text search over your _entire_ ContextDock doc library that links one individual doc at a time at a compression level you pick — reachable even when you have no bundles or lists (see _Adding an individual doc from the Library tab_ below). **Bundle rows** carry a single **Add** button (tooltip: "Add this bundle to project docs") that links the whole bundle in one click using the bundle's author-set compression preference (falls back to **Key Points** when the bundle doesn't specify one). **List rows** carry two buttons: a primary **Add index** (tooltip: "Adds a titles-only index of this list. Use Browse to add individual files with their content.") that writes a **synthesized titles-only table of contents** for the list — just the list name, optional description, and a numbered list of every doc's title + id — and a secondary **Browse** that drills into the list's per-doc picker so you can add real doc content. The picker reads from a SQLite cache populated at startup, so opening the dialog renders both sub-tabs instantly; a small `Updated Xm ago` caption plus a 🔄 refresh icon sits in the top-right of the picker body.
3. **ProjectDocsSection rows** — every linked bundle, list, or per-doc item appears in your project's docs list with the ContextDock logo on the left (so you can tell at a glance the doc came from ContextDock rather than from a plain `.claude/docs/` paste), the full display name (wraps to multiple lines if long — never truncated), and a "from ContextDock · 14,237 tokens · refreshed 5m ago" sub-line. **Left-click the row** (or focus + Enter / Space) to preview the snapshot's rendered markdown in Omniscio's FilePeek viewer — same viewer used everywhere else in Omniscio for `.md` files. **Middle-click** the row to unlink it instantly with a Ctrl+Z undo, matching the inbox-row + tag-chip middle-click contract used elsewhere in Omniscio. **Right-click the row** opens a context menu with the row actions (refresh, open in browser, change compression for per-doc rows, unlink) — the row itself is icon-free so the title can use the full available width. **Long-press the row** (~0.4s) then drag to reorder it within the Auto Context list — there's no separate grip handle, a quick click still opens the preview, and ContextDock rows reorder by the same whole-row gesture as plain docs. Per-doc (`kind: doc`) rows additionally show an inline **compression chip** in the sub-line — a sliders icon plus the current level (e.g. _Key Points_) — that you click to cycle Original → Key Points → Summary in place (the right-click **Change compression** submenu still works too). The **Change compression** group only appears for per-doc items (the ones added via a list's drill-in — `kind: doc`); whole-bundle and whole-list rows don't show it because bundles carry an author-set compression preference and lists are committed at the index level.

### Opting in

The integration is **opt-in at two levels**:

1. **Feature toggle (2026-05-23):** ContextDock is registered in
   `UNRELEASED_FEATURES` (`settingKey: contextdockEnabled`, **off by default**), so
   it is revealed through **Settings → Lab → Built-in Apps → ContextDock**.
   (2026-08-14 declutter, decision 3A: the Settings nav row is **hidden by
   default** and the old Features-hub toggle is gone — the Lab toggle is the only
   reveal.) When `contextdockEnabled` is off, the ContextDock-related sidebar
   surfaces are hidden, and every ContextDock IPC handler (key validation/state,
   bundle/list refresh, picker fetches, link, refresh) rejects with
   `{success: false, error: "ContextDock is disabled..."}`. Existing installs that
   already have a saved ContextDock API key or any linked bundle/list/doc rows get
   migrated ON automatically on first launch via
   [opt-in-toggle-migration.ts](../../src/main/services/opt-in-toggle-migration.ts).
2. **API-key setup:** the ContextDock tab is always present in the Add Project Doc dialog (once the feature toggle is on); until you paste an API key in Settings, the tab body shows an empty state ("ContextDock isn't connected yet. Set up ContextDock in Settings → Tools & Maintenance → ContextDock.") with an **Open Settings** button that deep-links into Settings → ContextDock.

## Where to find it

ContextDock shows up in exactly three places, and it has no sidebar row or panel of its own. The key lives in the **ContextDock** card in Settings, where you paste it and validate it. The picker lives in the **ContextDock** tab of the **Add Project Doc** dialog, which you open from any project. And once something is linked, it appears as a row in that project's docs section.

Settings search keywords "contextdock", "bundles", "lists", "external docs", "knowledge base" all land on the feature toggle; the **`contextdock-api-key`** and **`contextdock-bg-refresh`** settings entries stay searchable even while the nav row is hidden (decision 3A).

## How it behaves

### One-time setup

1. Mint an API key in your ContextDock workspace (`contextdock.web.app` → workspace settings → API keys → New key with **read** scope).
2. In Omniscio: **gear icon → ContextDock** in the Settings sidebar. Paste the key into the input.
3. Click **Validate & Save**. Omniscio runs `whoami` against your key, and on success shows a green check mark + an identity label. The label is your account `email` when present, otherwise the key's `keyName` (e.g. "Personal"), otherwise the raw `userId` — whichever is the most informative thing the API returned.
4. Done. The key is encrypted via Windows DPAPI (`safeStorage`) and stored as `enc:<ciphertext>` in Omniscio's `config.json`.

If the network is down during validation, Omniscio offers a **"Save without validating"** escape hatch — the draft key is written and you can validate later by clicking the row.

### Linking a bundle or list to a project

1. Open the project. Click **Add Project Doc** (the `+ Doc` button in the project's docs section).
2. Pick the **ContextDock** tab (fourth tab, always present — but only shows bundles/lists once an API key is saved; without one, the tab body is the empty state with an Open Settings link).
3. The picker renders bundles and lists instantly from the local SQLite cache (populated by the startup preload — see "Cache + startup preload" below). Pick a sub-tab — **Bundles** (📦), **Lists** (🏷️), or **Library**. The Bundles and Lists tabs each have their own search box that filters only that sub-tab's rows; the **Library** tab has its own full-library search (see _Adding an individual doc from the Library tab_). The top-right of the picker shows `Updated Xm ago` plus a 🔄 refresh button — clicking it forces a foreground re-fetch from contextdock.web.app and shows a success toast on done.
4. **Bundles**: each row carries a single **Add** button. Clicking it links the whole bundle in one shot at the bundle's author-set `preferLevel` (Original / Key Points / Summary), falling back to **Key Points** when the bundle metadata didn't specify one. The picker shows the per-level token estimate next to the description so you know roughly how much context you're attaching before you click. There is no per-row compression picker — bundles are author-curated, so the bundle owner has already chosen the level you should use.
5. **Lists**: each row carries two buttons. The metadata line below the list name shows the doc count (or `List index` when the upstream `docCount` is absent) and, when the cache has a `keyPoints` token estimate for this list, a `· <N>k tokens` chip — same format the bundle rows use — so you can see the index size before clicking. The primary **Add index** writes a **synthesized titles-only table of contents** for the list as a single doc — same `contextdock-<listId>.md` file shape as a bundle, with `kind: list` on the sentinel header. What lands on disk is just the list name, optional description, and a numbered list of every doc's title + id (plus its original-version token count when known). **No doc content is included.** The synthesized body explicitly tells the agent that "the document content is NOT in this snapshot" and instructs it to ask the user to add specific docs via Browse if it needs them — because there is no MCP wiring, the agent cannot pull individual docs at runtime. This makes "Add index" a true context-switching aid: the agent learns what's available in the list without burning the ~125K-token injection cap on doc bodies. The secondary **Browse** drills into the list's per-doc picker (see "Drill into a list" below) so you can pick a subset of the list's docs and individually choose the compression level for each — that's how doc **content** actually enters context. Use **Add index** when you want a navigable manifest; use **Browse** when you want the doc bodies themselves.
6. On success, Omniscio fetches the rendered markdown, prepends a sentinel header, and atomically writes it to `<project>/.claude/docs/contextdock-<id>.md` (whole bundle or whole list) — or one `contextdock-<docId>.md` per individually-picked doc when you commit from a list's drill-in. The dialog closes and the new row(s) appear in the project's docs section.

The bundle or list is now **part of the project**. Omniscio inlines the contents into the **first user message** of every new Claude session you spawn in this project (see [project-docs-auto-injection.md](project-docs-auto-injection.md) for the injection mechanics). Because injection happens once, on the first message, **already-running and paused sessions won't pick up a newly-linked bundle** — they're already past their first message. Spawn a new session (or the next scheduled/recipe run) to pick up freshly-linked content.

**Bundles vs lists.** A bundle is a hand-picked, author-curated set of docs you've assembled in ContextDock — including a `preferLevel` for the right compression. A list is a saved tag query — you pick one or more tag IDs and a match mode (`any` / `all`), and the list resolves to whatever docs currently match. The snapshot Omniscio writes to disk is the **rendered markdown at link time** for both kinds; refreshing a list re-runs the tag query against the current state of your workspace.

### Pinning bundles

You can **pin** a bundle so it always sorts to the top of the Bundles sidebar pane. Two controls set the pin state:

- **Hover-reveal pin icon** — hover over any bundle row in the sidebar to reveal a small pin icon on the right edge. Click it to toggle pin on/off. When pinned, the icon stays visible in accent color; when unpinned, it only appears on hover. The icon pulses briefly while the update is in flight.
- **Pinned toggle in the edit dialog** — open a bundle's edit dialog (only in edit mode, not create). A **Pinned** toggle sits below the Shared toggle, with the description "Pinned bundles appear at the top of the sidebar."

Pinned bundles sort before unpinned bundles; within each group, bundles sort alphabetically by name. The pin state is stored as `isPinned` on the `BundleSummary` type and persisted in the `is_pinned` column of the `contextdock_bundles_cache` table.

### Drill into a list (per-doc selection)

A ContextDock list resolves to a tag-matched set of docs. Click anywhere on a list row in the picker to enter the per-doc selection surface — it replaces the picker body in place (the Add Project Doc dialog stays open):

1. **Doc list** — every doc the list currently resolves to is rendered as a row with a checkbox, the doc title, and a per-row native `<select>` dropdown (Original / Key Points / Summary). The select is indented below the title so it visually reads as a sub-control of the doc, not a peer of the checkbox. Each option label embeds the per-level token count when the CLI provided one (e.g. `Summary — 400 tokens`); options with no count render label-only and still pick the right level.
2. **Per-row select change sets the row's level AND selects it.** This makes "I want this doc at Summary" a single interaction. Clicking the checkbox directly toggles selection without mutating the stored level — selection ≠ level.
3. **Bulk action toolbar** — `Select All` and `Deselect All` for selection, plus a single **Apply level to selected docs** `<select>` that applies the chosen level to every currently-selected row. The dropdown is a **one-shot picker** — it stays at its placeholder (`Apply level to selected…`), fires the bulk-apply on change, then resets back to the placeholder so re-applying the same level is a fresh choice each time rather than a stuck "current selection". The bulk select is disabled while nothing is selected.
4. **Running totals** — the top bar shows `<count> of <total> selected · ~<tokens> tokens` summed across the selected rows at their per-row levels. Updates live as you change selects and toggle checkboxes.
5. **Add (N)** — clicking the primary button commits the selection. Omniscio fires one `contextdock:link-doc` IPC per selected doc, in sequence; each successful one writes its own `contextdock-<docId>.md` file. The toast roll-up shows `Added N` on full success, or `Added N of M — F failed (<first-error-code>)` when some rows fail. Failed rows stay selected so you can retry; succeeded rows are deselected and removed from the active set.
6. **`← Back to ContextDock`** always returns to the picker without committing. Selection state is dropped on back-out (intentional — the drill-in is a per-list scratchpad).
7. **Deselect All** clears selection but PRESERVES the per-row level overrides so you can re-select a row and pick up where you left off. Selection is your intent for "what to commit now"; level is a property of the row.

Each successful drill-in commit is rolled up into a single `drill_in_commit` telemetry event with `{ docCount, levelMix: { original, keyPoints, summary }, errorCount }` — no doc IDs, no titles, no error messages. The per-doc `bundle_linked` / `fetch_failed` rows are still emitted by `link-doc` for individual successes and failures.

### Adding an individual doc from the Library tab

The **Library** sub-tab links a single doc from your whole ContextDock library — without going through a bundle or a list. It's the fastest path when you know the doc you want and don't care which bundle/list it lives in, and it's the only picker path that works when you have zero bundles and zero lists.

1. Open **Add Project Doc → ContextDock → Library**. The tab lazily loads your doc roster on first open (so the picker still opens instantly on the default Bundles tab).
2. The **empty search box** shows your full library roster; **type to search** ("Search your library…") runs a debounced full-text search and shows matching hits with a snippet. Roster rows carry per-level token counts (shown in the level dropdown); search hits don't — the token count appears on the row once it's added.
3. Each result has a per-row compression **`<select>`** (Original / Key Points / Summary — default **Key Points**) and an **Add** button. Pick the level, click **Add**, and Omniscio links just that doc via `contextdock:link-doc` — the same per-doc channel the list drill-in uses — writing one `contextdock-<docId>.md` file. A success toast shows `Added <title> (<N> tokens)`; if the add pushes `.claude/docs/` over the ~125K-token cap you also get a clip warning.
4. The new doc appears as a per-doc row (`kind: doc`) in the project's Auto Context list, with the inline compression chip you can use to change its level later.

Library-linked docs are identical on disk to docs added via a list's **Browse** drill-in (`kind: doc` sentinel) — the only difference is the entry point: Browse is scoped to one list's tag-matched docs, while Library searches your entire library.

### Previewing a linked snapshot

To see what Omniscio actually wrote to disk for a linked bundle or list, **left-click the row** in the project's docs section. Omniscio opens the file in its standard FilePeek markdown viewer — the same one used for plain `.md` docs elsewhere — and renders the snapshot's body. The sentinel header at the top of the file is an HTML comment block, so the viewer renders it as invisible; you see only the bundle's / list's actual markdown content. Press Escape (or click the X) to close.

Keyboard access: tabbing through the docs section lands on each row in turn; pressing Enter or Space on a focused row opens the same viewer.

This is purely a viewer — it has no edit / save controls. If you want to change a linked snapshot, either refresh it from upstream (right-click → Refresh) or unlink and re-link.

### Refreshing

Two kinds of refresh, one for each surface:

- **The picker's library list** (bundles + lists shown in Add Project Doc → ContextDock). The 🔄 button next to the `Updated Xm ago` caption forces a foreground re-fetch from contextdock.web.app, rewrites the SQLite cache, and updates the on-screen list. Use this when you've just created or renamed a bundle/list at contextdock.web.app and want it to appear in the picker without waiting for the next startup preload.
- **A linked snapshot** (a `contextdock-<id>.md` file already in a project's docs). The snapshot does **not** auto-update. If you edit the bundle on contextdock.web.app — or if a list's tag-matching docs change underneath you — Omniscio's local copy stays at the version you originally linked. To pull a new snapshot, **right-click the row → Refresh**. Omniscio re-fetches the bundle or re-runs the list's tag query and rewrites the file atomically.

In-flight snapshot refreshes for the same `(projectId, contextdockId)` pair are deduplicated — clicking the refresh button twice while the first request is mid-flight is a no-op.

### Changing compression on a per-doc item

When you drilled into a list and committed a subset of its docs, each landed as its own per-doc row in the project's docs section (`contextdock-doc-<docId>-<level>.md` on disk, with `kind: doc` on the sentinel header). After the fact, you can change which compression level (Original / Key Points / Summary) any of those rows is fetched at without leaving the project. Two equivalent controls do the same swap:

- **Inline compression pill (quick).** Every per-doc row shows a small pill in its sub-line — the current level label, a compact token count, and a down-arrow (e.g. _Key Points · 1.2k ▾_). **Click the pill** to open a short dropdown and pick a level (Original / Key Points / Summary). While the swap is in flight the pill shows a spinner and is disabled, and clicking it never opens the row's preview (the click is captured by the pill).
- **Right-click menu (discoverable).** **Right-click the row → Change compression → pick a level** — the explicit menu, handy when you want to jump straight to a specific level rather than cycling.

The submenu lives between **Open in browser** and **Unlink** in the per-doc row's right-click menu, rendered as three radio-style items (one per level). The currently-active level shows a filled dot (●) and is disabled — clicking your current level is a no-op, not a refetch.

Picking a different level **atomically swaps** the snapshot:

1. Omniscio fetches the doc at the new level from contextdock.web.app.
2. On success, the new file (`contextdock-doc-<docId>-<newLevel>.md`) is written to `.claude/docs/`.
3. The old file at the old level is unlinked.

If the fetch fails (network down, key revoked, doc deleted upstream), nothing on disk changes — the old file stays exactly where it was at the old level. Specifically:

- `not_found` (doc deleted upstream between link-time and change-time) → row gets the same yellow ⚠ stale marker that refresh-fails use; the local snapshot at the old level is untouched and still works.
- All other error codes (`auth`, `auth_revoked`, `rate_limit`, `budget`, `network`, `vendor_broken`) → toast with the same wording the refresh path uses; old file untouched.

The "Change compression" menu is **only shown for per-doc rows** (`sentinel.kind === 'doc'`). Whole-bundle and whole-list rows don't expose it — bundles carry an author-set compression preference (the picker uses it at link time), and lists are committed at the index level rather than a per-doc level. If you want to change the level of a whole bundle or list, unlink it and re-link.

In-flight change-level operations on the same per-doc row are deduplicated by `(projectId, filename, newLevel)`. Picking the same target level twice while the first request is mid-flight is a no-op.

### Open-in-browser

**Right-click the row → Open in browser** opens the right URL for the kind: `https://contextdock.web.app/bundles/<bundleId>` for a bundle, `https://contextdock.web.app/lists/<listId>` for a list. Quick jump back to ContextDock to edit it.

### Unlinking

Two paths, same result — delete the local snapshot from `.claude/docs/`. Omniscio does **not** delete the bundle or list on contextdock.web.app — only the local snapshot. The next session prompt will not include it.

- **Fast path — middle-click the row.** No confirmation. The file is moved to system trash via the existing `FILES_DELETE` IPC, the row disappears from the docs section, and a toast shows `Unlinked "<name>"` with an **Undo** action plus the `Ctrl+Z` shortcut hint. Hit **Undo** (or press Ctrl+Z) to re-link the bundle/list/doc — the snapshot is re-fetched fresh from contextdock.web.app via the same `contextdock:link` or `contextdock:link-doc` IPC the picker uses, so you get the latest content, not the trashed copy. Redo via Ctrl+Y re-deletes. This is the recommended path for routine cleanup.
- **Discoverable path — right-click the row → Unlink.** Opens a confirmation dialog. On confirm, deletes the file. No undo toast (the confirm step is the safety net). Use this when you want a hard-stop confirmation or you don't remember the middle-click shortcut.

Why re-link rather than restore from trash? `FILES_DELETE` uses Electron's `shell.trashItem()` which has no programmatic restore. But the Omniscio sentinel header at the top of every `contextdock-<id>.md` file carries everything the link IPC needs — `kind`, `contextdockId`, `displayName`, `preferLevel` — so re-linking from the sentinel snapshot is equivalent and always succeeds when the upstream bundle/list/doc still exists. The trade-off: if the upstream bundle was deleted on contextdock.web.app between unlink and undo, the undo will fail with a `not_found` error and the local snapshot is gone for good. Right-click → Unlink is the safer path when you suspect that.

## Related

This page is split into three parts. [Part 2](contextdock-part-2.md) covers the limits the integration deliberately holds to, the full error-and-recovery map, the injection caps, and what to gather before filing a bug; [part 3](contextdock-part-3.md) covers the internals, where a linked snapshot is written on disk, the sentinel header that marks it as Omniscio-managed, how the picker cache is kept warm, and the full IPC surface.

The first-message injection that puts a linked snapshot in front of the agent is the same mechanism plain project docs use, described on the [project docs auto-injection](project-docs-auto-injection.md) page. The separate, still-in-development local-first rebuild of the ContextDock library is on the [ContextDock (Native)](contextdock-native.md) page.
