OpenKnowledge

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)
  • User: ~/.ok/global.yml (every project on your machine)
  • 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.

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)
  • 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.

FieldTypeDefaultScopeDescription
content.dirstring"."projectContent 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.
content.attachmentFolderPathstring"./"projectWhere 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.
contentRules.markdownlint.enabledbooleanfalseprojectEnable the 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.enabledbooleanfalseprojectEnable the Frontmatter schemas content-rules plugin for this project (shared via git). Off by default — turn it on in Settings → This project → Plugins.
contentRules.frontmatter.schemasarray[]projectFrontmatter 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.
appearance.theme"light" | "dark" | "system"(unset)userEditor light/dark theme.
appearance.language"system" | "en" | "zh-Hans" | "zh-Hant" | "hi" | "es" | "ar" | "fr" | "bn" | "pt-BR" | "id" | "ur"(unset)userInterface 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 nine of them (English, 简体中文, 繁體中文, हिन्दी, Español, Français, বাংলা, Português do Brasil, Indonesia). 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. 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.colorThemeLighttheme id ([a-z0-9-], 1–32 characters)(unset)userIDE 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 with the tile's sun.
appearance.colorThemeDarktheme id ([a-z0-9-], 1–32 characters)(unset)userIDE 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.colorThemetheme id ([a-z0-9-], 1–32 characters)(unset)userLegacy 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.colorThemeEnabledbooleantrueuserWhether the Themes plugin appears in Settings → Plugins. On by default (absent = enabled).
appearance.customThemeobject(unset)userThe custom palette's base16 scheme: sixteen base00base0F #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.showHiddenFilesbooleanfalseproject-localShow 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 macOS View → Show Hidden Files item.
autoSync.mode"off" | "follow" | "full" | nullnullproject-localPer-machine sync mode for this project: off (no sync), follow (one-directional — pull remote changes, never push your own; the earlier value pull is accepted as an alias), full (bidirectional pull and push). null means "unanswered"; the editor's onboarding modal triggers on first remote-detected open. Supersedes autoSync.enabled.
autoSync.enabledboolean | nullnullproject-localLegacy 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 | nullnullprojectCommitted 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; 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.
remote.urlstring(unset)projectPublic tunnel URL remote clients connect to (e.g. https://myproject.ngrok.app); its host is admitted through the Host-header allowlist. Used only when the server is started with ok start --remote (config alone never enables remote access, and there is no server-side auth). See Remote access.
remote.portnumber24550projectFixed TCP port the server binds when remote access is enabled, so the tunnel's port mapping survives restarts. See Remote access.
editor.wordWrapbooleantrueuserSoft-wrap long lines in the source-mode CodeMirror editor. A personal preference, not project-shared. Toggle from the Settings pane.
editor.previewTabsbooleantrueuserReuse 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.autoOpenbooleantrueuserWhether 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.enabledboolean | nullnullproject-localOpt-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.
slides.enabledbooleanfalseuserWhether the Slidev plugin 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 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.autoApproveOkToolsbooleantrueuserAuto-approve OpenKnowledge's own MCP tools (and the ok open command on Claude) for agents launched from the built-in docked terminal, so reading and writing the knowledge base runs without a per-call approval prompt. Destructive tools (delete, move, share_link, install) 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/deny list, Codex uses its per-server approve mode (and only when its OpenKnowledge entry is already configured).
telemetry.localSink.enabledbooleantrueprojectWrite 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.maxBytesnumber52428800 (~50 MB)projectMax size of the local diagnostic spans file before it rotates.
telemetry.localSink.logs.maxBytesnumber26214400 (~25 MB)projectMax size of the local diagnostic logs file before it rotates.
telemetry.localSink.attributeDenyliststring[]8 credential keysprojectAttribute keys whose values are redacted ([REDACTED]) before any local span/log is written. Extends the built-in denylist (authorization, password, cookie, etc.).
search.semantic.enabledbooleanfalseproject-localAdd 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 below.
search.semantic.baseUrlstring"https://api.openai.com/v1"project-localBase 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.modelstring"text-embedding-3-small"project-localEmbeddings model id. Must be served by the provider at baseUrl. Changing it re-embeds the corpus.
search.semantic.dimensionsnumber(auto)project-localOptional 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.similarityFloornumber(unset)project-localOptional 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.
linkPreviews.enabledbooleantrueproject-localShow a rich preview card (site name, page title, description, favicon) when you hover an external link in the editor. 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 — 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 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.

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 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 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.

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:

VariableSets
HOSTServer bind host. The --host flag wins over this.
PORTServer bind port. The --port flag wins over this.
OK_MCP_AUTOSTARTSet to 0 to stop MCP tool calls from auto-starting the project's OpenKnowledge server; tools error until you run ok start
OK_LOG_LEVELLog verbosity (info, debug, trace). Falls back to LOG_LEVEL if unset.

Advanced / operator:

VariableSets
OPEN_KNOWLEDGE_GITHUB_CLIENT_IDOverride the GitHub OAuth App client ID
OK_SHOWALL_MAX_ENTRIESMax entries the sidebar's disk-walk listing returns per level before it stops and marks the list truncated (default 50000)
OK_BRIDGE_TOLERANCE_TELEMETRYSet 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_DISABLEDOpenTelemetry OTLP push gate. Inverted sense: set to false to enable push (it is off by default).
OK_BUG_REPORT_INTAKE_URLBase URL of the bug-report intake the desktop app's Help → Report a bug… upload posts to (e.g. https://openknowledge.ai). Must be https: — plain http: is accepted only for loopback hosts (local testing). Unset (the default), Send makes no network request — it opens a prefilled email draft to support@inkeep.com that you send yourself.
OK_FEEDBACK_INTAKE_ORIGINOrigin the optional uninstall feedback 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_KEYFallback embeddings key for 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.

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. With it on, the omnibar gains a By meaning mode alongside its default lexical mode: typing never embeds; pressing Enter fires one semantic search, sending the query to the embeddings provider.

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, 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 localhost 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.

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. If the key is missing, the provider errors, or you're offline, search degrades to lexical — it never blocks or errors. Because that degradation is quiet, Test connection is the way to tell a working custom endpoint apart from one that is failing. Each MCP search response reports embedding coverage so an agent knows when vectors are still filling in.

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.

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

URL egress. When linkPreviews.enabled is on, hovering an external link sends that link's URL to the destination site — one metadata request per previewed link, from your machine, 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. Nothing else is sent: 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.