ESLint now analyzes Bash, POSIX sh, and mksh scripts with @eslint/shell, an official plugin released on November 6, 2026. We tested it against ShellCheck with the same script: they agree on almost everything, each detects something the other doesn’t, and we found undocumented behavior worth knowing before taking it to CI.
yoDEV Tested · November 11, 2026
@eslint/shell 0.1.2 with ESLint 10.14.0 against ShellCheck 0.11.0 on Linux (Node 22), on a 16-line deploy script. The results we report as ours come from that test.
What is ESLint and what does @eslint/shell add?
ESLint is the standard linter in the JavaScript ecosystem, and since July 2024 its team has worked to turn it into a general-purpose linter for any language. It already had official support for JSON, Markdown, and CSS; @eslint/shell adds shell scripts.
The plugin has three pieces:
- A parser built on
mvdan/sh, the same oneshfmtuses. It converts the script into an ESTree-style syntax tree, so if you’ve ever written a custom ESLint rule, you already know how to write one for shell. You can explore that tree in the ESLint Code Explorer (explorer.eslint.org). - Ten rules that the project itself describes as inspired by ShellCheck. The README links each rule to the ShellCheck code it replicates.
- A
shell/recommendedconfiguration that activates all ten.
The license is Apache-2.0 and the code is at github.com/eslint/shell. It’s signed by Nicholas C. Zakas, creator of ESLint and member of its Technical Steering Committee. At the time of publishing this note it’s at version 0.1.2, before 1.0, so rule names and behavior may change.
How do you analyze a Bash script with ESLint?
You need Node.js 20.19 or later, 22.13 or later, or 24 onwards. The README indicates the plugin is tested with ESLint v10.
npm install -D eslint @eslint/shell
Then add the recommended configuration to eslint.config.js:
import { defineConfig } from "eslint/config";
import shell from "@eslint/shell";
export default defineConfig([
{
plugins: { shell },
extends: ["shell/recommended"],
},
]);
shell/recommended applies to **/*.sh and **/*.bash files and interprets them as Bash. Run npx eslint . and your scripts are analyzed along with your JavaScript, in the same CI step and with the same editor integration you already maintain. If you were looking for a bash linter or shell script linter that isn’t one more tool to install, this is it.
If you prefer to choose rules one by one, the announcement shows the manual way:
import { defineConfig } from "eslint/config";
import shell from "@eslint/shell";
export default defineConfig([
{
files: ["**/*.sh"],
plugins: { shell },
language: "shell/posix",
rules: {
"shell/no-backticks": "error",
"shell/no-unquoted-expansions": "error",
},
},
]);
Bash vs sh? How to analyze POSIX scripts instead of Bash
Define language alongside the recommended configuration:
export default defineConfig([
{
plugins: { shell },
extends: ["shell/recommended"],
language: "shell/posix",
},
]);
The three dialects are shell/bash (the default), shell/posix, and shell/mksh.
Two things we saw in testing:
- The shebang doesn’t choose the dialect; the configuration does. A file starting with
#!/bin/shthat uses Bash arrays and[[ ]]passed without errors using the default configuration. If your repository mixes Bash andshscripts, separate them withfilespatterns. - In POSIX mode, a bashism is a parse error, not a rule. The example with arrays stopped at
Parsing error: … arrays are a bash/mksh featureand the rest of the file wasn’t analyzed. Bashisms are fixed one at a time.
What ShellCheck rules does @eslint/shell include?
These are the ten, with the ShellCheck code each one replicates according to the README:
| Rule | ShellCheck | What it detects | Auto-fix | Recommended |
|---|---|---|---|---|
no-backticks |
SC2006 | Old backticks instead of $(...) |
Yes | error |
no-expansions-in-single-quotes |
SC2016 | Variables in single quotes that don’t expand | No | warn |
no-ls-iteration |
SC2045 | Looping over ls output |
No | error |
no-unquoted-expansions |
SC2086 / SC2046 | Expansions without quotes subject to word splitting and globbing | Yes | error |
no-unused-vars |
SC2034 | Variables assigned that are never used | No | error |
no-useless-cat |
SC2002 | cat file | command |
No | error |
no-useless-echo |
SC2116 | $(echo ...) |
No | error |
no-variables-in-printf-format |
SC2059 | Variables in printf format |
No | error |
require-cd-guard |
SC2164 | cd without handling failure |
No (suggestions) | error |
require-read-r |
SC2162 | read without -r |
Yes | error |
ShellCheck has hundreds of checks; these are ten. They’re well chosen: quotes, failing cd, and read -r explain a good part of real errors in scripts. But it’s a subset, not a reimplementation, and the project doesn’t claim otherwise.
ESLint or ShellCheck? What each detected in the same bash script
We wrote a 16-line “deploy” script with an example of each problem and some extras: a [ -z $TARGET ] without quotes, an eval "$TARGET", and a glob passed directly to ls. Then we ran both tools.
| Problem in the script | @eslint/shell 0.1.2 | ShellCheck 0.11.0 |
|---|---|---|
for f in $(ls *.log): looping over ls |
||
…same line, $(ls ...) without quotes |
— | |
| …same line, glob that may expand to options | — | |
cd /opt/app without || exit |
||
| Backticks | ||
rm $files without quotes |
||
cat config.txt | grep PORT |
— (didn’t report it in our test) | |
printf "$TARGET\n" |
||
read line without -r |
||
'$HOME' in single quotes |
||
x=$(echo hola) |
||
[ -z $TARGET ] without quotes |
||
| Three unused variables | ||
eval "$TARGET" |
— | — |
Neither marked the eval, and neither says it does.
The important row is the unused variables one.
Why does an eval disable no-unused-vars?
shell/no-unused-vars works: in a three-line file with foo=1 it reports foo. But in our test script it reported nothing, while ShellCheck marked all three unused variables.
We isolated it. A single eval anywhere in the file silences no-unused-vars throughout the entire file. A three-line script with foo=1 followed by eval "$1" exits with code 0. If you remove the eval from the original script, the three warnings come back.
The logic can be defended: eval can refer to any variable with a name constructed at runtime, so the linter can’t prove a variable isn’t used. ShellCheck takes the opposite position and reports anyway. But at the time of publishing this note, with version 0.1.2, the README doesn’t document this behavior and the output doesn’t warn that the rule was disabled. If your scripts use eval, common in deploy and bootstrap scripts, ESLint isn’t checking for unused variables in those files.
Can it fix the script automatically?
Three rules auto-fix: no-backticks, no-unquoted-expansions, and require-read-r. Running npx eslint --fix on our script:
- converted
files=`find . -name "*.tmp"`tofiles=$(find . -name "*.tmp") - added quotes in
rm "$files"and in[ -z "$TARGET" ] - converted
read linetoread -r line
It didn’t touch anything requiring human judgment, like the loop over ls, the cd guard, or the printf format. That’s right.
How do you disable a rule in a bash script?
Standard ESLint comments work, written as shell comments:
# eslint-disable-next-line shell/no-unused-vars
foo=1
bar=2 # eslint-disable-line shell/no-unused-vars
For a variable defined in one file and read in another, like a config value loaded by a script with source, no-unused-vars accepts an allowed list:
rules: {
"shell/no-unused-vars": ["error", { allowed: ["CONFIG"] }],
},
We tested both options.
Should you replace ShellCheck with ESLint?
If your team already runs ESLint in CI and your scripts are the usual glue (deploy, setup, CI utilities), @eslint/shell gives you the checks that catch most real errors without a second tool to install, set up, and configure. Auto-fixing quotes is a real advantage.
If shell is an important part of your codebase, or your scripts use eval, keep ShellCheck. It covers many more checks and in our test it caught things ESLint didn’t. They don’t conflict: using both is a reasonable setup while the plugin is still before 1.0.
The ESLint team takes rule ideas at github.com/eslint/shell/issues, and being a 0.1 version, the list of rules is what’s most likely to grow.
What about you? What error in a shell script has cost you the most in production?
- ShellCheck
- ESLint with @eslint/shell
- I don’t use a linter for shell
- I prefer another option (tell us which)
Comment below or, if this article reached you by email, reply directly to the email: your response gets published here.
Related: @shadcn/lint: the linter that forces your AI agent to respect your design system in Tailwind CSS