Prompt file linter with front matter, variable and tag checks
A Python linter and CI workflow for prompt templates in prompts/. It catches undeclared {{variables}}, unclosed XML-style tags, missing front matter, duplicate prompt names and invisible characters before they reach a model.
Automation in Engineering by AISkillHub Library
What it does
Prompt templates break in ways normal linters do not see. A `{{custmer_name}}` typo ships as literal braces. An `<instructions>` tag that is never closed swallows the rest of the prompt. Two files claim the same `name`, so the loader picks one at random. A non-breaking space pasted from a doc changes tokenization. This linter checks prompt files for exactly these problems.
**Expected file shape**
``` --- name: ticket-summary description: Summarizes a support ticket thread. variables: [product_name, ticket_thread] --- Prompt body using {{product_name}} and <ticket>{{ticket_thread}}</ticket>. ```
**Rules**
| Code | Problem | |---|---| | E001 | No front matter block | | E002 | Required key missing (default: `name`, `description`) | | E003 | `{{variable}}` used but not declared in `variables` | | E004 | XML-style tag opened and not closed, or closed without opening | | E005 | Two files share the same `name` | | W101 | Variable declared but never used | | W102 | Trailing whitespace | | W103 | Zero-width space, BOM or non-breaking space | | W104 | Body longer than `--max-chars` |
Content inside fenced code blocks is ignored, so example JSON or HTML in a prompt does not trigger tag errors. Errors fail the run; warnings fail only with `--strict`.
**What it will not do**
- Judge whether the prompt is good. Pair it with evals for that. - Parse full YAML. Front matter is read as flat `key: value` lines, which covers the keys it checks. - Understand template engines other than `{{name}}` placeholders (no Jinja blocks or Handlebars helpers).
The listing includes a sample prompt, `prompts/example-ticket-summary.md`, that passes the linter, so you can see the expected format.
How to use it
1. Copy `scripts/lint_prompts.py` and `.github/workflows/lint-prompts.yml` into your repository. Optionally copy `prompts/example-ticket-summary.md` as a reference, or delete it once you have your own prompts. 2. Put prompt files under `prompts/` with the front matter shown in the description. `README.md` files in that folder are skipped. 3. Run locally: ``` python3 scripts/lint_prompts.py # lints ./prompts python3 scripts/lint_prompts.py prompts --strict # warnings fail too python3 scripts/lint_prompts.py --require name --require description --require owner python3 scripts/lint_prompts.py --ext .md --ext .txt --max-chars 12000 ``` 4. The workflow runs on pull requests that touch `prompts/**` and uses `--strict`. Remove `--strict` in the workflow if you only want errors to block merges.
Exit codes: 0 clean, 1 errors (or warnings with `--strict`), 2 path not found or no prompt files. Python 3.8+, no packages or environment variables.
Files
Save each file at the path shown, relative to your project root.