zsh-lint is a standalone, Go-based semantic analyzer for Zsh.
It parses Zsh source into an abstract syntax tree with the Zsh dialect (LangZsh) of mvdan/sh, evaluates static analysis rules, enforces Zsh Plugin Standard v2, and reports compiler-style diagnostics or structured JSON.
The parser is a maintained fork in third_party/mvdan-sh that adds Zsh grammar upstream does not yet cover; it does not parse Zsh as POSIX sh or Bash.
General shell linters such as ShellCheck reject Zsh scripts, so zsh-lint covers Zsh code and Zsh plugins directly.
Important
Canonical documentation lives on the Z-Shell Wiki: wiki.zshell.dev/community/zsh_lint. Consult the wiki for end-user guides, file selection strategies, suppression syntax, CI workflows, and the published rule reference.
- Semantic static analysis: Evaluates syntax trees for unquoted variables, backquote command substitutions, special parameter shadowing, unsafe
evalcalls, and style issues without executing scripts. - Zsh Plugin Standard v2 enforcement: Every run checks unload functions, function-scoped options,
$0handling, andfpathhygiene. Projects with a validatedzsh-lint.jsonadditionally get thez-shell/project@2profile: function and parameter namespaces, the sharedPluginsregistry, load-only helpers, and repeated external commands. - Automatic project discovery: Discovers
zsh-lint.jsonconfiguration files up the directory hierarchy to contextualize standalone scripts, plugin entrypoints, autoloaded functions, and completions. - Inline suppression: Silences one intentional finding at a time with
# zsh-lint disable=<rule-id> -- reason; suppressions must name a rule and are audited throughmeta/*diagnostics. - Greppable and JSON diagnostics: Outputs standard
file:line:col: [rule] messagediagnostics for terminal and editor workflows, or structured JSON for automated CI checks. - Corpus survey tooling: Includes the companion
zsh-lint-surveyCLI to survey parser front-end coverage across large Zsh codebases without running lint rules.
- Go 1.27 or newer (when compiling from source or installing via
go install).
Note
zsh-lint neither sources nor executes the files it analyzes, and it does not call zsh. A native syntax check such as zsh -f -n -- file.zsh is a separate, recommended step.
Build and install the latest tagged release:
go install github.com/z-shell/zsh-lint/cmd/zsh-lint@latestTo install the parser survey tool:
go install github.com/z-shell/zsh-lint/cmd/zsh-lint-survey@latestEnsure $(go env GOPATH)/bin is included in your PATH.
git clone https://github.com/z-shell/zsh-lint.git
cd zsh-lint
go build ./cmd/zsh-lintLet Zi clone the repository, build the binary with the local Go toolchain, and put it on PATH:
zi as"program" nocompletions atclone"go build ./cmd/zsh-lint" atpull"%atclone" pick"zsh-lint" for z-shell/zsh-lintPass one or more file paths to zsh-lint:
zsh-lint script.zsh plugin.plugin.zsh| Option | Type | Description |
|---|---|---|
--config PATH |
string | Explicit project configuration path; disables discovery. |
--no-config |
boolean | Disable automatic zsh-lint.json discovery and use default rules. |
--format=json |
string | Emit machine-readable diagnostics in JSON format. |
--config and --no-config are mutually exclusive.
Standard terminal output format:
script.zsh:2:8: [quoting/unquoted-var] Variable expansion should be double-quoted
plugin.plugin.zsh:3:1: [plugin/unload-function] Unload function 'example_plugin_unload' should unfunction itself upon completion
Findings at error or warning severity fail the run; info and hint findings are reported but do not change the exit code.
The rule reference lists every rule with its severity.
Note
The CLI analyzes each explicitly provided file path as Zsh source. It does not recurse into directories or filter files by shebang.
Add a zsh-lint.json file at the root of a repository to provide project context for multi-file plugins and tools.
zsh-lint walks upward from each input file to find the nearest configuration automatically.
{
"version": 2,
"project": {
"kind": "plugin",
"minimum_zsh": "5.8",
"identifier": "example"
},
"sources": [
{ "root": "example.plugin.zsh", "profile": "sourced-library" },
{ "root": "functions", "profile": "autoload-function" },
{
"root": "completions",
"profile": "autoload-function",
"role": "completion"
},
{ "root": "tests", "profile": "test-fixture" }
]
}A validated version 2 configuration selects the z-shell/project@2 rule profile automatically; there is no profile key to set.
This file is examples/plugin/zsh-lint.json, and examples/standalone shows the application kind.
Schema fields and source profiles are described in the Project Configuration Guide.
Advanced: Parser survey utility (zsh-lint-survey)
zsh-lint-survey executes the parser front end across target files and reports parser gaps without evaluating static analysis rules.
It is used for corpus validation and upstream syntax compatibility tracking.
zsh-lint-survey path/to/*.zsh
zsh-lint-survey -trace-parses path/to/file.zsh # also report parse count and adapter depth on stderr
zsh-lint-survey -compare ./base/zsh-lint-survey -native path/to/*.zsh # verdict changes against a base build
zsh-lint-survey -compare ./base/zsh-lint-survey -native -runtime -table rows.md grid/*.zsh # probe rows, run where zsh -n cannot decide
zsh-lint-probe -bodies bodies.txt -out grid/ # place construct variants in every scanner context
zsh-lint-probe -rows rows.txt -out grid/ # one probe file per line
zsh-lint-survey -judge path/to/file.zsh # gap, false accept or agree, by zsh -f -n and zsh-lint
zsh-lint-survey -reduce path/to/file.zsh > reduced.zsh # shrink a gap or false accept, keeping its message and place
bash .github/scripts/verify-parser-change.sh --rows rows.txt # a parser change's whole verificationAdvanced: JSON output contract
--format=json writes one versioned envelope to stdout.
Parser failures share it under the reserved rule parse/error, and an unpositioned diagnostic omits range:
{
"version": 1,
"diagnostics": [
{
"rule": "quoting/unquoted-var",
"severity": "info",
"message": "Variable expansion should be double-quoted",
"file": "script.zsh",
"range": {
"start": { "line": 2, "column": 8, "offset": 15 },
"end": { "line": 2, "column": 10, "offset": 17 }
}
}
],
"summary": {
"files": 1,
"diagnostics": 1,
"errors": 0,
"warnings": 0,
"infos": 1,
"hints": 0
}
}The full contract, including sort order and versioning rules, is docs/project/output-contract.md.
Exit code conventions
| Exit code | Description |
|---|---|
0 |
Every input parsed, and no finding at error or warning severity; info and hint findings may still be printed. |
1 |
At least one parser failure, unreadable input file, per-file configuration discovery failure, or finding at error or warning severity. |
2 |
Invocation error: unknown or repeated flags, no input paths, --config with --no-config, or an explicit --config file that fails to load or does not cover an input path. |
Run the test suite and static analysis from the repository root:
go build ./... && go vet ./... && go test ./...
golangci-lint run ./...To regenerate API and code-derived reference documentation:
go tool gomarkdoc --output ref.md ./cmd/zsh-lint ./cmd/zsh-lint-survey ./internal/survey ./internal/rulesBeyond the unit tests, CI runs the parser fork's upstream and Zsh grammar tests with native-shell confirmation, checks the repository's Zsh files with zsh -n, and parses and lints a pinned reference corpus of real Z-Shell projects.
Parser changes are also verified with sampled mutation testing, as the parser-gap workflow describes.
Contributor workflows and local testing practices are documented in docs/README.md.
Report parser gaps and propose rules through the issue forms; anything else can be a blank issue.
Pull requests link their owning issue or carry the meta:no-issue label.
- Z-Shell Wiki: Zsh Lint Documentation
- Zsh Plugin Standard v2
- Official Zsh Manual
- Zi Plugin Manager
- Issue Tracker
Contributions integrate on main via squash merge after review.
Annotated vX.Y.Z tags mark official releases and trigger automated publication workflows.
Contributions follow the Z-Shell Organization Guidelines. Distributed under the terms of the GNU General Public License v3.0. See LICENSE for details.
Developed with ❤️ by the Z-Shell Community.