| name | open-knowledge |
| description | MUST invoke before reading or editing any `.md` / `.mdx` file, and before any `mcp__open-knowledge__*` tool call (`exec`, `search`, `write_document`, `edit_document`, and the rest). This skill is installed into the repository by `ok init`, so its presence alone means this is an Open Knowledge project — its runtime contract governs every markdown file here, with no need to probe for a `.ok/` directory. Authoritative agent-runtime contract; supersedes the overlapping MCP server `instructions` echo. |
| compatibility | Claude Code, Claude Desktop, Claude Cowork, Claude.ai web. Requires Open Knowledge MCP server + code execution. |
| metadata | {"version":"0.6.0","author":"Inkeep","repository":"https://github.com/inkeep/open-knowledge"} |
Open Knowledge — agent guidance
Open Knowledge (OK) is a markdown-CRDT collaboration platform exposed via MCP. This skill carries the behavioral rules agents need to use it fluently. Every section is a MUST unless marked otherwise.
Authoritative source. Where the MCP server's instructions echo overlaps with this skill, this skill wins — the full attach rule, grounding rule, media rules, dead-link verification, and failure-mode guidance live only here.
Skill version: tracks @inkeep/open-knowledge-server package version. Check cat ~/.ok/skill-state.yml to see what's installed locally. Version floor: ok seed (referenced below) requires @inkeep/open-knowledge >= 0.4.0. If ok seed errors with unknown command, upgrade: npm install -g @inkeep/open-knowledge.
TL;DR — the 90% case
- Reads:
exec("cat …") for a single doc, exec("ls -A …") for a directory (with folder defaults + template menu), exec("grep …") for literal, search for ranked retrieval. Native Read / Grep only on source code (.ts / .py / …), never on in-scope .md / .mdx.
- Writes:
write_document for new or full-replace, edit_document for body-only find/replace, edit_frontmatter for 1-2 frontmatter keys (JSON Merge Patch — preferred). Full frontmatter rewrites use write_document({ position: "replace" }). edit_document rejects frontmatter (HTTP 400).
- Preview: every OK read/write response carries a route-only
previewUrl (/#/<doc>, no host:port). If you have a preview_* tool, call preview_start("open-knowledge-ui"); otherwise call get_preview_url once for the full browser URL and open it in your own in-app browser. Surface to the user on a start-ui warning (no UI running). Don't preview_screenshot to confirm edits — the CRDT tool response is the confirmation.
- Workflow tools (
ingest / research / consolidate / discover) return procedural guides, not data. Use them when the work fits the layer; follow their numbered steps.
Everything below is depth. Read on demand.
Tool index — 22 tools
The full MCP surface, grouped by risk-level. Every tool's kind / action set is single-risk-level (never a read and a write behind one discriminator).
- Reads —
exec (primary; shell-style cat/ls/grep/find with frontmatter + backlink + history enrichment), search (ranked, BM25 + recency), get_history (versions for a doc), links (kind: 'backlinks'|'forward'|'dead'|'orphans'|'hubs'|'suggest'), get_config (resolved config), get_components (canonical component JSX schemas), get_authoring_palette (markdown-native authoring forms + themed html preview embed starters + theme tokens), get_preview_url (browser-reachable preview URL on demand).
- Writes —
write_document (new or full-replace; supports template: instantiation), edit_document (body-only find/replace), edit_frontmatter (1-2 keys via RFC 7396 JSON Merge Patch — preferred), delete_document, rename (probes file vs folder; rewrites referrers), version (action: 'save'|'rollback'), folder_config (action: 'set-rule'|'write-template'|'delete-template').
- GitHub-sync conflicts —
list_conflicts (enumerate), get_conflict_content (base/ours/theirs stages + lifecycle), resolve_conflict (write a chosen resolution + commit; destructive). Mutating writes against a doc in conflict return RFC 9457 urn:ok:error:doc-in-conflict (409); exec("cat …") returns lifecycle: {status, reason} | null so you can detect the state proactively. See Conflict-aware writes.
- Workflow —
ingest, research, consolidate, discover (return procedural guides, not data).
Tools NOT in OK MCP (they belong to your agent host): preview_start, preview_screenshot, WebFetch, WebSearch, native Read / Grep / Glob / Edit. The STOP rule below governs which of those you may use on in-scope markdown.
STOP — native tools on in-scope .md / .mdx
When this workspace has Open Knowledge MCP configured, do not use your host's native file tools on markdown paths inside the content directory. The ban covers every common rationalization:
- Native
Read / Grep / Glob on in-scope .md / .mdx — the original case.
Bash ls / Bash find / Bash cat on dirs containing in-scope markdown — use exec("ls -A …") / exec("find … -name '*.md'") / exec("cat …") instead. Native returns bare names; exec returns frontmatter, backlink counts, and recent activity per child. -A shows hidden entries (.ok/, .okignore) which OK projects carry; omit . and .. rows that -a would add.
- Glob patterns that target markdown (
**/*.md, any dir known to be markdown-heavy like specs/**, reports/**, docs/**) — use exec with find, or exec("ls -A <dir>").
- Dispatching the Explore / general-purpose subagent for markdown-heavy exploration — subagents use native
Read / Grep / Glob internally and bypass Open Knowledge entirely. Do markdown exploration yourself via exec / search. Subagents remain appropriate for source-code exploration.
- Native
Read / Grep on any in-scope markdown inside .ok/ — the .ok/ directory is in-scope; if it carries .md / .mdx, treat those the same as any other knowledge-base file.
Why: native tools skip frontmatter, backlinks, shadow-repo activity, and project git history that OK's tools return for every matched knowledge-base file. exec is the primary read surface; it runs read-only bash (cat, ls, grep, find, head, tail, wc, sort, uniq, cut — pipes OK) and returns raw stdout plus enriched metadata per file.
MCP tool visibility — not seeing exec is NOT the escape hatch. MCP wiring varies by client. Claude Code, Cursor, Codex, Windsurf, VS Code — each surfaces MCP differently. Server labels are user-defined; tools may not appear as top-level symbols named exec in your specific UI. If Open Knowledge is registered as an MCP server in this workspace, route markdown reads through its exec / search via your client's documented MCP invocation (including any generic "call MCP tool" flow). Registration is the test, not top-level-symbol visibility.
Escape hatch. Native Read / Grep / Glob on .md / .mdx is allowed only when no Open Knowledge MCP server is registered for this project, or immediately after you tried an MCP call and it failed — then begin a user-visible sentence with Open Knowledge MCP unavailable:. Never use the hatch because you skipped your client's MCP path, didn't see exec as a top-level tool, or rationalized the skill wasn't necessary.
Source code and non-markdown files (.ts, .py, package.json, …): native Read / Grep / Glob always.
Reads — examples
- Read a file:
exec("cat <path>.md") — contents + full rich enrichment.
- List a directory:
exec("ls -A <dir>") — per-child frontmatter, recursive markdown counts, most-recently-updated doc per subdir, folder-level frontmatter_defaults + templates_available. Prefer -A over plain ls to surface dot-prefixed entries (.ok/, .okignore) without the noisy ./.. rows that -a adds.
- Literal search:
exec("grep -rn <term> <dir> | head -5") — matches + enrichment on matched files.
- Ranked search:
search({ query }) — cmd-K parity (title boost + body BM25 + recency); use when picking the best doc, not when listing every occurrence.
Preview — open the browser at session start
The user watches your edits land in a live browser preview. Open it once at session start, then keep working. Re-navigate only when the user asks to open a different doc, or to land them on a finished deliverable (see below) — not to re-check your own edits.
End a turn on the deliverable, not your scratch space. Keep the preview steady during a multi-doc task — don't yank it around to re-check your own edits. But when a turn created or substantially changed user-facing docs, navigate the preview to the primary deliverable before you hand back: the hub / overview / index page when you created several docs, or the changed doc when you changed one. Don't step the user through every supporting source card — the user is watching, so leave them on the result.
previewUrl is a route, not a URL to open. Every read response (per-doc, on exec / search / links rows) and every write response carries a previewUrl — a route fragment like /#/specs/foo/SPEC, with no scheme, host, or port. It identifies which doc to preview, not a URL to hand a browser by itself. Never construct or guess preview URLs.
Pick how to open the preview by tool capability — not by host name. Look at the tools actually available to you this session:
- You have a
preview_* tool (e.g. preview_start) → call preview_start("open-knowledge-ui") and nothing else. The pane supplies its own origin; you never need a URL. Do NOT also call get_preview_url. Do NOT read, diagnose, or edit .claude/launch.json — that file is host-managed, and the OK lock-collision proxy handles the UI-already-running case automatically. If preview_start fails, report it to the user; do not try to "fix" launch.json.
- No
preview_* tool, but you have an in-app / built-in browser tool → call get_preview_url once to get the full browser URL ({ url, baseUrl, running }), then navigate your in-app browser to url. Pass a docName to get_preview_url to jump straight to a doc, or omit it for the UI root.
- No browser tool at all (a stdio host such as the Codex CLI, which has no embedded browser) → call
get_preview_url, then open <url> in the system browser as a last resort, and say so plainly to the user ("opening the preview in your system browser — this host has no in-app browser"). The system browser is the fallback, never the default.
Opening or reading a file IS a preview navigation. On any "open <file>" / "read <file>" request, navigate the browser to that doc's previewUrl route from the tool response — not a separate fetch, not a fresh system-browser launch.
Four signals to check if the preview is already attached (read these from each write response):
- You opened/navigated earlier this session → don't reopen.
- Write response has
previewUrl (non-null route) and NO warning → a browser is attached somewhere; do nothing.
warning: { action: "attach-preview-once", previewUrl, message } → UI reachable, no browser attached; navigate one-shot (preview_start, or get_preview_url → in-app browser).
warning: { action: "start-ui", previewUrl: null, message } → no UI running anywhere. Surface the message verbatim — recovery options are in the in-band copy. Don't loop on retries.
Warnings fire at most once per session in the fresh-start case.
previewUrl: null only means "no UI reachable" on the three attach-warning tools: write_document / edit_document / edit_frontmatter. Workflow tools return prose and don't carry previewUrl. delete_document / rename emit previousPreviewUrl (different field, for closing stale tabs) and don't fire attach warnings. get_preview_url reports running: false + url: null when no UI is running.
If you see "Hocuspocus server is not running", run ok start and retry.
OK Electron and ok ui share ui.lock; when a second UI binds a different port, the OK lock-collision proxy bridges it to the live server transparently. That is exactly why previewUrl is route-only — the port behind the proxy is not the agent's to use. Do not nudge the user to quit OK Electron to free a port — the proxy handles it, and quitting tears down a UI in active use.
The preview is read-only for the agent — it is the user's view, not a surface you read back. You cannot click or type to drive edits — the CRDT flow is one-way (agent → MCP → CRDT → preview).
No screenshots to confirm edits, no generic verification loop. Do NOT take preview_screenshot (host tool, not OK MCP) after a write, and do not run a generic snapshot/eval/screenshot verification loop — OK's preview is a read-only, one-way mirror, so the CRDT tool response is the confirmation that an edit landed. Screenshot only when debugging a visual rendering issue or when the user explicitly asks to see the preview — never to confirm an edit landed.
Writing
Call write_document / edit_document as soon as you have content. Native Edit / sed / direct Write on in-scope markdown is forbidden — it bypasses the CRDT and loses agent attribution in the shadow repo.
To delete a doc, call delete_document — never rm / unlink / native Bash removal on in-scope markdown. The MCP path closes open agent sessions and unloads the doc from Hocuspocus before unlinking; native rm desynchronizes those. Deletion is irreversible — call version({ action: "save" }) first if you may need to roll back (restore via version({ action: "rollback" }); list snapshots via get_history), and links({ kind: "backlinks", docName }) first if you want to fix referrers that will become redlinks. To move or rename a doc instead of delete + rewrite, use rename({ from, to }) — it auto-detects file vs folder and rewrites incoming references atomically.
If edit_document returns "Text not found" on text you can verify exists on disk (via exec("cat …")), the MCP session is likely stale (e.g., after a folder rename or server restart). Treat this as the escape-hatch trigger from the STOP block: prefix your next user-visible sentence with Open Knowledge MCP unavailable: and report the inconsistency. Don't loop on retries — the symptom is structural, not transient.
Conflict-aware writes
Projects with GitHub sync enabled may carry docs in a merge-conflict state. The MCP server refuses every mutating call against such a doc with a structured RFC 9457 response:
{
"type": "urn:ok:error:doc-in-conflict",
"title": "Document is in conflict.",
"status": 409,
"detail": "The document is in a merge-conflict state. Call get_conflict_content + resolve_conflict before retrying.",
"file": "notes/sso.md",
"resolutionOptions": ["mine", "theirs", "content"]
}
The gate covers write_document, edit_document, delete_document, rename, version (action: 'rollback'), folder_config (action: 'write-template'|'delete-template'), and agent undo. You cannot route around it by writing content that byte-matches one of the merge stages — the gate refuses on lifecycle state, not on body equality.
Detect proactively. exec("cat <path>.md") always returns lifecycle: {status, reason} | null alongside the body. When status === 'conflict', switch to the resolution flow before attempting any mutation.
Resolution flow. Three tools compose:
list_conflicts() → enumerate every doc currently tracked in conflict.
get_conflict_content({ file }) → fetch base / ours / theirs stages plus the doc's lifecycleStatus. ours reflects the live Y.Text (what the human user sees in the editor) when the doc is loaded server-side and is marker-free; falls back to git show :2:<file> otherwise (e.g. after an editor reopen seeded markers into Y.Text).
resolve_conflict({ file, strategy, content? }) → write the chosen bytes and commit. Strategies: mine writes git show :2:, theirs writes git show :3:, content writes the bytes you supply.
file is a .md / .mdx path relative to the project dir (extension included) — mirrors the on-disk shape, not the extension-less docName used by other tools.
The resolve operation is best-effort and NOT atomic: git checkout --ours/--theirs && git add may succeed but the subsequent git commit --no-edit can fail (pre-commit hook rejection, locked index). On commit failure the staged files are re-git add-ed back into the unmerged index and the tracked entry remains in conflicts.json — re-call resolve_conflict after the user clears the blocker.
Components — write the markdown-native form, not JSX
OK auto-promotes markdown-native syntax into themed canonical components at parse time. Write the markdown-native form — don't reach for JSX when one exists. The promoted component is themed, accessible, and part of the content graph; hand-rolled JSX is none of those, and it fights the model's markdown prior instead of using it.
| Want | Write this (markdown-native) | Promotes to |
|---|
| Callout / admonition | > [!NOTE] + body — 15 types (NOTE, TIP, IMPORTANT, WARNING, CAUTION, …); append + / - (> [!NOTE]+) to make it foldable | themed Callout |
| Collapsible section | <details><summary>Title</summary> … </details> | themed Accordion |
| Diagram | a ```mermaid fenced block (flowchart, sequence, class, state, ER, gantt, pie) | Mermaid diagram |
| Math | $x$ inline, $$…$$ block | KaTeX Math |
| Inline a doc or asset | ![[file]] | wiki embed |
Tabs is the lone canonical with no markdown-native form — write the JSX directly (<Tabs><Tab label="…">…</Tab></Tabs>). For any canonical's full JSX prop schema, call get_components({ ids }). If no canonical fits, any <TagName>…</TagName> falls through as raw MDX — but prefer a canonical when one matches.
Discover the palette in one call. get_authoring_palette returns every markdown-native form (copy-ready example + guidance), the themed html preview embed starters, and the injected theme-token list — the source of truth for component-forward, themed authoring. Canonical names/counts beyond the markdown-native set are project-specific; the inventory in the write_document / edit_document descriptions and get_components are authoritative for those.
Show findings, don't just tell them. When a point is quantitative or comparative — a trend over time, a breakdown, a before/after, a ranking, a distribution — present it visually: a chart or stat-card html preview embed, a ```mermaid diagram, a table, or a Callout for the headline takeaway. Prose-only buries the insight. This matters most where the document's job is to make findings legible — research reports and consolidate articles especially, and any write-up meant to present results. A research article with three dense paragraphs of numbers should have been a chart. Reach for get_authoring_palette as you draft, not after.
html preview — themed interactive embeds
A ```html preview fence (also htm / xml) renders a standalone HTML/CSS/JS page as a live sandboxed iframe — the extend-to-anything primitive for charts, stat cards, custom SVG, calculators, demos. The iframe auto-sizes to its content; pass h= / w= (e.g. ```html preview h=400px) only to pin a fixed size.
Start from a starter — don't hand-roll. get_authoring_palette returns embedPatterns (chart, stat cards, custom SVG, interactive control), each already wired to the theme tokens. Copy one and fill in your data — that is the only path that cannot render unthemed. Hand-author a fence from scratch only when no starter is close.
MUST — never hardcode colors in an html preview embed. OK injects its theme tokens into every preview iframe; an embed that hardcodes hex / rgb() renders unthemed — a white box on a dark page, clashing with every component around it. This is the single most common embed mistake. Wire every color to a token: var(--chart-1..5) for chart series, var(--foreground) / var(--muted-foreground) for text, var(--card) / var(--background) for surfaces, plus var(--border), var(--primary), var(--radius). Don't set a body background at all unless you specifically mean to — the iframe already carries a themed one.
```html preview
<div style="font-family:system-ui;padding:20px;color:var(--foreground)">
<h3 style="margin:0 0 10px">Themed embed</h3>
<div style="display:flex;gap:8px">
<div style="flex:1;height:48px;background:var(--chart-1);border-radius:var(--radius)"></div>
<div style="flex:1;height:48px;background:var(--chart-2);border-radius:var(--radius)"></div>
<div style="flex:1;height:48px;background:var(--chart-3);border-radius:var(--radius)"></div>
</div>
</div>
```
Done wrong, that same embed is body{background:#fff;color:#1a1a1a} with a background:#2563eb bar — a white box with a hardcoded blue, blind to the reader's theme.
Charts. A pure-CSS or inline-SVG chart wired to var(--chart-*) re-skins on a theme toggle for free — prefer it. A JS charting library (Chart.js, D3) works too, but a themed body does NOT theme the colors you pass the library in JS — read the token at runtime instead of hardcoding:
const c1 = getComputedStyle(document.documentElement).getPropertyValue('--chart-1').trim();
Boundary. Reach for a canonical (via its markdown-native form) when one matches the semantic need — it is themed and integrated. Reach for ```html preview for interactive or bespoke content no canonical covers. ```<lang> fences for other languages are plain syntax-highlighted code, no preview.
Grounding — every factual claim needs a source (MUST)
Knowledge-base docs are factual artifacts — whether the project is a wiki, an LLM brain, a spec collection, a research log, or anything else markdown-shaped. Every claim must be traceable, and the source has to live inside the knowledge base, not float on the public web.
- The knowledge base is source-of-truth — closed loop. External sources don't get cited out to the live web; they get pulled in via
ingest, then cited locally. A bare [source](https://...) URL inside a knowledge-base doc is not a finished citation — it's a TODO that says "this source still needs to be ingested." The chain only works if every leaf is a local doc.
- Every factual claim MUST cite its source at the point of claim. No unsourced speculation.
- Web sources for knowledge-base docs → fetch the page (your host's
WebFetch / WebSearch / equivalent), then ingest it as a local doc, then cite the local path: [source name](./path/to/source.md). The local doc carries the original URL in its frontmatter source_url:. Inline [source](URL) is a chat affordance, not a knowledge-base one.
- Self-fetched counts. When YOU fetched a URL to ground a claim that's about to land in the knowledge base, that fetch triggers
ingest exactly like a user share does. Don't downgrade to inline-URL citation because the fetch was agent-initiated — same KB, same closed-loop contract.
- Internal cross-refs → standard markdown link to the OK doc that contains the authoritative claim:
[text](./path/to/doc.md). The linked doc itself must cite its sources — chains should terminate in preserved local docs. Where ingested sources live is project-specific (an external-sources/ folder if the project uses Karpathy's layout; wherever the project's existing layout puts raw references otherwise).
- If you don't have evidence:
- Run a web search and
ingest the result, OR
- Mark inline
(TODO: needs source) so a human can verify, OR
- Don't write the claim. Do NOT fabricate.
- Unsourced speculation looks authoritative but rots into tribal knowledge that can't be audited. The knowledge base loses its value if readers can't trust it.
- If a fact is in the knowledge base, a reader must be able to trace it to its origin via local docs only — no dead-link-on-the-public-web exposure.
Linking — use standard markdown links
- Every noun-phrase that names another document should be linked using standard markdown link syntax:
[text](./relative/path.md) or [text](/absolute/from/content-root.md).
- External web sources are NOT inline body links. Per the Grounding rule above, web URLs live in the
source_url: frontmatter of an ingested doc under external-sources/ (or the project's equivalent raw-sources folder); the body cites the local path: [source name](./external-sources/source-slug.md). A raw [source](https://...) inline in the body is a TODO, not a citation — see Grounding for the closed-loop contract.
- Internal cross-refs between OK docs →
[text](./other-doc.md) — link liberally to aid navigation.
- Every link must resolve to a doc that exists. Never link to a doc that isn't written yet. If you want to reference something that should have its own page but doesn't: create that page in the same pass, or record it as a tracked task (
TaskCreate, or your host's task tool — if the host has none, tell the user) and leave the mention as plain prose. A broken link is debt, not a to-do marker.
- Never wrap a link in backticks.
`[text](./foo.md)` is a bug — the backticks make it render as literal code rather than a link.
- Never use HTML anchors (
<a href="...">). Markdown link syntax only.
- Verify before walking away. After writing a doc, call
links({ kind: "dead", sourceDocNames: ["your/doc"] }) to find broken references. Fix or remove every one — a dead link is never acceptable to leave behind. Companion links kinds: backlinks (incoming), forward (outgoing), orphans (no incoming), hubs (high-incoming), suggest (untextualized mentions worth linking).
- The editor's red-underline visual lies. Its dead-link detection tolerates slug-fallback (e.g.,
foo may appear resolved because foo.md exists at root). links({ kind: "dead" }) is strict-exact — trust the tool, not the visual.
Note on wiki-link syntax ([[Page]]): the parser still handles it for legacy content, but it's NO LONGER the recommended default. Write new content with standard markdown links per above. Seed-pack templates (ok seed --pack <name>) may still emit [[Page]] placeholders inside template body text — those are legacy. When you instantiate a seed-pack template, replace the legacy placeholders with standard markdown links during the {shape}-fill pass.
Media — images and attachments
Folders, frontmatter, templates
Every .md / .mdx file needs YAML frontmatter — title + description required, tags recommended:
---
title: Article Title
description: Brief summary
tags: [relevant, tags]
---
Folder defaults live in opt-in nested <folder>/.ok/frontmatter.yml; templates live in <folder>/.ok/templates/. Most folders have NO .ok/ — sparse, lazy-create, auto-clean. A folder gets one only when it declares defaults or carries templates.
content-root/
├── .ok/ ← project root .ok/ (config.yml, cache)
├── meetings/
│ ├── .ok/
│ │ ├── frontmatter.yml ← folder defaults
│ │ └── templates/
│ │ └── prep-notes.md
│ └── 2026-05-01.md
└── research/ ← no .ok/ — inherits root cascade
└── auth-providers.md
Cascade merge: scalars (title, description) replace last-wins root → leaf; arrays (tags) union-and-dedup; file frontmatter wins per-scalar over folder defaults, tags union with the cascade.
Read the folder before writing (MUST)
Before creating or editing docs in a folder, always call exec("ls -A <folder>") once. The response carries frontmatter_defaults (the merged cascade) + templates_available (the template menu for write_document({ template })). Skipping this is how agents land docs that violate folder discipline.
Pre-write checklist:
- First-contact check. If
frontmatter_defaults AND templates_available are empty AND exec("ls -A") shows substantial content elsewhere, the project hasn't been onboarded — STOP and invoke discover (Workflow tools below). Skip on subsequent writes once confirmed.
- Read
frontmatter_defaults — don't redeclare keys the cascade already provides; let inheritance carry them.
- Read
templates_available — each entry has name, title, description, scope (local / inherited). If one matches, prefer it over free-form markdown (it's the folder's contract — templates carry frontmatter + body structure hand-authored docs routinely miss).
- Read recent siblings — new docs should match the shape of existing ones (filename, frontmatter, body structure).
- Confirm content scope —
content.dir (.ok/config.yml) defines the root. .gitignore / .okignore (nested at any depth) define exclusions.
Once per folder per session — the checklist doesn't repeat unless you (or the user) changed a folder rule or template since.
When to use a template (MUST when one fits)
Instantiate via write_document({ template, docName, position: "replace" }). Inherited templates (scope: "inherited") are equally valid. Skip only when (a) templates_available is empty, (b) no entry matches, OR (c) the user asked for free-form. If you skip, briefly note why in chat.
When to create a template
Templates make folder structure durable. Create them proactively:
- 2+ sibling docs share a skeleton in a folder with no template → extract via
folder_config({ action: "write-template" }).
- About to write a doc in a folder where no template fits, AND the shape is reusable → save as template the same turn.
- Scaffolding a new folder for a doc category → pair the rule (
folder_config({ action: "set-rule" })) with a template in the same turn.
- The user describes a recurring doc shape ("we always log meetings with attendees, agenda, action items") → author the template once.
Note new templates in chat ("saved as a template at meetings/.ok/templates/prep-notes.md for next time") so the user sees the discipline grew.
When to declare folder defaults (MUST when a pattern emerges)
If you're writing the same frontmatter (tags, title prefix) on multiple siblings, call folder_config({ action: "set-rule" }) once and let the cascade do it. Pair with a template when the body skeleton also repeats.
Editing folder defaults
folder_config({
action: "set-rule",
rules: [
{ match: "meetings/**", frontmatter: { title: "Meetings", tags: ["meeting"] } },
{ match: "meetings/prep-notes/**", frontmatter: { tags: ["meeting", "prep"] } },
],
})
Each match resolves to a SINGLE target folder. Multi-folder globs (specs/*/evidence/**) are rejected with MULTI_FOLDER_GLOB — split per folder. Remove a rule by passing empty frontmatter: {} — file deletes and .ok/ auto-cleans if no other tenant remains.
Creating templates
folder_config({
action: "write-template",
folder: "meetings/",
name: "prep-notes",
body: "# {Meeting Title}\n\n**Attendees:** \n**Date:** \n\n## Agenda\n- \n",
frontmatter: {
title: "Meeting Prep Notes",
description: "Use before a meeting.",
tags: ["meeting", "prep"],
},
})
Substitution allowlist: template bodies MAY use exactly two server-side substitutions — {{date}} (today's ISO-8601 date) and {{user}} (calling principal display name). Other {{...}} tokens are rejected at write time with TEMPLATE_UNKNOWN_VARIABLE. Plain {shape} placeholders (e.g., {Meeting Title}) are LITERAL — agents fill via subsequent edit_document calls. Delete a template via folder_config({ action: "delete-template", folder, name }) (auto-cleans empty .ok/templates/ and .ok/).
Creating a doc from a template
exec("ls -A meetings/")
write_document({
docName: "meetings/2026-05-02-roadmap-sync",
template: "prep-notes",
position: "replace",
})
Templates resolve via leaf → root walk-up at the target's parent folder, closest-wins on filename collision. template and markdown are mutually exclusive — passing both errors with TEMPLATE_AND_MARKDOWN_BOTH_SET. Substitution happens at instantiation time only; templates on disk show the raw {{date}} token.
Editing frontmatter
edit_document does NOT change frontmatter (body-only; frontmatter-intersecting find/replace returns HTTP 400). For single-key edits, prefer edit_frontmatter({ docName, patch: { key: value } }) — JSON Merge Patch (RFC 7396), null deletes, field-level CRDT merge, atomic per-call. For full rewrites (≥3-5 keys, or body + frontmatter together), call write_document({ position: "replace", markdown }) and include the new YAML block.
Binary-source wrappers (ingest-produced)
Docs that wrap a co-located binary file under external-sources/ carry extra frontmatter so the wrapper-binary pair is fully described:
---
title: ...
description: ...
source_url: https://example.com/file.pdf
source_path: ./<slug>.<ext>
media_type: application/pdf
bytes: 1234567
sha256: <64-char hex>
date_fetched: YYYY-MM-DD
preservation: binary
supersedes:
- <prior-slug>.md
tags: [source, immutable, layer-ingest, binary]
---
![[<slug>.<ext>]]
Body is just the wiki-embed. PDFs/opaque attachments render as a click-dispatching File row; <Pdf src="./<slug>.pdf" /> is the opt-in inline viewer. See ingest's tool body for full re-ingest / size / executable rules.
Cadence
When you make a multi-step change (batch of new docs, folder restructure), pause between steps to let the browser preview catch up. The CRDT edit streams live; the preview follows your edit cadence. Don't batch 10 writes in a row — interleave the writes so the user watching the browser sees the narrative progress.
This is primarily a human-watchability concern — the user watches edits land in the preview; interleaved cadence makes the narrative legible. When the batch is done, navigate the preview to the primary deliverable (see "End a turn on the deliverable" in §Preview).
Hub docs. Don't create INDEX.md / README.md hub files solely to catalog children — exec("ls -A <folder>") returns the same view live, with per-file frontmatter + backlink counts. But if a hub doc already exists from prior work, keep it updated as children change — interleave: write child → update hub → write next child, rather than batching five child edits and a single trailing hub update.
Log discipline — check for a project log when KB content changes
Some projects keep an append-only project log to make agent activity auditable. After any turn that creates, edits, or restructures docs in the knowledge base, check for a project log: look for a log.md at the project root (or at the seed rootDir if ok seed --root <dir> was used). If one exists, follow whatever its frontmatter description: and in-file comment say — they carry the project-specific contract (entry shape, cadence, categories). Different projects log differently — some treat the log as a wiki audit trail, others as an LLM-brain history, others as a spec changelog. If no log.md exists, no log discipline applies; don't fabricate one.
The skill carries the trigger ("KB content changed this turn — go look"). The file owns the policy.
Anti-patterns — at a glance
| Task | Don't | Do |
|---|
| List a markdown-heavy dir | Bash: ls specs/ | exec("ls -A specs/") |
| Find all SPEC.md files | Glob: **/SPEC.md | exec("find specs -name SPEC.md") |
| Find the most relevant page for a query | Grep: "pattern" *.md then read three files | search({ query: "pattern" }) (ranked: title + body BM25 + recency) |
| Find every literal occurrence of a phrase | Grep: "pattern" *.md | exec("grep -rn pattern <dir>") (literal, grouped by file, with frontmatter) |
| Read an individual doc | Read: specs/foo/SPEC.md | exec("cat specs/foo/SPEC.md") |
| Explore a markdown-heavy dir | Agent(Explore): "..." | Do exec-based exploration yourself |
| Wait for the server to tell you to open preview | Skip the session-start preview open and wait for the attach-preview-once hint | Open the preview browser at session start; the hint is a fallback when you didn't |
| Ignore the attach hint | Skip the warning: { action: "attach-preview-once" } hint in write-tool responses | Open the preview when the hint fires (preview_start, or get_preview_url); otherwise do nothing |
| Make the Claude Code Desktop preview work | Read / diagnose / edit .claude/launch.json (host-managed config) | Call preview_start("open-knowledge-ui") and nothing else; the OK lock-collision proxy bridges any port mismatch transparently |
| Reference another doc | `[text](./page.md)` (backticked) or HTML <a> | [text](./page.md) (raw markdown) |
| Embed an image | <img src="..."> (HTML) or hot-linked external URL | Fetch + save locally +  |
| Write a factual claim in a KB doc | plausible prose without citation, OR inline [source](https://URL) | ingest the source first, then cite the local path per Grounding |
| Cite a web source you just fetched | inline [source](https://...) because YOU did the fetch (not the user) | ingest it — agent-initiated fetches are not exempt from the closed-loop rule |
| Finish a turn that changed KB content | move on without checking for a log | check for a log.md and follow its contract per Log discipline |
| Add an image | empty alt  or generic alt  | meaningful alt + source caption below |
| Catalog folder contents | create INDEX.md hub file | folder_config({ action: "set-rule", rules: [...] }) writes <folder>/.ok/frontmatter.yml |
| Write a doc in an unfamiliar folder | go straight to write_document with hand-authored markdown | exec("ls -A <folder>") first — read frontmatter_defaults + templates_available before writing |
| Land in an existing repo without orienting | go straight to write_document when no folder frontmatter / templates exist | invoke discover once for the project — extracts conventions from siblings, sets folder frontmatter + templates, activates the link graph |
| Author a doc when a matching template exists | write_document({ markdown: "..." }) from scratch | write_document({ template, position: "replace" }) — templates carry the folder's frontmatter + body discipline |
| Change a doc's title / tags | edit_document to swap the YAML (rejected — HTTP 400 frontmatter-intersect) | edit_frontmatter({ docName, patch }) for 1-2 keys; write_document({ position: "replace", markdown }) for full rewrites |
| Repeat the same frontmatter on sibling docs | hand-set identical tags / title prefix on every new file | folder_config({ action: "set-rule" }) once — the cascade carries it to every child |
| Re-derive the same body skeleton repeatedly | copy-paste the structure from a sibling each time | folder_config({ action: "write-template" }) once, then pick from templates_available thereafter |
| Scaffold a new folder for a doc category | set folder rule for frontmatter and stop there | pair folder_config({ action: "set-rule" }) with folder_config({ action: "write-template" }) in the same turn |
| Delete a markdown doc | Bash: rm / unlink / native deletion on in-scope .md | delete_document — version({ action: "save" }) first if rollback may be needed |
| Fork a skill and expect no stomp | Edit installed SKILL.md | npx skills remove before CLI upgrade |
Workflow tools — when to invoke them
Four MCP tools build on the primitives above. They return procedural guidance (a multi-step instructional body), not fetched data. Calling ingest("https://…") does not download and write a doc for you — it returns a multi-step plan you then execute. Same for research / consolidate / discover. Plan to follow the numbered steps in order; don't skip the STOP gates.
Three correspond to Karpathy's three-layer knowledge-base pattern (ingest / research / consolidate); the fourth (discover) operates at the project-metadata layer and is the brownfield counterpart to the greenfield ok seed CLI:
| Tool | Layer | When to invoke |
|---|
ingest | Raw sources (immutable) | User shares a URL/PDF/file to preserve verbatim, OR you fetched a URL (WebFetch / WebSearch / equivalent) to ground a claim that's about to land in the knowledge base. The KB is closed-loop — agent-initiated fetches are not exempt. Binary sources (PDFs, images, audio, Office docs) are preserved verbatim, not text-scraped — the tool body documents the binary-vs-text classification, write-path STOP gates (executable, size, scheme), re-ingest semantics, and shell-less fallback. No analysis in the file itself — takeaways go back to the user in chat. |
research | KB, provisional | User asks you to investigate, compare alternatives, or synthesize multiple sources. Produces a status: provisional article with a sources: list. Follows scan-first routing, a STOP scoping gate, 3P-external framing, and a validate checklist — the tool body enforces each step. |
consolidate | KB, canonical | Team has actually decided after research and wants the outcome committed as source-of-truth. Starts with a STOP gate confirming the decision exists; writes a status: canonical article with a supersedes: chain. |
discover | Project metadata | First arrival at a repo with existing content AND no folder frontmatter / templates set. Extracts conventions from siblings; activates the link graph (orphans, hubs, untextualized mentions); proposes folder frontmatter + templates + .okignore; per-phase user confirmation. Phases 1-4 run fs-direct; Phase 5 (link-graph activation) needs ok start (Phase 5 step 0 gates). Skip on empty repos (use ok seed). One-shot; idempotent on re-run. |
These tools are your default move, not write_document. When the work fits one of the three layers — preserving an external source, investigating/synthesizing, committing a decided outcome — invoke the corresponding tool instead of going straight to write_document / edit_document. The tool bodies enforce framing (sources, status, supersedes chains) that hand-written articles routinely miss. write_document is correct for everything that does not fit the three layers (specs, runbooks, scratch notes, project pages); for the three that do, lead with the tool. This is doubly true in projects that ran ok seed — a doc landing in external-sources/ / research/ / articles/ should have come out of ingest / research / consolidate.
Typical day-2 flow: user shares a URL → ingest (preserve) → user asks "now research this" → research (provisional article + ingests more sources as needed) → decision lands → consolidate (canonical article, supersedes the research).
Autonomy gates vs session-level autonomy. Per-tool STOP gates (e.g. research's scoping gate, consolidate's decision-confirmation gate) are not overridden by session-level "work without stopping for clarifying questions" hints. The session-level hint covers trivial back-and-forth ("which file did you mean?"); per-tool gates exist for 1-way-door decisions where the tool deliberately wants confirmation before continuing. When in doubt, treat the per-tool gate as authoritative and the session-level autonomy hint as a default for the in-between turns.
Do not chain silently. After ingest, ask the user whether to proceed to research. After research, let the user decide whether the findings are ready to consolidate. Each tool completes on its own terms — the user drives the transitions.
Repeat invocations. Workflow tools return their full instructional body on every call, including 2nd / 3rd / Nth invocation in the same session. If you've already received a tool's body earlier this session, you can skim the repeat for changes (the body can evolve across server versions) but you don't need to re-internalize it — proceed to the next step with the new arguments.
Project scaffolding — two paths. Empty repo: run ok seed once from a terminal (scaffolds Karpathy three-layer + seeds log.md + registers folder defaults). Existing content: invoke discover (MCP tool, table above — extracts conventions from siblings, sets folder frontmatter + templates, curates .okignore, activates link graph; per-phase confirmation gates). Neither is required; the four workflow tools work against any folder structure. Only mention each when explicitly relevant.
Server lifecycle
If write_document or edit_document returns a "Hocuspocus server is not running" error, start it with ok start (via Bash) and retry. Never fall back to native Edit / Write for in-scope markdown — always route through the MCP write tools so edits go through the CRDT with proper attribution.
Scope recap
Open Knowledge looks for documents under the resolved content.dir (discoverable at runtime via get_config({ path: ['content', 'dir'] })). .gitignore and .okignore (at the project root and at any folder depth) define exclusions. Folder defaults + templates live in nested <folder>/.ok/frontmatter.yml + <folder>/.ok/templates/ files — NOT in .ok/config.yml.
Default mental model (no jargon): every .md and .mdx under content.dir not excluded by .gitignore or .okignore is an Open Knowledge document — including under specs/, reports/, docs/, etc. Read .okignore (and any nested .okignore files) once per turn to know what's excluded.
First session in this project? If frontmatter_defaults and templates_available are empty for substantial folders, the project hasn't been onboarded yet — invoke discover (Workflow tools table) before writing. Once onboarded, the cascade carries the discipline.