Open KnowledgeOpen Knowledge
Guides

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:

  1. Its extension is supported (.md or .mdx).
  2. Its directory is not in the walker's built-in skip list (.git/, .ok/, node_modules/, .venv/, build outputs, etc.).
  3. No .gitignore or .okignore rule 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).

.okignore
# Exclude drafts from the document index
drafts/

# Exclude any file matching a pattern
*.draft.md

# Re-include a file .gitignore excluded
!keep.md

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

  1. The file extension must be .md or .mdx.
  2. The walker skips built-in system directories regardless of .gitignore / .okignore.
  3. Patterns from .gitignore and .okignore (root + nested) are unioned in one ignore-lib instance.
  4. The last matching rule wins, including across files — a !pattern in .okignore overrides an earlier pattern from .gitignore.

Example

Given a project with:

.gitignore
node_modules/
dist/
*.log
secret.md
.okignore
drafts/
!secret.md

The results:

FileResultReason
docs/setup.mdIncluded.md extension, no rule excludes it
node_modules/pkg/README.mdExcludedBuilt-in skip dir
drafts/wip.mdExcludedExcluded by .okignore
src/index.tsExcludedNot a Markdown extension
dist/README.mdExcludedExcluded by .gitignore
secret.mdIncluded.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 preview

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

.ok/config.yml
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"
.okignore (after)
drafts/
*.draft.md

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