structlint uses YAML or JSON configuration files to define validation rules.
Resolution order:
--configflag value (orSTRUCTLINT_CONFIGenv) — exact path, no discovery..structlint.yamlin the current directory.- Upward search — starting from
--path(or cwd), each ancestor directory is checked for.structlint.yaml,.structlint.yml, then.structlint.json. The search stops after checking the first directory containing.git(inclusive — a config sitting alongside.gitis discoverable) or at the filesystem root.
When discovery finds a config, structlint logs using config: <path> (discovered) at info level.
Discovered configs behave exactly like explicit ones: their glob patterns match paths relative to the validation root (--path, defaulting to cwd), not to the directory the config lives in. If you run structlint validate --path . from a subdirectory of a monorepo whose config lives at the root, patterns like internal/** are matched against paths relative to your subdirectory, not the root — a repo-root config typically won't do the right thing when validating a single subtree.
For monorepos, put a .structlint.yaml in each package/service root and run structlint per-package. extends (spec 007) lets each package share a common preset.
dir_structure:
allowedPaths: [] # Glob patterns for allowed directories
disallowedPaths: [] # Glob patterns for disallowed directories
requiredPaths: [] # Directories that must exist
file_naming_pattern:
allowed: [] # Glob patterns for allowed files
disallowed: [] # Glob patterns for disallowed files
required: [] # Files that must exist (supports globs)
placement: [] # Files that must live under specific directories
requiredGroups: [] # One-of and per-directory required files
boundaries: [] # Go import boundary rules
ignore: [] # Paths to skip during validationConfiguration loading is strict. Unknown keys such as allowed_paths fail before validation starts, which helps catch CI drift caused by typos.
A config can inherit from one or more parents:
# Requires structlint >= v0.6.0 — older binaries reject the `extends` key.
extends: go-standard
dir_structure:
allowedPaths:
- "tools/**" # additional paths on top of the presetextends accepts a string or a list. Each entry is either a built-in preset or a filesystem path relative to this file.
Built-in presets:
| Name | Baseline for |
|---|---|
go-standard |
Go projects (cmd/, internal/, pkg/, ...) |
node-standard |
Node.js / TypeScript projects |
python-standard |
Python projects (src/, tests/, ...) |
generic |
Language-agnostic starter |
Merge rules:
- String slices (
ignore,dir_structure.*,file_naming_pattern.*) — parent entries first, then child entries not already present. Order stable, exact-string dedup. placement,requiredGroups,boundaries— keyed byid. Same ID → child rule replaces the parent's wholesale. New IDs append.- Parents resolve depth-first. Chains are cycle-checked and capped at depth 10.
Compatibility warning: the extends key requires structlint v0.6.0 or newer. Older binaries would strict-parse the file and reject it with field extends not found in type config.Config — a confusing error for the actual problem.
To make old-binary failures actionable, add a # requires structlint >= vX.Y comment at the top of any config that uses extends:
# requires structlint >= v0.6.0
extends: go-standardWhen a binary older than the pragma sees this comment, it stops before strict parsing and prints:
.structlint.yaml requires structlint >= v0.6.0, but running version is v0.5.0.
Upgrade the binary (go install github.com/AxeForging/structlint/cmd/structlint@latest) …
The pragma is checked before strict parsing, so it works even on binaries too old to know about extends. Dev builds (unstamped go run) skip the check.
structlint ships a JSON Schema at schema/structlint.schema.json. Add this modeline to the top of your .structlint.yaml for completion, hover docs, and pre-run validation:
# yaml-language-server: $schema=https://raw.githubusercontent.com/AxeForging/structlint/main/schema/structlint.schema.jsonThe Red Hat YAML extension picks this up in VS Code. JetBrains and Neovim with yaml-language-server behave the same way. JSON configs can use a top-level "$schema" key:
{
"$schema": "https://raw.githubusercontent.com/AxeForging/structlint/main/schema/structlint.schema.json",
"dir_structure": { "allowedPaths": ["."] }
}The schema uses additionalProperties: false throughout, mirroring the strict parser — so typos like placment: get flagged in the editor with the same rejection the CLI would produce.
structlint supports standard glob patterns:
| Pattern | Description |
|---|---|
* |
Matches any sequence of characters (not including /) |
** |
Matches any sequence including / (recursive) |
? |
Matches any single character |
[abc] |
Matches any character in the set |
[!abc] |
Matches any character not in the set |
allowedPaths:
- "." # Root directory only
- "cmd/**" # cmd/ and all subdirectories
- "src/*.go" # Go files directly in src/
- "test/*" # Direct children of test/Directories that are permitted. If specified, any directory not matching these patterns is a violation.
dir_structure:
allowedPaths:
- "."
- "cmd/**"
- "internal/**"
- "pkg/**"
- "test/**"Directories that are explicitly forbidden.
dir_structure:
disallowedPaths:
- "vendor/**"
- "node_modules/**"
- "tmp/**"
- ".cache/**"Directories that must exist.
dir_structure:
requiredPaths:
- "cmd"
- "internal"
- "docs"File patterns that are permitted.
file_naming_pattern:
allowed:
- "*.go"
- "*.yaml"
- "*.yml"
- "*.json"
- "*.md"
- "Makefile"
- "Dockerfile*"
- ".gitignore"File patterns that are forbidden.
file_naming_pattern:
disallowed:
- "*.env*"
- "*.log"
- "*.tmp"
- "*~"
- "*.bak"Files that must exist.
file_naming_pattern:
required:
- "go.mod"
- "README.md"
- ".gitignore"
- "*.go" # At least one Go filePaths to completely skip during validation.
ignore:
- ".git"
- "vendor"
- "node_modules"
- "bin"
- "dist"
- ".idea"
- ".vscode"Placement rules ensure files of a given kind live in the expected part of the repository.
placement:
- id: sql-in-migrations
files: ["*.sql"]
mustBeUnder: ["migrations/**"]
- id: tests-near-code
files: ["*_test.go"]
mustBeUnder: ["internal/**", "pkg/**", "test/**"]| Field | Description |
|---|---|
id |
Stable rule identifier used in JSON, SARIF, and GitHub annotations |
files |
File name or path globs to match |
mustBeUnder |
Directory globs where matching files are allowed |
severity |
Optional severity, defaults to error |
Required groups model repository contracts that are more expressive than a single required path.
requiredGroups:
- id: build-entrypoint
oneOf: ["Makefile", "Taskfile.yml", "justfile"]
- id: commands-have-main
eachDirMatching: "cmd/*"
mustContain: ["main.go"]
requireMatch: true
- id: packages-have-docs
eachDirMatching: "internal/*"
mustContainOneOf: ["README.md", "doc.go"]| Field | Description |
|---|---|
oneOf |
At least one listed path or glob must exist |
eachDirMatching |
Directory glob to apply per-directory checks to |
mustContain |
Every matching directory must contain each listed file |
mustContainOneOf |
Every matching directory must contain at least one listed file |
requireMatch |
Fail if eachDirMatching finds no directories |
Boundary rules parse imports and block unwanted dependencies between layers. They are language-aware for Go, JavaScript, TypeScript, and Python source files.
boundaries:
- id: domain-no-db
from: "internal/domain/**"
cannotImport:
- "internal/db/**"
- "internal/http/**"For Go module imports, structlint reads go.mod and converts imports like example.com/app/internal/db to internal/db before matching cannotImport. For JS/TS relative imports, paths like ../db/client are resolved relative to the importing file. For Python, dotted imports like app.db.client are normalized to app/db/client.
# .structlint.yaml - Go Project Configuration
dir_structure:
allowedPaths:
- "."
- "cmd/**"
- "internal/**"
- "pkg/**"
- "api/**"
- "web/**"
- "configs/**"
- "scripts/**"
- "test/**"
- "docs/**"
- ".github/**"
disallowedPaths:
- "vendor/**"
- "node_modules/**"
- "tmp/**"
- "temp/**"
requiredPaths:
- "cmd"
- "internal"
file_naming_pattern:
allowed:
- "*.go"
- "*.mod"
- "*.sum"
- "*.yaml"
- "*.yml"
- "*.json"
- "*.toml"
- "*.md"
- "*.txt"
- "Makefile"
- "Dockerfile*"
- ".gitignore"
- ".golangci.yml"
- "go.work"
disallowed:
- "*.env*"
- "*.log"
- "*.tmp"
- "*~"
- "*.swp"
- ".DS_Store"
required:
- "go.mod"
- "README.md"
- ".gitignore"
placement:
- id: migrations-only
files: ["*.sql"]
mustBeUnder: ["migrations/**"]
requiredGroups:
- id: build-entrypoint
oneOf: ["Makefile", "Taskfile.yml", "justfile"]
- id: commands-have-main
eachDirMatching: "cmd/*"
mustContain: ["main.go"]
boundaries:
- id: domain-no-infrastructure
from: "internal/domain/**"
cannotImport: ["internal/db/**", "internal/http/**"]
ignore:
- ".git"
- "vendor"
- "bin"
- "dist"
- ".idea"
- ".vscode"All configuration options can be overridden via environment variables:
| Variable | Description |
|---|---|
STRUCTLINT_CONFIG |
Path to configuration file |
STRUCTLINT_LOG_LEVEL |
Log level (debug, info, warn, error) |
STRUCTLINT_NO_COLOR |
Disable colored output |