ESLint or ShellCheck for your Bash scripts? We tested @eslint/shell ✅ yoDEV Tested

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.

:white_check_mark: 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 one shfmt uses. 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/recommended configuration 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/sh that uses Bash arrays and [[ ]] passed without errors using the default configuration. If your repository mixes Bash and sh scripts, separate them with files patterns.
  • 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 feature and 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 :white_check_mark: :white_check_mark: SC2045
…same line, $(ls ...) without quotes :white_check_mark: —
…same line, glob that may expand to options — :white_check_mark: SC2035
cd /opt/app without || exit :white_check_mark: :white_check_mark: SC2164
Backticks :white_check_mark: :white_check_mark: SC2006
rm $files without quotes :white_check_mark: :white_check_mark: SC2086
cat config.txt | grep PORT :white_check_mark: — (didn’t report it in our test)
printf "$TARGET\n" :white_check_mark: :white_check_mark: SC2059
read line without -r :white_check_mark: :white_check_mark: SC2162
'$HOME' in single quotes :white_check_mark: (warn) :white_check_mark: SC2016
x=$(echo hola) :white_check_mark: :white_check_mark: SC2116
[ -z $TARGET ] without quotes :white_check_mark: :white_check_mark: SC2086
Three unused variables :cross_mark: 0 of 3 :white_check_mark: SC2034 × 3
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"` to files=$(find . -name "*.tmp")
  • added quotes in rm "$files" and in [ -z "$TARGET" ]
  • converted read line to read -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?

How do you review your Bash scripts today?
  • ShellCheck
  • ESLint with @eslint/shell
  • I don’t use a linter for shell
  • I prefer another option (tell us which)
0 votantes

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