
A documentation linter for Terraform providers. swissshepherd compares a providerβs schema against its Markdown documentation and reports missing, phantom, misordered, mislabeled, or misformatted docs.
π Source code: https://github.com/YakDriver/swissshepherd
swissshepherd compares a Terraform providerβs schema against its markdown documentation and reports:
<Kind>: prefixregion argumentgo install github.com/YakDriver/swissshepherd@latest
Requires Terraform in PATH when using provider_dir (for automatic schema generation).
Create .swissshepherd.hcl in your provider repo root:
provider_source = "registry.terraform.io/hashicorp/aws"
provider_dir = "."
Then run:
swissshepherd
Thatβs it. swissshepherd builds the provider, generates the schema, runs all checks, and reports findings.
# Check a single target (auto-detects type)
swissshepherd --target aws_iam_role
# Disambiguate when a name exists as both resource and data source
swissshepherd --target aws_instance --type resource
# Check all targets matching a prefix
swissshepherd --prefix aws_dms_
# Check all data sources
swissshepherd --type data_source
# Check a service's resources
swissshepherd --prefix aws_s3_ --type resource
swissshepherd [flags]
swissshepherd check [flags]
| Flag | Description |
|---|---|
--config |
Config file path (default: .swissshepherd.hcl) |
--schema-json |
Path to pre-generated terraform providers schema -json output |
--provider-source |
Provider source address (e.g., registry.terraform.io/hashicorp/aws) |
--provider-dir |
Path to provider source directory (triggers automatic build + schema generation) |
--target |
Check a single named target |
--type |
Restrict to a specific type (resource, data_source, ephemeral, function, list_resource, action) |
--prefix |
Check all targets whose name begins with this prefix |
--refresh-schema |
Regenerate cached schema even if schema_json file exists |
--json |
Output results as JSON |
--verbose |
Verbose logging (shows enabled checks, scoping, schema stats) |
--version |
Print version and exit |
CLI flags override config file values.
ERROR [rule_name] resource_name (path/to/file.md:42): message
WARN [rule_name] resource_name (path/to/file.md): message
Exit code 0 = all checks passed. Exit code 1 = one or more errors found.
All configuration lives in a single HCL file. The full reference:
# .swissshepherd.hcl
# βββ Provider identity βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
provider_source = "registry.terraform.io/hashicorp/aws"
provider_dir = "."
# βββ Schema caching βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# When set, the schema is cached at this path (relative to provider_dir).
# First run builds the provider and writes here; subsequent runs reuse it.
# Use --refresh-schema to regenerate after provider code changes.
schema_json = "terraform-providers-schema/schema.json"
# βββ Global ignore lists ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Suppress all schema+doc rule findings for deprecated/removed stubs.
# File rules (frontmatter, file_check) still run.
ignore_contents_check = ["aws_kms_secret"]
ignore_contents_check_file = "website/ignore-contents-check.txt"
# Map schema names to doc file names when they differ.
# Keys: plain "name" (all types) or "type/name" (scoped).
file_aliases = {
"list_resource/aws_ebs_volume" = "aws_ec2_ebs_volume"
"aws_alb" = "aws_lb"
}
# βββ Check blocks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Each check block configures one rule. All rules are enabled by default.
# Add `enabled = false` to disable. See "Rules reference" for details.
All relative paths in the config (provider_dir, schema_json, *_file options) resolve against the current working directory β the same convention as terraform, go, and make. The config fileβs location has no effect.
In practice: cd into the provider repo root and run swissshepherd from there:
cd terraform-provider-aws
swissshepherd --config .ci/swissshepherd.hcl
Options ending in _file (ignore_targets_file, allow_subcategories_file, ignore_missing_file, etc.) read one entry per line. Empty lines and lines starting with # are ignored.
| Rule | Kind | Description |
|---|---|---|
example_section |
per-target | Example code block validation |
file_check |
per-file | File size, extension, and link style validation |
file_match |
global | Fileβschema alignment: missing docs, orphan files, mixed layouts |
frontmatter |
per-file | YAML frontmatter field validation |
import_section |
per-target | Import section style and structure |
region_argument |
per-target | Region argument presence for region-aware types |
schema_docs |
per-target | Schema coverage, ordering, description style, heading style, format, labels, deprecation, and bylines |
section_presence |
per-target | Section presence, order, and recognition of unknown level-2 headings |
signature_section |
per-target | Function signature validation |
timeouts_section |
per-target | Timeout actions match schema bidirectionally |
title_section |
per-target | Level-1 heading validation |
Per-target rules run once per (resource, data source, etc.) and compare schema against parsed markdown. Per-file rules run on raw file bytes. Global rules run once per invocation.
The two largest rules β schema_docs and section_presence β have detailed reference docs in rules/. The smaller rules are documented inline below.
example_sectionValidates example code blocks:
check "example_section" {
allow_languages = ["terraform", "hcl"] # default
}
file_checkValidates file-level properties.
check "file_check" {
max_file_size = 500000 # bytes (default: 500KB registry limit)
allow_extensions = [".md", ".html.markdown", ".html.md"] # accepted extensions
allow_registry_extensions = [".md"] # stricter set for docs/ layout
inline_links = true # flag reference-style [ref]: url links
}
When inline_links is enabled, reference-style link definitions ([label]: url) are flagged with a suggestion to use inline [text](url) style instead.
file_matchValidates fileβschema alignment (runs once per invocation):
website/docs/) and registry (docs/) layoutscheck "file_match" {
require_doc = true # default: true
require_schema = true # default: true
mixed_layout = true # default: true
ignore_missing = ["aws_alb", "aws_alb_listener"] # no doc required
ignore_missing_file = "website/ignore-file-missing.txt"
ignore_extra = ["aws_removed_thing"] # orphan doc OK
ignore_extra_file = "website/ignore-file-mismatch.txt"
}
The ignore_missing and ignore_extra lists suppress findings for specific targets. Use ignore_missing_file / ignore_extra_file for file-based lists.
frontmatterValidates YAML frontmatter at the top of each doc file. Requirements can come from both the check block (global) and the type block (per-type via frontmatter_require / frontmatter_forbid). Both sources are merged β a field required by either is enforced.
check "frontmatter" {
# Require fields to be present
require_subcategory = true
require_page_title = true
require_description = true
require_layout = false # legacy layout only
# Forbid fields from being present
forbid_subcategory = false
forbid_page_title = false
forbid_description = false
forbid_layout = true # registry layout
forbid_sidebar_current = true # always forbidden in modern docs
# Subcategory allowlist (empty = anything goes)
allow_subcategories = ["S3", "VPC", "IAM"]
allow_subcategories_file = "website/allowed-subcategories.txt"
# Targets where subcategory: "" is acceptable
allow_empty_subcategory_targets = ["arn_build", "arn_parse"]
}
import_sectionValidates import section content and structure:
terraform and console)### Identity Schema subsectioncheck "import_section" {
require_identity_section = true # default: true
}
region_argumentValidates that region-aware resources document the region argument. Only fires for types with region_aware = true in their type block (resources, data sources, ephemerals by default).
check "region_argument" {
ignore_resources = ["aws_global_accelerator"]
ignore_resources_file = "website/ignore-region.txt"
}
schema_docsThe primary rule. Validates argument and attribute documentation against the provider schema. Eight sub-checks: byline, coverage, deprecated, description, format, heading, labels, ordering. Coverage enforces the Required / Optional / Read-Only model at every depth of nesting and supports inline (Read-Only) labels via allow_inline_read_only.
See rules/schema_docs.md for the full configuration reference, sub-check details, and the schema model.
section_presenceOwns the structural integrity of a doc file: presence, order, and recognition of level-2 sections. Configured in two places β the type block declares which sections may appear and in what order; the check "section_presence" block toggles enforcement.
See rules/section_presence.md for the full configuration reference, custom-section conventions, and schema-driven Timeouts behavior.
signature_sectionValidates function signature documentation:
No config options.
timeouts_sectionValidates that documented timeout actions match the schema bidirectionally:
No config options.
title_sectionValidates the level-1 heading (# Resource: aws_thing).
Checks:
<Kind>: prefix (from allow_prefixes or the typeβs title_prefix)## headingcheck "title_section" {
allow_prefixes = ["Resource", "Data Source", "Ephemeral", "Function"]
}
Every check block accepts path-scoping fields that control which targets the rule runs against. This is the primary mechanism for incremental rollout across a large provider.
check "schema_docs" {
# Type axis: only these type names (empty = all)
types = ["resource", "data_source"]
# Name axis (OR'd together): prefix match OR exact match
prefixes = ["aws_s3", "aws_appflow"]
targets = ["aws_instance"]
# Deny list: always excluded, even when allowlists match
ignore_targets = ["aws_legacy_resource"]
ignore_targets_file = "website/ignore-ordering.txt"
ignore_prefixes = ["aws_appstream"]
}
All four name-axis fields (prefixes, targets, ignore_targets, ignore_prefixes) support type/name notation for type-scoped entries:
check "schema_docs" {
# Only match aws_thing when it's a data source, not a resource
ignore_targets = ["data_source/aws_thing"]
# Only match aws_s3_ prefixed data sources
prefixes = ["data_source/aws_s3_"]
}
Plain entries (no /) match any type. Qualified entries (type/name or type/prefix) only match when the type matches.
Evaluation order:
ignore_targets / ignore_prefixes β if matched, target is excluded unconditionallytypes β if non-empty, targetβs type must be in the listprefixes / targets β if either is non-empty, target must match at least one; both empty = all names passswissshepherd ships with built-in type definitions for all standard Terraform documentation categories. Override any default or add new types in your config:
| Type | Schema kind | Default doc path | Region-aware |
|---|---|---|---|
resource |
resource |
website/docs/r/{name}.html.markdown |
yes |
data_source |
data_source |
website/docs/d/{name}.html.markdown |
yes |
ephemeral |
ephemeral |
website/docs/ephemeral-resources/{name}.html.markdown |
yes |
function |
function |
website/docs/functions/{name}.html.markdown |
no |
list_resource |
list_resource |
website/docs/list-resources/{name}.html.markdown |
yes |
action |
action |
website/docs/actions/{name}.html.markdown |
no |
guide |
none |
website/docs/guides/{name}.html.markdown |
no |
index |
none |
website/docs/index.html.markdown |
no |
type "resource" {
schema_kind = "resource"
website_paths = [
"docs/resources/{name}.md",
"website/docs/r/{name}.html.markdown",
]
title_prefix = "Resource"
region_aware = true
section "title" { required = true }
section "example" { required = true }
section "arguments" { required = true }
section "attributes" { required = true }
section "timeouts" {}
section "import" {}
section "signature" { forbidden = true }
frontmatter_require = ["description", "page_title"]
frontmatter_forbid = ["sidebar_current"]
arguments_bylines = [
"This resource supports the following arguments:",
"The following arguments are required:",
"The following arguments are optional:",
]
attributes_bylines = [
"This resource exports the following attributes in addition to the arguments above:",
]
}
| Field | Description |
|---|---|
schema_kind |
Schema accessor: resource, data_source, ephemeral, function, list_resource, action, none |
website_paths |
Ordered list of path templates. {name} = provider-prefix-stripped target name |
title_prefix |
Expected # <Prefix>: in the level-1 heading |
arguments_heading |
Override the expected heading text (default: "Argument Reference") |
arguments_bylines |
Allowed paragraph texts under ## Argument Reference |
attributes_bylines |
Allowed paragraph texts under ## Attribute Reference |
allow_missing_arguments_byline |
Donβt require a byline paragraph |
section "<name>" { ... } |
Declares a section in the doc structure. Order is the canonical doc order. Block fields: required, forbidden. See section_presence for details. |
frontmatter_require |
Fields that must be present in YAML frontmatter |
frontmatter_forbid |
Fields that must be absent |
region_aware |
Whether region_argument rule applies to this type |
The block_heading_styles list controls which ### heading formats are recognized as block documentation within argument/attribute sections.
| Placeholder | Matches | Example | Extracted name |
|---|---|---|---|
{Block} |
snake_case identifier | network_interface |
network_interface |
{Title} |
Title Case words (β snake_case) | Network Interface |
network_interface |
| Template | Matches heading | Extracted |
|---|---|---|
`{Block}` Block |
`network` Block |
network |
{Block} Configuration Block |
vpc_config Configuration Block |
vpc_config |
{Block} Argument Reference |
filter Argument Reference |
filter |
{Title} |
Credit Specification |
credit_specification |
Note: goldmark strips backticks from inline code in headings, so ### `network` Block becomes network Block in parsed text.
Set prefer_block_heading_styles to emit warnings when a heading matches an accepted style but not the preferred one:
check "schema_docs" {
block_heading_styles = ["`{Block}` Block", "{Block} Block", "{Block}", "{Title}"]
prefer_block_heading_styles = ["`{Block}` Block"]
}
When provider_dir is set, swissshepherd automatically:
go buildterraform init + terraform providers schema -jsonFor large providers where the build takes minutes, set schema_json to cache:
provider_dir = "."
schema_json = "terraform-providers-schema/schema.json"
First run builds and caches. Subsequent runs reuse the file instantly. Regenerate with --refresh-schema or delete the cached file.
Skip the build entirely by providing a pre-generated schema:
swissshepherd --schema-json path/to/schema.json --provider-source registry.terraform.io/hashicorp/aws
terraform providers schema -json, not Go source parsingRule (schema + AST) and FileRule (raw bytes). The runner reads each file once and feeds it to bothtypes, prefixes, targets, ignore_targets, ignore_prefixesMPL-2.0