Content filtering
How Open Knowledge uses .gitignore and .okignore patterns to control which files enter the document system.
Open Knowledge indexes Markdown files (.md and .mdx) and respects .gitignore rules. To exclude additional files without putting OK-specific patterns in .gitignore, drop a .okignore at the project root using the same syntax.
How it works
Symlinks inside the content directory are followed transparently -- they participate in the document system like regular files. Cyclic symlinks are detected and skipped; symlinks that resolve outside the content directory are excluded.
A file is indexed when:
- Its extension is supported (
.mdor.mdx). - Its directory is not in the walker's built-in skip list (
.git/,.ok/,node_modules/,.venv/, build outputs, etc.). - No
.gitignoreor.okignorerule excludes it.
.gitignore and .okignore patterns load into a single ignore-lib instance, so cross-source negation works: a leading ! in .okignore re-includes a file .gitignore excluded.
.okignore
Drop a .okignore next to your .gitignore at the project root. It uses gitignore syntax, parsed by the ignore npm library — a JavaScript implementation of the gitignore pattern spec (Git itself uses a separate C implementation; the spec is shared but the engines are not).
# Exclude drafts from the document index
drafts/
# Exclude any file matching a pattern
*.draft.md
# Re-include a file .gitignore excluded
!keep.mdNested .okignore files at any folder depth are honored — same mechanic as nested .gitignore files.
ok init scaffolds a starter .okignore at the project root with a commented header explaining the syntax. Edit it freely; it's checked into version control alongside .gitignore.
Resolution rules
- The file extension must be
.mdor.mdx. - The walker skips built-in system directories regardless of
.gitignore/.okignore. - Patterns from
.gitignoreand.okignore(root + nested) are unioned in one ignore-lib instance. - The last matching rule wins, including across files — a
!patternin.okignoreoverrides an earlierpatternfrom.gitignore.
Example
Given a project with:
node_modules/
dist/
*.log
secret.mddrafts/
!secret.mdThe results:
| File | Result | Reason |
|---|---|---|
docs/setup.md | Included | .md extension, no rule excludes it |
node_modules/pkg/README.md | Excluded | Built-in skip dir |
drafts/wip.md | Excluded | Excluded by .okignore |
src/index.ts | Excluded | Not a Markdown extension |
dist/README.md | Excluded | Excluded by .gitignore |
secret.md | Included | .gitignore excluded it; .okignore !secret.md re-included it |
Previewing content scope
To see exactly which files Open Knowledge will index:
npx @inkeep/open-knowledge previewThis is read-only -- it doesn't write anything or start a server. It uses the same ContentFilter as the file watcher, so the output matches what the editor sidebar will show.
Works before init (uses schema defaults) and after config edits.
Admission on rename
The same ContentFilter that gates the file watcher also gates rename-time admission, so a file or folder cannot be moved INTO a path the filter would otherwise reject:
POST /api/rename-path { kind: 'file' }— rejects with 400 if the destination doc would be excluded by.gitignore/.okignore.POST /api/rename-path { kind: 'folder' }— rejects with 400 if the destination folder is excluded as a directory.- MCP
rename_document/rename_folder— surface the 400 to the agent before any file moves on disk.
Adding a .okignore rule that excludes a tree that already contains documents does not retroactively delete those documents — they remain readable through the editor and MCP tools — but they cannot be moved INTO the excluded tree.
content.dir
content.dir in .ok/config.yml selects the root of content. Default is . (the project root). Most projects don't need to change this.
content:
dir: .content.include and content.exclude are no longer config keys. If they appear in your config.yml, OK rejects the file with a source-located error. The error is per-key: content.exclude patterns can be 1:1 migrated to .okignore; content.include patterns cannot — .okignore is exclude-only, so an include whitelist needs to be expressed differently (use content.dir to scope to a subdirectory; rely on the upstream .md/.mdx extension gate; or invert specific include patterns into exclude rules for everything else).
Migrating from content.include / content.exclude
Patterns convert 1:1 — gitignore syntax is a superset of the picomatch globs the old keys used:
# config.yml (before)
content:
exclude:
- drafts/**
- "*.draft.md"drafts/
*.draft.mdThen remove the content.include and content.exclude keys from your config.yml — either via ok config migrate (which strips them automatically; see CLI reference) or by hand. If any removed keys remain, OK refuses to start with a source-located REMOVED_KEY error pointing at the offending lines, so you can't miss them.
content.include had only one real job — gating which extensions count as content — and .md/.mdx extension matching is now hardcoded upstream. If your old content.include was the default (['**/*.md', '**/*.mdx']), you don't need to add anything to .okignore for it.
Performance
The walker skips built-in system directories before reading any ignore file, and best-effort ignore globs are also passed to the native file watcher for kernel-level filtering, so excluded paths never trigger filesystem events.