How Octo’s Docs System Works
This page explains how octo.steinbok.net is built, from the source in the octo repo to the published site, and how to build something similar for your own OpenClaw instance.
Overview
Most of Octo’s public documentation is generated from the repo itself (plugin manifests, agent files, config, skills, cron jobs, service READMEs) rather than written by hand. A few pages, including this one, are hand-written Markdown files in the octo repo’s docs/ folder that the same pipeline publishes.
The pipeline runs on every push to main (changes that only touch posts/ are skipped) and can also be started by hand:
- Extract — Python scripts read the repo and write structured JSON (the bundle)
- Redact — secrets and private addresses are stripped from the bundle
- Render — deterministic renderers turn the bundle into Jekyll Markdown pages
- Validate — the rendered pages are scanned to make sure nothing private leaked
- Publish — the rendered site is handed to the separate
octo-docsrepo, which commits it and deploys it with GitHub Pages
There is no LLM in the pipeline. Every step is deterministic, version-controlled and tested.
Two repos
The system is split across two repos on purpose:
| Repo | Purpose |
|---|---|
JeffSteinbok/octo | Source of truth: plugins, config, agents, and the pipeline code |
JeffSteinbok/octo-docs | Hosts the published Jekyll site at octo.steinbok.net |
octo generates the docs; octo-docs publishes them. This means:
octocan stay private whileocto-docsis public- The rendered output can be audited before it goes live
- The docs repo never contains raw source code
The handoff uses a GitHub Actions repository_dispatch event. octo uploads the rendered site as a docs-site artifact and sends a docs-site-ready event to octo-docs, which downloads the artifact, commits the pages and deploys the site.
Visibility: config/doc-manifest.json
config/doc-manifest.json is the single source of truth for what appears in the public docs. It has a section per kind of thing: plugins, services, skills, jobs, clis, hooks and pages.
{
"plugins": {
"config-backup": {
"public": true,
"origin": "octo",
"docsMode": "local",
"name": "Config Backup",
"summary": "Backs up OpenClaw config to Git with SHA-256 change detection"
},
"fastmail": {
"public": true,
"origin": "openclaw-hub",
"docsMode": "external",
"name": "FastMail tools",
"summary": "Send email and manage calendar events in Fastmail",
"docsUrl": "https://github.com/JeffSteinbok/openclaw-hub/tree/main/plugins/fastmail",
"sourceUrl": "https://github.com/JeffSteinbok/openclaw-hub/tree/main/plugins/fastmail"
},
"my-private-plugin": {
"public": false
}
},
"services": { ... },
"skills": { ... },
"jobs": { ... },
"clis": { ... },
"hooks": { ... },
"pages": { ... }
}
Rules:
- Anything not listed is private by default, even if it is in the repo
- An entry needs
"public": trueto appear in the docs - The manifest also holds display metadata:
name,summary,emoji,sourceUrl,docsUrl,docsMode,author - A public plugin entry must have
origin,name,summaryanddocsMode. An unknown key fails the build, so typos are caught instead of silently ignored
Where plugins come from
origin says where a plugin’s code lives, and the Plugins page groups plugins by it:
origin | Where the code lives |
|---|---|
octo | This repo, under plugins/<id>/ |
openclaw-hub, carapace-* | Jeff’s public open-source repos, such as openclaw-hub |
builtin | OpenClaw itself (for example Browser or Google) |
external | Someone else’s repo |
docsMode says where the plugin’s documentation lives:
local— the pipeline renders a full page for the plugin, with every tool documentedexternal— the plugin is listed with a link todocsUrl(orsourceUrl);docsMode: externalrequires one of them
External plugins
For a plugin installed from outside Jeff’s repos, put the author and links in doc-manifest.json, not in the plugin’s own openclaw.plugin.json. The pipeline reads author and sourceUrl from the manifest:
"restaurant-cli": {
"public": true,
"origin": "external",
"docsMode": "external",
"name": "restaurant-cli",
"emoji": "🍽️",
"author": "OmarShahine",
"sourceUrl": "https://github.com/omarshahine/restaurant-cli",
"summary": "Pluggable reservation booking via Resy, OpenTable, Tock, and other providers",
"docsUrl": "https://github.com/omarshahine/restaurant-cli"
}
Pipeline stages
tools/docs/publish/emit_bundle.py runs all five stages in order. The bundle is written to out/docs-bundle/ and the rendered site to out/docs-site/.
1. Extract (tools/docs/extract/)
Each extractor is a standalone, read-only Python script that writes JSON into out/docs-bundle/. The orchestrator also copies config/doc-manifest.json into the bundle and writes a manifest.json listing every file.
| Extractor | Reads | Writes |
|---|---|---|
api_inventory.py | Each plugin’s openclaw.plugin.json and tool schemas | api-index.json |
agent_summary.py | agents/*/ identity files | agents.json, agents/<id>.json |
plugin_summary.py | plugins/*/openclaw.plugin.json and plugin tools | plugins/<id>.json |
changelog_summary.py | CHANGELOG.md | release/changes.json |
config_summary.py | config/openclaw.json | config.json |
skill_summary.py | agents/*/skills/*/SKILL.md | skills.json, skills/<name>.json |
job_summary.py | config/cron_jobs_export.json | jobs.json |
service_summary.py | services/*/README.md | services.json, services/<id>.json |
libs_summary.py | Shared libraries under libs/ts/ | libs.json, libs/<id>.json |
usps_runtime_summary.py | The shared USPS action-module README | libs/mail_action_usps-action.json |
note_summary.py | Setup notes in the repo root’s Markdown files | notes.json |
cli_summary.py | The clis manifest section and CLI READMEs in openclaw-hub | clis.json |
memory_summary.py | Agent memory folders and memory config | memory.json |
visibility_registry.py | config/doc-manifest.json | (a helper the other extractors use) |
2. Redact (tools/docs/sanitize/redact.py)
After extraction, the redactor rewrites every JSON file in the bundle and replaces:
- GitHub, Slack and
sk-style API tokens, and bearer tokens - Credential-looking hex strings next to words like
tokenorsecret - URLs on internal domains (
.local,.internal,.corp,.intranet,.private) - Private IP addresses and URLs that use them
- Links to private GitHub repos
3. Render (tools/docs/render/render_site.py)
The renderer reads the page specs in tools/docs/page_specs/ and writes Jekyll Markdown pages to out/docs-site/. A page spec looks like this:
id: plugins-overview
output_path: docs/plugins.md # written to out/docs-site/plugins.md
strategy: bundle-plugins # which renderer to use
sources:
- path: doc-manifest.json
- path: plugins/*.json
front_matter:
layout: default
title: Plugins
nav_order: 4
strategy picks a rendering function. Every strategy is plain Python, with no templating engine and no LLM:
| Strategy | What it generates |
|---|---|
bundle-plugins | Plugin inventory page and per-plugin pages |
bundle-services | Services overview |
bundle-service-detail | One service’s page |
bundle-skills | Skills overview and per-skill pages |
bundle-scheduled-tasks | Scheduled Tasks page |
bundle-agents-channels | Agents & Channels page |
bundle-models | Models page |
bundle-memory | Memory page |
bundle-clis | CLI Tools page |
bundle-hooks | Hooks overview and per-hook pages |
bundle-release | Release notes |
static-markdown | A hand-written page, published from the Markdown file named in the spec’s source: |
Hand-written pages live in the octo repo’s docs/ folder (this page is docs/DOCS-PIPELINE.md). When the renderer publishes one, it rewrites links to other hand-written pages so they work on the site, and turns links to repo files that aren’t published into plain text.
4. Validate (tools/docs/sanitize/validate_public.py)
After rendering, every .md and .json file in the site is scanned for token shapes, internal hostnames (including Tailscale .ts.net names), private IP addresses and private repo links. Known JSON files are also checked for their required keys. Any match fails the build before anything is uploaded.
5. Publish
In the octo repo, the docs-bundle.yml workflow (Build Docs Bundle) runs:
push to main
│
▼
npm ci && npm run build --workspaces --if-present # build the TypeScript plugins
│
▼
emit_bundle.py # extract → redact → render → validate
│
▼
validate_public.py out/docs-site # validate again, as a separate CI step
│
▼
upload-artifact (docs-site)
│
▼
repository_dispatch → octo-docs (docs-site-ready)
In octo-docs, the Docs Main Pipeline workflow (docs-main-pipeline.yml) receives the event and:
- Runs a secret scan
- Runs
generate-docs.yml(Receive Rendered Docs), which downloads the artifact from theoctorun and copies it intodocs/. Each generated subfolder (such asplugins/orservices/) is replaced in full, so retired pages are deleted. Top-level pages are copied over without deleting anything, and a short list of pages maintained by hand inocto-docs, such as the home page, is never overwritten. It then commits the result tomain - Deploys the site with GitHub Pages (
jekyll-gh-pages.yml)
Running the pipeline locally
## Install Python dependencies
pip install -r tools/docs/requirements.txt
## Build the TypeScript plugins first (needed for tool extraction)
npm ci && npm run build --workspaces --if-present
## Run the full pipeline: extract, redact, render, validate
python tools/docs/publish/emit_bundle.py
## The site is in out/docs-site/
You can also run single stages:
## Run one extractor
python tools/docs/extract/plugin_summary.py
## Render from an existing bundle
python tools/docs/render/render_site.py --bundle out/docs-bundle --out out/docs-site
## Validate the rendered site
python tools/docs/sanitize/validate_public.py out/docs-site
To check the manifest against the live config as well, run tools/docs/scripts/validate_docs.py (see Docs validation in the Engineering Harness, which runs it weekly).
Adding content
A plugin from this repo
- Build the plugin in
plugins/<id>/with anopenclaw.plugin.json - Add it to
config/doc-manifest.jsonunderpluginswith"public": true,"origin": "octo"and"docsMode": "local" - Push to
main; the pipeline picks it up
An external plugin
- Install the plugin as usual (
openclaw plugins install ...) - Add an entry to
config/doc-manifest.json:
"<plugin-id>": {
"public": true,
"origin": "external",
"docsMode": "external",
"name": "Display Name",
"emoji": "🔌",
"author": "AuthorName",
"sourceUrl": "https://github.com/author/repo",
"summary": "One-line description",
"docsUrl": "https://github.com/author/repo"
}
- Don’t add
authororrepositoryto the third-party plugin’sopenclaw.plugin.json
A hand-written page
- Write the page as Markdown in
docs/ - Add a page spec in
tools/docs/page_specs/<id>.ymlwithstrategy: static-markdown,source:pointing at the file, and the page’sfront_matter
A new kind of generated page
- Add a page spec in
tools/docs/page_specs/<id>.yml - Add a rendering strategy in
tools/docs/render/render_site.py - Add extractors in
tools/docs/extract/if the bundle doesn’t have the data yet - Add the matching entries to
config/doc-manifest.json
Building your own
To build something similar for your own OpenClaw instance:
- Copy
tools/docs/. All the pipeline code lives there. The extractors assume Octo’s repo layout (agents/,plugins/,services/,config/), so expect to adapt them to yours. - Create
config/doc-manifest.jsonlisting your plugins, services, skills and jobs, each with"public": trueorfalse. - Set up the build workflow by copying
.github/workflows/docs-bundle.yml. - Create a separate docs repo with a Jekyll site (GitHub Pages works) and a workflow that listens for
repository_dispatch, downloads the artifact and deploys it. - Add the secrets. The source repo needs
OCTO_DOCS_TOKEN, a token that can send the dispatch event to the docs repo. The docs repo needsOCTO_DOCS_BUNDLE_READ_TOKEN, a token that can download artifacts from the source repo.
The only Python dependency outside the standard library is PyYAML.
Tests
cd ~/git/octo
python -m pytest tools/docs/tests/ -q
The tests cover the extractors, the renderer, the bundle orchestrator, page writing, the bundle selectors, the visibility registry and validate_docs.py. Run them before pushing any pipeline change.