# Project scaffold

> `.ok/` directory layout, three config scopes (project, user, project-local), `.okignore` exclusions, and `content.dir` content-root semantics.

- Repository: sashimikun/open-knowledge
- GitHub: https://github.com/sashimikun/open-knowledge
- Human docs: https://grok-wiki.com/public/docs/sashimikun-open-knowledge-5c45105c876e
- Complete Markdown: https://grok-wiki.com/public/docs/sashimikun-open-knowledge-5c45105c876e/llms-full.txt

## Source Files

- `packages/server/src/init-project.ts`
- `packages/core/src/config/schema.ts`
- `packages/core/src/config/merge-layered.ts`
- `packages/core/src/config/bind-okignore-doc.ts`
- `packages/cli/src/constants.ts`
- `packages/core/src/config/field-registry.ts`
- `packages/core/src/config/schema-version.ts`

---

---
title: "Project scaffold"
description: "`.ok/` directory layout, three config scopes (project, user, project-local), `.okignore` exclusions, and `content.dir` content-root semantics."
---

An Open Knowledge project is anchored by a regular file at `.ok/config.yml` under the **project root**. `ok init` (via `initContent`) scaffolds that marker plus `.ok/.gitignore`, a commented `.ok/config.yml` template, and a root-level `.okignore` — it does not create content folders, `.ok/local/`, or editor integrations. The collaboration server, MCP tools, and CLI resolve the project root by walking ancestors until `.ok/config.yml` exists as a file.

## Project root anchor

The canonical marker is `.ok/config.yml` (`OK_PROJECT_MARKER`). A bare `.ok/` directory, a directory at that path, or a sibling `.ok/frontmatter.yml` without `config.yml` does **not** qualify.

| Signal | Meaning |
| --- | --- |
| `.ok/config.yml` is a regular file | Valid project root |
| `.ok/` exists but `config.yml` is missing | Not a project root; ancestor walk continues |
| `.ok/config.yml` is a directory | Rejected; walk continues |
| No marker found above `cwd` | `findProjectDir` / MCP throw *No Open Knowledge project found* |

`resolveProjectRoot` (used by `ok init`) may place `.ok/` at a git working-tree root when `cwd` sits inside a repo and no ancestor already owns a project — `defaultContentDir` stays `.` in all promotion paths.

## `.ok/` directory layout

`initContent` creates a **config-only** scaffold. Runtime state appears later under `.ok/local/` when you run `ok start` or open the editor.

:::files
project-root/
├── .okignore                 # created at project root (if missing)
├── .ok/
│   ├── config.yml            # committed project config (created if missing)
│   └── .gitignore            # ignores machine-local paths (created/merged)
└── [content tree]            # markdown docs; default = project root itself

.ok/local/                    # created at runtime; entire tree gitignored
├── config.yml                # project-local overrides
├── server.lock               # collaboration server lock
├── principal.json            # (legacy path at .ok/ root also gitignored)
├── state.json
├── telemetry/
├── logs/
└── diagnostics/
:::

<Note>
Only `.ok/config.yml` is intended for git commit at the `.ok/` root. `.ok/.gitignore` excludes `local/` and per-machine files (`principal.json`, `state.json`, `server.lock`, `ui.lock`, `sync-state.json`, `last-spawn-error.log`). Legacy runtime files may still exist directly under `.ok/`; populated `.ok/local/` is the current lock and diagnostics location.
</Note>

`initContent` refuses to follow symlinks on `.ok/`, `.ok/config.yml`, `.ok/.gitignore`, or project-root `.okignore` — a defense against upstream repos redirecting scaffold writes.

## Three config scopes

Configuration splits across three YAML files. Each field in `ConfigSchema` carries a `scope` in the field registry (`user`, `project`, or `project-local`) that governs merge behavior.

| Scope | Path | Git | Typical keys |
| --- | --- | --- | --- |
| **User** | `~/.ok/global.yml` | Per user, not in repo | `appearance.theme`, `editor.wordWrap`, `appearance.preview.autoOpen` |
| **Project** | `.ok/config.yml` | Committed, shared | `content.dir`, `autoSync.default`, `telemetry.localSink.*` |
| **Project-local** | `.ok/local/config.yml` | Gitignored, per machine | `autoSync.enabled`, `search.semantic.*`, `terminal.enabled`, `appearance.sidebar.showHiddenFiles` |

Published JSON Schema (major version `v0`):

- `config.project.schema.json` → `.ok/config.yml`
- `config.user.schema.json` → `~/.ok/global.yml`
- `config.project-local.schema.json` → `.ok/local/config.yml`

Scaffolded `config.yml` includes a yaml-language-server `$schema` comment pointing at the project schema on unpkg.

### Precedence and merge rules

Effective config is built from built-in Zod defaults plus the three YAML layers. `mergeLayered(user, project, projectLocal)` applies **scope-aware leaf short-circuiting**:

- **`user` scope** — value comes only from `~/.ok/global.yml`; project and project-local values are ignored even when set.
- **`project` scope** — value comes only from `.ok/config.yml`; user and project-local overrides are ignored (`content.dir` is project-scoped).
- **`project-local` scope** — value comes from `.ok/local/config.yml` when present; otherwise falls through to project, then user for that leaf.

For object branches without a registered leaf scope, later layers win: project-local → project → user.

```mermaid
flowchart TB
  subgraph layers [Config layers]
    D[Built-in defaults]
    U["~/.ok/global.yml"]
    P[".ok/config.yml"]
    L[".ok/local/config.yml"]
  end
  subgraph merge [mergeLayered]
    M[Scope-aware leaf pick]
    E[Effective Config]
  end
  D --> M
  U --> M
  P --> M
  L --> M
  M --> E
```

<Info>
The CLI `loadConfig` merges **user + project** only (deep merge, then `ConfigSchema.parse`). Project-local values load separately — in the web editor via Hocuspocus CRDT docs (`__config__/project`, `__local__/project`, `__user__/config.yml`), and in the server via `readConfigSafely` for features like `autoSync` and semantic search.
</Info>

<Warning>
Do not put `content.include` or `content.exclude` in `config.yml`. Those keys are removed and rejected at load time. Use `content.dir` for subdirectory scoping and `.okignore` for pattern exclusions. Run `ok config migrate` to strip obsolete keys.
</Warning>

## `content.dir` content root

<ParamField body="content.dir" type="string" default=".">
Folder where Open Knowledge reads and writes documents, **relative to the project root** (the directory containing `.ok/`), not relative to `config.yml`. Project-scoped — only `.ok/config.yml` applies; user and project-local files cannot override it.
</ParamField>

Resolution:

```ts
resolveContentDir(config, projectRoot) → resolve(projectRoot, config.content.dir)
```

| `content.dir` value | Resolved path (project root `/repo`) |
| --- | --- |
| `.` (default) | `/repo` |
| `docs` | `/repo/docs` |
| `./content` | `/repo/content` |
| `/var/vault` (absolute) | `/var/vault` (ignores `projectRoot`) |

When `ok init` passes a non-`.` `contentDir` option, the scaffold writes an **uncommented** `content.dir` block instead of the commented placeholder. Git-root promotion does not auto-set a subfolder — `defaultContentDir` remains `.`; narrow scope by editing `content.dir` after init.

The suggested three-tier lifecycle (`external-sources/`, `research/`, `articles/`) in the scaffold template is documentation only — `initContent` does not create those directories.

Verify indexing scope:

```bash
ok preview
```

## `.okignore` exclusions

`.okignore` lives at the **project root** (scaffolded beside `.ok/`). It uses **gitignore syntax** (parsed by the `ignore` package) and combines with `.gitignore` in a single ignore instance inside `createContentFilter`.

| Behavior | Detail |
| --- | --- |
| Root patterns | Read from `<projectRoot>/.okignore` and `<projectRoot>/.gitignore` |
| Content-root patterns | When `content.dir` is a subfolder, also reads `<contentDir>/.okignore` and `<contentDir>/.gitignore`, prefixing patterns to project-relative paths |
| Nested files | Walks the content tree; honors nested `.okignore` / `.gitignore` at any folder depth (skips already-ignored directories) |
| Re-include | Leading `!` re-includes a path that `.gitignore` excluded |
| Git excludes | Also loads `.git/info/exclude` and global `core.excludesfile` when git is available |

Hard skips (independent of `.okignore`) include `.git`, `.ok`, `node_modules`, build caches, and secret-bearing paths (`.env`, `.ssh`, key files).

In the editor, `.okignore` syncs through the `__config__/okignore` CRDT document (`bindOkignoreDoc`); filesystem edits trigger a `ContentFilter` rebuild.

<RequestExample>

```gitignore title=".okignore"
# Exclude drafts from the document index
drafts/
*.draft.md

# Re-include a file gitignored elsewhere
!keep.md
```

</RequestExample>

<Tip>
Run `ok preview` after editing `.okignore` to see indexed document count and sample paths before starting the server.
</Tip>

## Scaffold lifecycle

<Steps>
<Step title="Run init">

```bash
ok init
```

`initContent` creates `.ok/config.yml`, `.ok/.gitignore`, and `.okignore` when missing. Existing files are never overwritten (idempotent). `.ok/.gitignore` entries are merged if the file already exists.

</Step>
<Step title="Optional: set content root">

Edit `.ok/config.yml` and uncomment or set:

```yaml
content:
  dir: docs
```

Paths are relative to the project root. Use `.okignore` for exclusions, not removed `content.include` / `content.exclude` keys.

</Step>
<Step title="Verify anchor">

Confirm `.ok/config.yml` exists as a file and `ok preview` reports the expected content directory and document count.

</Step>
<Step title="Start server">

```bash
ok start --open
```

Creates `.ok/local/` (config, locks, telemetry). Project-local settings written here stay off git.

</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Initialize a project" href="/initialize-project">
Run `ok init` end-to-end: git setup, skills, and MCP registration beyond the bare scaffold.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full YAML key catalog, precedence details, JSON Schema paths, and environment-only variables.
</Card>
<Card title="Core concepts" href="/core-concepts">
Three-layer model, filesystem-as-database, link graph, and git-backed attribution.
</Card>
<Card title="Folders and templates" href="/folders-and-templates">
Nested `<folder>/.ok/frontmatter.yml` and template resolution inside the content tree.
</Card>
</CardGroup>
