# Lint specs in CI with a spec linter

Canonical: https://specscore.md/articles/lint-specs-in-ci/

Last updated: 4 November 2026

A spec linter is a program that checks a specification for structural mistakes and exits with a failure code when it finds them. [Continuous integration](https://specscore.md/validators/) (CI) is an automatic check on every proposed change. Continuous integration and continuous delivery (CI/CD) is the pipeline that runs that check and keeps a passing change releasable. [Spec-driven development](https://specscore.md/spec-driven-development/) (SDD) is the practice of writing the specification before the code and treating the specification as the source of truth. A product requirements document (PRD) is the prose that states the problem and the scope. In [SpecScore](https://specscore.md/), a requirement (REQ) is a named rule inside a feature, and an acceptance criterion (AC) is an optional abstract condition that points at one or more of those rules. The check is a [command-line interface](https://specscore.md/cli/) (CLI) command, `specscore spec lint`, from the CLI in [specscore/specscore-cli](https://github.com/specscore/specscore-cli) ([Apache-2.0](https://github.com/specscore/specscore-cli/blob/main/LICENSE)). [Easy Approach to Requirements Syntax](https://specscore.md/articles/ears-requirements/) (EARS) is a sentence pattern for a requirement. This linter checks headings, required sections, and references. It does not grade the English.

## What linting a specification means

Linting a spec is the same kind of check as linting code. A deterministic program reads the files and reports rule violations. It does not decide whether the product is a good idea, and it does not rewrite the requirement into better prose. The [validators page](https://specscore.md/validators/) separates that structural check from a manual AI review. `specscore spec lint` is the structural check.

The command’s description, in [internal/cli/spec.go](https://github.com/specscore/specscore-cli/blob/v0.55.1/internal/cli/spec.go) at CLI tag v0.55.1 and at the v0.39.1 tag this repository’s workflow installs, says it validates the spec tree for structural convention violations. A clean run prints `0 violations found` and exits 0. A run that finds violations prints one line per finding and exits 1. That exit code is what fails a CI job. Invalid arguments exit 2. A missing `specscore.yaml` exits 3, and the message tells you to run `specscore init`.

## What the spec linter checks

The rules are the CLI’s, not a separate cloud service. The generated catalog [docs/lint-rules.md](https://github.com/specscore/specscore-cli/blob/v0.55.1/docs/lint-rules.md) at tag v0.55.1 lists 183 rules. This website’s workflow installs v0.39.1, an older release, so a rule that exists only in a later tag is not what that job runs. The rules named in this section are in both tags. The default `--severity` is `error`. A warning is printed only when you pass `--severity warning` or `info`, and the default command does not fail on it. The catalog marks `dogfood-version-bump` as a warning, so the default command does not fail when the workflow pin is behind the binary.

### Required files and sections

Rule `readme-exists` ([readme_exists.go](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/readme_exists.go)) requires a `README.md` in every spec directory. Rule `oq-section` ([oq_section.go](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/oq_section.go)) requires a feature README to contain an `## Open Questions` section. A legacy `## Outstanding Questions` heading is a separate finding, and `--fix` rewrites that heading in place. The message in the example below, “Open Questions section not found”, is the finding from a feature README that had the section removed.

### Index rows and child directories

Rule `index-entries` ([index_entries.go](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/index_entries.go)) checks a feature index against the child directories on disk. A child that is not listed is a violation, and a row whose link points at a child that is not there is a violation. `--fix` can delete the phantom row and insert the missing one. This is directory and index integrity. It is not a crawl of every URL in the prose.

### Acceptance-criteria heading shape

Rule `ac-heading-format` ([ac_heading.go](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/ac_heading.go)) applies to a feature README, a file whose title is `# Feature:`. A line that looks like an acceptance-criterion heading must be exactly `### AC: <id>` with one space after the colon and a kebab-case id. A heading of the form `### AC: <id> (verifies ...)` is also accepted. `--fix` repairs spacing only. An id with uppercase letters or underscores is reported and left for a person. The rule does not require the `**Requirements:**` line, does not score words such as “graceful” or “fast”, and does not warn when the prose contains Given, When, or Then. How to write the criterion itself is in [how to write acceptance criteria](https://specscore.md/articles/acceptance-criteria/).

### Plan references to those IDs

On a plan that names a source feature, [plan_rules.go](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/plan_rules.go) reads `### AC:` headings in that feature’s README. Rule `P-002` reports a task `**Verifies:**` value that does not resolve to `<feature-slug>#ac:<ac-slug>` for one of those headings, an empty `**Verifies:**` line, a missing or unrecognized source line, or a source feature that does not resolve to a feature README. Rule `P-001` reports a heading that no task covers and that is not listed under `## Deferred AC Coverage`. Idea-sourced plans (`**Source:** idea:<slug>`) and plans with `**Source:** none` are exempt, and so is a plan in a terminal disposition status. The one-line catalog text for `P-002` says “valid dependency references”; the checker in that file is the AC-reference check just described.

### What a passing lint is not

The same command does not decide whether a requirement solves the stated problem, whether a name makes sense, or whether two features contradict each other in meaning. The validators page lists those as outside the CLI linter. It also does not check scenario step keywords. `specscore rehearse` runs scenario files. The workflow on this repository runs `specscore spec lint` and does not call rehearse. The catalog names further families — ideas, decisions, entities, properties, lessons, issues, and the rest of the plan rules — and each rule applies to the files it owns. The catalog is the list. This page does not restate all 183 descriptions.

## Run specscore spec lint in CI

The command looks upward from the working directory for `specscore.yaml`. Without that file it exits 3 and names `specscore init`. A CI job therefore needs the config file in the checkout. [scripts/install.sh](https://github.com/specscore/specscore-cli/blob/v0.55.1/scripts/install.sh) reads `SPECSCORE_VERSION` and defaults to `latest` when the variable is unset. Pin a tag if you want the same rules on every run.

This repository’s check is [dogfood.yml](https://github.com/specscore/specscore/blob/main/.github/workflows/dogfood.yml). It installs the CLI with `SPECSCORE_VERSION` set to `v0.39.1` and then runs `specscore spec lint` with no extra flags, on pull requests and on pushes to main. GitHub Actions fails the step when that process exits non-zero. The excerpt below keeps those install and lint steps. The file also sets `permissions: contents: read`, a concurrency group, and a comment on why the pin is not `latest`.

### Install and lint steps from dogfood.yml

```yaml
name: Dogfood lint

on:
  push:
    branches: [main]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
      - name: Install specscore CLI (pinned)
        env:
          SPECSCORE_VERSION: v0.39.1
        run: |
          curl -fsSL https://specscore.md/install/get-cli | \
            SPECSCORE_INSTALL_DIR="$HOME/.local/bin" sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
          specscore --version
      - name: Lint spec tree
        run: specscore spec lint
```

### Failure report (specscore 0.55.0, same line shape in v0.39.1)

```text
features/apply-discount/README.md:0 [error] oq-section: Open Questions section not found
features/apply-discount/README.md:0 [error] index-entries: Child directory not listed in index: refunds
features/apply-discount/refunds:0 [error] readme-exists: README.md not found

3 violations found (3 errors)
```

### Clean report

```text
0 violations found
```

Each finding is `file:line [severity] rule: message`. The summary is `N violations found` plus a parenthetical count, such as `(3 errors)`. The exit code is the process status, not a line in the report. The three lines above were captured from specscore 0.55.0 on a feature whose Open Questions section had been removed and whose index omitted an empty child directory. They are the same report the CLI page shows. `--format` accepts `text` (the default), `json`, or `yaml`. `--rules` and `--ignore` select rules and cannot be combined. `--fix` rewrites the findings that have a fixer, including the Outstanding Questions heading. The dogfood job does not pass `--fix`.

## Tools that do not lint this format in CI

A requirements quality check, in this CLI, is the structural check above. It is not a score of the prose. Other spec-driven tools publish different checks. The paragraphs below use only what those projects’ own pages say. The [tools roundup](https://specscore.md/spec-driven-development/tools/) records the same pages as read on 30 October 2026, except the Spec Kit documentation home, which prints 28 September 2026, and the Kiro feature-spec page, which prints 4 August 2026.

[GitHub Spec Kit](https://github.github.io/spec-kit/) is an agent workflow that writes Markdown artifacts. The [agentic SDD reference](https://github.github.io/spec-kit/reference/agentic-sdd.html) describes `/speckit.analyze` as a report of inconsistencies across the spec, the plan, and the tasks, without editing the files. You run it in the agent. The documentation home also names community extensions, including one called CI Guard, that add compliance gates. That is not a command that lints a SpecScore feature README.

[OpenSpec](https://github.com/Fission-AI/OpenSpec) does publish a structural check. `openspec validate`, documented in the [CLI reference](https://openspec.dev/docs/cli), checks OpenSpec’s change and spec structure, exits non-zero when validation fails, and the reference includes a shell snippet a CI job can run. That check is OpenSpec’s shape, not `### AC:` headings or a SpecScore index.

[Kiro’s feature-spec page](https://kiro.dev/docs/specs/feature-specs/) describes Analyze Requirements as a check inside Kiro, before design, looking for inconsistencies, ambiguity, conflicting constraints, and gaps. It is not the command in `dogfood.yml`. The sentence patterns Kiro writes are in [EARS requirements syntax](https://specscore.md/articles/ears-requirements/).

The [BMAD Method](https://github.com/bmad-code-org/BMAD-METHOD) README describes the method’s artifacts and agent workflows. It does not document a command that lints a fixed spec schema in CI. The [Task Master](https://github.com/eyaltoledano/claude-task-master) README describes parsing a PRD into tasks with `task-master parse-prd`. It does not describe a linter for that PRD. A PRD can still be the input to a SpecScore feature, and `specscore spec lint` is what then checks the feature file.

## FAQ

### What is a spec linter?

A spec linter reads specification files and reports structural violations. `specscore spec lint` is that command for a SpecScore tree. It exits 0 when it finds nothing at the chosen severity, and 1 when it finds violations.

### Does the spec linter judge requirements quality in the wording?

No. `ac-heading-format` checks the `### AC:` heading. It does not score words such as “graceful” or “fast”, and it does not warn when a criterion contains Given, When, or Then. Wording is a writing task. The validators page separates that deterministic lint from the manual AI review.

### How do I lint specs in CI?

Install the CLI with `SPECSCORE_VERSION` set to a release tag, then run `specscore spec lint`. The checkout needs a `specscore.yaml`. [dogfood.yml](https://github.com/specscore/specscore/blob/main/.github/workflows/dogfood.yml) is a working GitHub Actions example. It pins v0.39.1.

### What does a failing run print?

One line per finding, `file:line [severity] rule: message`, then a summary such as `3 violations found (3 errors)`. The process exits 1. A clean tree prints `0 violations found` and exits 0.

### Do other spec-driven tools validate specifications in CI/CD?

OpenSpec’s `openspec validate` can fail a CI job, and it checks OpenSpec’s own change structure. Spec Kit’s `/speckit.analyze` runs in the agent session. Kiro’s Analyze Requirements runs inside Kiro. The BMAD Method and Task Master READMEs do not document a format linter for CI. SpecScore’s dogfood workflow runs `specscore spec lint` on the SpecScore tree.

## Sources

CLI behavior checked 4 November 2026 against specscore-cli tags v0.55.1 and v0.39.1. Competitor pages are the ones linked below; the tools roundup records them as read on 30 October 2026, except where the page prints its own date.

- [specscore/specscore-cli](https://github.com/specscore/specscore-cli)
- [specscore-cli — Apache-2.0 licence](https://github.com/specscore/specscore-cli/blob/main/LICENSE)
- [specscore/specscore](https://github.com/specscore/specscore)
- [specscore-cli v0.55.1 — lint rule catalog (183 rules)](https://github.com/specscore/specscore-cli/blob/v0.55.1/docs/lint-rules.md)
- [specscore/specscore — dogfood.yml (CLI v0.39.1)](https://github.com/specscore/specscore/blob/main/.github/workflows/dogfood.yml)
- [specscore-cli — install.sh (SPECSCORE_VERSION)](https://github.com/specscore/specscore-cli/blob/v0.55.1/scripts/install.sh)
- [specscore-cli v0.55.1 — readme-exists](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/readme_exists.go)
- [specscore-cli v0.55.1 — oq-section](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/oq_section.go)
- [specscore-cli v0.55.1 — index-entries](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/index_entries.go)
- [specscore-cli v0.55.1 — ac-heading-format](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/ac_heading.go)
- [specscore-cli v0.55.1 — plan rules P-001 and P-002](https://github.com/specscore/specscore-cli/blob/v0.55.1/pkg/lint/plan_rules.go)
- [What SpecScore validates](https://specscore.md/validators/)
- [How to write acceptance criteria AI agents can verify](https://specscore.md/articles/acceptance-criteria/)
- [EARS requirements syntax explained, with examples](https://specscore.md/articles/ears-requirements/)
- [GitHub Spec Kit documentation (page date 28 September 2026)](https://github.github.io/spec-kit/)
- [GitHub Spec Kit — agentic SDD reference (/speckit.analyze)](https://github.github.io/spec-kit/reference/agentic-sdd.html)
- [OpenSpec CLI reference — openspec validate](https://openspec.dev/docs/cli)
- [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec)
- [Kiro — feature specs (updated 4 August 2026)](https://kiro.dev/docs/specs/feature-specs/)
- [bmad-code-org/BMAD-METHOD](https://github.com/bmad-code-org/BMAD-METHOD)
- [eyaltoledano/claude-task-master](https://github.com/eyaltoledano/claude-task-master)
- [Spec-driven development tools](https://specscore.md/spec-driven-development/tools/)

## Related

- [Spec-driven development with Claude Code and Cursor](https://specscore.md/articles/claude-code-specs/)
- [PRD template in Markdown for AI coding](https://specscore.md/articles/feature-spec-template/)
- [How to write acceptance criteria AI agents can verify](https://specscore.md/articles/acceptance-criteria/)
- [EARS requirements syntax explained, with examples](https://specscore.md/articles/ears-requirements/)
- [What SpecScore validates](https://specscore.md/validators/)
- [CLI](https://specscore.md/cli/)
- [Install the CLI](https://specscore.md/cli/install/)
- [Spec-driven development](https://specscore.md/spec-driven-development/)
- [Spec-driven development tools](https://specscore.md/spec-driven-development/tools/)
- [Feature specification](https://specscore.md/feature-specification)
- [Acceptance criteria specification](https://specscore.md/acceptance-criteria-specification)
