# Configuration (https://openknowledge.ai/docs/reference/configuration)

The .ok/config.yml schema and environment variables.

OpenKnowledge reads YAML config from three places:

- **Project**: `./.ok/config.yml` (this project; committed to git only when **Config sharing** is set to **Shared**, otherwise kept on this machine)
- **User**: `~/.ok/global.yml` (every project on your machine; OpenKnowledge Beta reads `~/.ok-beta/global.yml`)
- **Project-local**: `./.ok/local/config.yml` (this project on this machine; gitignored). Holds per-machine-per-project preferences such as `autoSync.mode`. Maintained by the editor (auto-sync onboarding modal, sync popover, Settings pane Sync section); you don't normally hand-edit it.

All files are optional; defaults cover everything.

You can inspect and edit config three ways:

- **Settings pane** in the editor: Cmd/Ctrl-, or App menu → Settings…
- **Your IDE**: `.ok/config.yml` ships with a `$schema` magic comment for autocomplete and inline field descriptions
- **CLI**: `ok config validate` checks the merged result

Edits made anywhere reflect everywhere; an open Settings pane refreshes live.

In the desktop app, Settings follows the focused window. An editor opens the full Settings pane. From the project navigator, Settings opens the same pane with your user-level **Preferences** and **Hotkeys**; on macOS it also opens there when all windows are closed and the app is still running. Changes save to your user config and apply to every project. Panes that need a project, such as Agent connections, Git, Plugins, Skills Studio, and everything under **This project**, are shown but disabled until you open one. To connect your AI tools from the navigator, use **File → Set up OpenKnowledge integrations…**. See the [quickstart](https://openknowledge.ai/docs/get-started/quickstart) for first-launch setup.

## Schema

The **Scope** column says which file a setting belongs in:

- **project** → `.ok/config.yml` (shared in git)
- **user** → `~/.ok/global.yml` (personal, all projects; Beta: `~/.ok-beta/global.yml`)
- **project-local** → `.ok/local/config.yml` (this machine only, gitignored)

A key set in a file more specific than its scope (a user-scope key in `.ok/config.yml`, for example) is ignored.

| Field                                    | Type                                                                                                                            | Default                                                   | Scope         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content.dir`                            | string                                                                                                                          | `"."`                                                     | project       | Content directory, relative to project root. Defaults to `.` (the project root) even when `.ok/` is scaffolded at the git working-tree root from a sub-folder, so opened folder and content scope align by default. To narrow scope to a sub-folder, pass `--content-dir <dir>` to `ok init`, or uncomment `content.dir` in `.ok/config.yml` post-init. Excluded paths live in `.okignore`; see [Ignore patterns](https://openknowledge.ai/docs/features/ignore-patterns).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `content.attachmentFolderPath`           | string                                                                                                                          | `"./"`                                                    | project       | Where pasted and dropped assets are stored. `"./"` colocates beside the current document; `"/"` targets the content root; `"./subdir"` targets a subfolder under the current folder; `"folder"` targets a fixed folder under the content root. A configured folder also widens what counts as content: linkable files inside it (images, PDFs, media) are treated as project content without needing a Markdown document beside them, so in shared projects they auto-sync to teammates. The default `"./"` keeps the historical rule (assets sync only beside a document).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `contentRules.markdownlint.enabled`      | boolean                                                                                                                         | `false`                                                   | project       | Enable the [markdownlint](https://openknowledge.ai/docs/advanced/content-rules/markdownlint) content-rules plugin for this project (shared via git). Off by default — turn it on in **Settings → This project → Plugins**. The rules themselves live in your native `.markdownlint.*` file, not here.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `contentRules.frontmatter.enabled`       | boolean                                                                                                                         | `false`                                                   | project       | Enable the [Frontmatter schemas](https://openknowledge.ai/docs/advanced/content-rules/frontmatter) content-rules plugin for this project (shared via git). Off by default — turn it on in **Settings → This project → Plugins**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `contentRules.frontmatter.schemas`       | array                                                                                                                           | `[]`                                                      | project       | Frontmatter schema mappings: each entry scopes one JSON Schema file to a set of docs — `file` (project-root-relative path), optional `appliesTo` (glob or list; leading `!` excludes; absent means every doc), optional `enabled: false` to park a mapping. Schema content lives in the mapped files, not here. See [Frontmatter schemas](https://openknowledge.ai/docs/advanced/content-rules/frontmatter).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `validation.links`                       | `"off" \| "warning" \| "error"`                                                                                                 | `"warning"`                                               | project       | How broken internal links are reported on the validation plane: `off` hides them, `warning` (default) or `error` sets their severity. Shared via git, like `contentRules`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `validation.suppressLogLinkAdvisories`   | boolean                                                                                                                         | `true`                                                    | project       | Whether broken internal links in a reserved log are left out of the Problems panel, file sidebar tints and badges, editor diagnostics, `ok audit`, and the advisories a write or edit response hands an agent. On by default, because a log is an append-only history whose entries deliberately reference the past and pages not yet written, and an agent told to repair broken links would otherwise edit that history. It matches the name `log.md` or `log.mdx` at any depth. The stem has to be lowercase, so `LOG.md` and `catalog.md` keep their findings, but the extension's case is not what decides, so `log.MD` is a reserved log too. It matches on the file the link is written in, not the file it points at. Raw link state is untouched either way: the Links panel, the editor's unresolved-link styling, `links({ kind: "dead" })`, and `GET /api/dead-links` still show those links. Toggle it as **Ignore broken links in log.md** under **Settings → This project → Preferences → Content rules**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `validation.fileTreeIndicators`          | boolean                                                                                                                         | `true`                                                    | project       | Whether the file sidebar tints and badges files that have validation problems. The badge is also the row's entry point into the Problems panel, so turning this off removes both the indicator and that shortcut (see [Where problems show up](https://openknowledge.ai/docs/advanced/content-rules/overview#where-problems-show-up)).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `appearance.theme`                       | `"light" \| "dark" \| "system"`                                                                                                 | (unset)                                                   | user          | Editor light/dark theme.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `appearance.language`                    | `"system" \| "en" \| "zh-Hans" \| "zh-Hant" \| "hi" \| "es" \| "ar" \| "fr" \| "bn" \| "pt-BR" \| "id" \| "ur" \| "ko" \| "pl"` | (unset)                                                   | user          | Interface language. `system` (the effective value when unset) follows your operating system or browser, and keeps following it. A personal preference, not project-shared — and it applies to the app's own chrome only: your documents, titles, file and folder names, frontmatter values and tags stay in the language you wrote them in. **Settings → Preferences → Language** offers eleven of them (English, 简体中文, 繁體中文, हिन्दी, Español, Français, বাংলা, Português do Brasil, Indonesia, 한국어, polski). Every catalog is complete, but only `es` has been through a Spanish reader; the rest, `zh-Hans` included, are machine-translated and nobody who reads them has checked the wording. Treat it as provisional and [tell us what is wrong](https://openknowledge.ai/docs/contribute/translations). `ar` and `ur` are complete translations too, but they stay out of the picker until the interface lays out right to left; set either here by hand to run the app in it, and note the app will not select them from your OS language alone. `OK_LANG=<tag>` overrides the desktop app's native menus for a single run; the rest of the interface follows this field.                                                                                                                                                                                                                                                                                                            |
| `appearance.colorThemeLight`             | theme id (`[a-z0-9-]`, 1–32 characters)                                                                                         | (unset)                                                   | user          | IDE color palette applied while the resolved mode is light. Built-in ids include `default`, `dracula`, `catppuccin-frappe`, `catppuccin-latte`, `monokai`, `gruvbox`, `solarized`, and `custom`; saved themes use their generated `saved-*` id. `default` (the effective value when unset or when no available palette matches an explicitly configured id) means no palette — the base stylesheet shows through. `custom` applies `appearance.customTheme`. Pick it in **Settings → Plugins → [Themes](https://openknowledge.ai/docs/plugins/themes)** with the tile's sun.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `appearance.colorThemeDark`              | theme id (`[a-z0-9-]`, 1–32 characters)                                                                                         | (unset)                                                   | user          | IDE color palette applied while the resolved mode is dark — the tile's moon. Built-in and saved-theme ids follow the same constrained string grammar. `appearance.theme` chooses between the two; on `system` your OS does, so the palette follows the OS appearance. Any available palette may sit in either slot, and it keeps forcing its own light/dark variant. An explicitly configured id that is not available falls back to `default`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `appearance.colorTheme`                  | theme id (`[a-z0-9-]`, 1–32 characters)                                                                                         | (unset)                                                   | user          | Legacy single-palette field, superseded by `colorThemeLight` / `colorThemeDark` above. A valid available id supplies the palette only for a slot that is truly unset; an explicit unknown per-mode id falls back to `default` instead of reviving this legacy value. Writing either slot from **Settings → Plugins → Themes** removes this key.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `appearance.colorThemeEnabled`           | boolean                                                                                                                         | `true`                                                    | user          | Whether the Themes plugin appears in **Settings → Plugins**. On by default (absent = enabled).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `appearance.customTheme`                 | object                                                                                                                          | (unset)                                                   | user          | The `custom` palette's [base16](https://github.com/tinted-theming/schemes) scheme: sixteen `base00`–`base0F` `#rrggbb` slots, plus optional `name`, `author` (a credit line carried through from an imported scheme), and `variant` (`"dark" \| "light"`, auto-detected from the tonal ramp when absent). A config written before base16 carried six semantic seed colors (`background`/`surface`/`foreground`/`primary`/`accent`/`border`) instead; that shape still parses and is upgraded to the sixteen slots automatically on read. Edit in **Settings → Plugins → Themes**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `appearance.sidebar.showHiddenFiles`     | boolean                                                                                                                         | `false`                                                   | project-local | Show files whose path segments start with `.`. The sidebar otherwise lists every file on disk under the content directory; tooling internals (`.git/`, `.ok/`, `node_modules/`) stay hidden regardless. Toggled from the sidebar's right-click menu or the **View → Show Hidden Files** menu item.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `appearance.sidebar.showSkillsSection`   | boolean                                                                                                                         | `true`                                                    | project-local | Show the **Skills Studio** dock at the bottom of the file sidebar. Toggled from the sidebar's tree view options or the **Skills section** palette command. See [Skills Studio](https://openknowledge.ai/docs/features/skills).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `appearance.sidebar.showSkillGroups`     | boolean                                                                                                                         | `true`                                                    | project-local | Group skills in the dock by where they came from: the publisher they were imported from, or the plugin that ships them. Skills you authored stay ungrouped at the top of their scope. Toggled with **Group skills by source** in the tree view options.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `appearance.sidebar.pinnedProjectSkills` | string\[]                                                                                                                       | `[]`                                                      | project-local | Project-scope skills pinned to the top of the dock, by name. A pinned skill keeps its normal row too, so a source group still lists everything from that source.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `appearance.sidebar.pinnedGlobalSkills`  | string\[]                                                                                                                       | `[]`                                                      | user          | Global-scope skills pinned to the top of the dock, by name. Stored per user rather than per project, so a pinned global skill follows you into every project.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `autoSync.mode`                          | `"off" \| "follow" \| "full" \| null`                                                                                           | `null`                                                    | project-local | Per-machine sync mode for this project: `off` (shown as **Manual** — nothing scheduled; the manual pull/push actions still work), `follow` (shown as **Auto (Pull only)** — pull remote changes on a schedule, never push; the earlier value `pull` is accepted as an alias), `full` (shown as **Auto (Pull and Push)** — bidirectional). `null` means "unanswered"; the editor's onboarding modal triggers on first remote-detected open. Supersedes `autoSync.enabled`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `autoSync.pullIntervalSeconds`           | `number`                                                                                                                        | `30`                                                      | project-local | Seconds between scheduled pulls while `autoSync.mode` is `follow` or `full` (30–3600). Shown in **Settings → Sync** as **Check for updates every**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `autoSync.pushIntervalSeconds`           | `number`                                                                                                                        | `60`                                                      | project-local | Seconds between scheduled pushes while `autoSync.mode` is `full` (30–3600). Shown in **Settings → Sync** as **Push my edits every**. Ignored in every other mode, which never pushes on a schedule. Each cycle with pending edits authors a commit, so a shorter interval means more, smaller commits in shared history.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `autoSync.enabled`                       | `boolean \| null`                                                                                                               | `null`                                                    | project-local | Legacy per-machine auto-sync toggle, superseded by `autoSync.mode`. Read only when `mode` is absent (`true` = full, `false` = off). `null` means "unanswered".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `autoSync.default`                       | `"off" \| "follow" \| "full" \| boolean \| null`                                                                                | `null`                                                    | project       | Project seed for each machine's first-open sync mode: `off` / `follow` / `full`, or the legacy boolean (`true` = full, `false` = off). `null` = ask. Lets a maintainer pre-answer the onboarding prompt for everyone who clones, but only when **Config sharing** is set to **Shared** so `.ok/config.yml` is committed; with **Only me** the value stays on this machine. A per-machine `autoSync.mode` overrides it. Compatibility note: a committed string value (`off` / `follow` / `full`) is rejected by app versions released before `autoSync.mode` existed, resetting that machine to config defaults until it updates (it never silently syncs). The legacy boolean seed (`true` / `false`) stays readable by older apps — commit `follow` only once collaborators are on a current version.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `git.hosts.<host>.provider`              | `"github"`                                                                                                                      | (unset)                                                   | user          | Declares that `<host>` is a GitHub Enterprise Server instance, so OpenKnowledge treats remotes on it the way it treats `github.com`: OpenKnowledge's own credential handling, the push-permission check, share links, and the default host for `ok auth`. `github` is the only accepted value today. Shown in **Settings → Git** as **GitHub Enterprise Server hosts**, which adds and removes entries for you. A host that is neither `github.com` nor declared here is treated as a generic git remote, so OpenKnowledge leaves the machine's own git credentials alone, unless a token is stored for that host and the remote uses `https:`, in which case the running server's sync uses the token, and reports push permission as unknown instead of pausing sync. Per-machine: one entry covers every project on this machine, and a declaration committed in a project's `.ok/config.yml` is ignored. Read at server start. Matching uses the hostname without its port, across transports. Without a running server, `ok sync`, `ok pull` and `ok push` use OpenKnowledge's sign-in only for an `https://` remote that names the host in lowercase and with no port; any other form, even an explicit `:443`, keeps your own git credentials. Invalid entries produce config diagnostics without erasing valid siblings. Undeclared origins cause GitHub-specific CLI commands and project sign-in requests to refuse; CLI commands can still target a declared host with `--host`. |
| `server.bind`                            | list of addresses                                                                                                               | `["127.0.0.1"]`                                           | project-local | Addresses the server binds, e.g. `[127.0.0.1]` or `[0.0.0.0]`. The loopback default keeps everything off-machine out. A non-loopback bind additionally requires the `server.allowExternal` consent interlock. Per-machine — a value committed to `.ok/config.yml` is ignored (the exposing host would otherwise refuse to boot for every teammate who clones and runs locally, since the interlock needs per-machine consent that is never committed). Set it where the server is exposed: `OK_BIND` (space-separated), `ok start --bind <address>`, or `.ok/local/config.yml`. Lists replace, never merge. Read at server start.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `server.port`                            | number                                                                                                                          | (unset)                                                   | project       | TCP port the server listens on. Unset = a local start picks a free port dynamically (deployment platforms inject `PORT` instead). Read at server start.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `server.externalUrl`                     | string                                                                                                                          | (unset)                                                   | project       | Canonical external origin the server is reached at, e.g. `https://kb.example.com` — its host joins the Host/Origin allowlists and issued URLs. Setting it declares external exposure, which additionally requires the `server.allowExternal` consent interlock. Env: `OK_EXTERNAL_URL`; flag: `ok start --external-url <url>`. Read at server start.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `server.allowExternal`                   | boolean                                                                                                                         | `false`                                                   | project-local | Exposure consent interlock: a non-loopback `server.bind` (or external requests behind a proxy declared via `server.externalUrl`) is refused without it. Per-machine — consent never travels via git, clone, or share; containers consent via `OK_ALLOW_EXTERNAL=1` instead. There is no server-side auth: restrict who can reach the server at the edge.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `server.openBrowser`                     | boolean                                                                                                                         | derived: `true` loopback-only, `false` otherwise          | project-local | Open the editor in a browser once at start. The derived default means a laptop start pops the UI and an exposed or containerized server stays headless. Env: `OK_OPEN_BROWSER`; flag: `--no-open-browser`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `server.idleShutdown`                    | `"off"` or duration (`90s`, `30m`, `2h`)                                                                                        | derived: `30m` loopback-only & unexposed, `off` otherwise | project-local | Shut the server down after this long with no connected editor (WebSocket) clients. Remote MCP sessions are plain HTTP the timer cannot see, so an exposed deployment (`server.allowExternal` + `server.externalUrl`) derives `off` automatically. Set `off` manually (or `OK_IDLE_SHUTDOWN=off`) only in the residual case the derivation misses — a loopback server reached by remote agents with no `externalUrl` (e.g. behind a same-box reverse proxy) — or it tears down under them mid-session.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `editor.wordWrap`                        | boolean                                                                                                                         | `true`                                                    | user          | Soft-wrap long lines in the source-mode CodeMirror editor. A personal preference, not project-shared. Toggle from the Settings pane.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `editor.previewTabs`                     | boolean                                                                                                                         | `true`                                                    | user          | Reuse one replaceable preview tab for single clicks in the Files tree and Skills sidebar, so browsing does not accumulate tabs. Double-click the tab title, pin it, or choose **Keep open** from its context menu to make it permanent. Set `false` to open every click in its own tab. Rename transitions still reuse the current tab; back/forward navigation focuses the target's existing tab, or opens a permanent one when it is no longer open. A personal preference; toggle it from **Settings → Preferences → Preview tabs**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `appearance.preview.autoOpen`            | boolean                                                                                                                         | `true`                                                    | user          | Whether the agent should open or refresh the OpenKnowledge preview when it edits a doc through the MCP. Default `true` lets the agent route the preview by host capability: the host's in-app browser (Cursor preview pane, Codex's built-in browser, Claude Code Desktop) when one exists, the system browser otherwise. Set `false` to keep the agent's hands off your preview window. This is useful when you're already viewing the doc in OK Desktop, a browser tab on a second display, a non-default browser, or any flow where your extensions / accessibility tooling only work in your own browser. The agent then surfaces the URL on request but does not navigate. The change takes effect on the next preview-related tool call. Toggle from **Settings → Preferences → "Open preview when agent edits"**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `terminal.enabled`                       | `boolean \| null`                                                                                                               | `null`                                                    | project-local | Opt-out for the in-app terminal (a real OS shell at full user privilege). On by default; set `false` to disable it for this project on this machine.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `terminal.shell`                         | string                                                                                                                          | (unset)                                                   | project-local | Absolute Windows shell executable override, for example `C:\Program Files\PowerShell\7\pwsh.exe`. PowerShell, `cmd.exe`, and Git Bash (`bash.exe`) support OpenKnowledge-managed launches (agent chat tabs and fixed commands such as **Resolve in terminal** or **Install Slidev**) and dropped-file path insertion. A different existing executable remains active for plain terminal tabs and shows a capability notice; requested agent and command launches open only the plain shell without running the agent or command, and dropped-file paths are refused. Use `wsl` inside a supported shell rather than setting `wsl.exe` here. Agent-launch prompts are pasted without being submitted on Windows, so review the prompt and press Enter to send it. Empty or unset values use automatic discovery (PowerShell 7, Windows PowerShell, then `ComSpec` / `cmd.exe`). Unreadable, invalid, relative, or missing paths show a notice and fall back to discovery. Human-managed and never shared through git, clone, or sync.                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `slides.enabled`                         | boolean                                                                                                                         | `false`                                                   | user          | Whether the [Slidev plugin](https://openknowledge.ai/docs/plugins/slidev) appears in **Settings → Plugins**. Off by default. When on, a document whose frontmatter contains `slides: true` gets an **Open in Slidev** action that opens the deck in its own window, rendered by a [Slidev](https://sli.dev/) you install yourself — OpenKnowledge does not download or bundle one. Install it globally (`npm install -g @slidev/cli @slidev/theme-default`) or add those packages to the project (a project-local install takes precedence). Slidev themes ship separately from the CLI and a deck cannot open without the theme it declares. Desktop only; the action stays hidden until a `slidev` binary resolves.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `agents.autoApproveOkTools`              | boolean                                                                                                                         | `true`                                                    | user          | Auto-approve OpenKnowledge's own MCP tools for agents launched from the built-in [docked terminal](https://openknowledge.ai/docs/features/editor#terminal) (plus the `ok open` command on Claude) and for in-app Agents-panel chats with any ACP agent, so reading and writing the knowledge base runs without a per-call approval prompt. Destructive tools (`delete`, `move`, `share_link`, `install`, `import`) still prompt; other shell commands and non-OpenKnowledge file edits are untouched. Per-machine preference; toggle from **Settings → Terminal**. Best-effort per agent — Claude uses an allow/ask list, Codex uses its per-server `approve` mode (and only when its OpenKnowledge entry is already configured).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `agents.browserTools`                    | boolean                                                                                                                         | `false`                                                   | user          | Give in-app Claude Code and Codex chats a browser they can drive: each one started after you turn it on gets the [Playwright MCP](https://github.com/microsoft/playwright-mcp) server, so the agent can open pages, click, type, and take screenshots. It is launched with the `npx` on the agent's PATH from OpenKnowledge's own folder, so npm never uses the project's npm files; without a usable `npx`, or when the browser can't be set up, the chat starts without it and says so in the conversation. Needs Google Chrome and Node.js installed. The browser opens in its own window (headless on Linux with no display) with a fresh in-memory profile; everything the browser saves goes to a folder for the chat under `~/.ok/agent-browser/` (Beta: `~/.ok-beta/agent-browser/`) and is removed with the chat. Every browser action asks for approval, with no option to always allow it, unless the agent's own mode or permission settings skip approvals. Other agents' chats don't get it. Per-machine preference; toggle from **Settings → Preferences**.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `telemetry.localSink.enabled`            | boolean                                                                                                                         | `true`                                                    | project       | Write local diagnostic spans + logs under `.ok/local/` for `ok diagnose bundle` to collect. Local-only: nothing leaves the machine until you run `bundle`. Set `false` for sensitive workspaces.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `telemetry.localSink.spans.maxBytes`     | number                                                                                                                          | `52428800` (\~50 MB)                                      | project       | Max size of the local diagnostic spans file before it rotates.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `telemetry.localSink.logs.maxBytes`      | number                                                                                                                          | `26214400` (\~25 MB)                                      | project       | Max size of the local diagnostic logs file before it rotates.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `telemetry.localSink.attributeDenylist`  | `string[]`                                                                                                                      | 8 credential keys                                         | project       | Attribute keys whose values are redacted (`[REDACTED]`) before any local span/log is written. Extends the built-in denylist (`authorization`, `password`, `cookie`, etc.).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `telemetry.skillInstallReports.enabled`  | boolean                                                                                                                         | `false`                                                   | user          | Report installs of published skills to skills.sh so their public install count is accurate: the skill's name, its source repository, and which agent tools it was installed for, once per skill per machine (per project for a skill installed into a project). Never file contents, and never for a private or local source. Off by default; toggle from **Settings → Preferences → Count skill installs publicly**. `DO_NOT_TRACK=1` / `DISABLE_TELEMETRY=1` suppress it even when on.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `search.semantic.enabled`                | boolean                                                                                                                         | `false`                                                   | project-local | Add embeddings-based semantic ranking to the MCP `search` tool and a **By meaning** mode to the cmd-K omnibar. Default off. **When on and a key is set, the search query and matching page content are sent to the configured embeddings provider** (content egress). See [Semantic search](https://openknowledge.ai/docs/reference/configuration#semantic-search) below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `search.semantic.baseUrl`                | string                                                                                                                          | `"https://api.openai.com/v1"`                             | project-local | Base URL of the OpenAI-compatible embeddings API. Override to point at a self-hosted server (Ollama / vLLM / LM Studio) or another provider. The API key is **not** stored here — set it with `ok embeddings set-key`; it is sent to whichever endpoint this names.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `search.semantic.model`                  | string                                                                                                                          | `"text-embedding-3-small"`                                | project-local | Embeddings model id. Must be served by the provider at `baseUrl`. Changing it re-embeds the corpus.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `search.semantic.dimensions`             | number                                                                                                                          | (auto)                                                    | project-local | Optional output vector size. Omit (recommended) and the size is detected from the endpoint's first response and reused across restarts — that is what lets a non-OpenAI model work without knowing its size up front. Set a smaller value to shrink the on-disk cache, trading a little quality; an endpoint that ignores the request parameter then fails loudly instead of silently.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `search.semantic.similarityFloor`        | number                                                                                                                          | (unset)                                                   | project-local | Optional hard cutoff (0–1): drops semantic matches whose cosine similarity is below it. Retrieval is rank-based, so most setups leave it unset; set it only for a provider/model whose cosine scale you know.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `search.semantic.maxBatchSize`           | integer (1–2048)                                                                                                                | `96`                                                      | project-local | Maximum text chunks per indexing embeddings request. The ceiling follows [OpenAI's input-array limit](https://developers.openai.com/api/reference/resources/embeddings/methods/create); other providers may impose different limits. Lower values reduce peak memory use but require more requests. This changes transport batching only, not document chunking or vector identity. Applies to the next indexing pass; an in-progress pass finishes with its existing settings and retains its vectors.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `search.semantic.maxBatchChars`          | integer (1–16384000)                                                                                                            | `96000`                                                   | project-local | Approximate character budget per indexing embeddings request. OpenAI separately caps total input at **300,000 tokens per request**; this character budget does not enforce that limit, and high values can produce rejected requests. Other providers have their own limits. A single chunk larger than the budget is still sent alone; this setting does not split chunks or invalidate cached vectors. Applies to the next indexing pass.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `search.semantic.docTimeoutMs`           | integer (1–600000 milliseconds)                                                                                                 | `30000`                                                   | project-local | Timeout for each indexing request attempt (up to 10 minutes). A request can retry four times, and a failed grouped request can trigger individual-document requests. There is no total indexing-pass deadline; large timeouts can delay newly queued work for hours. Settings displays seconds. Applies to the next indexing pass and keeps cached vectors. Interactive query embeddings retain a fixed 8-second timeout per attempt, with up to four retries; this setting does not raise it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `autolinks`                              | list                                                                                                                            | `[]`                                                      | project       | Turn ticket references in Agents-panel chat messages into links. Each entry pairs a `prefix` (for example `PRD-`) with a `url` template containing `<num>` (for example `https://linear.app/inkeep/issue/PRD-<num>`); the prefix followed by digits links to that URL with the digits in place of `<num>`. Case-sensitive, and references inside code are left alone. An entry with an invalid `prefix` or `url` is skipped with a warning naming its position, and the other entries still apply. Qualified GitHub references (`owner/repo#123`) link without any configuration. Shared with collaborators.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `linkPreviews.enabled`                   | boolean                                                                                                                         | `true`                                                    | project-local | Show a rich preview card (site name, page title, description, favicon) when you hover an external link in the editor, and a status card when you hover a GitHub pull request or issue link in an agent chat. Default on (set to `false` to opt out). **When on, hovering an external link sends that link's URL to the destination site** to fetch its preview metadata, and hovering a GitHub reference asks your GitHub host's API for it, signed in as you when a GitHub sign-in is available — outbound egress, one request per previewed link. Previews of links to other documents in the project are read from the local index with no network request and are always on. See [Link previews](https://openknowledge.ai/docs/reference/configuration#link-previews) below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

## Removed keys

As new versions of OpenKnowledge are released, some keys are retired from the schema. If any of your config files contains a key the schema no longer reads, that key alone is dropped from the loaded config and reported with the setting that replaced it.

This applies to genuinely retired keys only. A file that cannot be trusted — invalid YAML, or a value that fails validation — is rejected and falls back to defaults. The embedding transport fields listed above handle invalid values individually by using that field's default while keeping the original file and other valid settings. `ok config validate` reports these as recovered values, with the affected field, its source location, and the default in use. Server logs warn once per affected field when that config is loaded or reloaded; subsequent searches do not repeat the warning.

Run `ok config validate` to see which keys are stale and what replaced each one. `ok config migrate` removes them for you; it defaults to every layer, so a bare run reaches whichever file the key lives in. Narrow it with `--scope project`, `project-local`, `user`, or `all` (the default), and preview with `--dry-run`. Migration deletes retired keys, but it never rewrites anything else on your behalf. A key whose replacement is not a one-to-one translation is removed and reported for you to recreate by hand.

## Folder properties

A folder's own metadata lives in that folder's `.ok/` directory, not in `config.yml`: its own open-shape frontmatter (any keys; `title` / `description` / `tags` are the conventional ones) in `.ok/frontmatter.yml`, and its templates in `.ok/templates/`. Folder properties describe only their own folder: they are not inherited by the docs inside it. (Templates do resolve leaf-to-root, so a root template is available in every subfolder.) Set them from the editor's **Folder properties** pane, or via the `edit({ folder })` / `write({ folder })` MCP tools. See [Folders and templates](https://openknowledge.ai/docs/advanced/folders-and-templates#folder-properties) for the full walkthrough.

## Ignore patterns

Path exclusions live in a project-root `.okignore` file using gitignore syntax. `.gitignore` rules are honored automatically alongside it, and a leading `!` re-includes a file `.gitignore` excluded. Edit patterns from the **Ignore patterns** section of the Settings pane, the right-click menu in the file tree, or by hand in any editor; the running server picks up changes without a restart. See [Ignore patterns](https://openknowledge.ai/docs/features/ignore-patterns) for the full UX.

## Precedence

CLI flags (when running the bundled `ok` CLI) > environment variables > project-local config > project config > user config > defaults. Leaf values at higher levels override the same leaf below; arrays replace, they don't concatenate.

## Desktop updates

Automatic updates have no `config.yml` key, because they describe the machine rather than the person: `config.yml` can follow you between machines, and this preference does not. Set it in **Settings → About & updates**.

| **Download and install updates automatically** | What happens                                                                                                                                                                                     |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| On (default)                                   | OpenKnowledge checks at launch and every hour and downloads new versions in the background. On macOS and Windows it installs them when you quit; on Linux it asks you to authorize each install. |
| Off                                            | OpenKnowledge never contacts the update server on its own: no check at launch or on the hourly schedule, no new downloads, nothing installed when you quit, and no update notices.               |

Turn it off on a machine whose software you don't control, such as a work laptop under IT policy. **Check for updates** still works while it is off; if it finds a newer version, it offers to open the releases page instead of downloading. Turning it back on checks right away.

A download already in progress when you turn it off still finishes, but it is not installed when you quit. On macOS, an update whose download had already finished before you turned it off may still be applied at the next launch, because macOS takes over a completed download.

## Environment variables

Most users never set these — the schema settings above cover the common cases. These have no `config.yml` key or Settings toggle; they are environment-only.

**Common:**

| Variable           | Sets                                                                                                                                                                                                                        |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HOST`             | Server bind host. The `--bind` flag wins over this.                                                                                                                                                                         |
| `PORT`             | Server bind port. The `--port` flag wins over this.                                                                                                                                                                         |
| `OK_MCP_AUTOSTART` | Set to `0` to stop MCP tool calls from auto-starting the project's OpenKnowledge server; tools error until you run `ok start`                                                                                               |
| `OK_FORCE_BROWSER` | Set to `1` so bare `ok` runs the browser server instead of opening the installed desktop app. `ok start` without `--mode app` runs the browser server either way, and `ok start --mode app` refuses to run while it is set. |
| `OK_FORCE_DESKTOP` | Set to `1` so bare `ok` and `ok start --mode app` open the installed desktop app even when output is not an interactive terminal, over SSH, or on Linux without a display. `OK_FORCE_BROWSER=1` takes precedence.           |
| `OK_LOG_LEVEL`     | Log verbosity (`info`, `debug`, `trace`). Falls back to `LOG_LEVEL` if unset.                                                                                                                                               |

**Advanced / operator:**

| Variable                          | Sets                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPEN_KNOWLEDGE_GITHUB_CLIENT_ID` | Override the GitHub OAuth App client ID                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `OK_SHOWALL_MAX_ENTRIES`          | Max entries the sidebar's disk-walk listing returns per level before it stops and marks the list truncated (default `50000`)                                                                                                                                                                                                                                                                                                                                                                                               |
| `OK_BRIDGE_TOLERANCE_TELEMETRY`   | Set to `1` to record each bridge-tolerance-class fire as a JSONL line in `<project>/.ok/local/tolerance-telemetry.jsonl` (opt-in, local-only, off by default; unstable diagnostic for triaging markdown-fidelity incidents). **Records doc paths in cleartext** — independent of `telemetry.localSink`'s `attributeDenylist`, which redacts span/log attributes, not this file. Deliberately outside the `.ok/local/telemetry/` subtree `ok diagnose bundle` harvests, so the unredacted paths never ship in a bug bundle. |
| `OTEL_SDK_DISABLED`               | OpenTelemetry OTLP push gate. Inverted sense: set to `false` to **enable** push (it is off by default).                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `OK_BUG_REPORT_INTAKE_URL`        | Base URL of the intake the desktop app uploads to: reports sent from **Help → Report a bug…**, and images attached to the feedback form. Defaults to `https://openknowledge.ai`. Must be `https:` — plain `http:` is accepted only for loopback hosts (local testing).                                                                                                                                                                                                                                                     |
| `OK_FEEDBACK_INTAKE_ORIGIN`       | Origin the optional [uninstall feedback](https://openknowledge.ai/docs/reference/what-open-knowledge-writes#what-leaves-your-machine) submission posts to (default `https://openknowledge.ai`). Must be `https:` — plain `http:` is accepted only for loopback hosts (local testing). An unusable value drops the submission rather than falling back to the default.                                                                                                                                                      |
| `OK_EMBEDDINGS_API_KEY`           | Fallback embeddings key for [semantic search](https://openknowledge.ai/docs/reference/configuration#semantic-search), used when no project key is stored **and only for the default OpenAI endpoint** (a machine-wide env key is never sent to a custom host). Prefer `ok embeddings set-key` for normal use; the env var is convenient for CI / scripted runs against OpenAI.                                                                                                                                             |

## Semantic search

The MCP `search` tool can fuse an **embeddings-based semantic signal** into its ranking, so an agent's query surfaces conceptually-related pages even when they share no keywords (a query about "auth retries" can surface a page titled "Session Token Refresh"). It is **off by default** and additive. With it off, search everywhere (the MCP tool and the cmd-K omnibar) is purely lexical and stays on-machine. When it is enabled, the omnibar's **By meaning** pill appears once the endpoint has a usable credential: a stored key, or a loopback endpoint that needs none (step 2 below). A remote endpoint without a stored key stays lexical and hides the pill. Typing never embeds; pressing Enter fires one semantic search, sending the query to the embeddings provider.

> **Warn**
>
> **Content egress.** When semantic search is enabled **and** a key is set (or the endpoint is a keyless local server), the search query and the matching page content are sent to whichever OpenAI-compatible endpoint the project has configured (OpenAI by default). Only content that is already in your corpus is embedded — anything excluded by `.okignore` / `.gitignore` is never sent — and embedding is lazy: nothing leaves the machine until a semantic search actually runs. Keys live only in a 0600 `~/.ok/secrets.yml` file (`~/.ok-beta/secrets.yml` in Beta), keyed by project + endpoint so a key never travels to a host it wasn't set for; never in `config.yml`, the project tree, logs, or telemetry.

To turn it on (per project, per machine):

1. Enable it for the project: the **Settings → This project → Search** toggle, or `ok embeddings enable`. Either sets `search.semantic.enabled: true` in project-local config (`<project>/.ok/local/config.yml`), picked up live by a running server; `ok embeddings disable` turns it off.
2. Set the API key right there on the same screen (or `ok embeddings set-key` in the project). Keys are **per project** — one project's key is never shared with another unless both point at the same endpoint. A **loopback** endpoint (Ollama / LM Studio) needs no key at all — the field shows "not required". `ok embeddings list` shows every stored key (redacted); `ok embeddings clear-key` removes this project's.
3. (Optional) point at your own OpenAI-compatible endpoint (a self-hosted Ollama / vLLM / LM Studio server, or another provider): open **Custom endpoint** in that same section, set the endpoint URL and the model id, and press **Test connection** — it runs one throwaway embed and reports either the detected vector size or the specific reason it failed. The same knobs are available from the CLI (`ok embeddings set-url <url>` / `clear-url`, `ok embeddings set-model <id>` / `clear-model`) and in project-local config (`search.semantic.baseUrl` / `model`). The vector size is detected automatically, so `dimensions` normally stays unset. Changing the endpoint or the model discards the cached vectors and re-embeds the corpus.
4. (Optional) if a slow local model needs smaller requests or more time, open **Embedding request settings** and explicitly override the text-chunk batch count, character budget, or indexing timeout. Most setups should keep the defaults (`96` / `96000` / `30000` ms). For example, one user might choose the following for a particularly slow local model — this is an override example, **not** a preset or general recommendation:

   ```yaml
   search:
     semantic:
       enabled: true
       baseUrl: http://localhost:11434/v1
       model: qwen3-embedding:4b
       maxBatchSize: 2
       maxBatchChars: 16000
       docTimeoutMs: 120000
   ```

   These three controls change HTTP request batching and the indexing timeout per attempt. They apply to the next indexing pass, preserving completed vectors and allowing a pass already in progress to finish with its existing settings. They do not change chunking, retries, vector identity, or the vector cache, and do not trigger a provider-change confirmation or a full re-embed. Clear a field in Settings to restore its default. Invalid transport values in hand-edited YAML fall back to that field's default while preserving the file and other valid settings. `ok config validate` identifies the recovered field, location, and default in use; server logs warn once per affected field on load or reload. `ok embeddings status` (or `--json`) shows the effective transport values.

   Indexing requests can retry up to four times, with backoff between attempts. If a grouped request fails, indexing can try each document separately, each with its own requests and retries. There is **no overall indexing-pass deadline**: at large timeout settings, a failing endpoint can hold up newly queued documents for hours. Lower the timeout when prompt failure and fresh indexing matter more than waiting for a slow endpoint.

   The character budget also does not count tokens. [OpenAI's hosted endpoint](https://developers.openai.com/api/reference/resources/embeddings/methods/create) limits all inputs in a request to 300,000 tokens in total, alongside its per-input limits. A large character budget can exceed those limits; the ratio of characters to tokens varies by text and model. Check your provider's limits before raising both batch controls.

   Interactive query embeddings keep a fixed **8-second timeout per attempt**, with up to four retries. Raising `docTimeoutMs` does not extend this query timeout, so a slow model can still fall back to lexical search. **Test connection** uses a separate 10-second attempt with no retries: success confirms connectivity and a valid response, but does not guarantee that live queries will meet their shorter deadline.

The first semantic search kicks off a background embed of the corpus (cents for a whole vault with `text-embedding-3-small`); vectors are cached incrementally under `.ok/local/` and only changed docs re-embed. The omnibar's default lexical mode never uses embeddings, with or without a key. If a semantic MCP/API request has no usable key, its ranking stays lexical. The omnibar's **By meaning** mode distinguishes indexing, no match, a query under 3 characters that never reaches the provider, an unavailable provider configuration, and retryable provider failures. Repeated changes to the provider's vector size turn semantic search off until OpenKnowledge restarts. If a provider ignores a configured size, remove `search.semantic.dimensions` from project-local `.ok/local/config.yml` to use the model's own size; a running server applies the change without a restart. When cached vectors are available, a semantic search waits for the query embedding and any retries before returning, so an unresponsive provider can delay results. **Test connection** helps diagnose endpoint failures using the separate probe deadline described above. Each MCP `search` response carries the same semantic outcome and embedding coverage, plus a one-line `> Semantic:` note that tells an agent whether to retry, wait for indexing, change configuration, or restart.

## Link previews

Hovering a link in the editor shows a preview card. For **internal links** (other documents in your project) the card — title, folder, tags, last-edited time, backlink count, and a short excerpt — is built entirely from the local index and file contents: always on, no configuration, and nothing leaves the machine.

For **external links**, a card shows the destination's site name, page title, description, and favicon. It is **on by default**; turn it off per machine in Settings.

In an **agent chat**, the same setting covers links to GitHub pull requests and issues: hovering one shows its title, state, and author, and a pull request also shows its lines changed and whether it can merge. The server asks github.com's API, or your GitHub Enterprise host's for hosts declared under `git.hosts`, using your GitHub sign-in when there is one (the `gh` CLI's login, then the account you connected in OpenKnowledge). With a sign-in, an open pull request also shows what it is waiting on, such as its place in the merge queue, a requested review, or failing checks. Answers are kept briefly in memory, and a reference the server can't read stays a plain link.

> **Info**
>
> **Desktop app:** external link previews and GitHub reference cards are not yet available in the packaged desktop app (its `file://` renderer is rejected by the preview routes' anti-proxy gate), so this default applies to the browser (`ok start`) surface. Internal document-to-document previews work everywhere.

> **Warn**
>
> **Egress.** When `linkPreviews.enabled` is on, two kinds of hover make a request from your machine. Hovering an external link sends that link's URL to the destination site — one metadata request per previewed link, with no cookies or credentials attached. Responses are size- and time-capped, results are cached locally under `.ok/local/`, and requests that resolve to private or internal addresses are refused. Hovering a GitHub pull request or issue link in an agent chat sends the repository and number to github.com's API, or to your GitHub Enterprise host's, **with your GitHub sign-in when one is available**, so private repositories you can read preview too; the sign-in is only used for requests from this machine, never for a remote-access client. Internal-link cards never touch the network.

To turn it off (per machine): the **Settings → This project → Link previews** toggle, which sets `linkPreviews.enabled: false` in project-local config (`<project>/.ok/local/config.yml`), picked up live by a running server. The setting is enforced by the local server, so with it off no external request is made. A preview that can't be fetched (offline, timeout, the site blocks it) quietly falls back to the plain URL pill.

## Verifying changes

The Settings pane validates `.ok/config.yml` against the schema as you edit and surfaces errors inline. The **Ignore patterns** section shows a live count of files each pattern matches, so you can confirm scope changes without leaving the editor.