Skills
A skill is a package of structured files that teaches an AI coding agent how to work with a specific tool or framework. Install it in your agent and it will be able to run commands, edit configuration, write content, and troubleshoot problems without step-by-step guidance from you.
Any agent — install with npx:
npx skills add https://ninerealmlabs.github.io/precommit-template/Codex / OpenCode
Tell the agent:
Fetch the skill file at https://ninerealmlabs.github.io/precommit-template/skill.md and follow the instructions.Manual — download the skill file:
curl -O https://ninerealmlabs.github.io/precommit-template/skill.mdOr browse the SKILL.md file.
SKILL LAYOUT
precommit-template/
├── SKILL.md
└── references/
├── generated-files.md
├── survey-questions.md
└── troubleshooting.md
SKILL.md
--- name: precommit-template description: > Apply and maintain the ninerealmlabs pre-commit Copier template in a repository. Covers generating a config with `copier copy`, answering the survey, updating with `copier update`, resolving `.rej` conflicts, and the tool configs the template emits (ruff, biome, prettier, mdformat, rumdl, yamllint, typos, shellcheck, hadolint, editorconfig, commitizen). Use when adding, updating, or debugging pre-commit hooks in a repo that is (or should be) managed by this template. license: CC0-1.0 compatibility: Requires copier >=9 and either prek or pre-commit. The prettier hook needs prettier on PATH. metadata: author: ninerealmlabs repository: https://github.com/ninerealmlabs/precommit-template tags: - pre-commit - copier - linting - formatting --- # Pre-commit Template A Copier template that generates a `.pre-commit-config.yaml` plus matching tool configuration files for a repository. Hooks are declared once and read by both [prek](https://github.com/j178/prek) (the default runner) and [pre-commit](https://pre-commit.com/). ## Quick start ```bash # One-time tool install uv tool install copier --with copier-templates-extensions --with jinja2-time uv tool install prek # Generate into an existing repo (run from the repo root) copier copy --trust "gh:ninerealmlabs/precommit-template" "$(git rev-parse --show-toplevel)" prek run --all-files # fix whatever the new rules flag prek install # run the checks on every commit ``` `--trust` is mandatory. `copier.yaml` loads `extensions/detect.py` as a Jinja extension, and Copier refuses to execute extension code without it. ## Skill directory structure ```text skills/precommit-template/ ├── SKILL.md ← This file └── references/ ├── survey-questions.md ← Every question and what it generates ├── generated-files.md ← What lands in the target repo └── troubleshooting.md ← Error patterns and fixes ``` ## When to use what | Need | Use | | ---------------------------------------- | ----------------------------------------------------------- | | Add hooks to a repo for the first time | `copier copy --trust gh:ninerealmlabs/precommit-template .` | | Pull in newer template revisions | `copier update --trust --answers-file .copier-answers.yaml` | | Re-render with the same answers | `copier recopy --trust --answers-file .copier-answers.yaml` | | Change an answer (e.g. biome → prettier) | `copier update --trust` and re-answer at the prompt | | Run every hook now | `prek run --all-files` | | Run one hook | `prek run --all-files <hook-id>` | | Install the git hooks | `prek install` | | Skip a hook for one commit | `SKIP=<hook-id> git commit ...` | ## Core concepts ### The answers file `copier copy` writes `.copier-answers.yaml` recording the template version and every answer. `copier update` reads it to replay your choices, so **never edit it by hand** — a stale or hand-tweaked answers file makes the next update produce conflicts that look like template bugs. ### Detection instead of questions Two values are worked out from the target repo rather than asked: | Value | Detected from | Fallback | | ----------------- | ------------------------------------------------------------------------------------------------ | -------- | | hook runner | installed git hook shims, a `prek.toml`, how CI invokes the runner, or which runner is on `PATH` | `prek` | | `web_format_tool` | an existing `.pre-commit-config.yaml`, biome/prettier config files, or `package.json` deps | `biome` | The hook runner is **not** a survey question and is **not** recorded in `.copier-answers.yaml`. Both runners read the same `.pre-commit-config.yaml`, so the detected value only decides which runner the generated comments and post-copy messages name. It is recomputed on every run, so it follows the repo if you switch runners. `web_format_tool` **is** a question, because biome and prettier need genuinely different config files. Detection only pre-selects the prompt's default; the answer is recorded and reused on every update. ### Conditional file generation Every optional file is named with a Jinja conditional, so answering `no` means the file is never written: ```text template/{% if python %}.ruff.toml{% endif %}.jinja template/{% if web_format and web_format_tool == "prettier" %}.prettierrc.yaml{% endif %}.jinja ``` Full list in [references/generated-files.md](references/generated-files.md). ### Externally-installed tools The hook runner manages its own environments for most hooks, but one tool must already be on `PATH`: | Tool | Required when | Install | | ---------- | --------------------------- | ------------------------- | | `prettier` | `web_format_tool: prettier` | `npm install -g prettier` | Without them the corresponding hooks fail with "command not found", not with a config error. ## Workflows ### Adopting the template in an existing repo 1. Commit or stash everything — Copier refuses to run against a dirty working tree. 2. Run `copier copy --trust "gh:ninerealmlabs/precommit-template" "$(git rev-parse --show-toplevel)"`. 3. Answer the survey (see [references/survey-questions.md](references/survey-questions.md)). 4. Review the generated files and the auto-created commit. 5. Run `prek run --all-files`. Expect a large diff on first run: the formatters rewrite the whole repo. 6. Commit the formatting churn separately from the config so review stays readable. 7. Run `prek install`. ### Updating to a newer template revision 1. Check out a feature branch — updates can touch many files. 2. Commit or stash current work. 3. Run `copier update --trust --answers-file .copier-answers.yaml`. 4. Resolve any `*.rej` files, then delete them. A `forbid-rej` hook blocks commits while they exist, which is deliberate: an unresolved `.rej` means template changes were silently dropped. 5. Run `prek run --all-files` and commit. ### Changing an answer Re-run `copier update --trust` and give a different answer at the prompt. Files gated on the old answer are deleted and files gated on the new one are created. Local edits to a file that gets deleted are lost, so move anything worth keeping out of the way first. ## Reference files - [references/survey-questions.md](references/survey-questions.md) — every question, its default, and the files and hooks it controls. - [references/generated-files.md](references/generated-files.md) — the full map from answer to generated file, and which hooks read each config. - [references/troubleshooting.md](references/troubleshooting.md) — error patterns and fixes for failed copies, updates, and hook runs. ## Gotchas 1. **`--trust` is not optional.** Without it Copier skips `extensions/detect.py` and the survey aborts on an undefined `detect_hook_runner`. 2. **Never hand-edit `.copier-answers.yaml`.** It is Copier's record of what was rendered; editing it desynchronizes the next update. 3. **Clean working tree required.** Both `copier copy` and `copier update` refuse to run with uncommitted changes. 4. **`.rej` files block commits.** That is the `forbid-rej` hook doing its job — resolve the rejected hunk rather than deleting the file unread. 5. **YAML files must use `.yaml`.** A `forbid-yml` hook fails on any `.yml` extension. The only carve-outs are `.copier-answers.yml` and the generator's own `great-docs.yml`. 6. **Commit messages must not carry a `Co-authored-by:` trailer.** Use an `AI-assistant:` trailer instead; a `commit-msg` hook enforces this. 7. **First run reformats everything.** Land the formatting churn in its own commit so the config change stays reviewable. 8. **`.typos.toml` may need to be committed before the `typos` hook can read it.** If the hook errors on a fresh copy, commit the config first, then re-run. ## Capabilities and boundaries **What agents can do:** - Run `copier copy`, `copier update`, and `copier recopy` in a repo - Answer or re-answer the survey - Resolve `.rej` conflicts - Tune the generated tool configs (`.ruff.toml`, `.rumdl.toml`, `.typos.toml`, and friends) - Add repo-local hooks to the generated `.pre-commit-config.yaml` **Requires human setup:** - Installing `copier`, a hook runner, and `prettier` if selected as the web formatter - Deciding whether to accept a breaking template revision - Reviewing the first bulk-formatting commit ## Resources - [Documentation](https://ninerealmlabs.github.io/precommit-template/) - [llms.txt](https://ninerealmlabs.github.io/precommit-template/llms.txt) — indexed overview for agents - [llms-full.txt](https://ninerealmlabs.github.io/precommit-template/llms-full.txt) — full context for agents - [GitHub repository](https://github.com/ninerealmlabs/precommit-template) - [Copier documentation](https://copier.readthedocs.io/) - [prek](https://github.com/j178/prek) · [pre-commit](https://pre-commit.com/)
references/generated-files.md
# Generated files — precommit-template What the template writes into a target repository, and which hook reads each file. ## Contents - Always generated - Conditionally generated - Naming convention in `template/` ## Always generated | File | Purpose | | ------------------------- | ---------------------------------------------------------------------- | | `.pre-commit-config.yaml` | The hook declarations, read by both `prek` and `pre-commit` | | `.copier-answers.yaml` | Copier's record of the template version and every answer — do not edit | | `.gitattributes` | Line-ending and diff settings | ### Baseline hooks These appear no matter how the survey is answered: - `forbid-yml` — fails on any `.yml` extension, excluding `.copier-answers.yml` - `forbid-rej` — fails while `*.rej` files from a conflicted update remain - `pre-commit/pre-commit-hooks` — large files, merge conflicts, private keys, case conflicts, Python AST, JSON/TOML/YAML syntax, end-of-file, mixed line endings, trailing whitespace - `remove-crlf` - `fix-smartquotes`, `fix-ligatures` - `gitleaks` - `strip-exif` The config also sets `default_install_hook_types` to `pre-commit`, `post-checkout`, `post-merge`, and `post-rewrite`, plus `pre-push` and `commit-msg` when `conventional_commits` is enabled. `exclude` skips `*copier-answers.ya?ml` and `*.rej` globally. ## Conditionally generated | Answer | File | Read by | | ------------------------- | ---------------------------------------------------------------------- | -------------------------------- | | `ai` | `AGENTS.md` | AI coding agents | | `editorconfig` | `.editorconfig` | `editorconfig-checker`, editors | | `markdown` | `.mdformat.toml` | `mdformat` | | `markdown` | `.rumdl.toml` | `rumdl-fmt` | | `markdown_render_check` | `<markdown_render_check_dir>/check_markdown_render.py` | you, on demand[^2] | | `github_actions` | — (hooks only) | `check-jsonschema`, `zizmor`[^3] | | `python` | `.ruff.toml` | `ruff-check`, `ruff-format`[^1] | | `python` | `tests/test_pypi_security_audit.py`, `tests/test_uv_security_audit.py` | your test runner | | `docker` | `.hadolint.yaml` | `hadolint` | | `shell` | `.shellcheckrc` | `shellcheck` | | `web_format` + `biome` | `.biome.jsonc` | `biome-check` | | `web_format` + `prettier` | `.prettierrc.yaml` | `prettier` | | `web_format` + `prettier` | `.prettierignore` | `prettier` | | `yaml` | `.yamllint.yaml` | `yamllint` | | `typos` | `.typos.toml` | `typos` | ### System-installed hooks One hook uses `language: system`, meaning the runner will not install the tool for you: | Hook | Requires | Install | | ---------- | -------------------- | ------------------------- | | `prettier` | `prettier` on `PATH` | `npm install -g prettier` | `shellcheck` and `hadolint` need no system install: they use [shellcheck-py](https://github.com/shellcheck-py/shellcheck-py) and [hadolint-py](https://github.com/AleksaC/hadolint-py), which fetch the tool's binary when the hook environment is installed. `shellcheck` skips `*.zsh` files, since ShellCheck has no zsh dialect. ## Naming convention in `template/` Optional files are gated by a Jinja conditional embedded in the filename. When the condition is false the filename renders empty and Copier writes nothing. ```text template/{% if python %}.ruff.toml{% endif %}.jinja template/{% if web_format and web_format_tool == "prettier" %}.prettierrc.yaml{% endif %}.jinja template/{% if python %}tests{% endif %}/test_uv_security_audit.py template/{% if markdown and markdown_render_check %}{{ markdown_render_check_dir }}{% endif %}/check_markdown_render.py ``` The `.jinja` suffix is stripped on render and is configured by `_templates_suffix` in `copier.yaml`. Files without the suffix — such as the generated test files — are copied verbatim. A directory name can also carry an answer, which is how the render check script gets a configurable destination. The answer is substituted verbatim: a `/` anywhere in the path template splits into another directory level, so the value cannot be normalized during rendering and `markdown_render_check_dir` is validated in `copier.yaml` instead. [^2]: Not wired to a hook. Run it after a formatting pass — `./<dir>/check_markdown_render.py` compares the working tree against `HEAD`, and `--run-hooks` compares against a throwaway copy the repo's own hooks have been run over. It needs `pandoc` on `PATH`; `--doctor` reports what is missing. [^3]: `check-github-workflows` and `check-github-actions` validate workflow and composite-action files against the SchemaStore schemas; `zizmor` audits their security posture. All three are scoped by their own `files` patterns to `.github/workflows/`, `.github/actions/`, `dependabot.yaml`, and a top-level `action.yaml`, so they are inert in a repo with none of those. [^1]: When `markdown` is also enabled, `ruff-format` gains `types_or: [python, pyi, jupyter, markdown]` and formats python code blocks inside markdown. `ruff-check` deliberately stays on python files only, so illustrative snippets in docs are not held to import and unused-name rules.
references/survey-questions.md
# Survey questions — precommit-template Every question is defined in `copier.yaml`. Answers are recorded in `.copier-answers.yaml` and replayed on `copier update`. ## Contents - Asked questions - Detected values - Changing an answer later ## Asked questions Every question except `markdown_render_check` defaults to `true` (or to a detected value), so accepting every default produces the full configuration without the optional render check. | Question | Type | Default | Prompt | | --------------------------- | ---- | --------- | --------------------------------------------------------------------------------------------- | | `ai` | bool | `true` | Prepare AGENTS.md? | | `conventional_commits` | bool | `true` | Use conventional commits? | | `editorconfig` | bool | `true` | Use editorconfig? | | `markdown` | bool | `true` | Lint and format markdown? | | `markdown_render_check` | bool | `false` | Include a script that checks markdown still renders the same after formatting? (needs pandoc) | | `markdown_render_check_dir` | str | `scripts` | Which directory should the render check script go in? | | `python` | bool | `true` | Lint and format python? | | `docker` | bool | `true` | Lint and check docker files? | | `github_actions` | bool | `true` | Validate GitHub Actions workflows and audit their security posture? | | `shell` | bool | `true` | Lint and format shell scripts? | | `web_format` | bool | `true` | Lint and format JS/TS/JSON/HTML/CSS and related files? | | `web_format_tool` | str | detected | Select the web formatter (`biome` or `prettier`) | | `yaml` | bool | `true` | Lint and format YAML? | | `typos` | bool | `true` | Check for typos? | `web_format_tool` is only asked when `web_format` is `true`. `markdown_render_check` is only asked when `markdown` is `true`, and `markdown_render_check_dir` only when both are. ### What each answer controls | Answer | Config files written | Hooks added | | ------------------------- | ---------------------------------------------- | ---------------------------------------------------------- | | `ai` | `AGENTS.md` | — | | `conventional_commits` | — | `commitizen`; adds `pre-push` and `commit-msg` hook types | | `editorconfig` | `.editorconfig` | `editorconfig-checker` | | `markdown` | `.mdformat.toml`, `.rumdl.toml` | `mdformat`, `rumdl-fmt` | | `markdown_render_check` | `<dir>/check_markdown_render.py` | — | | `python` | `.ruff.toml`, `tests/test_*_security_audit.py` | `ruff-check`, `ruff-format`, `nbstripout` | | `docker` | `.hadolint.yaml` | `hadolint` | | `github_actions` | — | `check-github-workflows`, `check-github-actions`, `zizmor` | | `shell` | `.shellcheckrc` | `shellcheck`, `shfmt` | | `web_format` + `biome` | `.biome.jsonc` | `biome-check` | | `web_format` + `prettier` | `.prettierrc.yaml`, `.prettierignore` | `prettier` (system) | | `yaml` | `.yamllint.yaml` | `yamllint` | | `typos` | `.typos.toml` | `typos` | Hooks that are always present regardless of answers: `forbid-yml`, `forbid-rej`, the `pre-commit/pre-commit-hooks` set (large files, merge conflicts, private keys, case conflicts, AST, JSON/TOML/YAML syntax, EOF, line endings, trailing whitespace), `remove-crlf`, `fix-smartquotes`, `fix-ligatures`, `gitleaks`, and `strip-exif`. ## Detected values Two values come from inspecting the target repo rather than from a prompt. Detection lives in `extensions/detect.py`, loaded as a Jinja extension — which is why `--trust` is required. ### `hook_runner` Declared with `when: false`, so it is a computed value, never a question, and never written to `.copier-answers.yaml`. It is recomputed on every run and follows the repo if the runner changes. Detection order: 1. Installed git hook shims in `.git/hooks/` 2. A `prek.toml` in the repo 3. How CI pipelines invoke the runner 4. Which runner is on `PATH` 5. Fallback: `prek` The value only decides which runner the generated comments and the post-copy message name. Both runners read the same `.pre-commit-config.yaml`, so a wrong guess is cosmetic. ### `web_format_tool` A real question whose _default_ is detected, because biome and prettier need different config files. The answer is recorded and reused on every update. Detection order: 1. An existing `.pre-commit-config.yaml` that already runs one of them 2. Biome or prettier config files in the repo 3. `package.json` dependencies 4. Fallback: `biome` ## Changing an answer later Run `copier update --trust --answers-file .copier-answers.yaml` and give a different answer at the prompt. Copier deletes files gated on the old answer and creates files gated on the new one. Switching `web_format_tool` from `biome` to `prettier` removes `.biome.jsonc` and adds `.prettierrc.yaml` and `.prettierignore`; any local edits to the removed file are lost, so copy them somewhere safe first. Answering `no` to a question that was previously `yes` removes that tool's config file and hook block.
references/troubleshooting.md
# Troubleshooting — precommit-template ## Contents - Copier errors - Update conflicts - Hook failures - Commit rejections ## Copier errors ### `'detect_hook_runner' is undefined` **Cause**: `--trust` was omitted, so Copier skipped `extensions/detect.py` and the Jinja globals it registers were never defined. **Fix**: re-run with `--trust`. It is required on `copy`, `update`, and `recopy`. ### `Destination repository is dirty` **Cause**: Copier refuses to run against a working tree with uncommitted changes, so it can compute a clean diff. **Fix**: commit or stash first. ### Survey aborts partway through **Cause**: a detection helper raised. `detect_hook_runner()` and `detect_web_format_tool()` are called while rendering question defaults, and an exception there kills the whole survey. **Fix**: this is a template bug, not a usage error — report it. As a workaround, pass the value on the command line: `copier copy --trust --data web_format_tool=biome ...`. ### Nothing was generated for a tool you wanted **Cause**: the corresponding question was answered `no`, so the conditional filename rendered empty. **Fix**: `copier update --trust` and answer `yes`. ## Update conflicts ### `*.rej` files after `copier update` **Cause**: Copier could not merge a template change into a file you had edited locally. **Fix**: open each `.rej`, apply the rejected hunk by hand, then delete the `.rej` file. The `forbid-rej` hook blocks commits until they are gone, deliberately: deleting a `.rej` unread silently drops a template change. ### Local edits vanished after switching an answer **Cause**: changing an answer deletes files gated on the old value. Copier renders a rename as delete-then-create, which discards local edits with no conflict marker. **Fix**: recover from git history. To avoid it, copy customizations out before re-answering. ### Update pulled in far more than expected **Cause**: `copier update` renders against the _latest_ template release, not the next one, so several revisions can land at once. **Fix**: pin with `--vcs-ref <tag>` to step forward one release at a time. ## Hook failures ### `prettier: command not found` **Cause**: the `prettier` hook uses `language: system` — the runner does not install it. **Fix**: install the tool (`npm install -g prettier`), or select `biome` as the web formatter instead. ### `typos` cannot find its config **Cause**: the `typos` hook may need `.typos.toml` to already be committed before it will read it. **Fix**: commit `.typos.toml` first, then re-run. If that is not possible, comment out the `typos` block until the config is committed. ### `shfmt` fails to build **Cause**: shfmt-py's sdist only builds on CPython ≤ 3.13, which is why the hook pins `language_version: python3.13`. **Fix**: make a 3.13 interpreter available to the runner. ### The whole repo was rewritten on the first run **Cause**: expected. `mdformat`, `rumdl-fmt`, `ruff-format`, `shfmt`, and biome or prettier all reformat on first contact. **Fix**: commit the formatting churn on its own so the config change stays reviewable. ### A formatter and a linter disagree **Cause**: `mdformat` and `rumdl` both touch markdown and can fight over the same construct. **Fix**: disable the offending rule in `.rumdl.toml`. `MD060` (table spacing) is already disabled in this template for exactly that reason. ### Python code blocks in markdown get reformatted **Cause**: expected. `ruff-format` owns python code blocks inside markdown, so a single pinned ruff formats both `.py` files and the snippets in your docs. **Fix**: none needed. To exempt one block, wrap it in `<!-- fmt:off -->` / `<!-- fmt:on -->`. To exempt markdown entirely, add `"*.md"` to `extend-exclude` in `.ruff.toml` and drop `markdown` from the `ruff-format` hook's `types_or`. ## Commit rejections ### `YAML file extensions must be .yaml` **Cause**: the `forbid-yml` hook rejects any `.yml` file. **Fix**: rename it to `.yaml`. If a tool hard-codes `.yml` and cannot be changed, add it to the hook's `exclude` pattern in `.pre-commit-config.yaml`. ### `Commit messages must not include a Co-authored-by trailer` **Cause**: a repo-local `commit-msg` hook requires an `AI-assistant:` trailer instead. **Fix**: replace the trailer. ### commitizen rejects the message **Cause**: `conventional_commits` is enabled, so messages must match the Conventional Commits format — `type(scope): subject`. **Fix**: reword, e.g. `fix(hooks): correct shfmt args`.