feat(bash-lint): add shell lint/format-on-edit plugin#16
Conversation
Port medley's bash-lint hook into the marketplace as a repo-agnostic plugin. ShellCheck lints every .sh/.bash edit (advisory; always exits 0); shfmt formats only when the consumer opts in with an .editorconfig, so no style is imposed on a repo that didn't choose one. Findings surface via additionalContext plus the marketplace hook::emit_telemetry envelope. Reads the consumer's own .shellcheckrc/.editorconfig and ships no rules. Kill switch: HOOK_BASH_LINT_ENABLED. Generalized from medley: dropped the medley-policy prose and the markdownlint-era cd (shfmt/shellcheck are file-anchored), and reused the marketplace's telemetry envelope rather than medley's emit_timed_event. hook-utils.sh is byte-identical to the markdown-formatter copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWt8nQzh8BAoZXRZRt2ARU
|
Claude finished @kyle-sexton's task in 2m 19s —— View job PR Review: feat(bash-lint)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2faa75ca03
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ortability Address Codex review on #16: - shfmt: pass --apply-ignore so an `ignore = true` editorconfig section is honored for the single edited file (shfmt skips ignore rules for direct-file invocations without it), so a repo's opt-out for generated/vendored scripts is not overwritten. --apply-ignore is a utility flag and does not disable editorconfig formatting options. - EPOCHREALTIME is Bash 5.0+; default the start stamp to empty and skip telemetry when it is unavailable, so the hook lints/formats on Bash 3.2/4.x instead of aborting under set -u before the advisory exit 0. Adds a regression test for the ignore=true opt-out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWt8nQzh8BAoZXRZRt2ARU
|
Claude finished @kyle-sexton's task in 1m 48s —— View job PR Review (post-43dd32d) — UpdatedTasks
What 43dd32d fixedThe latest commit addressed both Codex P2 findings cleanly:
Still open from the previous reviewThree issues from the first review pass were not addressed in 43dd32d. 1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43dd32dc2e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ported shfmt added --apply-ignore in 3.8.0 (2024-02); on older shfmt the flag errors and leaves the file unformatted while the run still reports ok. Fall back to a plain in-place format so pre-3.8 shfmt still formats (it cannot honor direct-file ignore rules regardless). On 3.8+ the first call succeeds and the fallback is never reached, so ignore=true opt-outs stay honored (covered by the existing regression test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWt8nQzh8BAoZXRZRt2ARU
Previously any .editorconfig in the tree opted shell files into formatting, so a repo configuring only other languages (e.g. [*.md]) had its shell files rewritten to shfmt's defaults — against the documented "only when you choose a shell style" promise. Now require an EditorConfig section that governs the file: a [*] catch-all or a shell glob ([*.sh]/[*.bash]/brace list), walking up and respecting root=true. Path-only sections are not treated as shell opt-in (safe bias: leave files untouched). Adds tests for the non-shell-only and [*] catch-all cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWt8nQzh8BAoZXRZRt2ARU
|
Claude finished @kyle-sexton's task in 2m 33s —— View job PR Review (post-149090e) — UpdatedTasks
What 9bc923f + 149090e fixedBoth new Codex P2 findings from the last review pass are cleanly addressed:
Still open from previous reviewsThree items from the prior review passes were not addressed in either new commit. 1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 149090eed2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| section_applies_to_shell() { | ||
| local h="$1" | ||
| [[ "$h" == '*' ]] && return 0 | ||
| [[ "$h" =~ \*\.(sh|bash)([^[:alnum:]]|$) ]] && return 0 |
There was a problem hiding this comment.
Match EditorConfig globs against the edited path
When a repo uses a path-scoped shell section such as [scripts/*.sh] or [generated/*.sh], this check returns true for every edited .sh file even if that section does not match the file being edited. EditorConfig path globs containing / are relative to the .editorconfig directory and only matching sections apply, so editing tools/foo.sh in that repo will still run bare shfmt and can rewrite a file the repo did not opt in; this is fresh evidence beyond the earlier non-shell-section comment because the current fix still checks only for a shell-looking header, not whether it matches FILE_REL.
Useful? React with 👍 / 👎.
| # (.shellcheckrc) is auto-discovered from the file's directory upward. | ||
| if command -v shellcheck >/dev/null 2>&1; then | ||
| ran_any=1 | ||
| SC_OUTPUT=$(shellcheck -x -f gcc -S warning "$FILE" 2>&1) || true |
There was a problem hiding this comment.
Add SCRIPTDIR to ShellCheck source lookup
For scripts that source sibling files with paths like . ./lib.sh, this invocation depends on Claude Code's hook working directory rather than the edited script's directory. ShellCheck's docs for source-path say sourced files are searched from the working directory by default and that SCRIPTDIR is the special value for the checked script's directory, so edits made while the hook runs from the repo root or another cwd will surface false SC1091 findings for valid relative sources unless the user has a compensating .shellcheckrc.
Useful? React with 👍 / 👎.
| # in-place format — those versions cannot honor direct-file ignore rules | ||
| # anyway (that is exactly the capability --apply-ignore adds). On shfmt 3.8+ | ||
| # the first call succeeds (skipping ignored files), so the fallback never runs. | ||
| shfmt --apply-ignore -w "$FILE" 2>/dev/null || shfmt -w "$FILE" 2>/dev/null |
There was a problem hiding this comment.
Do not fall back over shfmt ignore rules
On systems with shfmt older than 3.8, --apply-ignore is an unknown flag, so this || shfmt -w fallback formats the file directly without applying .editorconfig ignore = true rules. The shfmt manual says ignore rules are skipped for direct file formatting without --apply-ignore, so a generated/vendor script that the repo explicitly ignored can still be rewritten; this is fresh evidence after the earlier ignore-rule thread because the current fallback reintroduces the direct-file behavior specifically on older shfmt installs.
Useful? React with 👍 / 👎.
…#17) Default start=${EPOCHREALTIME:-} and skip telemetry when unavailable so the hook formats on Bash 3.2/4.x instead of aborting under set -u before the advisory exit 0. hook-utils.sh stays byte-identical to bash-lint. Version 0.1.0 -> 0.1.1 so installed consumers receive the fix. Propagation follow-up from #16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WWt8nQzh8BAoZXRZRt2ARU

Summary
Ports medley's
bash-linthook into the marketplace as the second plugin (aftermarkdown-formatter), as a repo-agnostic, plugin-form-safe capability.Behavior — PostToolUse hook on
Write|Editof*.sh/*.bash:0..editorconfig(walking up to the repo root). With none present, the file is left untouched rather than rewritten to shfmt's defaults — the plugin never imposes a style the repo didn't choose. Run with no formatting flags so.editorconfigstays authoritative.additionalContextplus the marketplacehook::emit_telemetryenvelope. Reads the consumer's own.shellcheckrc/.editorconfig; ships no rules. Kill switch:HOOK_BASH_LINT_ENABLED.Generalized from medley (not a byte-copy)
cd— shfmt/shellcheck resolve config from the file's path, so no working-directory assumption is needed.shfmton.editorconfigopt-in vs medley's unconditionalshfmt -w.hook::emit_telemetryenvelope over medley's olderemit_timed_event.hook-utils.shis byte-identical to themarkdown-formattercopy (git blob6342c502…).Verification (all green locally)
claude plugin validate --strict— plugin manifest and catalog manifesthook-utils.test.sh37/0,bash-lint.test.sh28/0 (includes shfmt gate ON/OFF and telemetry envelope shape)hooks.jsoncommand +${CLAUDE_PLUGIN_ROOT}with an envelope on stdin — formatted the file and surfacedSC2154, exit 0🤖 Generated with Claude Code