"Zenzic is the silent guardian of your documentation. It doesn't just check links; it audits your brand's technical integrity."
Engineering-grade documentation linter โ standalone, engine-agnostic, and security-hardened.
Documentation doesn't fail loudly. It decays silently.
Broken links, orphan pages, invalid code snippets, stale placeholder content, and leaked API keys accumulate over time โ until users hit them in production. Zenzic catches all of these across MkDocs and Zensical projects as a standalone CLI.
Zenzic is agnostic โ it works with any Markdown-based documentation system (MkDocs, Zensical,
or a bare folder of .md files) without installing any build framework. And it is opinionated:
absolute links are a hard error, and if you declare engine = "zensical" you must have
zensical.toml โ no fallback, no guessing.
Current Zensical compatibility baseline: v0.0.31+.
Project attribution: Zenzic is a PythonWoods project. Zensical, MkDocs, and other referenced ecosystem tools are third-party projects.
- Smart Initialization:
zenzic initdetectspyproject.tomland offers to embed config as[tool.zenzic]instead of a standalonezenzic.toml. Use--pyprojectto skip the prompt. Engine auto-detection in both modes. - Sentinel UI: monolithic Indigo banner, traceback gutter with 2-space
padding (
โ 16 โฑ), surgical caret underlines, and vertical breathing between findings. - Agnostic Target:
zenzic check all README.mdorzenzic check all content/scopes audits to a single file or directory.VanillaAdapterauto-selected for out-of-docs targets. - Plugin SDK:
zenzic init --plugin <name>scaffolds a ready-to-edit rule package.zenzic.rulespublic namespace is stable โBaseRule,RuleFinding,CustomRule,Violation,Severity. - Hybrid Adaptive Engine:
scan_docs_referencesselects sequential or parallel execution automatically based on repository size (threshold: 50 files). Deterministic two-phase anchor validation eliminates race-induced false positives. - Z001/Z002 Split: broken links (Z001 error) vs orphan-page links (Z002 warning).
Without
--strict, orphan warnings don't block the build. - Mutation-tested: 86.7% mutation score (242/279 killed on
rules.py). 706 tests, Hypothesis property-based testing with tiered profiles. pyproject.tomlconfig: embed Zenzic config in[tool.zenzic]whenzenzic.tomlis absent.zenzic.tomlalways wins if both exist.- Zenzic Shield: credential detection (7 secret families) + path traversal protection. Exit code 2 reserved for security events.
Zenzic provides an extensive, engineering-grade documentation portal:
- ๐ User Guide: Installation, CLI usage, and all available checks.
- ๐ Badges: Official Zenzic Shield and Score badge snippets for your README.
- ๐ CI/CD Integration: GitHub Actions workflows, dynamic badges, and regression detection.
- โ๏ธ Developer Guide: Deep dive into the deterministic pure-core architecture, Two-Pass Pipeline, and state-machine parsing.
- ๐ค Contributing: Set up the local development environment (
uv,nox), run the test suite, and submit PRs.
Explore the full documentation โ
| Check | CLI command | What it detects |
|---|---|---|
| Links | zenzic check links |
Broken internal links, dead anchors, and path traversal attempts |
| Orphans | zenzic check orphans |
.md files absent from nav |
| Snippets | zenzic check snippets |
Python, YAML, JSON, and TOML blocks with syntax errors |
| Placeholders | zenzic check placeholders |
Stub pages and forbidden text patterns |
| Assets | zenzic check assets |
Images and files not referenced anywhere |
| References | zenzic check references |
Dangling References, Dead Definitions, Zenzic Shield |
Beyond pass/fail, zenzic score aggregates all checks into a deterministic 0โ100 quality score.
zenzic diff compares the current score against a saved baseline โ enabling regression detection
on every pull request.
Autofix: Zenzic also provides active cleanup utilities. Run zenzic clean assets to automatically deleting the unused images identified by check assets (interactive or via -y).
Zenzic enforces two rules that make documentation portable across any hosting environment and independent of any specific build engine.
Zenzic rejects internal links that start with /. Absolute paths are environment-dependent:
a link to /assets/logo.png works when the site is at the domain root, but returns 404 when
hosted in a subdirectory (e.g. https://example.com/docs/assets/logo.png โ
https://example.com/assets/logo.png).
<!-- Rejected by Zenzic -->
[Download](/assets/guide.pdf)
<!-- Correct โ works at any hosting path -->
[Download](../assets/guide.pdf)The error message includes an explicit fix suggestion. External URLs (https://...) are not
affected.
Zenzic natively supports both i18n strategies used by mkdocs-static-i18n:
Suffix Mode (page.locale.md) โ translated files are siblings of the originals:
docs/
guide.md โ default locale (EN)
guide.it.md โ Italian translation (same depth, path-symmetric)
assets/
logo.png โ shared asset, same relative path from both files
Folder Mode (docs/it/page.md) โ non-default locales live in a top-level directory:
docs/
guide.md
assets/
logo.png
it/
guide.md โ Italian translation
In Folder Mode, Zenzic uses the [build_context] section in zenzic.toml to know which
top-level directories are locale trees. Asset links from docs/it/guide.md that resolve to
docs/it/assets/logo.png are automatically re-checked against docs/assets/logo.png โ
mirroring the engine's own fallback behaviour. Locale files are never reported as orphans.
# zenzic.toml
[build_context]
engine = "mkdocs" # "mkdocs" or "zensical"
default_locale = "en"
locales = ["it", "fr"] # non-default locale directory namesWhen zenzic.toml is absent, Zenzic reads locale configuration directly from mkdocs.yml
(respecting docs_structure, fallback_to_default, and languages). No configuration is
required for projects that do not use i18n.
Zenzic is build-engine agnostic. It works with any Markdown-based documentation system โ
MkDocs, Zensical, or a bare folder of .md files. No build framework needs to be installed;
Zenzic reads raw source files only.
Where a documentation ecosystem defines well-known conventions for multi-locale structure or build-time artifact generation, Zenzic provides enhanced, opt-in support by reading the project's configuration file as plain YAML โ never by importing or executing the framework itself.
Zenzic translates engine-specific knowledge into engine-agnostic answers through a thin adapter layer:
zenzic.toml โ get_adapter() โ Adapter โ Core (Scanner + Validator)
The adapter answers the questions the Core needs without knowing anything about MkDocs or Zensical internals:
| Method | Question |
|---|---|
is_locale_dir(part) |
Is this path component a non-default locale directory? |
resolve_asset(path) |
Does a default-locale fallback exist for this missing asset? |
is_shadow_of_nav_page(rel, nav) |
Is this locale file a mirror of a nav-listed page? |
get_nav_paths() |
Which .md paths are declared in the nav? |
get_ignored_patterns() |
Which filename patterns are non-default locale files (suffix mode)? |
Three adapters are available, selected automatically by get_adapter():
| Adapter | When selected | Config source |
|---|---|---|
MkDocsAdapter |
engine = "mkdocs" or unknown engine |
mkdocs.yml (YAML) |
ZensicalAdapter |
engine = "zensical" |
zensical.toml (TOML, zero YAML) |
VanillaAdapter |
No config file, no locales declared | โ (all no-ops) |
Native Enforcement โ engine = "zensical" requires zensical.toml to be present.
If it is absent, Zenzic raises ConfigurationError immediately. There is no fallback to
mkdocs.yml and no silent degradation. Zensical identity must be provable.
Most documentation linters check whether a linked file exists on disk. Zenzic goes further: it builds a Virtual Site Map before any rule fires.
Source files โโโบ Adapter โโโบ VSM โโโบ Rule Engine โโโบ Violations
.md + config (engine- (URL โ status) (pure functions)
specific
knowledge)
The VSM maps every .md source file to the canonical URL the build engine
will serve โ without running the build. Each route carries a status:
| Status | Meaning |
|---|---|
REACHABLE |
Page is in the nav; users can find it. |
ORPHAN_BUT_EXISTING |
File exists on disk but is absent from nav:. Users cannot find it via navigation. |
CONFLICT |
Two files map to the same URL (e.g. index.md + README.md). Build result is undefined. |
IGNORED |
File will not be served (unlisted README.md, Zensical _private/ dirs). |
This makes Zenzic uniquely precise: a link to an ORPHAN_BUT_EXISTING page
is caught as UNREACHABLE_LINK โ the file exists, the link resolves, but
the user will hit a 404 after the build because the page is not navigable.
Ghost Routes (reconfigure_material: true) โ when mkdocs-material
auto-generates locale entry points (e.g. /it/) at build time, those pages
never appear in nav:. Zenzic detects this flag and marks them REACHABLE
automatically, so no false orphan warnings are emitted.
Content-addressable cache โ Zenzic avoids re-linting unchanged files by
keying results on SHA256(content) + SHA256(config). For VSM-aware rules
the key also includes SHA256(vsm_snapshot), ensuring invalidation when any
file's routing state changes. Timestamps are never consulted โ the cache is
correct in CI environments where git clone resets mtime.
When mkdocs.yml declares the i18n plugin with fallback_to_default: true, Zenzic mirrors
the plugin's resolution logic: a link from a translated page to an untranslated page is not
reported as broken, because the build will serve the default-locale version. Supported for both
docs_structure: suffix and docs_structure: folder.
# mkdocs.yml
plugins:
- i18n:
docs_structure: folder
fallback_to_default: true
languages:
- locale: en
default: true
build: true
- locale: it
build: trueIf mkdocs.yml is absent (or the i18n plugin is not configured), Zenzic falls back to standard
single-locale validation โ no errors, no warnings, no framework required.
Applies to any documentation system. If links point to files generated at build time (PDFs,
ZIPs), declare their glob patterns in zenzic.toml:
# zenzic.toml
excluded_build_artifacts = ["pdf/*.pdf", "dist/*.zip"]Zenzic suppresses errors for matching paths at lint time. The build remains responsible for generating the artifacts; Zenzic trusts the link without requiring the file on disk.
[text][id] links are resolved through the same pipeline as inline links โ including i18n
fallback โ for all documentation systems.
[API Reference][api-ref]
[api-ref]: api.mduv is the fastest way to install and run Zenzic:
# Zero-install, one-shot audit
uvx --pre zenzic check all
# Global CLI tool โ available in any project
uv tool install --pre zenzic
# Project dev dependency โ version-pinned in uv.lock
uv add --dev --pre zenzic# Global install (consider a virtual environment)
pip install --pre zenzic
# Inside a virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --pre zenzicZenzic performs a static analysis of your configuration files (mkdocs.yml, zensical.toml, pyproject.toml). It does not execute the build engine or its plugins.
This means you do not need to install MkDocs, Material for MkDocs, or any other build-related plugins in your linting environment. Zenzic remains lightweight and dependency-free, making it ideal for fast, isolated CI/CD pipelines.
Build artifacts: If your documentation links to files generated at build time (PDFs, ZIPs), add their glob patterns to
excluded_build_artifactsinzenzic.tomlrather than pre-generating them. See the First-Class Integrations section above.
zenzic init # creates zenzic.toml with auto-detected engine
zenzic init --pyproject # embeds [tool.zenzic] in pyproject.toml insteadWhen pyproject.toml exists, zenzic init asks interactively whether to embed
configuration there. Pass --pyproject to skip the prompt.
# Individual checks
zenzic check links --strict
zenzic check orphans
zenzic check snippets
zenzic check placeholders
zenzic check assets
# Autofix & Cleanup
zenzic clean assets # Interactively delete unused assets
zenzic clean assets -y # Delete unused assets immediately
zenzic clean assets --dry-run # Preview what would be deleted
# Reference pipeline (v0.2.0)
zenzic check references # Harvest โ Cross-Check โ Shield โ Integrity score
zenzic check references --strict # Treat Dead Definitions as errors
zenzic check references --links # Also validate reference URLs via async HTTP
# All checks in one command
zenzic check all --strict
zenzic check all --exit-zero # Report without blocking the pipeline
zenzic check all --format json # Machine-readable output
# Quality score (0โ100)
zenzic score
zenzic score --save # Persist baseline snapshot
zenzic score --fail-under 80 # Exit 1 if below threshold
# Regression detection against saved snapshot
zenzic diff # Exit 1 on any score drop
zenzic diff --threshold 5 # Exit 1 only if drop > 5 points
# Development server (engine-agnostic)
zenzic serve # Auto-detect mkdocs or zensical
zenzic serve --engine mkdocs
zenzic serve --port 9000
zenzic serve --no-preflight| Code | Meaning |
|---|---|
0 |
All selected checks passed |
1 |
One or more checks reported issues |
2 |
SECURITY CRITICAL โ Zenzic Shield detected a leaked credential |
Warning: Exit code 2 is reserved exclusively for security events. If
zenzic check referencesexits with code 2, a secret (OpenAI API key, GitHub token, or AWS access key) was found embedded in a reference URL inside your documentation. Rotate the credential immediately.
The Zenzic Shield is a two-layer security system built into the core engine:
| Layer | Introduced | Protects against |
|---|---|---|
| Credential detection | v0.2.0 | Leaked API keys / tokens embedded in reference URLs |
| Path traversal | v0.3.0 | ../../../../etc/passwd-style escape from docs/ |
The credential layer runs during Pass 1 (Harvesting) of the reference pipeline and scans every reference URL for known credential patterns before any HTTP request is issued.
<!-- This definition would trigger an immediate Exit 2 -->
[api-docs]: https://api.example.com/?key=sk-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789012345678901โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SECURITY CRITICAL โ
โ Secret(s) detected in documentation โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[SHIELD] docs/api.md:12 โ openai-api-key detected in URL
https://api.example.com/?key=sk-AbCdEfGhIj...
Build aborted. Rotate the exposed credential immediately.
How it works:
- The Shield runs inside Pass 1 โ before Pass 2 validates links and before any HTTP ping is issued. A document containing a leaked credential is never used to make outbound requests.
- Patterns use exact-length quantifiers (
{48},{36},{16}) โ no backtracking, O(1) per line. - Seven credential families are covered out of the box:
| Type | Pattern |
|---|---|
| OpenAI API key | sk-[a-zA-Z0-9]{48} |
| GitHub token | gh[pousr]_[a-zA-Z0-9]{36} |
| AWS access key | AKIA[0-9A-Z]{16} |
| Stripe live key | sk_live_[0-9a-zA-Z]{24} |
| Slack token | xox[baprs]-[0-9a-zA-Z]{10,48} |
| Google API key | AIza[0-9A-Za-z\-_]{35} |
| PEM private key | -----BEGIN [A-Z ]+ PRIVATE KEY----- |
- No blind spots โ the Shield scans every line of the source file, including lines inside
fenced code blocks (
bash,yaml, unlabelled, etc.). A credential committed inside a code example is still a committed credential.
Tip: Add
zenzic check referencesto your pre-commit hooks to catch leaked credentials before they are ever committed to version control.
The path traversal layer runs inside InMemoryPathResolver during check links. It normalises
every resolved href with os.path.normpath (pure C, zero kernel calls) and verifies the result
is contained within docs/ using a single string prefix check โ
Attack href: ../../../../etc/passwd
After resolve: /etc/passwd
Shield check: /etc/passwd does not start with /docs/ โ PathTraversal returned, link rejected
Any href that escapes the docs root is surfaced as a distinct PathTraversal error โ never
silently collapsed into a generic "file not found".
- name: Lint documentation
run: uvx --pre zenzic check all
- name: Check references and run Shield
run: uvx --pre zenzic check referencesFull workflow: .github/workflows/zenzic.yml
For dynamic badge automation and regression detection, see the CI/CD Integration guide.
All fields are optional. Zenzic works with no configuration file at all.
Zenzic follows a three-level Agnostic Citizen priority chain:
zenzic.tomlat the repository root โ sovereign; always wins.[tool.zenzic]inpyproject.tomlโ used whenzenzic.tomlis absent.- Built-in defaults.
# zenzic.toml (or [tool.zenzic] in pyproject.toml)
docs_dir = "docs"
excluded_dirs = ["includes", "assets", "stylesheets", "overrides", "hooks"]
snippet_min_lines = 1
placeholder_max_words = 50
placeholder_patterns = ["coming soon", "todo", "stub"]
fail_under = 80 # exit 1 if score drops below this; 0 = observational mode
# Engine and i18n context โ required only for folder-mode multi-locale projects.
# When absent, Zenzic reads locale config directly from mkdocs.yml.
[build_context]
engine = "mkdocs" # "mkdocs" or "zensical"
default_locale = "en"
locales = ["it"] # non-default locale directory namesFor a faster, interactive development workflow using just, or for detailed instructions on adding new checks, see the Contributing Guide.
uv sync --group dev
nox -s dev # Install pre-commit hooks (once)
nox -s tests # pytest + coverage
nox -s lint # ruff check
nox -s format # ruff format
nox -s typecheck # mypy --strict
nox -s docs # mkdocs build --strict
nox -s preflight # zenzic check all (self-check)The full Sentinel audit: banner, gutter context, caret underlines, and quality score breakdown.
We welcome bug reports, documentation improvements, and pull requests. Before you start:
- Open an issue to discuss the change โ use the bug report, feature request, or docs issue template.
- Read the Contributing Guide โ especially the Local development setup and the Zenzic Way checklist (pure functions, no subprocesses, source-first).
- Every PR must pass
nox -s preflight(tests + lint + typecheck + self-dogfood) and include REUSE/SPDX headers on new files.
Please also review our Code of Conduct and Security Policy.
A CITATION.cff file is present at the root of the repository. GitHub renders
it automatically โ click "Cite this repository" on the repo page for APA or BibTeX output.
Apache-2.0 โ see LICENSE.
ยฉ 2026 PythonWoods. Engineered with precision.
Based in Italy ๐ฎ๐น ย ยทย Committed to the craft of Python development.
dev@pythonwoods.dev