A shell script linter, written in Rust.
Shuck parses and analyzes shell scripts to catch common bugs, style issues, and portability problems. It also lints shell embedded in supported non-shell files such as GitHub Actions workflows. A caching layer keeps incremental runs fast.
- High performance — ~20x faster than ShellCheck
- Linting with rules across correctness, security, performance, portability, and style categories
- Safe and unsafe fix support for selected diagnostics
- Multi-dialect support: bash, sh/POSIX, mksh, zsh
- Automatic file discovery via extensions and shebang detection
- Embedded shell extraction for GitHub Actions workflows and composite actions
- ShellCheck suppression compatibility (
# shellcheck disable=SC2086)
brew install ewhauser/tap/shuckcargo install shuck-cliPre-built binaries are available for macOS (aarch64) and Linux (x86_64) from the releases page.
# Check files and directories
shuck check script.sh src/
# Check the current directory
shuck check .
# Check GitHub Actions workflow `run:` blocks
shuck check .github/workflows/ci.yml
# Check a composite action
shuck check action.yml
# Read from stdin
echo 'echo $foo' | shuck check -
# Apply safe fixes automatically
shuck check --fix .
# Apply opt-in unsafe fixes too
shuck check --unsafe-fixes .
# Skip the cache
shuck check --no-cache .
# Override the cache location
shuck --cache-dir .tmp/shuck-cache check .# Remove cache entries for the current project
shuck cleanshuck check prints rich code-frame diagnostics by default:
warning[C001]: variable `tmp` is assigned but never used
--> deploy.sh:14:1
|
14 | tmp=$(mktemp)
| ^^^
|
Use --output-format concise to keep the legacy one-line format:
path:line:col: severity[CODE] message
deploy.sh:14:1: warning[C001] variable `tmp` is assigned but never used
deploy.sh:31:10: error[C006] undefined variable `DEPLY_ENV`
deploy.sh:45:3: warning[S005] legacy backtick command substitution
.github/workflows/ci.yml:12:11: warning[C001] jobs.test.steps[0].run: variable `summary` is assigned but never used
| Code | Meaning |
|---|---|
0 |
No issues found |
1 |
Lint violations or parse errors detected |
2 |
Runtime error (bad arguments, I/O failure) |
Shuck ships with rules organized into five categories:
| Category | Prefix | Description |
|---|---|---|
| Correctness | C | Bugs, errors, and likely mistakes. Enabled by default. |
| Style | S | Code quality and best-practice suggestions. |
| Performance | P | Inefficient patterns that have simpler or faster alternatives. |
| Portability | X | Bash-isms and shell-specific constructs that break under POSIX or other shells. |
| Security | K | Potentially dangerous shell patterns such as risky deletion, unsafe evaluation, or local expansion. |
Each rule has a short code (e.g., C006, S001) that appears in diagnostics and can be used in suppression directives. Diagnostics are classified as error, warning, or hint depending on severity.
Where possible, shuck rules align with ShellCheck rules. Shuck supports ShellCheck suppression syntax (# shellcheck disable=SC2086) and maps ShellCheck codes to their shuck equivalents, so existing suppression comments continue to work without changes. Both suppression syntaxes accept either code namespace, and native # shuck: disable=... follows ShellCheck's scope rules: before the first statement it is file-wide, otherwise it applies to the next command.
That said, shuck is not a port of ShellCheck. It is a clean-room reimplementation built on its own parser and analysis engine, so results will sometimes differ:
- Shuck's parser and analysis logic were written from scratch. Edge cases may be handled differently, and some diagnostics may fire in slightly different locations or contexts.
- In cases where ShellCheck's behavior appears incorrect or inconsistent with shell semantics, shuck intentionally chooses correctness over compatibility.
Compatibility is continuously validated against a large corpus of shell scripts from popular open-source projects including acme.sh, oh-my-zsh, nvm, pyenv, pi-hole, bats-core, powerlevel10k, dokku, gentoo, and many others. The latest conformance report is published at ewhauser.github.io/shuck/reports/corpus.
Suppress diagnostics with inline comments. Both native and ShellCheck-style directives are supported.
# Suppress a specific rule for the next command
# shuck:disable=C001
unused_var="ok"
# Suppress multiple rules
# shuck:disable=C001,S001
code_here
# Suppress for the entire file (place anywhere)
# shuck:disable-file=S001,S002
# ShellCheck-compatible syntax (also works)
# shellcheck disable=SC2034,SC2086
# Code aliases are interchangeable in either style
# shuck: disable=SC2086
# shellcheck disable=S001
# Before the first statement, disable becomes file-wide
# shuck: disable=S001For embedded GitHub Actions scripts, put suppression comments inside the run: block as shell comments:
- run: |
# shellcheck disable=SC2086
echo $FOOYAML comments outside the run: scalar are not visible to the shell parser and do not suppress shell diagnostics.
Project settings live in .shuck.toml or shuck.toml.
Use the [check] section to control embedded-script extraction:
[check]
# Lint supported embedded shell scripts in non-shell files such as
# GitHub Actions workflows and composite actions.
# Default: true
embedded = trueWhen given a directory, shuck recursively discovers standalone shell scripts by:
- Extension:
.sh,.bash,.zsh,.ksh,.dash,.mksh,.bats - Shebang: files starting with
#!/bin/bash,#!/usr/bin/env sh, etc.
Shuck also discovers embedded shell in supported non-shell files:
- GitHub Actions workflows:
.github/workflows/*.ymland.github/workflows/*.yaml - Composite actions:
action.ymlandaction.yaml
For GitHub Actions files, shuck lints run: blocks independently, remaps diagnostics back to the host YAML file, and includes the step path (for example jobs.test.steps[0].run) in the message. Steps that target unsupported shells such as PowerShell or cmd are skipped.
The following directories are skipped by default: .git, .hg, .svn, .jj, .bzr, .cache, node_modules, vendor, .shuck_cache.
Gitignore and .ignore files are respected by default. Use --no-respect-gitignore to disable.
Shuck caches lint results per file in a shared cache root outside the project tree by default. The default location follows the OS cache directory convention, which is typically ~/Library/Caches/shuck on macOS and $XDG_CACHE_HOME/shuck or ~/.cache/shuck on Linux.
Override the cache root with --cache-dir or SHUCK_CACHE_DIR.
Disable caching with --no-cache or remove a project's cache entries with shuck clean [PATH].
Shuck builds on ideas and inspiration from several excellent open-source projects. This section is a thank-you to those communities — it does not imply endorsement, affiliation, or any formal relationship between shuck and these projects.
- bashkit — shuck-parser was originally forked from bashkit's bash lexer and parser; it has since evolved substantially to meet the needs of a linter (comment and trivia preservation, error recovery, multi-dialect parse views, extended AST coverage).
- Ruff — Linter architecture inspiration, particularly around caching, rule organization, and diagnostic output.
- ShellCheck — An amazing project and the original source of inspiration for shuck. ShellCheck set the standard for shell script analysis.
- gbash — A lot of lessons learned from this earlier project carried forward into shuck.
MIT