feat(ci): add shell-portability-lint gate for GNU-only constructs - #1511
Conversation
No CI gate covers shell portability: shellcheck lints syntax/style and portability-lint (#531) matches skill-coupling tokens against changed *skill* files only, so a GNU-only construct like the `\brequire\b` word-boundary escape that nearly shipped a fail-open security predicate in markdown-format.sh (#1097) would pass every existing gate silently on BSD userland (macOS system grep/sed/date/stat/mktemp/sort) — no runner here covers that platform. Adds scripts/check-shell-portability.sh, a changed-file-scoped gate over **/*.sh mirroring check-skill-portability.sh's shape: an external token list (scripts/shell-portability-tokens.txt), a same-line auto-guard for a co-located BSD counterpart, a per-site `portability-ok:` opt-out, and a whole-file `portability-scope:` declaration for the gate's own fixture corpus. Wires a new shell-portability-lint job into ci.yml (self-test on push, diff-gated on PR) and adds it to the ci-status required-check list. Active classes (zero or auto-guarded corpus impact): the regex-escape family (\b \< \> \s \S \w \W), grep -P/--perl-regexp, echo -e, sort -V, unsuffixed sed -i, and realpath-guarded readlink -f. date -d, stat -c, and mktemp -p ship staged (commented, inactive) per a corpus survey that found real cross-statement or unguarded uses the same-line guard doesn't yet cover — tracked in the enable-trigger follow-up #1510. Closes #1491
Matches the sibling check-skill-portability.test.sh convention and satisfies the exec-bit hygiene gate for tracked shebang files.
|
Claude finished @kyle-sexton's task in 6m 7s —— View job Code Review
SummaryThis is a well-structured addition that mirrors the existing Medium —
|
|
Claude finished @kyle-sexton's task in 3m 26s —— View job Security Review — PR #1511
FindingsNo CRITICAL or IMPORTANT security issues found. One SUGGESTION below. SUGGESTION — Shell expansion of
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46a2a615fe
ℹ️ 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".
The comment-hygiene gate's tracker-ref detector flags "issue #N" phrasing in a code comment (an unresolved-marker class); a bare "(#N)" cross-reference, the style used throughout this repo's other shell scripts, is not flagged.
|
Claude finished @kyle-sexton's task in 3m 42s —— View job Code Review — PR #1511
Review FindingsThree bugs confirmed; one additional test coverage gap found. Listed most-severe first. Bug 1 —
|
|
Claude finished @kyle-sexton's task in 3m 40s —— View job Security Review — PR #1511
FindingsOne IMPORTANT finding (logic flaw that undermines the gate's core security guarantee), one SUGGESTION on token coverage, and a re-confirmation of the prior review's low-confidence GHA note below. IMPORTANT —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5908431f66
ℹ️ 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".
Two of automated review's findings on #1511 risked flagging the CORRECT portable form, so fix directly rather than defer: - grep -P / sort -V / echo -e now match the target flag anywhere in a combined short-option cluster (grep -Pn, sort -Vr, echo -ne), not only as the cluster's last letter. - The realpath auto-guard is now scoped to the readlink pattern match specifically, not the whole line, so an unrelated "realpath" mention no longer excuses a different active token's hit on the same line. - sed -i's empty-suffix idiom (-i '' / -i "") — the portable BSD-safe form this class exists to encourage — is now auto-guarded; previously the gate flagged the CORRECT usage as a violation. Remaining lower-severity findings (additional sed -i spellings, the portability-scope substring-match precision shared with the sibling gate, an awk-operand edge case on a pathological filename) are deferred to #1513. Adds 6 self-test cases covering the fixes.
|
Claude finished @kyle-sexton's task in 7m 29s —— View job Code Review — PR #1511 (post-fix pass, commit
|
|
Claude finished @kyle-sexton's task in 3m 18s —— View job Security Review — PR #1511 (HEAD: 695ca2d)
FindingsNo CRITICAL or IMPORTANT security issues found. The prior IMPORTANT finding from the Prior IMPORTANT finding: FIXED ✓
The SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 695ca2d034
ℹ️ 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".
The typos hygiene gate flags the literal token "Pn" (from the grep -Pn fixture/example) as a likely typo for "On". _typos.toml is synced verbatim from the standards constellation and not locally editable for a one-off exception, so use the repo's existing spellchecker:disable-line convention on the affected test lines and drop the redundant -Pn example from the token file's comment (-riP and -nP already illustrate P-not-last).
|
Claude finished @kyle-sexton's task in 4m 27s —— View job Code Review — PR #1511 (HEAD:
|
|
Claude finished @kyle-sexton's task in 2m 43s —— View job Security Review — PR #1511 (HEAD: 49a0dbf)
FindingsNo CRITICAL or IMPORTANT security issues found. The prior IMPORTANT finding from the Prior IMPORTANT finding: CONFIRMED FIXED ✓
The SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49a0dbf927
ℹ️ 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".
|
This was generated by AI during work-loop execution. This PR merged (8949b57) about a minute before a second round of automated review landed two confirmed correctness findings on the branch's later pushes — including that the |
*This was generated by AI during work-loop execution.* ## Summary - #1511 (`ci: add shell-portability-lint gate for GNU-only constructs`, refs #1491) merged (`8949b577`) before a second round of automated Codex review on that branch's later pushes could land two confirmed correctness fixes — the review comments landed on the PR only ~1 minute before an independent merge-lane session merged it as gate-proven-green, so this follow-up carries the fixes forward. Both were verified empirically against real tools before landing here, not assumed. - **The `sed -i ''` / `sed -i ""` auto-guard was wrong and is removed.** It was added believing a space-separated empty-suffix argument was "the portable BSD-safe idiom" for `sed -i`. Verified against a real GNU sed 4.9: `sed -i '' 's/foo/bar/' file` exits 2, because GNU consumes the space-separated empty string as sed's *SCRIPT* argument (not `-i`'s suffix), shifting the real script and target file to be read as filenames. That idiom is BSD-only — it breaks on this repo's own GNU/Linux CI — so it correctly stays flagged now. The genuinely dual-compatible spelling (an ATTACHED nonempty suffix, `sed -i.bak '...' file && rm -f file.bak`) was already, correctly, never matched by the token (no separating whitespace). - **The regex-escape family (`\b \< \> \s \S \w \W`) stays BARE — a co-located-`grep`/`sed` requirement was tried here and reverted in review.** The motivation was real (the bare token flags portable, non-regex uses like `printf '\b'`, a genuine backspace byte on GNU and BSD alike), but the requirement bought a worse defect than it removed: a pattern is very often assigned on one line and consumed several lines later, so requiring the command on the escape's own line silently un-catches exactly the near-miss shape the class exists for. Verified against this repo's corpus, not assumed — `plugins/claude-config/.../audit-instructions/scripts/instruction-scan.sh` assigns `\b`-bearing EREs to `I6_ERE`/`RATIONALE_ERE` (lines 66, 68) and passes them to `grep -niE` (lines 83-85); with the requirement the gate reports that file clean, bare it flags both assignment lines. The `printf '\b'` false positive is the mirror-image cost of the token file's documented over-flag direction, and the per-site `portability-ok: <reason>` annotation is the one-line escape already shipped for it. Narrowing the class precisely needs real shell parsing, not a token edit — tracked in #1517. - Also lands two precision fixes from the same review round that were correct and already tested, but likewise never made it into the merge: - The `readlink -f` / `realpath` guard now requires an actual `||` fallback relationship, not mere line co-location (`realpath "$1"; readlink -f "$1"` — semicolon-separated, no real fallback — still flags). - `sort --version-sort` (GNU's documented long-form alias for `-V`) is now a separate literal token alongside the existing short-flag pattern. ## Test plan - [x] `bash scripts/check-shell-portability.test.sh` — 40/40 passing (6 new regression tests: a `\b` pattern built in a variable and consumed later still fires, `printf '\b'` fires and is excused by a `portability-ok:` annotation, the `sed -i ''`/`-i ""` now-correctly-flagged cases, the `||`-required readlink guard, `sort --version-sort`), run against the current `main` baseline (this branch was cut fresh from `main` after #1511 merged, not carried over from the closed PR's stale branch). - [x] The reviewer's own counter-example run directly: `scripts/check-shell-portability.sh --paths .../instruction-scan.sh` exits 1 (flags lines 66 and 68) — the false negative the reverted requirement introduced is gone. - [x] `scripts/check-shell-portability.sh origin/main` run directly against this branch's own diff — clean. - [x] `shellcheck --rcfile=.shellcheckrc` on both changed scripts — clean. - [x] `typos --config _typos.toml` — clean. - [x] `actionlint .github/workflows/ci.yml` — clean (workflow itself untouched by this PR). - [x] `bash scripts/check-skill-portability.test.sh` (sibling gate) — still passing, no cross-gate regression. - [x] Empirically verified both defects against real tools before fixing (not assumed): `sed -i ''` exit code on GNU sed 4.9, and `printf '\b'` byte output via `xxd`. ## Related No related issue: #1491 (the original item) and #1511 (the PR this fixes) are both already closed — there is no open issue for this PR to close. Refs #1491 and #1511 for context only. This corrects a defect in #1511 found by automated review after that PR had already merged. #1517 tracks the same review round and stays OPEN: this PR lands its items 3 (`readlink`/`realpath` `||` fallback), 4 (`sed -i ''` scope) and 5 (`sort --version-sort`), while item 1 (escape-class scoping) is re-confirmed here as needing real shell parsing rather than a token edit, and item 2 (backslash line-continuation normalization) is untouched.
Closes #1513 ## Summary Fixes the three deferred detection-precision findings tracked in #1513 (a follow-up to #1491 / #1511's `check-shell-portability.sh` gate): - **`sed -i` additional GNU spellings.** Adds a `--in-place` (long-form) token, and a combined `-Ei` short-flag-cluster token (mirroring the existing `grep -P` / `sort -V` / `echo -e` combined-cluster treatment). Both are anchored to a `sed` COMMAND token — start of line or a non-identifier character, whitespace after — so neither fires on an identifier that merely contains those three letters (`used="$(grep -Ei ...)"`) and `--in-place` does not flag a script defining or forwarding an unrelated in-place option of its own. `i` must be the cluster's LAST letter: GNU's syntax is `-i[SUFFIX]`, so `-iE` is an attached backup suffix, not a second flag (verified against GNU sed 4.9 — `sed -iE 's/a/b/' f.txt` writes the backup `f.txtE`), which makes it the same dual-compatible shape as `-i.bak` and correctly never flagged. The attached no-space EMPTY-suffix ambiguity (`-i''`) stays explicitly deferred, per the issue, since it needs real sed-dialect research rather than a pattern tweak — `-Ei''` is likewise left unflagged for the same reason. - **`portability-scope:` whole-file exemption precision.** Both `check-shell-portability.sh` and `check-skill-portability.sh` (shared fix, as the issue calls for) anchored the exemption check to a genuine comment-line declaration (`^[[:space:]]*#[[:space:]]*portability-scope:`, plus the HTML-comment-opener spelling for the skill gate) instead of an unanchored substring search. This was not just a theoretical gap: both scripts' own header prose *documenting* the mechanism (`` a whole-file `portability-scope: <reason>` declaration `` ...) already contained the literal string and was silently self-exempting the script from its own gate — verified before and after the fix that both scripts stay genuinely clean on their own merits, not by accident of the bug. - **`awk` operand disambiguation.** `check-shell-portability.sh`'s `scan_file()` passed the scanned file positionally to `awk`; a changed file whose relative path is shaped like an `identifier=value` assignment (e.g. a top-level `FOO=bar.sh`) is silently consumed by awk as a variable assignment instead of opened as a file, dropping it from the scan with no error. Verified empirically against a real awk (`awk '{print}' "FOO=bar.sh"` exits 0 with no output; prefixing with `./` makes it read the file correctly). Fixed by prefixing an unrooted file operand with `./` before it reaches awk. ## Test plan - [x] `bash scripts/check-shell-portability.test.sh` — 56/56 passing (16 new: `-Ei` combined-cluster detection incl. command-token anchoring and no-double-fire checks, the `-iE` / `-Ei.bak` attached-suffix negatives, `--in-place` bare and `=SUFFIX` forms plus the unrelated-option-arm negative, the `portability-scope:` mention-vs-declaration precision cases, and the `identifier=value`-shaped-filename awk regression). - [x] `bash scripts/check-skill-portability.test.sh` — 19/19 passing (2 new: the shared `portability-scope:` precision fix, for both the mention-vs-declaration case and the `#`-comment-style declaration alongside the existing HTML-comment style). - [x] `scripts/check-shell-portability.sh origin/main` run directly against this branch's own diff — clean (4 files, no unexcused constructs). - [x] `scripts/check-skill-portability.sh origin/main` — clean (no skill files in scope). - [x] `shellcheck --rcfile=.shellcheckrc` on all four changed shell scripts — clean. - [x] `typos --config _typos.toml` on all five changed files — clean. ## Related - #1491, #1511 — added the `check-shell-portability.sh` gate this PR hardens. - #1517 — a sibling, concurrently-dispatched follow-up on the same gate; verified its five findings (escape-class command-context, backslash-continuation, realpath fallback control-flow, the `sed -i ''` auto-guard debate, `sort --version-sort`) are disjoint from this PR's three items — no overlap. - #1519 — removed the `sed -i ''`/`sed -i ""` auto-guard entirely and reworked the `readlink -f` guard. It has since landed on `main` and is merged into this branch; the one textual conflict (the `sed -i` token's comment block) was resolved by composing both sides — #1519's verified rationale for why the space-separated empty suffix IS flagged, plus this PR's `-i''` deferral note. - #1532 — filed during this PR's implementation: `check-skill-portability.sh` has the textually identical `awk` operand pattern this PR fixes in `check-shell-portability.sh`, but fixing it there was out of this issue's stated scope (item 3 named only the shell gate). Follow-up tracks closing that gap. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
) *This was generated by AI during work-loop execution.* ## Summary - #1517 is a bundle of five round-2 detection-precision findings on `scripts/check-shell-portability.sh` / `scripts/shell-portability-tokens.txt` (the gate added by #1491 / #1511). The issue's own text frames items 1-4 as needing scanner or scope changes with real design content, each carrying an explicit "Re-opens when: ..." condition — a closed record of a deliberate deferral, not a live TODO; a future review round that re-raises one of them would file a new issue, the same way #1517 itself followed #1511 and #1513 rather than reopening either. Item 5 is the one item the issue names as ready now: "the natural first item to pick up ... a literal token addition with no combined-cluster complexity." - This PR does item 5: GNU `sort`'s `-V` (natural/version sort) class already matched the short flag, including inside a combined cluster (`-Vr`), but not its two documented long-form spellings — `-V, --version-sort` and `--sort=WORD` where `WORD` includes `version` (verified against man7.org's `sort(1)` page before encoding, not assumed). Adds both as unambiguous literal ERE tokens, the same shape `--perl-regexp` already uses alongside `grep -P`'s combined-cluster pattern (no command-context prefix needed — neither string collides with anything else a shell script would plausibly contain). - Corpus-checked before landing: no existing `.sh` file in this repo uses either long form today, so this isn't retroactively red-lining anything already merged. - **#1519 overlap, and what to do about it.** #1519 (`fix(ci): correct two shell-portability-lint false results`) is still open and unmerged. It already carries fixes for #1517's items 1, 3, 4, and half of item 5 (`--version-sort` alone, not `--sort=version`). This PR was cut from current `main`, which does not yet have #1519's changes, so it adds both `sort` long forms independently rather than assuming #1519 lands first. **If both PRs merge**, `shell-portability-tokens.txt` ends up with a duplicated `--version-sort` line — harmless to the gate's pass/fail outcome, but it would make the scanner emit two `PORTABILITY:` lines and double-count `violations` for what is really one hit. Whoever merges second should drop the duplicate line as part of the routine merge-conflict resolution (they'll already be looking at that hunk). ## Test plan - [x] `bash scripts/check-shell-portability.test.sh` — 38/38 passing (3 new regression tests: `sort --version-sort` and `sort --sort=version` long-form detection via an isolated single-token fixture, plus one case proving both forms are active in the SHIPPED token list — not just the isolated-token matching mechanism — with a single fixture file containing both spellings and a distinct `PORTABILITY:` line asserted for each), run against this branch's own working tree. - [x] `scripts/check-shell-portability.sh origin/main` run directly against this branch's own diff — clean (no unexcused GNU-only constructs in the 2 changed files). - [x] `shellcheck --rcfile=.shellcheckrc` on both changed scripts — clean. - [x] `typos --config _typos.toml` on the changed files — clean. - [x] `grep`-swept every tracked `*.sh` file for `--version-sort` / `--sort=version` — no existing site outside this PR's own new test fixtures, so nothing else needed a `portability-ok:` annotation. - [x] Verified `--sort=version` and `--version-sort` against GNU coreutils `sort(1)` (man7.org) before encoding as tokens, rather than assuming from memory. ## Related Closes #1517. Items 1-4 stay documented-but-deferred in the closed issue per its own reopen conditions above — not carried forward as an open tracker. Follows #1491, #1511. Sibling items on the same gate, left untouched by this PR's scope: #1513 (distinct sed-spelling / portability-scope / awk-operand findings, still open), #1519 (still open, overlaps items 1/3/4 and half of item 5 — see the dedupe note above), #1510 (staged-class enable trigger). --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…1543) *This was generated by AI during work-loop execution.* ## Summary - BSD/macOS `mktemp` has no `-p` flag. #1510 left `mktemp -p` STAGED (inactive) in `scripts/shell-portability-tokens.txt` after enabling the sibling `date -d`/`stat -c` classes, because migrating the corpus was a real, multi-plugin effort out of scope for that PR. This item does that migration. - Migrated every `mktemp -p <DIR> <template>` / `mktemp -d -p <DIR> <template>` call site (fresh `grep -rn "mktemp[^\n]*-p" --include="*.sh"` at execution time found **61 call sites across 24 files in 13 plugins** — the issue's "~20 sites"/"~24 files, ~56 call sites" estimates undercounted) to the portable `mktemp [-d] "$DIR/template"` form, which both GNU and BSD `mktemp` accept identically via the positional TEMPLATE argument instead of `-p`. Sites with no explicit template (`mktemp -p "$DIR"`) get an explicit `tmp.XXXXXXXXXX` template (GNU's own default) rather than relying on TMPDIR-inheritance semantics, which differ between dialects. - Activated the `mktemp -p` token in `scripts/shell-portability-tokens.txt` (moved ACTIVE, extended to a combined-short-option-cluster match — `p` anywhere in a cluster like `-dp` — mirroring the existing `sort -V` / `grep -P` / `echo -e` tokens) and trimmed the STAGED section comment. - Updated `scripts/check-shell-portability.test.sh`'s staged-classes test: split the old combined "date -d, stat -c, mktemp -p all inactive" assertion into a "date -d, stat -c still inactive" assertion plus new assertions that `mktemp -p` (including the `-dp` combined-cluster form) is now flagged and that the portable `mktemp "$DIR/template"` form is not. - **A real, verified scope boundary.** Running `scripts/check-shell-portability.sh --all` against a pristine `origin/main` (before this PR, in a throwaway detached worktree) already exits 1 with 68 pre-existing `PORTABILITY:` findings — none of them `mktemp`. Running the identical scan on this branch produces the byte-for-byte identical finding set (`mktemp` migration nets zero), confirming those 68 are pre-existing corpus debt orthogonal to this issue's scope (the regex-escape family `\b \< \> \s \S \w \W`, deliberately bare/over-flag by design, plus one unrelated unrelated `sed -i` site). Filed separately as #1540 rather than expanding this PR's blast radius into an unrelated ~68-site triage effort. - **5 annotations, in scope.** Touching `block-hook-bypass.test.sh` and `markdown-format.test.sh` (to fix their own `mktemp -p` sites) makes the diff-gated CI check scan those files' FULL content, which surfaced 5 of the 68 pre-existing findings in those two files specifically (PowerShell module-qualified command strings, a Windows path literal, and one unsuffixed `sed -i` in a test fixture — all pre-existing, unrelated to `mktemp -p`, none on lines this PR touches). Since this PR's own act of touching those files is what makes them newly load-bearing for CI, annotated all 5 with `portability-ok: <reason>` rather than letting an unrelated pre-existing gap fail this PR's own CI run. Verified: `check-shell-portability.sh origin/main` (diff-mode, what CI actually runs) is clean before and after — 0 findings. - Bumped `plugin.json` + added a `CHANGELOG.md` entry for all 13 touched plugins (actionlint, autonomy, bash-format, biome-format, claude-ops, desktop-notification, eol-normalizer, go-format, guardrails, markdown-format, powershell-format, ruff-format, typos-format) — test-only changes, so each gets a patch bump under `### Changed`. `guardrails` landed at `0.17.2` (not `0.17.1`, which a concurrently-merged PR (#1503) claimed first) after a rebase conflict. ## Test plan - [x] `bash scripts/check-shell-portability.test.sh` — 71/71 passing, including the new mktemp assertions (flags `mktemp -p`, flags the `-dp` combined cluster, does not flag the portable `mktemp "$DIR/template"` form) and the retained date-d/stat-c-still-staged assertion. - [x] `scripts/check-shell-portability.sh origin/main` (diff-mode, what CI runs on this PR) — clean, 0 findings across the 24 changed shell files. - [x] `scripts/check-shell-portability.sh --all` against the full corpus — 63 pre-existing findings remain (68 minus the 5 annotated in this PR's own touched files), all pre-existing and tracked in #1540; zero `mktemp` findings anywhere. - [x] Every one of the 24 migrated `*.sh` files executed directly and passing: `actionlint-check`, `lane-stop-gate`, `bash-format`, `biome-format`, `desktop-notification`, `eol-normalizer`, `go-format` (skipped — no `goimports` binary on this host, pre-existing/unrelated), `block-dangerous-git` (305/305), `block-hook-bypass` (203/203), `block-no-verify` (112/112), `cli-flag-verify` (48/48), `flag-commit-pr-skill-bypass` (28/28), `hardcoded-path-check` (72/72), `secret-pattern-detection` (42/42), `skill-reference-verify` (68/68), `stale-path-verify` (73/73), `workflow-resilience-check` (15/15), `markdown-format` (92/92), `powershell-format`, `ruff-format` (52/52), `typos-format` (41/41). `claude-ops-test-helpers.sh` and `guardrails-test-helpers.sh` are sourced helpers (never directly executed). - [x] `shellcheck --rcfile=.shellcheckrc` on every changed `*.sh` file — clean. - [x] `typos --config _typos.toml` on every changed file — clean. - [x] `markdownlint-cli2` on every changed `CHANGELOG.md` — clean. - [x] `git diff --stat` reviewed: every `plugin.json` diff is a single version-line change (no unintended reformatting/escaping — an early `json.dump`-based approach mangled em-dashes into `—` escapes across whole files and was caught and redone as a targeted regex substitution before committing). ## Related Closes #1527. Follow-up filed: #1540 (the 68 pre-existing, untriaged `--all` corpus findings this issue's own acceptance criteria surfaced but which are out of scope for a `mktemp -p`-only migration). Follows #1510 (staged-class enable trigger), #1491/#1511 (the gate itself). --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…nated forms (#1546) *This was generated by AI during work-loop execution.* Closes #1537 ## Summary - #1530 (which added `--sort=WORD` for `sort -V`'s long form) fixed the whitespace-only-boundary false negative on its own new token, but deliberately left the pre-existing `sort -V` short-flag cluster token untouched — widening it changes the gate's firing envelope over the existing corpus, not just #1530's new token, so it was scoped out. #1537 asked for exactly that widening, plus the same treatment for the sibling `grep -P` / `echo -e` short-flag tokens, which share the identical defect: the boundary accepted only trailing whitespace or end of line, so a flag terminated by a shell control operator with no intervening whitespace evaded detection — `x=$(sort -V)`, `sort -V|head -n1`, `sort -V; echo done` (and the same shapes for `grep -P` / `echo -e`). - Widens all three tokens to the operator-terminated boundary #1530 already established for `--sort=WORD`: `([[:space:]|&;()<>'"`+"`"+`]|$)` — every character that can actually end a shell word (whitespace, a control operator, a redirection, a subshell close, a quote), not only whitespace. - **Verified before landing, per the issue's instruction.** Ran `check-shell-portability.sh --all` against the corpus before and after the token edit: the hit sets are byte-for-byte identical (68 pre-existing findings — the regex-escape family `\b \< \> \s \S \w \W` plus one unrelated `sed -i` site — none of them `sort`/`grep`/`echo`), so the widened boundary surfaces no new corpus violations and needs no `portability-ok:` annotations. - **Found the same defect in two more tokens while auditing the three named siblings — scoped out, not absorbed.** `sed -Ei` and `sed --in-place` (from #1513/#1534) carry the identical whitespace-or-end-of-line boundary and the identical false negative, verified empirically: `x=$(sed -Ei)`, `sed -Ei|cat`, `sed --in-place|cat`, `x=$(sed --in-place)` all pass the shipped list today. #1537 named only `sort -V` / `grep -P` / `echo -e`, so — following the same narrow-scope discipline #1530 itself modeled — filed as #1545 rather than expanding this PR's blast radius. ## Test plan - [x] `bash scripts/check-shell-portability.test.sh` — 78/78 passing (12 new regression cases: 3 operator-terminated forms each for `sort -V`, `grep -P`, `echo -e` at the isolated-token level, plus one shipped-list assertion proving all 6 forms are detected under the real `shell-portability-tokens.txt`, not just the isolated-token mechanism). - [x] Verified the new tests are meaningful: stashed only `shell-portability-tokens.txt` (reverting to the old boundary) while keeping the widened test file — the shipped-list assertion fails as expected (`PASS=77 FAIL=1`); restored and confirmed 78/78 again. (The isolated-token tests hardcode the widened pattern directly via `one_token_list` and so are unaffected by the tokens-file revert — same shape as the existing `--sort=WORD` tests.) - [x] `scripts/check-shell-portability.sh --all` — before/after hit sets identical (68 findings, unrelated to these tokens); see summary above. - [x] `scripts/check-shell-portability.sh origin/main` run directly against this branch's own diff — clean (no unexcused GNU-only constructs in the 1 changed `.sh` file). - [x] `shellcheck --rcfile=.shellcheckrc scripts/check-shell-portability.test.sh` — clean. - [x] `typos --config _typos.toml` on both changed files — clean. ## Related - #1530 (established the operator-terminated boundary shape this PR mirrors onto the sibling short-flag tokens) - #1545 (follow-up: `sed -Ei` / `sed --in-place` carry the identical boundary defect, found while auditing this PR's three named siblings, explicitly scoped out) - Follows #1491, #1511, #1513, #1519, #1534, #1538, #1543, #1544 on the same gate. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This was generated by AI during work-loop execution.
Summary
shellchecklints syntax/style, andportability-lint(ci: portability lint lane — enforce declared agnosticism mechanically (19-issue coupling class + top review-churn source) #531) matches skill-coupling tokens against changed skill files only. A GNU-only construct —
like the
\brequire\bword-boundary escape that nearly shipped a fail-open security predicate inmarkdown-format.sh(fixed onmainvia fix(security): patch six Claude Security scan findings (F1–F6) #1097) — passes both gates silently on BSD userland(macOS system
grep/sed/date/stat/mktemp/sort), which no runner in this repo's CI covers(a Windows runner's Git Bash still ships GNU
grep/sed, so it would not help either).scripts/check-shell-portability.sh, a changed-file-scoped gate over**/*.shmirroringcheck-skill-portability.sh's shape: an external ERE token list(
scripts/shell-portability-tokens.txt), a same-line auto-guard for a co-located BSD counterpart,a per-site
portability-ok: <reason>opt-out, and a whole-fileportability-scope: <reason>declaration (used on the gate's own test file, which necessarily contains the constructs it
detects as fixture data).
shell-portability-lintjob intoci.yml(self-test on every push, diff-gated onpull requests) and adds it to the
ci-statusrequired-check list.(
\b \< \> \s \S \w \W),grep -P/--perl-regexp,echo -e,sort -V, unsuffixedsed -i, andreadlink -f(guarded when arealpathattempt sits on the same line — the shapelib/hook-utils.shalready uses). All four flag-based classes (grep -P,sort -V,echo -e,plus
sed -i) match the target letter anywhere in a combined short-option cluster (-Pn,-Vr,-ne), not only as the cluster's last letter, andsed -i's portable BSD-safe empty-suffix idiom(
-i ''/-i "") is auto-guarded rather than flagged.date -d,stat -c,mktemp -p. A corpus survey duringthis change found real, already-legitimate uses (a cross-statement GNU-then-BSD dialect function in
morning-brief.sh; ~20 shared test-scaffoldingmktemp -psites with no BSD counterpart) that thesame-line auto-guard doesn't yet cover — enabling them is tracked in the follow-up below, the same
staged-rollout posture
scripts/skill-portability-tokens.txtalready documents for its own classes.Triage note
#1491's triage marked the token-list-vs-BSD-container design fork as decision-defaulted (token list,
vetoable) and separately delegated "the starter token list's exact membership" to the implementer as
reversible/low-stakes. The ACTIVE/STAGED split above is that delegated, reversible call, made from an
actual corpus survey rather than guesswork — not a second judgment call requiring escalation.
Review response
An automated Codex review left 6 findings. Two risked flagging the CORRECT portable form and were
fixed directly (the combined-short-option-cluster gap on
grep -P/sort -V/echo -e, and thesed -i ''/sed -i ""empty-suffix idiom being wrongly flagged) plus a guard-scoping tightening (therealpathauto-guard now applies only to the readlink pattern match, not the whole line). Theremaining three lower-severity findings (additional
sed -ispellings,portability-scope:substring-match precision — shared with the sibling gate, not unique to this PR — and an
awkoperand edge case on a pathological filename) are deferred to #1513. See the threaded replies on each
finding for the per-finding classification.
Two further review rounds followed and the unresolved-thread count grew 6 to 11 without net decrease —
including one finding that asks to REVERSE the
sed -i ''auto-guard added in response to round one.Per this repo's convergence posture, the fix loop is cut off here: the five new findings are grouped
and deferred to #1517 with per-item re-open triggers, and each thread carries the reasoning. None is a
defect in the shipped behavior — four are false-negative detection gaps (before this gate they all
passed silently), and the one false positive is the token file's own documented over-flag posture,
which ships a per-site
portability-ok:opt-out. Absorbing them would re-widen the change andinvalidate the corpus survey the ACTIVE/STAGED split rests on.
Test plan
bash scripts/check-shell-portability.test.sh— 35/35 passing, including: the literal\btoken actually fires (verified against the real awk resolved in this environment, gawk 5.4.0 —
not assumed; this is a distinct, POSIX-fundamental escape from the sibling token list's
documented
\b-as-boundary-anchor pitfall, which this gate does not use), each of\< \> \s \S \w \W,grep -P/-riP/-Pn(and that a comment merely naminggrep -Pdoes notfire),
echo -e/-ne,sort -V/-Vr, unsuffixedsed -ivs.sed -i.bakvs. the guardedsed -i ''/-i "",readlink -fbare vs.realpath-guarded (and that the guard does not leakto an unrelated token on the same line), same-line/comment-block-above/leak-boundary
portability-ok:annotation behavior, the whole-fileportability-scope:declaration,fail-closed behavior (malformed token, missing token file, invalid base ref),
--allscopeexclusion, a Git-quoted non-ASCII changed path, and — against the real corpus — that the
shipped list does not flag
markdown-format.sh's known-good reference implementation and thatthe staged classes stay inactive.
scripts/check-shell-portability.sh origin/mainrun directly against this PR's own diff — thenew gate's own source files (2 shell files in scope) pass clean.
shellcheck --rcfile=.shellcheckrcon both new scripts — clean.actionlint .github/workflows/ci.yml— clean.bash scripts/check-skill-portability.test.sh(sibling gate) still passes — no cross-gateregression.
shell-portability-lintjob and the requiredci-statusaggregate.
Related
Closes #1491. Follow-ups: #1510 (enabling the staged classes), #1513 (detection-precision findings
from review round 1), #1517 (detection-precision findings from review rounds 2-3).