- Go 99%
- Shell 1%
docsync
Documentation drift detector.
Your README is a contract. Enforce it.
Quick Start · Checks · Configuration · Contributing
What is docsync? #
docsync is a single-binary Go CLI that verifies documentation matches
codebase reality. It is a linter, not a test runner: it cross-references
.env.example, README files, AGENTS.md, and other markdown docs against
the actual code to detect drift — env vars documented but unused, env vars
used but undocumented, file paths referenced that no longer exist, commands
documented that no longer resolve, API routes documented that don't match
route definitions, and build pins that disagree.
It exists because documentation rots silently. Developers lose an hour
discovering the documented commands don't work; agents lose entire sessions
because they trust docs as ground truth. markdown-link-check does URLs
only, dotenv-linter does .env syntax only — nothing checks "does the
README describe this repo accurately." That gap is the product.
Features #
- Env var cross-reference —
.env.examplekeys vsos.Getenv,process.env,$VAR, composeenvironment:blocks, andenv:"KEY"struct tags. Flags missing keys (critical) and orphans (warning). - File path check — backtick paths, code-block tree lines, and markdown
links resolved against the repo root. URLs, API routes, package paths,
keyboard combos, and
cp-style create targets are filtered out. - Command check —
npm/pnpm/yarn/bun run <script>vspackage.jsonscripts,make/justtargets vs the build file,./script.shexistence,npxtools vsnode_modules/.bin. - API route check (opt-in) —
GET /api/...mentions in docs vs Gin/Echo/Chi/net-http and Express route definitions. - Version check — "Requires Node 24" vs
package.jsonengines,go.mod, CI matrices. - Pin cross-check —
FROM node:26-alpinevsnode-version: 22vs engines, across Dockerfiles, workflows, and manifests.expo install --checkshells out when a package depends on Expo. docsync fix— appends missing keys to.env.examplewith a TODO comment. Never edits prose.--dry-runsupported.- SARIF output —
--format sariffeeds GitHub code scanning; findings land as PR annotations. - Baseline adoption —
--baseline-out .docsync-baselinerecords today's drift;--baseline .docsync-baselinesuppresses it so legacy repos gate only new drift. - Next.js routes —
framework: nextjsmapsapp/**/route.*andpages/api/**files to routes, including[id]and[...slug]params; exported verbs become the methods. - Agent-ready —
--jsonon every command; stable snake_case keys.
Quick Start #
# one-liner — latest release binary to ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/Dvorinka/docsync/main/install.sh | sh
# or from source
go install github.com/Dvorinka/docsync/cmd/docsync@latest
Or build from source (Go 1.23+):
git clone https://github.com/Dvorinka/docsync.git && cd docsync
go build -o docsync ./cmd/docsync
Run it in any repo:
docsync check # gate: exit 2 on critical drift
docsync report # human-readable report, always exit 0
docsync fix --dry-run # preview env additions
docsync fix # append missing keys to .env.example
Commands #
docsync check [--json | --format sarif] [--baseline FILE] [--baseline-out FILE] #
The CI gate. Reads .docsync.yml if present (sensible defaults without
it), scans the repo, prints findings grouped by category.
Exit codes: 0 clean · 1 warnings only · 2 critical findings ·
5 malformed config or unreadable input.
--format sarif emits SARIF 2.1.0 — pipe it to
github/codeql-action/upload-sarif and drift findings become PR
annotations.
--baseline-out FILE writes the current finding set as a baseline;
--baseline FILE then suppresses exactly those findings (matched by
category|key, immune to line-number churn). The JSON summary reports
suppressed: N.
docsync report [--json] #
Same data as check, formatted for reading. Always exits 0.
docsync completion <bash|zsh|fish> #
Prints a completion script — source <(docsync completion bash).
docsync version #
Prints the binary version (dev for source builds; release binaries are
stamped).
docsync fix [--dry-run] [--json] #
Adds missing env keys to .env.example with empty values and a
# TODO: document this var comment. Never removes or modifies existing
entries, never touches markdown.
Check catalog #
| Category | Default severity | What it catches |
|---|---|---|
env_missing_in_example |
critical | code/composes uses a var .env.example lacks |
env_orphan_in_example |
warning | .env.example declares a var nothing reads |
path_not_found |
critical | doc references a file that doesn't exist |
command_not_found |
critical | doc references a script/target that doesn't exist |
route_not_found |
warning | doc lists a route no code defines |
route_undocumented |
warning | code defines a route docs don't mention |
version_mismatch |
warning | "Requires Node 24" vs engines/CI disagreeing |
pin_mismatch |
critical | Dockerfile vs workflow vs manifest pin conflict |
pin_unpinned |
warning | FROM image:latest-style floating tags |
Suppress noise per-repo:
# .docsync.yml
ignore:
- "path_not_found:docs/drafts/**"
- "env_orphan_in_example:LEGACY_*"
Glob syntax: * matches one segment, ** any depth, bare strings match
exactly.
Configuration #
All optional — .docsync.yml at the repo root:
docs: # markdown files to scan
- "README.md"
- "AGENTS.md"
- "docs/**/*.md"
env_file: ".env.example"
code_dirs: # where env-var references are scanned
- "apps/api"
- "apps/web"
languages: # env-access patterns per language
- go # os.Getenv("KEY"), env:"KEY" struct tags
- typescript # process.env.KEY, import.meta.env.KEY
- bash # $KEY, ${KEY} in .sh/Makefile/Dockerfile
- yaml # compose environment:, env_file, ${KEY}
api_routes: # opt-in route cross-reference
- dir: "apps/api"
framework: "go" # gin/echo/chi/net-http
- dir: "apps/web/src"
framework: "express" # app.get, router.post
- dir: "app"
framework: "nextjs" # app/**/route.* + pages/api/** file routes
# strict_paths: true disables the namespace heuristic — extensionless
# paths like `x/crypto/argon2` are normally skipped, but so is a doc
# referencing a top-level dir that doesn't exist at all. Turn this on
# when you want those flagged too.
strict_paths: false
dependency_files:
- "package.json"
- "go.mod"
- ".github/workflows/*.yml"
severity:
env_missing_in_example: critical
env_orphan_in_example: warning
path_not_found: critical
command_not_found: critical
route_not_found: warning
route_undocumented: warning
version_mismatch: warning
pin_mismatch: critical
pin_unpinned: warning
JSON output #
--json on check/report:
{
"findings": [
{
"severity": "critical",
"category": "env_missing_in_example",
"key": "INTAKE_TOKEN",
"detail": "referenced in apps/api/intake.go:15 but missing from .env.example",
"file": "apps/api/intake.go",
"line": 15
}
],
"summary": { "critical": 1, "warning": 0, "total": 1 }
}
--json on fix:
{
"fixed": [{"action": "env_added", "key": "INTAKE_TOKEN", "file": ".env.example"}],
"unfixed": [{"severity": "critical", "category": "path_not_found",
"key": "apps/mobile/plugins/x.js", "detail": "requires manual fix"}]
}
Clean runs emit "findings": [], never null.
What it is not #
- Not a test runner. It verifies references resolve, not that commands execute. Running README code blocks end-to-end is a literate testing framework — different product.
- Not a prose generator.
fixonly appends env keys. It never writes or rewrites markdown. - Not a link checker. External URLs are
markdown-link-check's job. - No daemon, no server, no web UI, no MCP. One-shot CLI.
Development #
go build ./...
go test ./... # fixture suites under testdata/fixtures/
go vet ./...
gofmt -l .
Single Go module, stdlib first; the only dependency is gopkg.in/yaml.v3.
Contributing #
See CONTRIBUTING.md.
Security #
See SECURITY.md.