@eslint-sh/eslint-plugin 0.7.0 → 0.8.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 (141) hide show
  1. package/README.md +14 -8
  2. package/dist/command-harvest/builtins/bash.d.ts +1 -1
  3. package/dist/command-harvest/builtins/bash.js +1 -1
  4. package/dist/command-harvest/builtins/bats.d.ts +2 -0
  5. package/dist/command-harvest/builtins/bats.js +3 -0
  6. package/dist/command-harvest/wrapper-table.d.ts +3 -19
  7. package/dist/command-harvest/wrapper-table.js +8 -38
  8. package/dist/configs/bash-3-2.d.ts +7 -6
  9. package/dist/configs/bash-3-2.js +4 -3
  10. package/dist/configs/posix-portable.d.ts +1 -1
  11. package/dist/declaration-conventions/read-declared-commands.d.ts +2 -2
  12. package/dist/declaration-conventions/read-declared-commands.js +2 -2
  13. package/dist/eslint-plugin-syntax.d.ts +4 -0
  14. package/dist/eslint-plugin.d.ts +86 -40
  15. package/dist/index.d.ts +5 -2
  16. package/dist/index.js +4 -2
  17. package/dist/language/shell-language-options.d.ts +18 -15
  18. package/dist/language/shell-language-options.js +33 -26
  19. package/dist/language/shell-language.d.ts +10 -9
  20. package/dist/language/shell-language.js +18 -35
  21. package/dist/language/shell-source-code.d.ts +17 -9
  22. package/dist/language/shell-source-code.js +53 -12
  23. package/dist/rules/bash-variable-semantics.d.ts +22 -0
  24. package/dist/rules/bash-variable-semantics.js +41 -0
  25. package/dist/rules/compat/create-compat-rule.js +1 -1
  26. package/dist/rules/compat/declaration-options.d.ts +2 -2
  27. package/dist/rules/compat/declaration-options.js +5 -2
  28. package/dist/rules/compat/no-append-all-redirect.js +1 -1
  29. package/dist/rules/compat/no-associative-arrays.js +2 -2
  30. package/dist/rules/compat/no-brace-sequence-padding-step.js +4 -4
  31. package/dist/rules/compat/no-case-attributes.js +3 -3
  32. package/dist/rules/compat/no-case-fallthrough.js +2 -2
  33. package/dist/rules/compat/no-case-modification.js +2 -2
  34. package/dist/rules/compat/no-coproc.js +1 -1
  35. package/dist/rules/compat/no-declare-g.js +3 -3
  36. package/dist/rules/compat/no-double-bracket.js +2 -2
  37. package/dist/rules/compat/no-epoch-variables.js +2 -2
  38. package/dist/rules/compat/no-fd-variable.js +1 -1
  39. package/dist/rules/compat/no-globstar.js +1 -1
  40. package/dist/rules/compat/no-lastpipe-semantics.js +2 -2
  41. package/dist/rules/compat/no-let.js +2 -1
  42. package/dist/rules/compat/no-mapfile.js +4 -2
  43. package/dist/rules/compat/no-nameref.js +3 -3
  44. package/dist/rules/compat/no-negative-array-index.js +39 -9
  45. package/dist/rules/compat/no-nounset-empty-array.js +15 -4
  46. package/dist/rules/compat/no-parameter-transformation.js +2 -2
  47. package/dist/rules/compat/no-pipe-all.js +1 -1
  48. package/dist/rules/compat/no-printf-time-format.js +4 -2
  49. package/dist/rules/compat/no-source.js +2 -1
  50. package/dist/rules/compat/no-test-v.js +4 -2
  51. package/dist/rules/compat/no-unicode-escape.js +2 -2
  52. package/dist/rules/compat/no-wait-n.js +4 -2
  53. package/dist/rules/compat/nounset-array-proof.d.ts +8 -0
  54. package/dist/rules/compat/nounset-array-proof.js +563 -0
  55. package/dist/rules/compat/posix-absent-command.d.ts +7 -8
  56. package/dist/rules/compat/posix-absent-command.js +17 -15
  57. package/dist/rules/compat/shopt-options.js +3 -1
  58. package/dist/rules/compat/version-gates.d.ts +9 -9
  59. package/dist/rules/compat/version-gates.js +13 -13
  60. package/dist/rules/create-wrapper-rule.js +12 -7
  61. package/dist/rules/no-dynamic-invocation.d.ts +0 -8
  62. package/dist/rules/no-dynamic-invocation.js +7 -15
  63. package/dist/rules/no-eval.js +3 -1
  64. package/dist/rules/no-masked-exit-status.js +15 -8
  65. package/dist/rules/no-secret-env-reads.d.ts +4 -3
  66. package/dist/rules/no-secret-env-reads.js +37 -14
  67. package/dist/rules/no-secret-flags.d.ts +2 -1
  68. package/dist/rules/no-secret-flags.js +7 -8
  69. package/dist/rules/no-unchecked-cd.js +3 -1
  70. package/dist/rules/no-undeclared-commands.d.ts +1 -15
  71. package/dist/rules/no-undeclared-commands.js +3 -15
  72. package/dist/rules/no-unused-declarations.d.ts +1 -18
  73. package/dist/rules/no-unused-declarations.js +9 -34
  74. package/dist/rules/prefer-local.js +4 -3
  75. package/dist/rules/quote-expansions.js +3 -28
  76. package/dist/rules/require-network-wrapper.d.ts +2 -17
  77. package/dist/rules/require-network-wrapper.js +33 -4
  78. package/dist/rules/resolve-declaration-convention.d.ts +5 -6
  79. package/dist/rules/resolve-declaration-convention.js +5 -6
  80. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  81. package/dist/rules/resolve-network-wrapper.js +57 -26
  82. package/dist/rules/resolve-transparent-wrappers.d.ts +43 -0
  83. package/dist/rules/resolve-transparent-wrappers.js +98 -0
  84. package/dist/rules/set-options.d.ts +2 -1
  85. package/dist/rules/set-options.js +17 -5
  86. package/dist/rules/shebang-interpreter.js +4 -2
  87. package/dist/rules/transparent-wrappers.d.ts +4 -3
  88. package/dist/rules/transparent-wrappers.js +5 -4
  89. package/dist/rules/user-pattern.d.ts +5 -3
  90. package/dist/rules/user-pattern.js +6 -4
  91. package/dist/syntax/index.d.ts +4 -0
  92. package/dist/syntax/index.js +4 -0
  93. package/docs/compat.md +76 -27
  94. package/docs/custom-rule-tutorial.md +9 -3
  95. package/docs/examples/no-listed-commands-rule.ts +2 -2
  96. package/docs/rules/README.md +36 -4
  97. package/docs/rules/_template.md +2 -2
  98. package/docs/rules/compat/no-append-all-redirect.md +12 -12
  99. package/docs/rules/compat/no-associative-arrays.md +14 -14
  100. package/docs/rules/compat/no-brace-sequence-padding-step.md +12 -12
  101. package/docs/rules/compat/no-case-attributes.md +13 -13
  102. package/docs/rules/compat/no-case-fallthrough.md +12 -12
  103. package/docs/rules/compat/no-case-modification.md +11 -11
  104. package/docs/rules/compat/no-coproc.md +13 -13
  105. package/docs/rules/compat/no-declare-g.md +13 -13
  106. package/docs/rules/compat/no-double-bracket.md +2 -2
  107. package/docs/rules/compat/no-epoch-variables.md +13 -13
  108. package/docs/rules/compat/no-fd-variable.md +12 -12
  109. package/docs/rules/compat/no-globstar.md +13 -13
  110. package/docs/rules/compat/no-lastpipe-semantics.md +13 -13
  111. package/docs/rules/compat/no-let.md +2 -2
  112. package/docs/rules/compat/no-mapfile.md +16 -16
  113. package/docs/rules/compat/no-nameref.md +13 -13
  114. package/docs/rules/compat/no-negative-array-index.md +14 -14
  115. package/docs/rules/compat/no-nounset-empty-array.md +72 -17
  116. package/docs/rules/compat/no-parameter-transformation.md +19 -19
  117. package/docs/rules/compat/no-pipe-all.md +13 -13
  118. package/docs/rules/compat/no-printf-time-format.md +13 -13
  119. package/docs/rules/compat/no-source.md +2 -2
  120. package/docs/rules/compat/no-test-v.md +12 -12
  121. package/docs/rules/compat/no-unicode-escape.md +13 -13
  122. package/docs/rules/compat/no-wait-n.md +13 -13
  123. package/docs/rules/no-dynamic-invocation.md +17 -12
  124. package/docs/rules/no-eval.md +6 -1
  125. package/docs/rules/no-masked-exit-status.md +4 -0
  126. package/docs/rules/no-secret-env-reads.md +34 -3
  127. package/docs/rules/no-secret-flags.md +19 -2
  128. package/docs/rules/no-unchecked-cd.md +9 -2
  129. package/docs/rules/no-undeclared-commands.md +16 -15
  130. package/docs/rules/no-unused-declarations.md +17 -20
  131. package/docs/rules/prefer-local.md +20 -2
  132. package/docs/rules/quote-expansions.md +8 -5
  133. package/docs/rules/require-network-wrapper.md +22 -17
  134. package/docs/rules/require-shebang.md +10 -4
  135. package/docs/rules/require-strict-mode.md +6 -0
  136. package/docs/shellcheck.md +9 -5
  137. package/docs/transparent-wrappers.md +97 -0
  138. package/docs/wrapper-rules.md +9 -0
  139. package/package.json +2 -2
  140. package/dist/language/end-of-input-marker.d.ts +0 -29
  141. package/dist/language/end-of-input-marker.js +0 -88
@@ -6,6 +6,10 @@ Disallow reading secret-shaped environment variables, such as `$GITHUB_TOKEN`, o
6
6
  | ---------------- | ----------- | --------- |
7
7
  | `agent-authored` (error) | not fixable | `sh/sh` |
8
8
 
9
+ Project wrapper facts are shared through
10
+ `settings: { sh: { transparentWrappers: ['with-retry'] } }`. This rule follows
11
+ those wrappers and the shared network wrapper; see [shared transparent wrappers](../transparent-wrappers.md).
12
+
9
13
  ## Rule details
10
14
 
11
15
  Once a secret is expanded into a command's arguments, every user on the machine can read it
@@ -34,6 +38,13 @@ Four things are not reported:
34
38
  command is resolved through transparent wrappers (`sudo vault …` is `vault`), and a command run
35
39
  by path matches by basename.
36
40
 
41
+ Only arguments actually passed after the final resolved sink command receive this
42
+ exemption. Wrapper-owned options and operands are checked normally. With an approved
43
+ `sink` and `settings: { sh: { transparentWrappers: [{ names: ['with-retry'], argFlags: ['--log'] }] } }`,
44
+ `with-retry --log "$API_TOKEN" sink` reports, while
45
+ `with-retry --log audit.log sink "$API_TOKEN"` stays clean. Likewise, the secret in
46
+ `env -u "$API_TOKEN" sink` is owned by `env`, so approving `sink` does not exempt it.
47
+
37
48
  Matching is a name pattern, not secret detection.
38
49
 
39
50
  ### Expected false positives
@@ -59,12 +70,16 @@ The rule matches parameter expansions only. These reads are not reported:
59
70
  ## Options
60
71
 
61
72
  - `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.
73
+ variable's name. They **replace** the default list. Sources use `u` (Unicode, case-sensitive):
74
+ `patterns: ['token']` does not catch `$GITHUB_TOKEN`, while `patterns: ['TOKEN']` does.
75
+ Lowercase names such as `$sort_key` do not match the uppercase default patterns.
76
+ See [regular-expression options](./README.md#regular-expression-options) for compilation and
77
+ configuration errors, which name the rule, option index and source.
64
78
 
65
79
  Default: `["(_TOKEN|_SECRET|_PASSWORD|_KEY)$"]`.
66
80
 
67
- - `allowedSinks` (`string[]`). Command names whose arguments may read a secret-shaped variable.
81
+ - `allowedSinks` (`string[]`). Command names whose own arguments may read a secret-shaped variable. Wrapper-owned
82
+ operands are not exempt.
68
83
 
69
84
  Default: `[]`.
70
85
 
@@ -80,6 +95,22 @@ The rule matches parameter expansions only. These reads are not reported:
80
95
  }
81
96
  ```
82
97
 
98
+ Regex syntax errors fail when this rule first runs on a file, naming the rule, option and
99
+ pattern. `--print-config` will not catch any regex syntax errors. See the
100
+ [shared compilation contract](./README.md#regular-expression-options).
101
+
102
+ ### Matching examples
103
+
104
+ These examples use only this rule; `patterns` replaces the defaults. Option values below are JSON.
105
+
106
+ | Option value | Shell input | Findings |
107
+ | --- | --- | --- |
108
+ | `{"patterns":["token"]}` | `echo "$GITHUB_TOKEN"` | 0 |
109
+ | `{"patterns":["TOKEN"]}` | `echo "$GITHUB_TOKEN"` | 1 |
110
+ | `{}` | `echo "$sort_key"` | 0 |
111
+ | `{"patterns":["^\\p{Lu}+$"]}` | `echo "$TOKEN"` | 1 |
112
+ | `{"patterns":["^\\p{Lu}+$"]}` | `echo "$token"` | 0 |
113
+
83
114
  ### Patterns run against script-controlled text
84
115
 
85
116
  `patterns` is tested against every variable name the linted script expands, and the script's author
@@ -60,8 +60,10 @@ positive with a disable comment that says why, or narrow the `patterns` option f
60
60
 
61
61
  - `patterns` (`string[]`, at least one entry). Regular-expression sources tested against each flag's
62
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.
63
+ with `iu` (Unicode, case-insensitive), like the default. For example, `patterns: ['token']`
64
+ catches `cli --TOKEN=abc`; capitals in a custom pattern also match lowercase flag names.
65
+ See [regular-expression options](./README.md#regular-expression-options) for compilation and
66
+ configuration errors, which name the rule, option index and source.
65
67
 
66
68
  Default: `["^(?!.*-(?:stdin|file)$)--?(?:access-|auth-?)?(?:password|passwd|token|secret|api-?key|auth(?:key|orization)?)(?![a-z])"]`.
67
69
 
@@ -78,6 +80,21 @@ positive with a disable comment that says why, or narrow the `patterns` option f
78
80
  }
79
81
  ```
80
82
 
83
+ Regex syntax errors fail when this rule first runs on a file, naming the rule, option and
84
+ pattern. `--print-config` will not catch any regex syntax errors. See the
85
+ [shared compilation contract](./README.md#regular-expression-options).
86
+
87
+ ### Matching examples
88
+
89
+ These examples use only this rule; `patterns` replaces the defaults. Option values below are JSON.
90
+
91
+ | Option value | Shell input | Findings |
92
+ | --- | --- | --- |
93
+ | `{"patterns":["token"]}` | `cli --TOKEN=abc` | 1 |
94
+ | `{"patterns":["TOKEN"]}` | `cli --token=abc` | 1 |
95
+ | `{"patterns":["token"]}` | `cli --other=abc` | 0 |
96
+ | `{"patterns":["^--\\p{Lu}+$"]}` | `cli --TOKEN=abc` | 1 |
97
+
81
98
  ### Patterns run against script-controlled text
82
99
 
83
100
  `patterns` is tested against every flag name the linted script writes, and the script's author
@@ -6,6 +6,10 @@ Require the failure of `cd`, `pushd` and `popd` to be checked, so a script never
6
6
  | ------------------------------- | ---------------------------- | --------- |
7
7
  | `recommended`, `agent-authored` | suggestions (append `\|\| exit`) | `sh/sh` |
8
8
 
9
+ Project wrapper facts are shared through
10
+ `settings: { sh: { transparentWrappers: ['with-retry'] } }`. This rule follows
11
+ those wrappers and the shared network wrapper; see [shared transparent wrappers](../transparent-wrappers.md).
12
+
9
13
  ## Rule details
10
14
 
11
15
  When `cd` fails, the script keeps running in the directory it was already in, and every relative
@@ -31,7 +35,8 @@ read as `cd`.
31
35
  by itself, so the rule is quiet there. "Prologue" is read exactly as
32
36
  [`require-strict-mode`](./require-strict-mode.md) reads it: the first five top-level statements,
33
37
  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.
38
+ window does not count. Only `set` statements count; shebang flags do not. Set
39
+ `ignoreWhenErrexit` to `false` to report anyway.
35
40
 
36
41
  **Files run by bats.** With `languageOptions.dialect` set to `"bats"`, a bare `cd` (or `pushd` or
37
42
  `popd`) written directly in the body of a `@test` block, or of a top-level `setup`, `teardown`,
@@ -143,4 +148,6 @@ shellcheck overlaps with SC2164 ("Use `cd ... || exit` in case `cd` fails"), and
143
148
  enforces the same practice. It exists anyway because it offers a suggestion, covers `pushd` and
144
149
  `popd`, can be turned off per file by `set -e` detection that follows the same prologue reading as
145
150
  `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.
151
+ shellcheck. The checks differ when errexit comes from a shebang such as `#!/bin/bash -e`:
152
+ shellcheck treats it as `set -e` and suppresses SC2164, while `ignoreWhenErrexit` reads only
153
+ `set` statements in the prologue, so this rule still reports an unchecked `cd`.
@@ -14,7 +14,7 @@ machine it lands on. When your project states each script's dependencies, this r
14
14
  statement complete: it reports each command the script uses but does not declare.
15
15
 
16
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
17
+ one with `settings: { sh: { declarationConvention: commentPragma } }`. The plugin ships one, `commentPragma`, which reads
18
18
  `# uses: jq curl` comments from a script's header. See
19
19
  [Declaration conventions](../declaration-conventions.md) to write your own. **With no convention
20
20
  bound, the rule reports nothing**, so turning it on is safe until your project picks a convention.
@@ -61,16 +61,17 @@ In a file without a shebang, the message says to add the line as the first line
61
61
  instead. A convention without a hint ends the message with the generic sentence
62
62
  "Declare it with the project's declaration convention." A command run by path (`./build.sh`) is reported by that path.
63
63
 
64
- ## Options
64
+ ## Shared settings
65
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)).
66
+ This rule has no options. Declare project wrapper facts once with
67
+ `settings: { sh: { transparentWrappers: ['with-retry'] } }`.
68
+ Every wrapper-following rule reads that setting, including the other declaration rule.
69
+ Entries accept names or flag-bearing specs, for example
70
+ `settings: { sh: { transparentWrappers: [{ names: ['with-retry'], argFlags: ['-n'] }] } }`.
71
+ Then `with-retry -n 3 curl` uses both `with-retry` and `curl`.
72
+ The shared network wrapper is also followed automatically. Utility wrappers invoked
73
+ by absolute or relative path count by basename; builtin specs require exact names.
74
+ See [shared transparent wrappers](../transparent-wrappers.md) for precedence and migration.
74
75
 
75
76
  The convention is not a rule option. Bind it in `settings`, because ESLint copies rule options
76
77
  with `structuredClone`, which cannot copy a convention's methods.
@@ -84,9 +85,9 @@ export default [
84
85
  files: ['**/*.sh'],
85
86
  plugins: { sh },
86
87
  language: 'sh/sh',
87
- settings: { sh: { declarationConvention: commentPragma } },
88
+ settings: { sh: { declarationConvention: commentPragma, transparentWrappers: ['with-retry'] } },
88
89
  rules: {
89
- 'sh/no-undeclared-commands': ['error', { transparentWrappers: ['with-retry'] }],
90
+ 'sh/no-undeclared-commands': 'error',
90
91
  },
91
92
  },
92
93
  ];
@@ -94,11 +95,11 @@ export default [
94
95
 
95
96
  ## Examples
96
97
 
97
- With `commentPragma` bound and `{ transparentWrappers: ['with-retry'] }`:
98
+ With `commentPragma` bound and `settings: { sh: { transparentWrappers: ['with-retry'] } }`:
98
99
 
99
100
  ### Incorrect
100
101
 
101
- <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}, "findings": 6} -->
102
+ <!-- example: {"convention": "commentPragma", "settings": {"sh": {"transparentWrappers": ["with-retry"]}}, "findings": 6} -->
102
103
  ```sh
103
104
  #!/usr/bin/env bash
104
105
  # uses: jq
@@ -110,7 +111,7 @@ with-retry git pull # git, run by the wrapper, is not declared (nor is
110
111
 
111
112
  ### Correct
112
113
 
113
- <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}} -->
114
+ <!-- example: {"convention": "commentPragma", "settings": {"sh": {"transparentWrappers": ["with-retry"]}}} -->
114
115
  ```sh
115
116
  #!/usr/bin/env bash
116
117
  # uses: curl jq date
@@ -15,7 +15,7 @@ script's real surface. This rule reports each declared command the script never
15
15
  converse of [`sh/no-undeclared-commands`](./no-undeclared-commands.md).
16
16
 
17
17
  The rule needs a **declaration convention**, which says how your project declares commands. Bind
18
- one with `settings.sh.declarationConvention`. The plugin ships one, `commentPragma`, which reads
18
+ one with `settings: { sh: { declarationConvention: commentPragma } }`. The plugin ships one, `commentPragma`, which reads
19
19
  `# uses: jq curl` comments from a script's header. See
20
20
  [Declaration conventions](../declaration-conventions.md) to write your own. **With no convention
21
21
  bound, the rule reports nothing**, so turning it on is safe until your project picks a convention.
@@ -135,18 +135,17 @@ the whole line goes with its `\n` or `\r\n`. When several names on one line are
135
135
  edits touch, so ESLint applies one per pass. `eslint --fix` runs passes until nothing changes, so
136
136
  a line whose every name is unused is removed entirely.
137
137
 
138
- ## Options
138
+ ## Shared settings
139
139
 
140
- - `transparentWrappers` (`string[]`, default `[]`): names of your project's own wrapper
141
- commands, added to the built-in wrappers. The word after a listed wrapper is the command it
142
- runs. With `['with-retry']`, `with-retry curl x` runs `curl`, and `with-retry "$tool"` runs a
143
- command the rule can't name, so the file is incomplete usage and reports nothing. A listed
144
- wrapper takes no flags, so `with-retry -n 3 curl` is incomplete usage too: the rule stops at
145
- `-n` and can't tell what runs. Give this option the same value as
146
- `sh/no-undeclared-commands`' option of the same name, or the two rules will disagree about what
147
- the script uses. Your network wrapper doesn't need listing here when it is set in
148
- `settings.sh.networkWrapper`: this rule looks through it, with the options its spec declares
149
- (see [`sh/require-network-wrapper`](./require-network-wrapper.md#declaring-the-wrapper-once-settingsshnetworkwrapper)).
140
+ This rule has no options. Declare project wrapper facts once with
141
+ `settings: { sh: { transparentWrappers: ['with-retry'] } }`.
142
+ Every wrapper-following rule reads that setting, including the other declaration rule.
143
+ Entries accept names or flag-bearing specs, for example
144
+ `settings: { sh: { transparentWrappers: [{ names: ['with-retry'], argFlags: ['-n'] }] } }`.
145
+ Then `with-retry -n 3 curl` uses both `with-retry` and `curl`.
146
+ The shared network wrapper is also followed automatically. Utility wrappers invoked
147
+ by absolute or relative path count by basename; builtin specs require exact names.
148
+ See [shared transparent wrappers](../transparent-wrappers.md) for precedence and migration.
150
149
 
151
150
  The convention is not a rule option. Bind it in `settings`, because ESLint copies rule options
152
151
  with `structuredClone`, which cannot copy a convention's methods.
@@ -155,17 +154,15 @@ with `structuredClone`, which cannot copy a convention's methods.
155
154
  // eslint.config.js
156
155
  import sh, { commentPragma } from '@eslint-sh/eslint-plugin';
157
156
 
158
- const wrappers = { transparentWrappers: ['with-retry'] };
159
-
160
157
  export default [
161
158
  {
162
159
  files: ['**/*.sh'],
163
160
  plugins: { sh },
164
161
  language: 'sh/sh',
165
- settings: { sh: { declarationConvention: commentPragma } },
162
+ settings: { sh: { declarationConvention: commentPragma, transparentWrappers: ['with-retry'] } },
166
163
  rules: {
167
- 'sh/no-undeclared-commands': ['error', wrappers],
168
- 'sh/no-unused-declarations': ['error', wrappers],
164
+ 'sh/no-undeclared-commands': 'error',
165
+ 'sh/no-unused-declarations': 'error',
169
166
  },
170
167
  },
171
168
  ];
@@ -173,11 +170,11 @@ export default [
173
170
 
174
171
  ## Examples
175
172
 
176
- With `commentPragma` bound and `{ transparentWrappers: ['with-retry'] }`:
173
+ With `commentPragma` bound and `settings: { sh: { transparentWrappers: ['with-retry'] } }`:
177
174
 
178
175
  ### Incorrect
179
176
 
180
- <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}, "findings": 3} -->
177
+ <!-- example: {"convention": "commentPragma", "settings": {"sh": {"transparentWrappers": ["with-retry"]}}, "findings": 3} -->
181
178
  ```sh
182
179
  #!/usr/bin/env bash
183
180
  # uses: jq curl docker # docker is never run
@@ -189,7 +186,7 @@ curl -s "$URL" | jq .
189
186
 
190
187
  ### Correct
191
188
 
192
- <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}} -->
189
+ <!-- example: {"convention": "commentPragma", "settings": {"sh": {"transparentWrappers": ["with-retry"]}}} -->
193
190
  ```sh
194
191
  #!/usr/bin/env bash
195
192
  # uses: jq curl git sudo date with-retry
@@ -88,9 +88,11 @@ This rule takes one options object:
88
88
  - `allow` (`string[]`, default `[]`): variable names that may be assigned in a function without
89
89
  `local`.
90
90
  - `allowPattern` (`string`, default `"^[A-Z][A-Z0-9_]*$"`): a regular expression, compiled with the
91
- `u` flag, that exempts every name it matches. A value that is not a valid regular expression is a
91
+ `u` flag (Unicode, case-sensitive), that exempts every name it matches. A value that is not a valid regular expression is a
92
92
  configuration error. Setting it replaces the default, so `allowPattern: '^g_'` stops exempting
93
- UPPER_CASE names.
93
+ UPPER_CASE names. For example, `'^g_'` exempts `g_cache` but not `G_cache`.
94
+ See [regular-expression options](./README.md#regular-expression-options) for the shared
95
+ compilation and configuration-error contract.
94
96
 
95
97
  ```js
96
98
  // eslint.config.js
@@ -104,6 +106,22 @@ This rule takes one options object:
104
106
  }
105
107
  ```
106
108
 
109
+ Regex syntax errors fail when this rule first runs on a file, naming the rule, option and
110
+ pattern. `--print-config` will not catch any regex syntax errors. See the
111
+ [shared compilation contract](./README.md#regular-expression-options).
112
+
113
+ ### Matching examples
114
+
115
+ These examples use only this rule; a finding means the assignment needs `local`.
116
+ Option values below are JSON.
117
+
118
+ | Option value | Shell input | Findings |
119
+ | --- | --- | --- |
120
+ | `{"allowPattern":"^g_"}` | `f(){ g_cache=1; }` | 0 |
121
+ | `{"allowPattern":"^g_"}` | `f(){ G_cache=1; }` | 1 |
122
+ | `{"allowPattern":"^\\p{Lu}+$"}` | `f(){ CACHE=1; }` | 0 |
123
+ | `{"allowPattern":"^\\p{Lu}+$"}` | `f(){ cache=1; }` | 1 |
124
+
107
125
  ### Patterns run against script-controlled text
108
126
 
109
127
  `allowPattern` is tested against every variable name the linted script assigns in a function, and
@@ -137,7 +137,8 @@ exec "$@" # "$@" keeps every argument intact
137
137
 
138
138
  ## When not to use it
139
139
 
140
- Disable the rule if you run shellcheck, which already reports the same problem as SC2086
140
+ Disable the rule if you run shellcheck and want to avoid double reports. This also drops
141
+ its checks in command-name position and `for … in` lists, which SC2086 does not report
141
142
  (see below). For one line that relies on splitting on purpose, such as passing `$CFLAGS` to a
142
143
  compiler, disable it on that line (`# eslint-disable-next-line sh/quote-expansions`) or rewrite
143
144
  the value as an array (`"${cflags[@]}"`).
@@ -147,11 +148,13 @@ the value as an array (`"${cflags[@]}"`).
147
148
  eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
148
149
  layer. Run shellcheck beside it.
149
150
 
150
- This rule is one of eslint-sh's two deliberate overlaps with shellcheck. It covers the same
151
+ This rule is one of eslint-sh's
152
+ [deliberate overlaps with shellcheck](../shellcheck.md#deliberate-overlaps). It covers the same
151
153
  problem as SC2086 (double quote to prevent globbing and word splitting), and SC2068 for unquoted
152
154
  `$@`. It exists for teams that run eslint-sh without shellcheck, so that this common mistake is
153
- still caught. **If you run shellcheck, disable this rule**, so the same line is not reported
154
- twice. The rule does not try to match SC2086's heuristics: shellcheck skips some expansions it
155
- can prove are safe, such as a variable it saw assigned a number, and this rule reports them
155
+ still caught. **Disabling this rule when you run shellcheck avoids double reports**, but also
156
+ drops checks in command-name position (`$cmd arg`) and `for … in` lists (`for x in $LIST`),
157
+ which SC2086 never reports. The rule does not try to match SC2086's heuristics: shellcheck skips
158
+ some expansions it can prove are safe, such as a variable it saw assigned a number, and this rule reports them
156
159
  because it does no dataflow analysis. That is why `recommended` enables it at `warn` instead of
157
160
  `error`.
@@ -62,7 +62,7 @@ It does not depend on `languageOptions`.
62
62
  ## Options
63
63
 
64
64
  - `wrapper` (`string` or `WrapperSpec` object, no default): the wrapper network commands must be
65
- run through. When it is unset, the rule uses `settings.sh.networkWrapper` (below); with
65
+ run through. When it is unset, the rule uses `settings: { sh: { networkWrapper: 'with-retry' } }` (below); with
66
66
  neither, the rule reports nothing. A string names a wrapper that takes no options
67
67
  (or one of the default transparent wrappers, such as `timeout`, whose options are known). An
68
68
  object declares the wrapper's options so the command after them is found:
@@ -85,31 +85,36 @@ It does not depend on `languageOptions`.
85
85
  }
86
86
  ```
87
87
 
88
- ### Declaring the wrapper once: `settings.sh.networkWrapper`
88
+ ### Declaring the wrapper once
89
89
 
90
- `sh/no-undeclared-commands` and `sh/no-unused-declarations` also need to know about your wrapper:
91
- in `with-retry curl x` the script runs `curl`, and only a rule that looks through `with-retry` sees
92
- that. ESLint doesn't let one rule read another rule's options, so a wrapper set only as this rule's
93
- `wrapper` option is invisible to them, and an undeclared `curl` behind it goes unreported.
94
-
95
- Set the wrapper in shared settings instead, and all three rules read it:
90
+ Set the network policy in shared settings so every wrapper-following rule sees the
91
+ same command chain, including the declaration rules, `sh/no-eval`, and
92
+ `sh/no-dynamic-invocation`:
96
93
 
97
94
  ```js
98
95
  // eslint.config.js
99
96
  {
100
97
  settings: { sh: { declarationConvention: commentPragma, networkWrapper: 'with-retry' } },
101
- rules: { 'sh/require-network-wrapper': 'error' }, // or extend sh.configs['agent-authored']
98
+ rules: { 'sh/require-network-wrapper': 'error' },
102
99
  }
103
100
  ```
104
101
 
105
- `networkWrapper` takes the same values as the `wrapper` option, a name or a `WrapperSpec` object,
106
- and the declaration rules follow a spec's options too (`with-retry -n 3 curl`). This rule's
107
- `wrapper` option, when set, takes precedence for this rule only. A malformed value stops linting
108
- with an error starting `Invalid settings.sh.networkWrapper:`.
109
-
110
- The setting names your **network** wrapper, a policy. Other wrappers that run a command, such as
111
- a `with-lock` helper, aren't network policy: list those in the declaration rules'
112
- `transparentWrappers` option.
102
+ `networkWrapper` accepts a name or a JSON-expressible `WrapperSpec` object, like the
103
+ `wrapper` rule option. A custom spec for a default wrapper name takes effect in every
104
+ consumer. Absolute and relative utility wrapper paths match by basename:
105
+ `/opt/bin/with-retry curl x` and `./bin/with-retry curl x` both run `curl` through
106
+ `with-retry`, and an undeclared `curl` is reported by the declaration rule.
107
+
108
+ A `wrapper` option that disagrees with the shared network policy now throws a clear
109
+ configuration error. Equivalent values are accepted; there is no silent override.
110
+ A wrapper set only as a rule option remains local to this network policy because other
111
+ rules cannot read its options. Prefer the shared setting. Malformed settings also stop linting.
112
+
113
+ Other project wrappers belong in
114
+ `settings: { sh: { transparentWrappers: ['with-lock'] } }`. This factory-based rule
115
+ reads those facts too, so `with-lock curl x` cannot hide an unwrapped network call.
116
+ [Shared transparent wrappers](../transparent-wrappers.md) documents the common resolver,
117
+ conflicting specs, path matching, and migration from the removed per-rule options.
113
118
 
114
119
  ## Examples
115
120
 
@@ -24,6 +24,11 @@ This rule reads line 1 of the file and reports two problems:
24
24
  `#!/usr/bin/env -S bash -e`. The rule skips `env` options, their arguments and
25
25
  `NAME=value` assignments to find the command `env` runs.
26
26
 
27
+ The space-separated long form `#!/usr/bin/env --split-string bash -eu` is reported:
28
+ GNU env receives `--split-string bash -eu` as one option name and rejects it. GNU
29
+ `--split-string=bash -eu` is accepted here, but macOS env does not support the long form;
30
+ use `-S` across both systems.
31
+
27
32
  A relative interpreter such as `#!bash` or `#!env bash` is reported, because the kernel
28
33
  resolves it against the caller's working directory, not `PATH`.
29
34
 
@@ -150,8 +155,9 @@ eslint-sh is a convention and contract linter: a policy layer, where shellcheck
150
155
  layer. Run shellcheck beside it.
151
156
 
152
157
  shellcheck's SC2148 ("Tips depend on target shell and yours is unknown") fires when a file has
153
- no shebang and no `shell=` directive. shellcheck's aim is to know which shell to analyze for,
154
- so a `# shellcheck shell=bash` directive or a `--shell` flag silences it. This rule enforces
155
- the policy that the script itself names its interpreter, which a directive or a command-line
156
- flag does not satisfy. It also autofixes the missing line from your configured dialect.
158
+ no shebang and it cannot otherwise infer the shell. A `.bash`, `.bats` or `.ksh` extension,
159
+ a `# shellcheck shell=bash` directive or a `--shell` flag supplies that information and
160
+ silences SC2148. This rule requires the script itself to name its interpreter, so it still
161
+ reports a missing shebang in those files. It also autofixes the missing line from your configured
162
+ dialect.
157
163
  shellcheck's SC1008 ("This shebang was unrecognized") overlaps `unrecognizedInterpreter`.
@@ -142,6 +142,12 @@ This rule takes one options object:
142
142
  `bash script.sh`, so a `set` line is the only prologue that applies however the script is
143
143
  started.
144
144
 
145
+ Relative interpreters such as `#!env -S bash -eu` receive no credit. Neither does
146
+ `#!/usr/bin/env --split-string bash -eu`: GNU env rejects the space-separated long
147
+ option when invoked through a kernel shebang. GNU's equals form
148
+ `#!/usr/bin/env --split-string=bash -eu` receives credit; macOS env does not support that long
149
+ option. Use an absolute env path and `-S` for both systems.
150
+
145
151
  ```js
146
152
  // eslint.config.js
147
153
  {
@@ -32,8 +32,8 @@ For a complete GitHub Actions job, exit codes, a pre-commit hook and editor setu
32
32
  [`eslint-sh` README](https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-sh/README.md#continuous-integration).
33
33
 
34
34
  Give them the same target shell, since each reads its own configuration. shellcheck takes it
35
- from the shebang or a `# shellcheck shell=…` directive; eslint-sh takes it from
36
- `languageOptions.dialect` and `languageOptions.shellVersion`. The `bash-3.2` and
35
+ from the shebang, a `# shellcheck shell=…` directive, or a `.bash`, `.bats` or `.ksh` extension;
36
+ eslint-sh takes it from `languageOptions.dialect` and `languageOptions.minBashVersion`. The `bash-3.2` and
37
37
  `posix-portable` presets set the latter for you, and
38
38
  [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md) reports a
39
39
  shebang that disagrees with the configured dialect, so the two tools do not end up advising for
@@ -68,10 +68,10 @@ reason given, and each rule's page has a "Relationship to shellcheck" section. T
68
68
 
69
69
  | eslint-sh rule | Overlapping shellcheck code | Why the rule exists anyway |
70
70
  | -------------- | --------------------------- | -------------------------- |
71
- | [`sh/quote-expansions`](./rules/quote-expansions.md) | SC2086, SC2068 | Makes unquoted expansions a project policy with a severity you choose, and offers a quoting suggestion. It is `warn` in `recommended`; disable it if you already enforce SC2086. |
71
+ | [`sh/quote-expansions`](./rules/quote-expansions.md) | SC2086, SC2068 | Makes unquoted expansions a project policy with a severity you choose, and offers a quoting suggestion. It is `warn` in `recommended`; disabling it when you enforce SC2086 avoids double reports, but also drops checks in command-name position and `for … in` lists, which SC2086 never reports. |
72
72
  | [`sh/no-eval`](./rules/no-eval.md) | SC2294 (`eval` on arrays), SC1090 (non-literal `source`) | shellcheck flags `eval` only in specific misuse patterns. The rule treats every `eval` and every non-literal `source` as a reviewable exception. |
73
- | [`sh/no-unchecked-cd`](./rules/no-unchecked-cd.md) | SC2164 | Kept for files that run without `set -e`; the two agree on the common cases. |
74
- | [`sh/require-shebang`](./rules/require-shebang.md) | SC2148, SC1008 | A required, recognized shebang is a rule of the repository, not a hint about the target shell. |
73
+ | [`sh/no-unchecked-cd`](./rules/no-unchecked-cd.md) | SC2164 | Offers a suggestion and covers `pushd` and `popd` too. shellcheck suppresses SC2164 for shebang errexit flags; `ignoreWhenErrexit` reads only prologue `set` statements, so the rule still reports those unchecked directory changes. |
74
+ | [`sh/require-shebang`](./rules/require-shebang.md) | SC2148, SC1008 | Requires a recognized shebang even when shellcheck infers the shell from a `.bash`, `.bats` or `.ksh` extension, a directive or a command-line flag and emits no SC2148. |
75
75
  | [`sh/no-masked-exit-status`](./rules/no-masked-exit-status.md) | SC2155 | `require-strict-mode` makes `set -euo pipefail` a contract, and `local x=$(cmd)` is the most common way a script that honours it still swallows a failure. shellcheck offers no fix; the rule offers the split as an editor suggestion and can be limited to files where errexit is on. It is not in `recommended`, because that preset runs beside shellcheck. |
76
76
  | [`sh/prefer-local`](./rules/prefer-local.md) | SC2155 (a related check) | Requires `local` declarations, and its fix declares the variable on a separate line, which avoids SC2155. |
77
77
  | [`sh/compat/no-associative-arrays`](./rules/compat/no-associative-arrays.md), [`no-case-modification`](./rules/compat/no-case-modification.md), [`no-negative-array-index`](./rules/compat/no-negative-array-index.md), [`no-mapfile`](./rules/compat/no-mapfile.md) | SC3044 (the `declare` or `mapfile` command, not the `-A` option), SC3043 (`local -A`), SC3054 (`${a[-1]}`), SC3059 (case modification) | shellcheck reports these only for scripts it treats as POSIX `sh`. These rules report them for a Bash version you name. |
@@ -85,3 +85,7 @@ reason given, and each rule's page has a "Relationship to shellcheck" section. T
85
85
  | [`sh/compat/no-lastpipe-semantics`](./rules/compat/no-lastpipe-semantics.md) | SC2030, SC2031 | shellcheck explains the subshell effect of lastpipe; the rule reports `shopt -s lastpipe` on a Bash that lacks it. |
86
86
  | [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md) | SC3xxx | shellcheck judges the syntax against the shebang. This rule compares the shebang with your configured dialect, a different question. |
87
87
  | [`sh/require-strict-mode`](./rules/require-strict-mode.md) | none directly (SC2164, an unchecked `cd`, is one consequence) | shellcheck has no check that requires strict mode. |
88
+
89
+ `sh/compat/no-nounset-empty-array` requires explicit opt-in for `bash-3.2`. Enable it when a
90
+ script runs with `set -u` on Bash below 4.4 and its arrays can really be empty. Local checks
91
+ cannot prove safety across every caller or helper contract. `posix-portable` still enables it.
@@ -0,0 +1,97 @@
1
+ # Shared transparent wrappers
2
+
3
+ A transparent wrapper runs a command named in its arguments. Declare that project fact
4
+ once so every wrapper-following rule sees the same command chain:
5
+
6
+ ```js
7
+ {
8
+ settings: {
9
+ sh: {
10
+ transparentWrappers: ['with-retry', { names: ['audit-run'], argFlags: ['--ticket'] }],
11
+ },
12
+ },
13
+ }
14
+ ```
15
+
16
+ Entries are non-empty names or JSON-expressible `WrapperSpec` objects. A name preserves
17
+ the known option shape of a default wrapper such as `timeout`; an unfamiliar name
18
+ takes no flags. A spec can declare `names`, `noArgFlags`, `argFlags`,
19
+ `unresolvableFlags`, `stopsChainFlags`, `positionalOperandsBeforeCommand`,
20
+ `assignmentOperands` (`shell-identifier`, `env`, or `sudo`), and `shellBuiltin`.
21
+ `names` is required and non-empty. Regex fields and functions are not accepted.
22
+ An unknown flag stops analysis rather than guessing which operand is the command.
23
+
24
+ Project specs precede each rule's built-in table, including when a project spec names a
25
+ default wrapper. Replaced names are removed from lower-priority entries while their
26
+ unrelated aliases remain available; a builtin override cannot regain utility path semantics.
27
+ Duplicate project names and malformed entries are configuration errors.
28
+ The shared network policy, for example
29
+ `settings: { sh: { networkWrapper: 'with-retry' } }`, also supplies a transparent-wrapper
30
+ fact. Overlapping names must have the same option shape; extra transparent aliases do
31
+ not become approved network wrappers. Different option shapes for shared names are
32
+ configuration errors. A network rule's `wrapper` option must agree with both the option
33
+ shape and the full approved name set of a configured shared network policy. It cannot
34
+ override that policy silently. Omitted flag lists and empty flag lists are equivalent;
35
+ object key order and flag-list order do not matter.
36
+
37
+ Both comparisons use the published matcher's effective grammar: omitted flag lists
38
+ are empty, `shellBuiltin: false` means utility matching, and
39
+ `positionalOperandsBeforeCommand: 0` consumes no positional operand. Assignment
40
+ handling uses the selected grammar; the internal legacy alias means
41
+ `shell-identifier` only when no explicit grammar was supplied. Expanded built-in
42
+ flag patterns compare by source and flags, including presence versus absence.
43
+ This is structural grammar comparison, not arbitrary regex-language equivalence.
44
+ Explicit specs remain complete replacements: a patternless `nice` spec does not
45
+ inherit the built-in numeric-priority pattern, and conflicts with the string default
46
+ when both are configured for the same name. Internal regex fields remain forbidden
47
+ in settings; the accepted JSON fields above are unchanged.
48
+
49
+ Utility wrappers invoked by absolute or relative path match by basename:
50
+ `/opt/bin/with-retry curl x` and `./bin/with-retry curl x` both run `curl`.
51
+ An explicit path spec takes precedence over a basename fallback in either list order.
52
+ Path aliases are also built for a rule's built-in table when no shared setting is present.
53
+ The written wrapper path remains a dependency in declaration harvesting. Specs with
54
+ `shellBuiltin: true` require exact names: `/tmp/command` cannot acquire the shell
55
+ builtin's semantics. Unused-declaration completeness uses the resolved table too,
56
+ without restoring overridden built-in path semantics. These are syntactic declarations; no wrapper or corpus script is
57
+ executed or inspected to infer its behavior.
58
+
59
+ Every built-in that follows wrappers reads these facts: `no-undeclared-commands`,
60
+ `no-unused-declarations`, `no-dynamic-invocation`, `no-eval`, `no-unchecked-cd`,
61
+ `no-secret-env-reads`, `no-masked-exit-status`, and the command-based compatibility
62
+ rules. `require-network-wrapper` and rules made with `createWrapperRule` do too.
63
+ Rules retain their own built-in scopes and reporting policies. `quote-expansions`
64
+ checks expansion positions directly and does not follow wrappers.
65
+
66
+ Allowed-secret-sink exemptions apply only to argument words after the final resolved
67
+ command. In `with-retry --log "$API_TOKEN" sink`, the secret is owned by the wrapper
68
+ and is checked normally, even if `sink` is approved. In
69
+ `with-retry --log audit.log sink "$API_TOKEN"`, it is an actual sink argument and can
70
+ be exempt. The same boundary applies to default wrappers such as `env -u`.
71
+
72
+ ## Migration
73
+
74
+ Remove the `transparentWrappers` option from `no-undeclared-commands` and
75
+ `no-unused-declarations`, and remove `allowWrappers` from `no-dynamic-invocation`.
76
+ Move the list to `settings: { sh: { transparentWrappers: ['with-retry'] } }`.
77
+ Removed rule options fail ESLint schema validation; no per-rule override exists.
78
+
79
+ `allowWrappers` added checks: it made the rule follow the wrapper and report a
80
+ dynamic operand such as `with-retry "$cmd"`. Moving its list preserves that check
81
+ and makes every other wrapper-following rule see it too. To suppress a dynamic
82
+ dispatcher, use `allowInFunctions` or a justified ESLint disable comment.
83
+
84
+ Findings can change on unchanged scripts once project wrappers are configured:
85
+ wrapped `eval`, unchecked `cd`, dynamic invocations, compatibility features, and
86
+ unwrapped network commands become visible. An allowed secret sink behind a project
87
+ wrapper can become recognized, and incomplete usage behind a wrapper can prevent an
88
+ unsafe unused-declaration fix.
89
+
90
+ ## Rule packs
91
+
92
+ Call `resolveTransparentWrappers(context, builtIns)` from a rule's `create()` and
93
+ pass its result as the analysis `transparentWrappers` table. Omit `builtIns` to use
94
+ the default table. The resolver composes shared facts with that scope, validates
95
+ configuration, and adds path spellings from the linted file without mutating inputs.
96
+ Both `eslint-sh` and `@eslint-sh/eslint-plugin` export it and
97
+ `TransparentWrappersBindingContext`.
@@ -9,6 +9,10 @@ Which commands need a wrapper, and which wrapper, is your project's policy. This
9
9
  wrapper name. You bind both, either in a rule pack with `createWrapperRule` or in your config
10
10
  with [`sh/require-network-wrapper`](./rules/require-network-wrapper.md)'s `wrapper` option.
11
11
 
12
+ Project wrapper facts are shared through
13
+ `settings: { sh: { transparentWrappers: ['with-retry'] } }`. This rule follows
14
+ those wrappers and the shared network wrapper; see [shared transparent wrappers](./transparent-wrappers.md).
15
+
12
16
  ## `createWrapperRule`
13
17
 
14
18
  ```ts
@@ -204,3 +208,8 @@ command runs after its own options, located the way a wrapped command is located
204
208
  `noArgFlags` and `argFlags` are skipped, and an option in `stopsChainFlags` means no subcommand
205
209
  runs. The message names the command with its subcommand: `` `git fetch` must be run through
206
210
  `retry`. ``
211
+
212
+ A `WrapperSpec` may set `shellBuiltin: true` for a wrapper that runs inside the current shell.
213
+ Such entries match exact names: an executable such as `/tmp/command` is not the `command`
214
+ builtin and its argument words remain opaque. Omitted or false retains utility-wrapper path
215
+ matching. The same boolean field is accepted in the network-wrapper option and setting.