Help us shape the future of Go! Take the 2026 Go Developer Survey

zsh-lint

module
v1.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: GPL-3.0

README

Zsh Lint logo

Zsh Lint

Standalone Go-based semantic analyzer and static linter for Zsh scripts.

Go CI status Latest release Go version Enforces Zsh Plugin Standard v2 rules

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.

Features

  • Semantic static analysis: Evaluates syntax trees for unquoted variables, backquote command substitutions, special parameter shadowing, unsafe eval calls, and style issues without executing scripts.
  • Zsh Plugin Standard v2 enforcement: Every run checks unload functions, function-scoped options, $0 handling, and fpath hygiene. Projects with a validated zsh-lint.json additionally get the z-shell/project@2 profile: function and parameter namespaces, the shared Plugins registry, load-only helpers, and repeated external commands.
  • Automatic project discovery: Discovers zsh-lint.json configuration 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 through meta/* diagnostics.
  • Greppable and JSON diagnostics: Outputs standard file:line:col: [rule] message diagnostics for terminal and editor workflows, or structured JSON for automated CI checks.
  • Corpus survey tooling: Includes the companion zsh-lint-survey CLI to survey parser front-end coverage across large Zsh codebases without running lint rules.

Requirements

  • Go 1.27 or newer (when compiling from source or installing via go install).

[!NOTE] Static linting neither sources nor executes the files it analyzes and does not require zsh. The optional MCP zsh_syntax tool invokes native Zsh; native checks require trusted input files, executable lookup and system startup configuration, not a security sandbox.

Installation

Go install

Build and install the latest tagged release:

go install github.com/z-shell/zsh-lint/cmd/zsh-lint@latest

To install the parser survey tool:

go install github.com/z-shell/zsh-lint/cmd/zsh-lint-survey@latest

Ensure $(go env GOPATH)/bin is included in your PATH.

Build from source

git clone https://github.com/z-shell/zsh-lint.git
cd zsh-lint
go build ./cmd/zsh-lint

Zi plugin manager

Let 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-lint

Usage

Pass one or more file paths to zsh-lint:

zsh-lint script.zsh plugin.plugin.zsh

Command-line options

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.
-h, --help boolean Print usage and options to standard output and exit 0.

--config and --no-config are mutually exclusive.

MCP server

Start the local stdio server from the directory against which relative file and configuration paths should resolve:

zsh-lint mcp
zsh-lint mcp --zsh /path/to/zsh
zsh-lint mcp --help

Configure an MCP client to launch command zsh-lint with arguments ["mcp"]; no HTTP listener or runtime registration is installed by the command. zsh_lint accepts files plus optional config or no_config and returns the same JSON diagnostics as --format=json. zsh_syntax accepts files and runs zsh -f -n plus zcompile, reporting each check's stderr and exit status. Both tools accept explicit file paths only, not inline source or directories.

Zsh is found on PATH unless --zsh PATH selects the executable at server startup. Its absence affects only zsh_syntax. Native syntax is judged by empty stderr, not exit status alone, so valid negation such as ! true is accepted. Compilation uses disposable temporary output, never a .zwc beside an input. These checks do not run the input program, but system zshenv still runs and native parsing is not a sandbox for hostile source. See the MCP output contract for result, error and protocol details.

Diagnostic output

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.

Project configuration

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 verification
Advanced: 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.

Verification

Run the test suite and static analysis from the repository root:

go build ./... && go vet -tags=integration ./... && go test ./...
go test -tags=integration ./internal/workflowcontract/
golangci-lint run ./...

The default suite omits the slower mutation-runner behavioral integration tests; the second command includes them and is required for full verification. Go CI runs both lanes, and release-tag tests enable the integration tag for the full suite. Vet and the golangci-lint configuration include the integration tag to retain analysis of these tests.

To regenerate API and code-derived reference documentation:

go tool gomarkdoc --output ref.md ./cmd/zsh-lint ./cmd/zsh-lint-survey ./internal/survey ./internal/rules

Beyond 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.

Release model

Contributions integrate on main via squash merge after review. Annotated vX.Y.Z tags mark official releases and trigger automated publication workflows.

Contributing and license

Contributions follow the Z-Shell Organization Guidelines. Distributed under the terms of the GNU General Public License v3.0. See LICENSE for details. The MCP integration uses the official Go SDK; its applicable MIT and Apache-2.0 terms are preserved in the SDK license notice.


Developed with ❤️ by the Z-Shell Community.

Directories

Path Synopsis
cmd
wikidoc command
Command wikidoc is a CI/dev tool (not product code) that reads a gomarkdoc-generated Markdown file, sanitizes it for MDX compatibility, and injects the result into a marked region of a Docusaurus .mdx page in place.
Command wikidoc is a CI/dev tool (not product code) that reads a gomarkdoc-generated Markdown file, sanitizes it for MDX compatibility, and injects the result into a marked region of a Docusaurus .mdx page in place.
zsh-lint command
Command zsh-lint performs static analysis of Zsh shell scripts.
Command zsh-lint performs static analysis of Zsh shell scripts.
zsh-lint-probe command
Command zsh-lint-probe writes a probe grid for a parser change (#428): every body from the body file placed in every scanner context, one file each.
Command zsh-lint-probe writes a probe grid for a parser change (#428): every body from the body file placed in every scanner context, one file each.
zsh-lint-survey command
Command zsh-lint-survey runs the parser front end across Zsh files and reports parser gaps without evaluating static-analysis rules.
Command zsh-lint-survey runs the parser front end across Zsh files and reports parser gaps without evaluating static-analysis rules.
internal
diag
Package diag defines the renderer-agnostic, parser-neutral diagnostics model shared by zsh-lint's analyzer: rule identity, severity, message, and source range.
Package diag defines the renderer-agnostic, parser-neutral diagnostics model shared by zsh-lint's analyzer: rule identity, severity, message, and source range.
parse
Package parse wraps the mvdan.cc/sh parser as the analyzer's front end.
Package parse wraps the mvdan.cc/sh parser as the analyzer's front end.
probe
Package probe generates probe grids for parser changes (#428): each body, a variant of the construct under repair, placed in each context a compatibility adapter's source scanner can meet.
Package probe generates probe grids for parser changes (#428): each body, a variant of the construct under repair, placed in each context a compatibility adapter's source scanner can meet.
projectconfig
Package projectconfig loads and resolves explicit zsh-lint project metadata.
Package projectconfig loads and resolves explicit zsh-lint project metadata.
reduce
Package reduce shrinks a source while a predicate holds, by delta debugging over lines and then over shell tokens (#562).
Package reduce shrinks a source while a predicate holds, by delta debugging over lines and then over shell tokens (#562).
suppress
Package suppress implements the inline suppression contract defined in docs/project/suppression.md (#19, implemented under #65).
Package suppress implements the inline suppression contract defined in docs/project/suppression.md (#19, implemented under #65).
survey
Package survey runs the parser front end across a set of Zsh files and reports, per file, whether parsing succeeded.
Package survey runs the parser front end across a set of Zsh files and reports, per file, whether parsing succeeded.
wikidoc
Package wikidoc transforms gomarkdoc-generated Markdown into MDX-safe content and injects it into a marked region of a Docusaurus .mdx page.
Package wikidoc transforms gomarkdoc-generated Markdown into MDX-safe content and injects it into a marked region of a Docusaurus .mdx page.
third_party
mvdan-sh/expand
Package expand contains code to perform various shell expansions on programs parsed by the syntax package as either syntax.LangBash or syntax.LangPOSIX, behaving like Bash as a result.
Package expand contains code to perform various shell expansions on programs parsed by the syntax package as either syntax.LangBash or syntax.LangPOSIX, behaving like Bash as a result.
mvdan-sh/fileutil
Package fileutil allows inspecting shell files, such as detecting whether a file may be shell or extracting its shebang.
Package fileutil allows inspecting shell files, such as detecting whether a file may be shell or extracting its shebang.
mvdan-sh/pattern
Package pattern allows working with shell pattern matching notation, also known as wildcards or globbing.
Package pattern allows working with shell pattern matching notation, also known as wildcards or globbing.
mvdan-sh/syntax
Package syntax implements parsing and formatting of shell programs.
Package syntax implements parsing and formatting of shell programs.
mvdan-sh/syntax/typedjson
Package typedjson allows encoding and decoding shell syntax trees as JSON.
Package typedjson allows encoding and decoding shell syntax trees as JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL