@eslint-sh/eslint-plugin 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +228 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +51 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +53 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
  66. package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
  67. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  68. package/dist/rules/compat/no-case-attributes.js +63 -0
  69. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  70. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  71. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  72. package/dist/rules/compat/no-case-modification.js +86 -0
  73. package/dist/rules/compat/no-coproc.d.ts +12 -0
  74. package/dist/rules/compat/no-coproc.js +47 -0
  75. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  76. package/dist/rules/compat/no-declare-g.js +59 -0
  77. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  78. package/dist/rules/compat/no-double-bracket.js +52 -0
  79. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  80. package/dist/rules/compat/no-epoch-variables.js +57 -0
  81. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  82. package/dist/rules/compat/no-fd-variable.js +48 -0
  83. package/dist/rules/compat/no-globstar.d.ts +12 -0
  84. package/dist/rules/compat/no-globstar.js +39 -0
  85. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  86. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  87. package/dist/rules/compat/no-let.d.ts +12 -0
  88. package/dist/rules/compat/no-let.js +62 -0
  89. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  90. package/dist/rules/compat/no-mapfile.js +122 -0
  91. package/dist/rules/compat/no-nameref.d.ts +12 -0
  92. package/dist/rules/compat/no-nameref.js +44 -0
  93. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  94. package/dist/rules/compat/no-negative-array-index.js +123 -0
  95. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  96. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  97. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  98. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  99. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  100. package/dist/rules/compat/no-pipe-all.js +68 -0
  101. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  102. package/dist/rules/compat/no-printf-time-format.js +99 -0
  103. package/dist/rules/compat/no-source.d.ts +13 -0
  104. package/dist/rules/compat/no-source.js +103 -0
  105. package/dist/rules/compat/no-test-v.d.ts +12 -0
  106. package/dist/rules/compat/no-test-v.js +280 -0
  107. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  108. package/dist/rules/compat/no-unicode-escape.js +120 -0
  109. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  110. package/dist/rules/compat/no-wait-n.js +73 -0
  111. package/dist/rules/compat/operator-range.d.ts +10 -0
  112. package/dist/rules/compat/operator-range.js +15 -0
  113. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  114. package/dist/rules/compat/posix-absent-command.js +134 -0
  115. package/dist/rules/compat/shopt-options.d.ts +9 -0
  116. package/dist/rules/compat/shopt-options.js +63 -0
  117. package/dist/rules/compat/version-gates.d.ts +76 -0
  118. package/dist/rules/compat/version-gates.js +205 -0
  119. package/dist/rules/create-rule.d.ts +91 -0
  120. package/dist/rules/create-rule.js +51 -0
  121. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  122. package/dist/rules/create-wrapper-rule.js +407 -0
  123. package/dist/rules/define-shell-rule.d.ts +184 -0
  124. package/dist/rules/define-shell-rule.js +56 -0
  125. package/dist/rules/index.d.ts +26 -0
  126. package/dist/rules/index.js +90 -0
  127. package/dist/rules/max-script-complexity.d.ts +28 -0
  128. package/dist/rules/max-script-complexity.js +130 -0
  129. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  130. package/dist/rules/no-dynamic-invocation.js +117 -0
  131. package/dist/rules/no-eval.d.ts +13 -0
  132. package/dist/rules/no-eval.js +89 -0
  133. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  134. package/dist/rules/no-masked-exit-status.js +667 -0
  135. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  136. package/dist/rules/no-secret-env-reads.js +133 -0
  137. package/dist/rules/no-secret-flags.d.ts +32 -0
  138. package/dist/rules/no-secret-flags.js +162 -0
  139. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  140. package/dist/rules/no-unchecked-cd.js +250 -0
  141. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  142. package/dist/rules/no-undeclared-commands.js +115 -0
  143. package/dist/rules/no-unused-declarations.d.ts +23 -0
  144. package/dist/rules/no-unused-declarations.js +259 -0
  145. package/dist/rules/prefer-local.d.ts +16 -0
  146. package/dist/rules/prefer-local.js +249 -0
  147. package/dist/rules/quote-expansions.d.ts +13 -0
  148. package/dist/rules/quote-expansions.js +229 -0
  149. package/dist/rules/require-network-wrapper.d.ts +40 -0
  150. package/dist/rules/require-network-wrapper.js +244 -0
  151. package/dist/rules/require-shebang.d.ts +29 -0
  152. package/dist/rules/require-shebang.js +101 -0
  153. package/dist/rules/require-strict-mode.d.ts +52 -0
  154. package/dist/rules/require-strict-mode.js +177 -0
  155. package/dist/rules/require-tty-guard.d.ts +13 -0
  156. package/dist/rules/require-tty-guard.js +318 -0
  157. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  158. package/dist/rules/resolve-declaration-convention.js +109 -0
  159. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  160. package/dist/rules/resolve-network-wrapper.js +116 -0
  161. package/dist/rules/set-options.d.ts +109 -0
  162. package/dist/rules/set-options.js +201 -0
  163. package/dist/rules/shebang-interpreter.d.ts +18 -0
  164. package/dist/rules/shebang-interpreter.js +81 -0
  165. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  166. package/dist/rules/shebang-matches-dialect.js +69 -0
  167. package/dist/rules/transparent-wrappers.d.ts +52 -0
  168. package/dist/rules/transparent-wrappers.js +99 -0
  169. package/dist/rules/user-pattern.d.ts +63 -0
  170. package/dist/rules/user-pattern.js +184 -0
  171. package/dist/syntax/analyze.d.ts +19 -0
  172. package/dist/syntax/analyze.js +21 -0
  173. package/dist/syntax/index.d.ts +23 -0
  174. package/dist/syntax/index.js +25 -0
  175. package/dist/version.d.ts +0 -1
  176. package/dist/version.js +0 -1
  177. package/docs/compat.md +354 -0
  178. package/docs/custom-rule-tutorial.md +780 -34
  179. package/docs/declaration-conventions.md +478 -0
  180. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  181. package/docs/examples/dep-line-convention.mjs +22 -0
  182. package/docs/examples/eslint.config.mjs +3 -2
  183. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  184. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  185. package/docs/examples/no-backticks-rule.mjs +60 -0
  186. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  187. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  188. package/docs/examples/no-listed-commands-rule.ts +63 -0
  189. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  190. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  191. package/docs/examples/sidecar-convention.mjs +38 -0
  192. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  193. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  194. package/docs/reading-nodes.md +159 -0
  195. package/docs/rules/README.md +79 -0
  196. package/docs/rules/_template.md +83 -0
  197. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  198. package/docs/rules/compat/no-associative-arrays.md +95 -0
  199. package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
  200. package/docs/rules/compat/no-case-attributes.md +82 -0
  201. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  202. package/docs/rules/compat/no-case-modification.md +74 -0
  203. package/docs/rules/compat/no-coproc.md +76 -0
  204. package/docs/rules/compat/no-declare-g.md +80 -0
  205. package/docs/rules/compat/no-double-bracket.md +72 -0
  206. package/docs/rules/compat/no-epoch-variables.md +79 -0
  207. package/docs/rules/compat/no-fd-variable.md +78 -0
  208. package/docs/rules/compat/no-globstar.md +74 -0
  209. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  210. package/docs/rules/compat/no-let.md +78 -0
  211. package/docs/rules/compat/no-mapfile.md +118 -0
  212. package/docs/rules/compat/no-nameref.md +81 -0
  213. package/docs/rules/compat/no-negative-array-index.md +87 -0
  214. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  215. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  216. package/docs/rules/compat/no-pipe-all.md +85 -0
  217. package/docs/rules/compat/no-printf-time-format.md +75 -0
  218. package/docs/rules/compat/no-source.md +98 -0
  219. package/docs/rules/compat/no-test-v.md +82 -0
  220. package/docs/rules/compat/no-unicode-escape.md +91 -0
  221. package/docs/rules/compat/no-wait-n.md +85 -0
  222. package/docs/rules/max-script-complexity.md +109 -0
  223. package/docs/rules/no-dynamic-invocation.md +139 -0
  224. package/docs/rules/no-eval.md +106 -0
  225. package/docs/rules/no-masked-exit-status.md +239 -0
  226. package/docs/rules/no-secret-env-reads.md +173 -0
  227. package/docs/rules/no-secret-flags.md +169 -0
  228. package/docs/rules/no-unchecked-cd.md +146 -0
  229. package/docs/rules/no-undeclared-commands.md +141 -0
  230. package/docs/rules/no-unused-declarations.md +216 -0
  231. package/docs/rules/prefer-local.md +221 -0
  232. package/docs/rules/quote-expansions.md +157 -0
  233. package/docs/rules/require-network-wrapper.md +171 -0
  234. package/docs/rules/require-shebang.md +157 -0
  235. package/docs/rules/require-strict-mode.md +253 -0
  236. package/docs/rules/require-tty-guard.md +131 -0
  237. package/docs/rules/shebang-matches-dialect.md +120 -0
  238. package/docs/selectors.md +2 -2
  239. package/docs/shellcheck.md +87 -0
  240. package/docs/untrusted-code.md +440 -0
  241. package/docs/wrapper-rules.md +206 -0
  242. package/package.json +26 -20
  243. package/dist/index.d.ts.map +0 -1
  244. package/dist/index.js.map +0 -1
  245. package/dist/language/selector-classes.d.ts.map +0 -1
  246. package/dist/language/selector-classes.js.map +0 -1
  247. package/dist/language/shell-language-options.d.ts.map +0 -1
  248. package/dist/language/shell-language-options.js.map +0 -1
  249. package/dist/language/shell-language.d.ts.map +0 -1
  250. package/dist/language/shell-language.js.map +0 -1
  251. package/dist/language/shell-source-code.d.ts.map +0 -1
  252. package/dist/language/shell-source-code.js.map +0 -1
  253. package/dist/language/source-coordinates.d.ts.map +0 -1
  254. package/dist/language/source-coordinates.js.map +0 -1
  255. package/dist/language/syntax-view.d.ts.map +0 -1
  256. package/dist/language/syntax-view.js.map +0 -1
  257. package/dist/language/word-resolution.d.ts.map +0 -1
  258. package/dist/language/word-resolution.js.map +0 -1
  259. package/dist/version.d.ts.map +0 -1
  260. package/dist/version.js.map +0 -1
@@ -0,0 +1,173 @@
1
+ # `sh/no-secret-env-reads`
2
+
3
+ Disallow reading secret-shaped environment variables, such as `$GITHUB_TOKEN`, outside allowed sink commands.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ---------------- | ----------- | --------- |
7
+ | `agent-authored` (error) | not fixable | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ Once a secret is expanded into a command's arguments, every user on the machine can read it
12
+ through `ps` and `/proc/<pid>/cmdline`, and it tends to land in logs and `set -x` traces. This
13
+ rule reports a read of an environment variable whose name looks like it holds a secret.
14
+
15
+ The rule inspects every parameter expansion: `$NAME`, `${NAME}`, and the forms with operators such
16
+ as `${NAME:-x}`. A read is reported when the variable's name matches a pattern. The
17
+ default pattern is `(_TOKEN|_SECRET|_PASSWORD|_KEY)$`. It is case-sensitive, so lower-case shell
18
+ locals such as `cache_key` are not matched.
19
+
20
+ Four things are not reported:
21
+
22
+ - **Assignments.** `API_KEY=value` writes the variable; it is not a read.
23
+ - **An assignment prefix of a command that runs.** `TOKEN=$GITHUB_TOKEN tool` expands the secret
24
+ into the child's environment, not its argv. This is the recommended pattern, and the message
25
+ suggests it. The read must sit directly in the prefix value: `T=$(echo "$GITHUB_TOKEN") tool`
26
+ reports, because the `echo` command reads it. A bare assignment with no command
27
+ (`TOKEN=$GITHUB_TOKEN`) and `export TOKEN=$GITHUB_TOKEN` still read the variable and report.
28
+ - **Expansions that never yield the value.** A length, `${#API_KEY}`, expands to a number. An
29
+ alternate value, `${API_KEY:+word}` or `${API_KEY+word}`, expands to `word` when the variable is
30
+ set and to nothing otherwise. Neither puts the secret in argv, logs, or traces. A read written
31
+ inside `word` is reported on its own: `${GH_TOKEN:+-H "Authorization: $GH_TOKEN"}` reports the
32
+ inner `$GH_TOKEN`.
33
+ - **Arguments of an allowed sink.** A command named in `allowedSinks` may read a secret. The
34
+ command is resolved through transparent wrappers (`sudo vault …` is `vault`), and a command run
35
+ by path matches by basename.
36
+
37
+ Matching is a name pattern, not secret detection.
38
+
39
+ ### Expected false positives
40
+
41
+ The rule is a heuristic screen. Expect reports in these classes, and suppress them with a disable
42
+ comment that says why, narrow `patterns`, or list the command in `allowedSinks`:
43
+
44
+ - Names that end in a secret-shaped suffix but hold nothing secret, such as `CACHE_KEY`,
45
+ `PRIMARY_KEY`, or `SORT_KEY`.
46
+ - Presence checks such as `[ -n "${API_KEY:-}" ]`. The test command never reaches argv, but the
47
+ value is expanded, and a `set -x` trace prints it. `[ -n "${API_KEY+set}" ]` checks presence
48
+ without expanding the value.
49
+ - Reads that copy the secret into another variable (`KEY=$API_KEY`), which the rule cannot follow.
50
+ - Reads by a command you have already vetted but not listed in `allowedSinks`.
51
+
52
+ ### Expected false negatives
53
+
54
+ The rule matches parameter expansions only. These reads are not reported:
55
+
56
+ - Arithmetic reads, such as `$((A_KEY+1))` and `(( A_KEY > 0 ))`, where the variable is named
57
+ without a `$`.
58
+
59
+ ## Options
60
+
61
+ - `patterns` (`string[]`, at least one entry). Regular-expression sources tested against each
62
+ variable's name. They **replace** the default list. Sources are compiled case-sensitively. An
63
+ invalid source fails config validation.
64
+
65
+ Default: `["(_TOKEN|_SECRET|_PASSWORD|_KEY)$"]`.
66
+
67
+ - `allowedSinks` (`string[]`). Command names whose arguments may read a secret-shaped variable.
68
+
69
+ Default: `[]`.
70
+
71
+ ```js
72
+ // eslint.config.js
73
+ {
74
+ rules: {
75
+ 'sh/no-secret-env-reads': [
76
+ 'error',
77
+ { allowedSinks: ['vault', 'gh'], patterns: ['(_TOKEN|_SECRET|_PASSWORD|_KEY|_PAT)$'] },
78
+ ],
79
+ },
80
+ }
81
+ ```
82
+
83
+ ### Patterns run against script-controlled text
84
+
85
+ `patterns` is tested against every variable name the linted script expands, and the script's author
86
+ chooses that text. When you lint scripts you do not control, a pattern with nested or overlapping
87
+ quantifiers can make the regular expression engine backtrack for a time that grows exponentially
88
+ with the name's length. Write anchored patterns with one level of repetition over a character class.
89
+
90
+ The rule rejects the most common accident: a pattern that repeats a group which itself repeats
91
+ (`*`, `+`, `{n,}` or `{n,m}` with `m > 1` applied to such a group). It is not a proof that an
92
+ accepted pattern is safe. Review every pattern you accept as if the guard were not there.
93
+
94
+ **What it does not catch.** These pass the check and can still be slow on a long name. Timings are
95
+ from Node 24 on a busy laptop and vary by machine; the shape of the growth is what matters.
96
+
97
+ - Alternation branches that overlap under a repetition grow exponentially. `^([A-Z]|[A-Z0-9])+_KEY$`
98
+ took about 0.4 s on a 24-character name, 1.2 s at 26 and 4.7 s at 28. `(a|a)+$` took 1.6 s at
99
+ 24 characters and 6.4 s at 26. `^(A|AA)*$` is the same shape.
100
+ - Adjacent quantifiers that can match the same text grow polynomially. `.*.*.*x` took 0.5 s on a
101
+ 255-character name and 0.9 s at 300; each extra `.*` raises the exponent. Nothing bounds the length
102
+ of a name or flag in a script, so a long enough one is slow.
103
+
104
+ **What it rejects that is in fact linear.** The check does not look at what the repeated group can
105
+ match, so it also refuses some patterns that are safe, such as `(a*b)*`,
106
+ `^(?:[a-z]+-)*[a-z]+$` and `^(?:SECRET|TOKEN|PASSWORD)(?:_[A-Z]+)*$`. A rewrite without the nesting
107
+ exists for some of these and not for others, so if you cannot rewrite a pattern exactly, loosen it to
108
+ the character class you mean. The last one can be rewritten exactly with a lookahead
109
+ (it accepts the same names; compared on every suffix of up to seven characters over `A`, `_`, `a`
110
+ and `1` after `SECRET`, `TOKEN` and `PASSWORD`):
111
+
112
+ ```js
113
+ { patterns: ['^(?:SECRET|TOKEN|PASSWORD)(?!.*_(?:_|$))(?:_[A-Z][A-Z_]*)?$'] }
114
+ ```
115
+
116
+
117
+ **When you see the error.** The error is thrown from the rule's `create()`, which ESLint calls for
118
+ each file it lints, so the check does not run when the config is loaded. ESLint stops the run at the
119
+ first file it lints with the rule enabled and prints "Error while loading rule" followed by
120
+ "Occurred while linting <that file>" and a stack trace, and exits with code 2. A run that lints no
121
+ file with the rule enabled never sees it.
122
+
123
+ The accident the check is for, and its fix:
124
+
125
+ ```js
126
+ // Rejected: `([A-Z]+_?)*` repeats a group that already repeats, so a long
127
+ // name that does not match can take seconds.
128
+ { patterns: ['^([A-Z]+_?)*_KEY$'] }
129
+
130
+ // Safe rewrite: one repetition over a character class.
131
+ { patterns: ['^[A-Z0-9_]*_KEY$'] }
132
+ ```
133
+
134
+ ## Examples
135
+
136
+ ### Incorrect
137
+
138
+ <!-- example: {"findings": 3} -->
139
+ ```sh
140
+ echo "$API_KEY" # expands the secret into argv and output
141
+ curl -H "Authorization: $AUTH_TOKEN" https://x # argv is world-readable
142
+ [[ -z $DB_PASSWORD ]] && exit 1 # a read, even though only tested
143
+ ```
144
+
145
+ ### Correct
146
+
147
+ ```sh
148
+ TOKEN=$GITHUB_TOKEN tool # passed to the child's environment
149
+ API_KEY=abc123 # an assignment, not a read
150
+ echo "$cache_key" # not matched by the default pattern
151
+ [ -n "${API_KEY+set}" ] # checks presence without expanding the value
152
+ ```
153
+
154
+ With `allowedSinks: ['vault']`:
155
+
156
+ <!-- example: {"options": {"allowedSinks": ["vault"]}} -->
157
+ ```sh
158
+ vault login "$VAULT_TOKEN" # an allowed sink
159
+ ```
160
+
161
+ ## When not to use it
162
+
163
+ Turn it off when your scripts run in an environment where argv and logs are not observable by
164
+ others and you accept the exposure, or when another secret scanner already covers the same files.
165
+
166
+ ## Relationship to shellcheck
167
+
168
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
169
+ layer. Run shellcheck beside it.
170
+
171
+ shellcheck has no equivalent check. It reports shell mistakes, not credential handling, and it has
172
+ no notion of which variable names your project treats as secrets or which commands may receive
173
+ them. This rule enforces that policy.
@@ -0,0 +1,169 @@
1
+ # `sh/no-secret-flags`
2
+
3
+ Disallow secret-shaped command-line flags, such as `--password=` or `--token`, with a literal value.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ----------------------------------------- | ------------ | --------- |
7
+ | `recommended` (warn), `agent-authored` (error) | not fixable | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ A command's arguments are visible to every user on the machine: `ps` and `/proc/<pid>/cmdline`
12
+ show them while the command runs. A secret written as a literal flag value is also committed with
13
+ the script. This rule reports a flag whose name looks like it carries a secret when its value is
14
+ written literally.
15
+
16
+ The rule inspects the arguments of every simple command (`CallExpr`), after the command word. It
17
+ recognizes two forms:
18
+
19
+ - One word: `--token=abc`. The flag name is the text before the first `=`, and the value is the
20
+ rest. An empty value (`--token=`) passes nothing and is not reported.
21
+ - Two words: `--token abc`. A flag word without `=` takes the next word as its value when that word
22
+ is static, non-empty, and doesn't start with `-`. So a flag followed by another flag
23
+ (`--auth --verbose`), by `-` (the usual "read from stdin" marker), or by nothing is valueless and
24
+ not reported.
25
+
26
+ A value is literal only when its whole word is static. Any parameter expansion, command
27
+ substitution, or other dynamic part makes the value fine: `--token="$TOKEN"` and
28
+ `--password=$(cat f)` are the correct pattern. Static-ness is decided by the plugin's shared word
29
+ resolution (`ShellSourceCode#resolveWordText`), so quoting that resolves to fixed text
30
+ (`"--password"='hunter2'`) is still literal.
31
+
32
+ Only command arguments are matched. Assignment values (`X="--token=abc"`, `export`, `local`),
33
+ heredoc bodies, here-strings, `[[ … ]]` operands, and `case` patterns are never reported. Commands
34
+ behind wrappers are still checked, because their flags are arguments of the same command:
35
+ `sudo mysql --password=hunter2` reports.
36
+
37
+ This is a heuristic screen on flag names, not secret detection. The default pattern matches a
38
+ flag whose name, after its dashes, is a secret word: `password`, `passwd`, `token`, `secret`,
39
+ `api-key` (or `apikey`), `auth`, `authkey`, or `authorization`. The word may follow an `access-`
40
+ or `auth` qualifier (`--access-token`, `--auth-token`, `--authtoken`). It may be followed by a
41
+ digit (`--password2`, `--token1`) or by another `-segment` (`--secret-key`,
42
+ `--authorization-token`). The pattern leaves two kinds of name alone:
43
+
44
+ - Names ending in `-stdin` or `-file`, such as `--password-stdin`, `--password-file`, and
45
+ `--token-file`. These flags read the secret from stdin or from a file, which are the alternatives
46
+ this rule recommends. In `docker login --password-stdin registry.example.com`, the next word is a
47
+ positional argument, not the flag's value.
48
+ - Names with a letter directly after the secret word, such as `--author` (`auth` + `or`),
49
+ `--tokens`, and `--tokenizer` (`token` + a letter). These are longer words that only start
50
+ with a secret word.
51
+
52
+ Other qualifiers aren't matched, so `--my-token` is not reported. A general prefix would also
53
+ match negations such as `--no-password`, which take no value. Add a project's own flag names
54
+ with the `patterns` option.
55
+
56
+ Expect some false positives: a name such as `--token-type` still matches. Suppress a single false
57
+ positive with a disable comment that says why, or narrow the `patterns` option for your project.
58
+
59
+ ## Options
60
+
61
+ - `patterns` (`string[]`, at least one entry). Regular-expression sources tested against each flag's
62
+ name, without `=` or the value. They **replace** the default list. Each source is compiled
63
+ case-insensitively, like the default. An invalid source fails config validation with an error that
64
+ names the rule and the source.
65
+
66
+ Default: `["^(?!.*-(?:stdin|file)$)--?(?:access-|auth-?)?(?:password|passwd|token|secret|api-?key|auth(?:key|orization)?)(?![a-z])"]`.
67
+
68
+ ```js
69
+ // eslint.config.js
70
+ {
71
+ rules: {
72
+ 'sh/no-secret-flags': [
73
+ 'error',
74
+ // Only these exact names, plus a project-specific flag.
75
+ { patterns: ['^--?(password|token|api-?key)$', '^--vault-pin$'] },
76
+ ],
77
+ },
78
+ }
79
+ ```
80
+
81
+ ### Patterns run against script-controlled text
82
+
83
+ `patterns` is tested against every flag name the linted script writes, and the script's author
84
+ chooses that text. When you lint scripts you do not control, a pattern with nested or overlapping
85
+ quantifiers can make the regular expression engine backtrack for a time that grows exponentially
86
+ with the name's length. Write anchored patterns with one level of repetition over a character class.
87
+
88
+ The rule rejects the most common accident: a pattern that repeats a group which itself repeats
89
+ (`*`, `+`, `{n,}` or `{n,m}` with `m > 1` applied to such a group). It is not a proof that an
90
+ accepted pattern is safe. Review every pattern you accept as if the guard were not there.
91
+
92
+ **What it does not catch.** These pass the check and can still be slow on a long name. Timings are
93
+ from Node 24 on a busy laptop and vary by machine; the shape of the growth is what matters.
94
+
95
+ - Alternation branches that overlap under a repetition grow exponentially. `^([A-Z]|[A-Z0-9])+_KEY$`
96
+ took about 0.4 s on a 24-character name, 1.2 s at 26 and 4.7 s at 28. `(a|a)+$` took 1.6 s at
97
+ 24 characters and 6.4 s at 26. `^(A|AA)*$` is the same shape.
98
+ - Adjacent quantifiers that can match the same text grow polynomially. `.*.*.*x` took 0.5 s on a
99
+ 255-character name and 0.9 s at 300; each extra `.*` raises the exponent. Nothing bounds the length
100
+ of a name or flag in a script, so a long enough one is slow.
101
+
102
+ **What it rejects that is in fact linear.** The check does not look at what the repeated group can
103
+ match, so it also refuses some patterns that are safe, such as `(a*b)*`,
104
+ `^(?:[a-z]+-)*[a-z]+$` and `^(?:SECRET|TOKEN|PASSWORD)(?:_[A-Z]+)*$`. A rewrite without the nesting
105
+ exists for some of these and not for others, so if you cannot rewrite a pattern exactly, loosen it to
106
+ the character class you mean. The last one can be rewritten exactly with a lookahead
107
+ (it accepts the same names; compared on every suffix of up to seven characters over `A`, `_`, `a`
108
+ and `1` after `SECRET`, `TOKEN` and `PASSWORD`):
109
+
110
+ ```js
111
+ { patterns: ['^(?:SECRET|TOKEN|PASSWORD)(?!.*_(?:_|$))(?:_[A-Z][A-Z_]*)?$'] }
112
+ ```
113
+
114
+
115
+ **When you see the error.** The error is thrown from the rule's `create()`, which ESLint calls for
116
+ each file it lints, so the check does not run when the config is loaded. ESLint stops the run at the
117
+ first file it lints with the rule enabled and prints "Error while loading rule" followed by
118
+ "Occurred while linting <that file>" and a stack trace, and exits with code 2. A run that lints no
119
+ file with the rule enabled never sees it.
120
+
121
+ The accident the check is for, and its fix:
122
+
123
+ ```js
124
+ // Rejected: `([A-Z]+_?)*` repeats a group that already repeats, so a long
125
+ // name that does not match can take seconds.
126
+ { patterns: ['^--([A-Z]+_?)*_KEY$'] }
127
+
128
+ // Safe rewrite: one repetition over a character class.
129
+ { patterns: ['^--[A-Z0-9_]*_KEY$'] }
130
+ ```
131
+
132
+ ## Examples
133
+
134
+ ### Incorrect
135
+
136
+ <!-- example: {"findings": 3} -->
137
+ ```sh
138
+ curl --token=abc123 https://api.example.com # literal value in the same word
139
+ mysql --user=app --password hunter2 # literal value in the next word
140
+ sudo deploy --api-key='k-123' # quoting doesn't make it dynamic
141
+ ```
142
+
143
+ ### Correct
144
+
145
+ ```sh
146
+ curl --token="$API_TOKEN" https://api.example.com # dynamic value
147
+ mysql --password="$(pass show db/app)" # command substitution
148
+ printf '%s' "$DB_PASSWORD" | db-cli --password - # read from stdin
149
+ printf '%s' "$REG_TOKEN" | docker login --password-stdin reg.example.com # a -stdin flag
150
+ db-cli --password-file=/run/secrets/db # a -file flag names a file
151
+ git commit --author="Jane <jane@example.com>" # --author is not --auth
152
+ mytool --auth --verbose # valueless flag
153
+ DB_PASSWORD="$DB_PASSWORD" db-cli # passed through the environment
154
+ X="--token=abc" # not a command argument
155
+ ```
156
+
157
+ ## When not to use it
158
+
159
+ Turn it off when literal flag values in your scripts are never real secrets, for example
160
+ throwaway fixtures for a local test service, or when another secret scanner already covers the same
161
+ files and you don't want duplicate reports.
162
+
163
+ ## Relationship to shellcheck
164
+
165
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
166
+ layer. Run shellcheck beside it.
167
+
168
+ shellcheck has no equivalent check. It reports shell mistakes, not credential handling, and it has
169
+ no way to configure which flag names your project treats as secrets. This rule enforces that policy.
@@ -0,0 +1,146 @@
1
+ # `sh/no-unchecked-cd`
2
+
3
+ Require the failure of `cd`, `pushd` and `popd` to be checked, so a script never carries on in the wrong directory.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ------------------------------- | ---------------------------- | --------- |
7
+ | `recommended`, `agent-authored` | suggestions (append `\|\| exit`) | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ When `cd` fails, the script keeps running in the directory it was already in, and every relative
12
+ path after it silently means something else. `cd "$build"; rm -rf *` is the classic disaster.
13
+ `pushd` and `popd` change the directory (and the directory stack) and fail the same way.
14
+
15
+ A directory-changing command is **checked** when its exit status decides what runs next:
16
+
17
+ - it is the left side of `&&` or `||` (`cd x || exit`, `cd x || return`, `cd x && make`);
18
+ - it is the last command of an `if`, `elif`, `while` or `until` condition;
19
+ - it is the right side of a list, or the last command of a `{ …; }` group or `( … )` subshell,
20
+ whose own status is checked in one of these ways (`a && cd x || exit`, `{ cd x; } || exit`).
21
+
22
+ Everything else reports: a bare `cd`, `cd` in a function, loop or `case` body, a pipeline stage, a
23
+ background job, and `cd` inside a subshell (`( cd x; make )`). The subshell keeps the script's own
24
+ directory intact, but the commands after a failed `cd` inside it still run in the wrong place, so
25
+ it must be checked there too. A negated `! cd x` is only as checked as the position it sits in.
26
+
27
+ The command is recognized after transparent wrappers, so `builtin cd x` and `command cd x` are
28
+ read as `cd`.
29
+
30
+ **Files running under `set -e`.** A file whose prologue enables `errexit` stops at a failed `cd`
31
+ by itself, so the rule is quiet there. "Prologue" is read exactly as
32
+ [`require-strict-mode`](./require-strict-mode.md) reads it: the first five top-level statements,
33
+ tracked in order, so a later `set +e` undoes an earlier `set -e`, and a `set -e` outside that
34
+ window does not count. Set `ignoreWhenErrexit` to `false` to report anyway.
35
+
36
+ **Files run by bats.** With `languageOptions.dialect` set to `"bats"`, a bare `cd` (or `pushd` or
37
+ `popd`) written directly in the body of a `@test` block, or of a top-level `setup`, `teardown`,
38
+ `setup_file` or `teardown_file` function, is not reported: bats runs those bodies with `errexit`
39
+ on, so a failed `cd` fails the test. The same holds for a `cd` directly in a `( … )` subshell that
40
+ is itself a statement of such a body (`( cd "$dir"; make )`), because the subshell inherits
41
+ `errexit` and its failing status fails the test. A `$( … )` command substitution does not count: it
42
+ is reported as before.
43
+
44
+ The rule reads the body in order. After a plain `set +e` (or `set +o errexit`) in the same body,
45
+ `errexit` is off, and a later `cd` is reported again, because the test carries on after the failed
46
+ `cd`. A later plain `set -e` turns it back on, so `set +e; set -e; cd x` is not reported while
47
+ `set +e; set -e; set +e; cd x` is. A `cd` written before the `set +e` is still covered.
48
+
49
+ What the rule cannot tell: a `set +e` (or `shopt`, `eval 'set +e'`, `set "$flags"`) that is nested
50
+ in an `if`, loop or function, that lives in a file the test `load`s or `source`s or in a helper it
51
+ calls, or that ran in `setup` before the test starts. It reads each body on its own, assuming
52
+ `errexit` starts on, so none of these are seen, and a `cd` after one of them is not reported.
53
+ For example, bats runs `setup` in the same shell as the test, so after
54
+ `setup() { set +e; }` a bare `cd` in a `@test` has no `errexit` either, yet it is not reported.
55
+ Likewise a `cd` nested in an `if`, loop or `case` inside the body, a negated `! cd x`, a `cd` in
56
+ any other function of the same file, and a `cd` in a file read under another dialect (a `.sh` file)
57
+ are reported as usual. `run cd x` is not reported either, as before: `run` is not one of the
58
+ wrappers this rule follows, so the command it sees is `run`.
59
+
60
+ **Suggestion.** The editor suggestion appends ` || exit` after the command and its redirections.
61
+
62
+ <!-- transform: {"kind": "suggestion"} -->
63
+
64
+ ```sh
65
+ cd "$build" > /dev/null
66
+ # becomes
67
+ cd "$build" > /dev/null || exit
68
+ ```
69
+
70
+ It is not offered where appending would change the meaning: when the `cd` is already part of an
71
+ `&&`/`||` list or a pipeline, negated, in the background, or followed by a here-document.
72
+
73
+ The rule reads `languageOptions.dialect` only for the bats exemption above; no other option affects it.
74
+
75
+ ## Options
76
+
77
+ | Option | Type | Default | Effect |
78
+ | ------------------- | --------- | ------- | --------------------------------------------------------------------------- |
79
+ | `ignoreWhenErrexit` | `boolean` | `true` | Skip files whose prologue enables `errexit` (for example `set -euo pipefail`). |
80
+
81
+ ```js
82
+ // eslint.config.js
83
+ {
84
+ rules: {
85
+ 'sh/no-unchecked-cd': ['error', { ignoreWhenErrexit: false }],
86
+ },
87
+ }
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ ### Incorrect
93
+
94
+ <!-- example: {"findings": 5} -->
95
+
96
+ ```sh
97
+ cd "$build" # a failure goes unnoticed
98
+ rm -rf ./* # ...and this runs in the wrong directory
99
+
100
+ pushd /tmp # pushd and popd fail the same way
101
+ popd
102
+
103
+ ( cd "$dir"; make ) # inside a subshell, still unchecked
104
+
105
+ f() {
106
+ cd "$1" # a function body does not check it
107
+ }
108
+ ```
109
+
110
+ ### Correct
111
+
112
+ ```sh
113
+ cd "$build" || exit 1
114
+ cd "$build" || return
115
+ cd "$build" && make
116
+
117
+ if cd "$build"; then make; fi
118
+ while ! cd "$build"; do sleep 1; done
119
+
120
+ ( cd "$dir" || exit; make )
121
+ ```
122
+
123
+ With `errexit` in the prologue (the first five top-level statements), a bare `cd` is fine:
124
+
125
+ ```sh
126
+ set -euo pipefail
127
+ cd "$build"
128
+ ```
129
+
130
+ ## When not to use it
131
+
132
+ Turn the rule off when every script already starts with `set -e` and you would rather not think
133
+ about the distinction (the default `ignoreWhenErrexit` already covers that), or when a project
134
+ deliberately tolerates a failed `cd`, such as `cd "$dir" 2>/dev/null; true`. Prefer writing the
135
+ `|| true` so the intent is explicit.
136
+
137
+ ## Relationship to shellcheck
138
+
139
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
140
+ layer. Run shellcheck beside it.
141
+
142
+ shellcheck overlaps with SC2164 ("Use `cd ... || exit` in case `cd` fails"), and this rule
143
+ enforces the same practice. It exists anyway because it offers a suggestion, covers `pushd` and
144
+ `popd`, can be turned off per file by `set -e` detection that follows the same prologue reading as
145
+ `require-strict-mode`, and is part of the shipped presets, so the policy applies without running
146
+ shellcheck. If you already run shellcheck with SC2164 enabled, the two agree on the common cases.
@@ -0,0 +1,141 @@
1
+ # `sh/no-undeclared-commands`
2
+
3
+ Require every external command a script uses to be declared, so that a script's dependencies
4
+ are stated up front instead of discovered when a command is missing.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ----------- | --------- |
8
+ | `agent-authored` (inert until a declaration convention is bound) | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ A script depends on every external command it runs, and any of them can be missing on the
13
+ machine it lands on. When your project states each script's dependencies, this rule keeps that
14
+ statement complete: it reports each command the script uses but does not declare.
15
+
16
+ The rule needs a **declaration convention**, which says how your project declares commands. Bind
17
+ one with `settings.sh.declarationConvention`. The plugin ships one, `commentPragma`, which reads
18
+ `# uses: jq curl` comments from a script's header. See
19
+ [Declaration conventions](../declaration-conventions.md) to write your own. **With no convention
20
+ bound, the rule reports nothing**, so turning it on is safe until your project picks a convention.
21
+
22
+ A command is **used** when the script runs it by a name known from the source. The rule looks
23
+ at every simple command in the file, wherever it is: the top level, function bodies, loops,
24
+ conditionals, pipelines, and command substitutions (`$(…)` and backquotes) in any position,
25
+ including arguments, assignment values, redirect targets, `for` lists, and `case` subjects.
26
+
27
+ - **Wrappers are followed to the command they run.** `sudo`, `env`, `nohup`, `nice`, `timeout`,
28
+ `time`, `command`, and `exec` run the command named after them, so that command is used. A
29
+ wrapper is also used itself when it is an external command: `sudo rsync -a . host:` uses both
30
+ `sudo` and `rsync`. `command` and `exec` are builtins, so `exec rsync …` uses only `rsync`.
31
+ `time` is a keyword in bash, so `time rsync …` uses only `rsync` there; in a `posix` file it
32
+ is the `time` utility and is used too. `command -v jq` only looks `jq` up, so it does not use
33
+ `jq`. In `bats` files, bats' `run` is a wrapper too, with its options (`-N`,
34
+ `!`, `--keep-empty-lines`, `--separate-stderr`, `--`), so `run ! curl x` uses `curl`.
35
+ - **Builtins don't count.** The builtin list depends on `languageOptions.dialect`. `bash` and
36
+ `bats` files use the bash builtins (`cd`, `echo`, `local`, `mapfile`, `source`, …). `bats`
37
+ files also leave out the helpers bats-core defines for every test file: `run`, `load`, `skip`,
38
+ `bats_pipe`, `bats_load_safe`, `bats_load_library`, `bats_require_minimum_version`, and
39
+ `bats_test_function`. Helpers from separate libraries, such as bats-assert's
40
+ `assert_success`, are not bats-core's and are still reported. `posix`
41
+ files use the POSIX special and regular built-ins plus `echo`, `printf`, `test` and `[`. So
42
+ `mapfile` is a command in a `posix` file. `mksh` and `zsh` files use the POSIX list for now,
43
+ so their own builtins (`print`, `whence`, `setopt`, …) are reported until they get lists of
44
+ their own.
45
+ - **Functions defined in the file don't count**, wherever they are defined, including after the
46
+ call or inside another function. A function named like a command (`curl() { … }`) shadows it.
47
+ - **Dynamic command words don't count.** `"$cmd" args`, `${tool} x`, and `$(pick) x` name no
48
+ command the rule can know. `sh/no-dynamic-invocation` reports them. A static wrapper in front
49
+ of one is still used: `sudo "$prog"` uses `sudo`.
50
+ - **Commands inside strings don't count.** `bash -c 'jq .'` uses `bash`, not `jq`.
51
+
52
+ Each undeclared command is reported once (message `undeclaredCommand`), at the word that names
53
+ it in its first use. The message says how to declare it, in the convention's own syntax when
54
+ the convention has a `declarationHint`. With `commentPragma`:
55
+
56
+ ```text
57
+ 'curl' is used but not declared. Add `# uses: curl` on a line below the shebang.
58
+ ```
59
+
60
+ In a file without a shebang, the message says to add the line as the first line of the file
61
+ instead. A convention without a hint ends the message with the generic sentence
62
+ "Declare it with the project's declaration convention." A command run by path (`./build.sh`) is reported by that path.
63
+
64
+ ## Options
65
+
66
+ - `transparentWrappers` (`string[]`, default `[]`): names of your project's own wrapper
67
+ commands, added to the built-in wrappers above. The word after a listed wrapper is the command
68
+ it runs, so with `['with-retry']`, `with-retry curl x` uses both `with-retry` and `curl`. A
69
+ listed wrapper takes no flags: in `with-retry -n 3 curl`, the rule stops at `-n` and does not
70
+ count `curl`. If the wrapper is a function defined in the same file, only the wrapped command
71
+ needs declaring. Your network wrapper doesn't need listing here when it is set in
72
+ `settings.sh.networkWrapper`: this rule looks through it, with the options its spec declares
73
+ (see [`sh/require-network-wrapper`](./require-network-wrapper.md#declaring-the-wrapper-once-settingsshnetworkwrapper)).
74
+
75
+ The convention is not a rule option. Bind it in `settings`, because ESLint copies rule options
76
+ with `structuredClone`, which cannot copy a convention's methods.
77
+
78
+ ```js
79
+ // eslint.config.js
80
+ import sh, { commentPragma } from '@eslint-sh/eslint-plugin';
81
+
82
+ export default [
83
+ {
84
+ files: ['**/*.sh'],
85
+ plugins: { sh },
86
+ language: 'sh/sh',
87
+ settings: { sh: { declarationConvention: commentPragma } },
88
+ rules: {
89
+ 'sh/no-undeclared-commands': ['error', { transparentWrappers: ['with-retry'] }],
90
+ },
91
+ },
92
+ ];
93
+ ```
94
+
95
+ ## Examples
96
+
97
+ With `commentPragma` bound and `{ transparentWrappers: ['with-retry'] }`:
98
+
99
+ ### Incorrect
100
+
101
+ <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}, "findings": 6} -->
102
+ ```sh
103
+ #!/usr/bin/env bash
104
+ # uses: jq
105
+ curl -s "$URL" | jq . # curl is not declared
106
+ echo "built $(date +%s)" # date, inside $(…), is not declared
107
+ sudo systemctl restart app # neither sudo nor systemctl is declared
108
+ with-retry git pull # git, run by the wrapper, is not declared (nor is with-retry)
109
+ ```
110
+
111
+ ### Correct
112
+
113
+ <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}} -->
114
+ ```sh
115
+ #!/usr/bin/env bash
116
+ # uses: curl jq date
117
+ log() { printf '%s\n' "$*"; } # a function defined here, using only builtins
118
+ log "fetching"
119
+ curl -s "$URL" | jq .
120
+ echo "built $(date +%s)"
121
+ cd /tmp && read -r answer # builtins need no declaration
122
+ "$cmd" --version # a dynamic command word: see sh/no-dynamic-invocation
123
+ command -v docker # looks docker up without running it
124
+ ```
125
+
126
+ ## When not to use it
127
+
128
+ Turn the rule off when your project doesn't track script dependencies, or tracks them somewhere
129
+ this rule can't read, such as a container image or a system package list. It is also noisy for
130
+ scripts that run many repository-local programs by path, since each path is reported until it is
131
+ declared.
132
+
133
+ ## Relationship to shellcheck
134
+
135
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
136
+ layer. Run shellcheck beside it.
137
+
138
+ shellcheck has no equivalent. It checks how a script uses commands, not which commands the
139
+ script may depend on, and it has no notion of a project's declaration convention to compare
140
+ against. A dependency manifest is project policy, which is the kind of rule shellcheck cannot be
141
+ configured to express.