Linting & formatting¶
Four tools enforce style across the repository — one editor baseline, one formatter, and two
linters. The npm-based tools are pinned in a single package.json; prettier's config lives
there too, while markdownlint-cli2 keeps its config in a dedicated .markdownlint-cli2.yaml
(see below for why).
EditorConfig¶
.editorconfig ships baseline indentation, line-ending, and trailing-whitespace rules. All
modern editors (including LazyVim — g.editorconfig = true) pick it up automatically.
Prettier¶
Prettier is pinned in package.json
under devDependencies; its config lives in the same file under the prettier key, and
.prettierignore lists what to skip.
CHANGELOG.md
.release-please-manifest.json
.config/nvim/lazyvim.json
.config/nvim/lazy-lock.json
.claude/settings.json
site/
node_modules/
.venv/
.cache/
backups/
.claude/plans/
.claude/worktrees/
- 120-char wrap for everything prettier touches (matches markdownlint's line length).
- Whole-repo scope —
make fmtrunsnpx prettier --write ., so every file prettier understands (Markdown, JSON, JSONC, YAML, CSS, HTML, …) gets formatted, not just docs. CHANGELOG.md/.release-please-manifest.jsonare owned by release-please..config/nvim/lazyvim.json/.config/nvim/lazy-lock.jsonare regenerated by LazyVim and lazy.nvim — let the tool own the format..claude/settings.jsonis rewritten by Claude Code itself — let it own the format (and the agent sandbox blocks rewriting the file, so prettier can't touch it anyway).site/is the zensical build output; the other directories are build/runtime caches (also listed in.gitignore, repeated here for clarity).
CI runs the same command with --check, so an unformatted file in any tracked path fails
the PR rather than getting silently rewritten.
markdownlint-cli2¶
markdownlint-cli2 is pinned in package.json under devDependencies, but its config lives
in a dedicated .markdownlint-cli2.yaml
at the repo root rather than under a markdownlint-cli2 key in package.json. The
package.json config object is
silently ignored when a .markdownlint-cli2.* file is present,
so keeping the config in its own file is the only reliable place for it.
config:
default: true
line-length:
line_length: 120
heading_line_length: 120
code_block_line_length: 120
tables: false
list-marker-space: false
table-column-style:
style: aligned
code-block-style: false
code-fence-style:
style: backtick
no-inline-html:
allowed_elements:
- span
- div
- br
- p
no-space-in-code: false
link-fragments: false
ignores:
- CHANGELOG.md
- node_modules/
- "**/node_modules/**"
- .venv/
- .cache/
- site/
- backups/
- .claude/plans
- .claude/worktrees
- Whole-repo scope —
make lintrunsnpx markdownlint-cli2 '**/*.md', so every markdown file in the repo is linted, not justdocs/. The top-levelREADME.mdandAGENTS.mdare now covered. - 120-char wrap across body, headings, and fenced code blocks; tables are exempt because prettier auto-pads them past 120.
list-marker-space: false— turns off the rule requiring exactly one space after-list markers; lets you align bullets visually when useful.no-inline-htmlallow list — span/div/br/p are needed by zensical's templates.code-block-style: false— disables MD046. Zensical admonitions require their body to be indented four spaces under the!!! type "title"line; MD046'sfencedstyle would flag that indented body as a stray indented code block. The body must also be separated from the title by a blank line so prettier leaves the indentation untouched instead of un-indenting it.no-space-in-code: false— inline code with intentional whitespace (e.g.`D `) is needed for tmux window names with nerd-font prefixes.link-fragments: false— pymdownxattr_listIDs (## Heading { #anchor }) render in zensical but markdownlint can't resolve them.CHANGELOG.mdis ignored — release-please writes it. The remainingignoresentries keep markdownlint out of build/runtime artifacts, including nested package installs like.config/opencode/node_modules/(markdownlint-cli2 does not honor.gitignoreautomatically, so these need to be listed explicitly).
shellcheck¶
shellcheck lints every shell script in the repo —
install.sh, restore.sh, backup.sh, setup/**/*.sh, every executable under
.local/share/scripts/, the git template hooks, and .ssh/rc. CI and make lint both run
it with --severity=warning --external-sources so style/info findings are skipped (most
notably SC1091 for dynamic ${SETUP_DIR}/printing.sh sources) while real warnings and
errors still fail.
Run them locally¶
make fmt # prettier --write
make lint # depends on fmt → shellcheck + markdownlint-cli2
make docs-build # depends on lint → uv sync + zensical build --clean
make fmt runs npm install first, so the first invocation populates node_modules/ from
package-lock.json. Re-runs are fast because npm install is a no-op when the lock is in
sync.
Need to run a single tool by hand?