Skip to content

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.

package.json (excerpt)
"prettier": {
  "printWidth": 120
}
.prettierignore
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 scopemake fmt runs npx prettier --write ., so every file prettier understands (Markdown, JSON, JSONC, YAML, CSS, HTML, …) gets formatted, not just docs.
  • CHANGELOG.md / .release-please-manifest.json are owned by release-please.
  • .config/nvim/lazyvim.json / .config/nvim/lazy-lock.json are regenerated by LazyVim and lazy.nvim — let the tool own the format.
  • .claude/settings.json is 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.

.markdownlint-cli2.yaml
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 scopemake lint runs npx markdownlint-cli2 '**/*.md', so every markdown file in the repo is linted, not just docs/. The top-level README.md and AGENTS.md are 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-html allow 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's fenced style 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 — pymdownx attr_list IDs (## Heading { #anchor }) render in zensical but markdownlint can't resolve them.
  • CHANGELOG.md is ignored — release-please writes it. The remaining ignores entries keep markdownlint out of build/runtime artifacts, including nested package installs like .config/opencode/node_modules/ (markdownlint-cli2 does not honor .gitignore automatically, 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?

npx prettier --check .            # prettier without writing — whole repo
npx markdownlint-cli2 '**/*.md'   # markdownlint over every .md file
shellcheck install.sh             # one-off shellcheck