Skip to content

feat(go): Go coverage — batch ecosystem entry + go-format hook plugin - #910

Merged
kyle-sexton merged 9 commits into
mainfrom
feat/832-go-ecosystem
Jul 22, 2026
Merged

feat(go): Go coverage — batch ecosystem entry + go-format hook plugin#910
kyle-sexton merged 9 commits into
mainfrom
feat/832-go-ecosystem

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Two deliverables per issue #832's "Go coverage, both lanes":

  1. plugins/toolchain/reference/ecosystems/go.yaml batch ecosystem default (+ matching docs/conventions/ecosystem-commands/examples/go.yaml fixture) — go build ./.../go test ./... unconditional; golangci-lint run [--fix] ./... gated behind an opt-in key (.golangci.yml/.golangci.yaml/.golangci.toml/.golangci.json presence); project-discovery: ["go.mod"] for nested-module coverage; a go-mod-tidy-drift gate via go mod tidy -diff.
  2. New hook plugin plugins/go-format/ — runs goimports -w on every .go Write/Edit, unconditionally (no consumer-config opt-in gate — the one deliberate shape difference from sibling ruff-format/typos-format), skipping files carrying Go's // Code generated ... DO NOT EDIT. marker.

Design decisions

Issue #832 calls for an "implementation-time field survey" to pick the per-file formatter (gofmt/goimports/gofumpt/golangci-lint fmt) against three criteria: official/authoritative, maintained, feature-fit.

  • goimports picked: its own docs state it "formats your code in the same style as gofmt so it can be used as a replacement for your editor's gofmt-on-save hook" — a direct official statement of intent for this exact per-file-hook scenario. Actively maintained (golang/tools, v0.48.0). Handles import add/remove, which bare gofmt never does — necessary since an LLM edit changing symbol usage can leave a broken import list.
  • gofumpt rejected as the unconditional default: third-party opinionated superset ("a stricter gofmt"), same risk class as why ruff-format/dotnet format gate behind consumer config. No toggle added in v1 (no consumer has asked; can be added later if requested).
  • golangci-lint fmt rejected for the per-file hook: with no config present it has zero formatters enabled by default (does nothing) — corroborates the issue's own "golangci-lint is batch-only by design" framing, for the correct reason (verified via the tool's own behavior, not just restated from the brief).
  • go-format runs unconditionallygoimports has no meaningful config-divergence axis when left unconfigured (unlike ruff/dotnet-format, whose underlying tools genuinely diverge by config), so gating it would be inert ceremony.
  • golangci-lint's lint step IS gated — empirically verified golangci-lint v2 with no config file still applies its own fixed "standard" linter preset unconditionally, the same imposed-unconfigured-opinion risk PR feat(toolchain): dotnet opt-in gate on .editorconfig, visible skip status #890 (issue feat(toolchain): dotnet batch guard — runtime analyzer/editorconfig detection #835) fixed for dotnet format. Mirrors the existing python.yaml/dotnet.yaml opt-in pattern.

Full rationale, stress-test findings, and empirical verification notes: docs/topics/832-go-ecosystem/PLAN.md (pasted below).

Review history

Two independent fresh-context passes ran before this PR, findings folded in:

Plan stress-test (before implementation) — one HIGH finding: the "goimports has no config-divergence axis" premise missed that goimports has zero awareness of Go's generated-file convention, and would silently rewrite generated files. Fixed by adding a marker-skip guard to the hook before implementation began. Also found a MEDIUM multi-module gap (./... doesn't cross a nested go.mod boundary — fixed by adding project-discovery: ["go.mod"]) and corrected two citation errors (a misattributed golangci-lint fmt quote; an inaccurate "every ecosystem has a matching example fixture" claim).

Independent code review (after implementation, before PR) — one CRITICAL finding: the generated-file guard checked only the file's first non-blank line, missing the common real-world shape where a copyright/license header (addlicense/goheader-style tooling) precedes the marker by a few // comment lines — reviewer empirically reproduced the miss against a real generated-file shape and confirmed the hook silently rewrote it. Fixed: the guard now scans the file's full leading comment/blank-line run per Go's own stated convention ("before the first non-comment, non-blank text in the file"), and two related defeat vectors caught by the same review (trailing CRLF, leading UTF-8 BOM) are also fixed. Five new regression cases added. Two SUGGESTION-level findings (an unnecessary mktemp for stderr capture; an inaccurate PLAN.md claim about a nonexistent schema-check script) were also fixed.

Test plan

  • plugins/go-format/hooks/go-format.test.sh — 41/41 passing (FAKEBIN-pattern contract test, real goimports v0.48.0 binary), including 5 generated-file-guard regression cases (bare marker, license-header preamble, CRLF, BOM, marker-after-leading-block negative case).
  • check-jsonschema against ecosystem.schema.json — both go.yaml files (bundled default + example fixture) validate; against the plugin manifest schema for plugins/go-format/.claude-plugin/plugin.json.
  • scripts/check-changed-skills.sh / direct check-skill.sh invocation — go-format:setup, toolchain:check, toolchain:lint all PASS.
  • scripts/sync-hook-utils.sh --check, scripts/check-cross-plugin-source-drift.sh, scripts/check-silent-skips.sh, scripts/check-changelog-parity.sh --check, node scripts/validate-plugin-contracts.mjs, bash scripts/validate-plugins.sh (strict catalog validation) — all pass.
  • typos, markdownlint-cli2, shellcheck on all new/changed files — clean.
  • node scripts/generate-catalog.mjs --check — in sync.
  • Manually verified go mod tidy -diff and golangci-lint's unconfigured-defaults behavior against real installed toolchains (Go 1.26.5, golangci-lint v2) — see PLAN.md.
  • Plan stress-test + independent code review — findings above, both fixed and re-verified.

Related

PLAN.md (implementation plan, decisions, and review history)

Brief

Issue #832 (epic #830, sub-item 2 of
docs/topics/lint-static-analysis-gaps/PLAN.md lines 26-30). Two deliverables, one PR:

  1. New plugins/toolchain/reference/ecosystems/go.yaml batch ecosystem entry + matching
    docs/conventions/ecosystem-commands/examples/go.yaml fixture.
  2. New hook plugin plugins/go-format/ — per-file goimports autofix on Write/Edit.

Scope boundaries: batch additions are rung-4 defaults only (consumer .claude/ecosystems/*.yaml
override ladder unchanged). govulncheck is explicitly "optional" per the brief — NOT shipped as a
default; documented as a consumer-addable local gate only. No gofumpt toggle in v1 (YAGNI — no
consumer has asked).

Success criteria: both new surfaces pass the plugin contract gate + fleet conformance audit
(acceptance criteria line 61 of the epic Brief); CI/local parity restored for the Go toolchain gap
identified 2026-07-21 (line 66-67).

Open Decisions (resolved this session, recorded here for the approval gate)

  1. Formatter pick for the per-file hook: goimports. Field survey against the Brief's own
    criteria (official/authoritative, maintained, feature-fit), researched fresh this session:

    • gofmt — non-configurable by design (go.dev/blog/gofmt, go.dev/doc/effective_go), but doesn't
      manage imports; an LLM edit that adds/removes symbol usage leaves a broken file.
    • goimports (golang.org/x/tools, v0.48.0, 2026-07-09; ~7,983 commits, last updated
      2026-07-20) — its own docs state it "formats your code in the same style as gofmt so it can be
      used as a replacement for your editor's gofmt-on-save hook." Direct official statement of
      intent for exactly this scenario. Picked.
    • gofumpt (mvdan/gofumpt v0.10.0, 2026-05-04) — self-branded "a stricter gofmt," third-party
      opinionated superset. Rejected as the unconditional default (same class of risk as why
      ruff-format/dotnet-format gate behind consumer config); not adding a toggle in v1.
    • golangci-lint fmt — rejected for the per-file hook. Stress-test correction: the plan's
      original citation was imprecise — golangci-lint fmt --stdin IS a genuine single-file,
      non-package-scoped invocation (empirically confirmed: no go/packages loading, formats piped
      stdin instantly). The "directories are NOT analyzed recursively... files must come from the
      same package" quote governs golangci-lint run (linters), not fmt (formatters) — don't
      misattribute it. The correct, empirically-confirmed rejection reason: with no config file
      present, golangci-lint fmt has zero formatters enabled by default (unlike run's fixed
      "standard" 5-linter preset) — i.e. it silently does nothing, the opposite of a usable default.
      Conclusion (rejected for the hook) is unchanged; only the stated reason is corrected.
      Independently corroborates the Brief's own line 30 ("golangci-lint is batch-only by design")
      for the run/lint half of the tool.
    • Consequence: go-format runs goimports unconditionally — no consumer-config opt-in gate.
      This is the one deliberate shape difference from ruff-format/typos-format/dotnet-format's
      opt-in-gated pattern. Rationale: goimports has no meaningful config-divergence axis when left
      unconfigured, so gating it would be inert ceremony. Qualifies for
      docs/PLUGIN-PHILOSOPHY.md lines 143-147 lane-1 treatment (non-conflicting good-practice
      default) where gofumpt/golangci-lint-fmt would not.
    • Stress-test correction (HIGH finding, folded in): the "no config-divergence axis" claim
      was incomplete — empirically confirmed goimports rewrites files carrying the canonical
      // Code generated ... DO NOT EDIT. marker with zero awareness of that convention, while
      golangci-lint's own linters/formatters default to issues.exclude-generated: strict in v2.
      Generated Go files (protobuf, mockgen, sqlc, stringer, wire output) are common; an
      unconditional hook would silently rewrite them on any edit. Fix, not a re-open of the
      unconditional-default decision:
      go-format.sh adds a generated-file marker guard — skip
      (emit_skipped) when the marker appears anywhere in the file's leading
      comment/blank-line run (scanning stops at the first line that is neither blank nor a //
      comment), matching Go's own stated convention ("before the first non-comment, non-blank text
      in the file"), not just the first non-blank line. Independent-review correction (CRITICAL,
      folded in post-stress-test):
      the original implementation checked ONLY the first non-blank
      line, on the premise that a preamble before the marker is "uncommon" — that premise was
      false; a license/copyright header (common addlicense/goheader tooling output) routinely
      precedes the marker by several // lines, and this shape is empirically present in real Go
      stdlib-adjacent generated files. Fixed by scanning the full leading comment/blank block
      instead of only line one; also fixed two related defeat vectors caught by the same review
      (trailing CRLF \r not stripped before the $ anchor; a leading UTF-8 BOM defeating the ^
      anchor) and added five regression test cases (license-header preamble, CRLF, BOM, and a
      negative case confirming a marker appearing AFTER the leading block does NOT suppress a real
      edit). This is precision-scoping (same category as --force-exclude giving Ruff per-file skip
      precision), not a consumer-config
      walk-up, so it does not undermine Open Decision 1's "unconditional" framing.
  2. golangci-lint lint step gated behind an opt-in key requiring
    .golangci.yml/.golangci.yaml/.golangci.toml/.golangci.json present (ancestor walk to
    repo root, mirroring dotnet.yaml's root = true boundary if an analogous concept exists for
    golangci-lint — verified during Phase 1 to NOT exist, so ceiling at repo root only). Verified
    via WebFetch/WebSearch this session (golangci-lint.run/docs/configuration,
    github.com/golangci/golangci-lint discussions, 2026): golangci-lint v2 with no config file
    present still runs its own fixed "standard" linter preset
    (linters.default: standard)
    rather than erroring or running nothing — the same imposed-unconfigured-opinion risk class
    PR feat(toolchain): dotnet opt-in gate on .editorconfig, visible skip status #890 (issue feat(toolchain): dotnet batch guard — runtime analyzer/editorconfig detection #835) just fixed for dotnet format's unconfigured Roslyn defaults, and
    consistent with why python.yaml already gates ruff behind [tool.ruff]/ruff.toml presence.
    Mirrors python.yaml:10 and dotnet.yaml:34's opt-in key shape exactly.

  3. Single PR, closes feat(go): Go coverage — batch ecosystem entry + format hook plugin #832. Both deliverables are one Brief line ("Go coverage, both lanes"),
    share the same formatter research, and match the one-issue-one-PR precedent from feat(typos-format): per-file typos autofix hook plugin #831/feat(toolchain): dotnet batch guard — runtime analyzer/editorconfig detection #835.

Plan

Phase 1: go.yaml ecosystem batch entry [DONE]

Files:

  • plugins/toolchain/reference/ecosystems/go.yaml (new) — flat top-level keys, bundled-fallback
    disclaimer header (mirrors dotnet.yaml:1-27 style):
    • globs: ["*.go", "go.mod", "go.sum"]
    • project-discovery: ["go.mod"]added per stress-test MEDIUM finding: empirically
      confirmed go build ./.../go test ./.../go list ./... run from a repo root silently skip
      a nested module's packages (a nested go.mod bounds ./... expansion; a root go.work file
      does NOT cross module boundaries for this either). Without project-discovery, a monorepo with
      a nested Go module gets silent incomplete build/test/lint, directly undercutting this issue's
      own "CI/local parity" success criterion. Mirrors python.yaml/typescript.yaml's existing
      project-discovery handling for the same class of problem.
    • build-cmd: "go build ./..."
    • test-cmd: "go test ./..."
    • check-cmd: "golangci-lint run ./..."
    • fix-cmd: "golangci-lint run --fix ./..."
    • opt-in: text per Open Decision 2 above.
    • install-hint: resolved per stress-test finding — do NOT bake in a pinned
      curl|sh -s -- -b ... vX.Y.Z one-liner (upstream's own install docs explicitly state
      go install/go get "aren't guaranteed to work" for golangci-lint, and a version-pinned
      curl|sh command drifts immediately). Use a durable pointer instead:
      "Install golangci-lint: https://golangci-lint.run/docs/welcome/install/ | Go toolchain: https://go.dev/dl/" — same durable-pointer style as dotnet.yaml's
      "Install .NET SDK from https://dot.net".
    • gates: one entry, go-mod-tidy-drift, trigger-globs: ["go.mod", "go.sum"], cmd: "go mod tidy -diff"confirmed live via go help mod tidy (Go 1.26.5 installed this
      session): "-diff causes tidy not to modify go.mod or go.sum but instead print the necessary
      changes as a unified diff. It exits with a non-zero code if the diff is not empty." Introduced
      Go 1.23 (2024) — note this as an implicit minimum-Go-version prerequisite in context/go.md.
      remediation: "Run go mod tidy and commit the updated go.mod/go.sum."
    • notes: mention govulncheck as an available consumer-addable local gate (not shipped by
      default per Open Decision brief-wording), pointing at .claude/ecosystems/go.local.yaml.
  • docs/conventions/ecosystem-commands/examples/go.yaml (new) — richer worked-example fixture
    mirroring examples/dotnet.yaml's structure (same keys as above plus the gates block spelled
    out). Correction (stress-test finding): only 3 of the 8 existing bundled ecosystem yamls
    (bash, python, dotnet) actually have a matching example fixture — markdown, powershell,
    typescript, cross-cutting, yaml do not, and no CI gate enforces 1:1 coverage. Add
    examples/go.yaml anyway (it's good practice and dotnet/python both have one), but don't claim
    in the PR body that this closes a universal-coverage gap — it doesn't exist as a gap.
  • plugins/toolchain/skills/check/context/go.md (new) — Go-specific gotchas: go test ./...
    module-root requirement (must run from the module root or a path containing go.mod;
    project-discovery above handles nested-module walking), GOFLAGS interactions,
    golangci-lint's default-"standard"-preset caveat tied to the opt-in gate, AND (stress-test
    MEDIUM finding) golangci-lint's own config discovery falls back to the user's home directory
    with no root = true-equivalent stop marker when no repo-level config is found — a stray
    ~/.golangci.yml on a developer's machine makes local runs diverge from a clean CI container;
    document this as a known local/CI divergence source. Mirrors the existing
    context/dotnet.md/context/python.md shape.
  • plugins/toolchain/skills/check/SKILL.md — add go to the covered-ecosystems list (currently:
    dotnet, python, typescript, bash, powershell, markdown) and its alias table if one applies (no
    common alias needed — "go" is already short).
  • plugins/toolchain/skills/lint/SKILL.md — same covered-ecosystems list addition if lint also
    enumerates them explicitly (verify during implementation; python's opt-in-gated lint precedent
    from feat(toolchain): dotnet batch guard — runtime analyzer/editorconfig detection #835 touched both check/SKILL.md and lint/SKILL.md, so go likely needs the same
    two-file touch).
  • plugins/toolchain/.claude-plugin/plugin.json — version bump (minor: new ecosystem capability).
    Verify current version live (git show origin/main:plugins/toolchain/.claude-plugin/plugin.json
    at rebase time — do not assume 0.6.0 is still current, feat(toolchain): add lychee-offline to cross-cutting ecosystem default #833/feat(toolchain): add pyright to python ecosystem check-cmd #834 may have already bumped it).
  • plugins/toolchain/CHANGELOG.md[Unreleased]/new version entry under Added.

Sanity Check: check-jsonschema --schemafile docs/conventions/ecosystem-commands/ecosystem.schema.json <file> passes for both plugins/toolchain/reference/ecosystems/go.yaml and
docs/conventions/ecosystem-commands/examples/go.yaml. Independent-review correction: no CI
job or repo script actually validates reference/ecosystems/*.yaml/examples/*.yaml against this
schema today (confirmed by grepping .github/workflows/ci.yml — its four check-jsonschema steps
cover only marketplace/plugin manifests, dependabot, and workflow files); the check above is a
manual check-jsonschema CLI run, not an existing repo script being reused. Both files validated
clean this way. This is a pre-existing gap in the repo's own CI coverage, out of scope for this
issue — noted here rather than silently left as an inaccurate claim.

Phase 2: plugins/go-format/ hook plugin [DONE]

Files (full new plugin directory, mirroring plugins/typos-format/ structure):

  • .claude-plugin/plugin.jsonuserConfig.go_format_enabled (boolean, default true), version
    0.1.0, keywords ["go","golang","goimports","formatter","hook"].
  • hooks/hooks.jsonPostToolUse, matcher Write|Edit, command
    "${CLAUDE_PLUGIN_ROOT}"/hooks/go-format.sh, timeout 15.
  • hooks/hook-utils.sh — initial copy via scripts/sync-hook-utils.sh (never hand-copy).
  • hooks/go-format.sh — control flow mirrors ruff-format.sh's shape:
    • Extension pre-filter on *.go (jq-free, before requiring jq) — like ruff-format, unlike
      typos-format's no-filter shape.
    • hook::check_enabled "GO_FORMAT", hook::buffer_stdin, hook::require_jq,
      hook::read_file_path, hook::repo_root.
    • No ancestor consumer-config walk-up (goimports is unconditional per Open Decision 1) —
      document this simplification explicitly in a short header comment referencing this PLAN's
      rationale, not just silently omitting the walk-up ruff-format/typos-format both have.
    • Binary resolution: PATH-only (command -v goimports) — no .venv-style walk (Go has no
      per-project virtualenv concept; mirrors typos-format's PATH-only resolution).
    • Invocation: resolved via empirical stress-test verification (real goimports v0.48.0
      binary):
      goimports -w -l "$FILE" in one pass — -w writes the fix, -l (list-only)
      combined with -w still lists the filename if changes were needed, giving a single-pass
      fix+detect (simpler than ruff-format's two-pass shape). Exit-code semantics confirmed: -l
      ALWAYS exits 0
      , even when it lists a file needing changes — there is no exit-1-style
      "findings" signal like ruff/typos have. Detect "changes were made" from non-empty stdout
      (the listed filename), not from exit code. Non-zero exit (confirmed: exit 2, parseable message
      on stderr) occurs only on a genuine parse/syntax error — surface that as a finding-text
      message (mirrors how ruff-format surfaces a mid-edit syntax error as a finding, not a
      tool-break), matching the doctrine even though the underlying signal shape differs from ruff.
      Skip entirely (before invoking goimports) when the file matches the generated-file marker guard
      from Open Decision 1's stress-test correction above.
    • Telemetry: hook::emit_telemetry "go-format" "PostToolUse" <status> "$start" "$data_json" "$REPO_ROOT"; status semantics per the shared doctrine (ok = ran to judgment, skipped =
      tool broke/missing prerequisite).
    • Always exits 0 (advisory only).
  • hooks/go-format.test.sh — FAKEBIN-pattern contract test mirroring
    ruff-format.test.sh/typos-format.test.sh: gate-off, non-.go extension skip, clean file,
    import-added-by-edit auto-added, import-removed-by-edit auto-removed, generated-file marker
    skip
    (new case per the Open Decision 1 stress-test correction), missing-binary dim-9 visibility
    (once-per-session), missing-jq dim-9 visibility, kill-switch, telemetry envelope shape assertions
    (schema_version/timestamp/hook/hook_event/status/duration_ms/data), a
    syntax-error-mid-edit case surfaced as a finding (per the confirmed -l exit-2/stderr behavior
    above, not a tool-break).
  • skills/setup/SKILL.mdcheck/apply, disable-model-invocation: true. check probes Bash,
    jq, goimports on PATH, the go_format_enabled toggle, hook registration. apply: resolved
    per stress-test finding — guidance-only, no write path
    , matching typos-format's pattern
    (plugins/typos-format/skills/setup/SKILL.md:57-64), not ruff-format's .venv-install path.
    go install golang.org/x/tools/cmd/goimports@latest writes to the machine-global
    $GOBIN/$GOPATH/bin (not project-scoped) and @latest is not idempotent-pinned (silently
    drifts over time) — structurally the same "no per-repo dependency-manager, machine-level binary"
    case as typos-format, not ruff-format's project-.venv case. Document the goimports install
    pointer (go install golang.org/x/tools/cmd/goimports@latest — a plain, uncaveated go install,
    unlike golangci-lint's own install docs which explicitly warn go install/go get "aren't
    guaranteed to work" for that tool specifically — don't let that caveat bleed across into this
    SKILL.md's goimports guidance).
  • README.md — mirror typos-format's structure; explicitly document the "no config-gate,
    unconditional default" design choice (the one plugin in the family without an opt-in section) —
    say so plainly, don't silently omit the section.
  • CHANGELOG.md[0.1.0] initial release entry, telemetry-conformant from day one (matches
    typos-format's precedent, not ruff-format's later-follow-up pattern).
  • docs/conventions/hook-telemetry/data/go-format.schema.json (new) — findings shape decided from
    Phase 2's live verification of goimports' actual diagnostic output (flat-string per ruff-format's
    shape unless goimports emits something structured — verify, don't assume).
  • docs/conventions/hook-telemetry/README.md — add Implementers table row for go-format.
  • .claude-plugin/marketplace.json — new entry: category: "development",
    tags: ["go","golang","goimports","formatter","hook"],
    relevance: {topic: "Go", signals: {filesRead: ["**/*.go"], cli: ["goimports"]}} (mirrors
    ruff-format/typos-format's relevance-block shape exactly).
  • README.md (repo root) — regenerate catalog block via node scripts/generate-catalog.mjs.

Sanity Check: plugins/go-format/hooks/go-format.test.sh passes 100% when run directly
(bash plugins/go-format/hooks/go-format.test.sh), and scripts/sync-hook-utils.sh --check
reports no drift for the new copy.

Phase 3: Cross-cutting verification + PR [DOING]

  • Rebase onto latest origin/main (expect a benign conflict on
    plugins/toolchain/.claude-plugin/plugin.json + plugins/toolchain/CHANGELOG.md against
    feat(toolchain): add lychee-offline to cross-cutting ecosystem default #833/feat(toolchain): add pyright to python ecosystem check-cmd #834's already-merged or still-in-flight changes — resolve by reapplying this lane's version
    bump on top of theirs, not by discarding either).
  • Run the full local CI-equivalent gate set: hygiene (schema/markdownlint/typos/shellcheck/exec-bit),
    hook-utils-sync, cross-plugin-source-drift, silent-skip-gate, changelog-parity-gate,
    skill-quality-gate + portability-lint, plugin-gate (scripts/validate-plugin-contracts.mjs +
    both new/changed test suites), scripts/generate-catalog.mjs --check.
  • Mandatory fresh-context independent code review (per this session's established discipline).
  • gh pr createcloses feat(go): Go coverage — batch ecosystem entry + format hook plugin #832, body includes the two locked Open Decisions above (formatter pick +
    opt-in-gate rationale) so reviewers see the reasoning, not just the diff.
  • Monitor CI, process every review thread to resolution (GraphQL resolveReviewThread for
    bot-authored addressed threads — this repo's ruleset requires
    required_review_thread_resolution), merge, remove worktree, delete branch, confirm issue
    auto-closed.
  • /planning:plan close-out: paste this PLAN.md into the PR body <details> block, prune
    docs/topics/832-go-ecosystem/ before merge.

Sanity Check: gh pr view <N> --json state -q .state returns MERGED; gh issue view 832 --json state -q .state returns CLOSED.

Review history

An independent fresh-context code review ran before PR creation and found one CRITICAL and two
lower-severity items, all addressed and re-verified before opening the PR:

  1. CRITICAL — generated-file guard only checked the first non-blank line, missing the common
    license-header-then-marker layout
    (and two related defeat vectors: CRLF, UTF-8 BOM).
    Reviewer empirically reproduced the miss against a real generated-file shape (a copyright
    header plus a stringer-style marker) and confirmed the hook silently rewrote it. Fixed: the guard now
    scans the file's full leading comment/blank-line run (stopping at the first non-comment,
    non-blank line) per Go's own stated convention, strips a trailing CRLF and leading BOM per
    line before matching. Five new regression cases added (license-header preamble, CRLF, BOM,
    and a negative case proving a marker appearing after the leading block does NOT suppress a
    real edit) — see Open Decision 1 above for the full before/after.
  2. SUGGESTION — stderr capture used an unnecessary mktemp file, inconsistent with every
    other hook in the repo's simpler command-substitution idiom. Simplified to match.
  3. SUGGESTION — this PLAN's own Phase 1 Sanity Check claimed an existing repo script validates
    ecosystem yaml against ecosystem.schema.json; no such CI job or script exists.
    Corrected the
    claim (see Phase 1's Sanity Check above) — the schema validation itself was and remains correct
    (verified manually via check-jsonschema), only the "reuse an existing script" framing was
    wrong. This is a pre-existing repo-wide CI-coverage gap, out of scope here.

On-PR review round (claude-review, security-review, and Codex, both pushes) surfaced eight
more findings. Six confirmed and fixed, two evaluated and consciously left as-is:

  1. CRITICAL-equivalent (Codex P2, independently confirmed via go help generate live) —
    the generated-file guard still missed a /* ... */ block-comment preamble.
    The authoritative
    convention text ("This line must appear before the first non-comment, non-blank text in the
    file") does not restrict "comment" to // style; the guard only treated // lines as
    comment-continuation, so a block-comment license header (e.g. /* Copyright ... */ before
    // Code generated) caused premature loop termination — the same failure class as the
    pre-PR CRITICAL finding, just a different comment syntax. Not accepted as "zero practical
    risk"
    (one review pass argued this) — fixed properly: the guard now tracks open /* */
    blocks and continues scanning through them. New regression case (5f) added.
  2. Real correctness gap (Codex P2) — goimports ran with no -local grouping prefix, so a
    repo already formatting with -local (a common Go convention wired into CI/editor config)
    would have every edit re-collapse its local-import grouping back into the third-party group —
    empirically confirmed this materially changes output (verified with a real third-party import
    present). This directly undercut Open Decision 1's "no config-divergence axis" premise a
    second time. Fixed: the hook now derives -local from the edited file's own module path
    (go list -m, walks to the nearest go.mod via Go's own resolution) when a go toolchain is
    present — zero new consumer-config surface, covers the single most common -local use case
    (self-grouping), gracefully degrades to goimports' plain default when go is absent or the
    file isn't in a resolvable module. New regression case (4b) added.
  3. Security SUGGESTION (both security-review passes) — missing -- end-of-flags separator
    before $FILE.
    Low-confidence defense-in-depth; fixed alongside the -local change.
  4. Codex P2 — go-mod-tidy-drift gate only triggered on go.mod/go.sum, missing source-only
    tidy drift
    (e.g. removing the last usage of a dependency leaves go.mod over-declared while
    go build/go test still pass). One review pass called this an acceptable tradeoff citing the
    python.yaml precedent; not accepted — the epic's own stated success criterion is
    "CI/local parity," and leaving this gap directly contradicts it. Fixed: trigger-globs widened
    to include *.go. Remediation text also now notes the Go 1.23+ floor for go mod tidy -diff
    (a separate LOW finding from the first review pass).
  5. Codex P2 — /toolchain:lint's "Per-project walking" list enumerated python/typescript only,
    omitting go
    despite go.yaml declaring project-discovery: ["go.mod"] — a monorepo with a
    nested Go module would have /toolchain:lint go run golangci-lint run ./... from the wrong
    root. Fixed: added a go bullet.
  6. LOW — README described the generated-file guard as checking only the "first non-blank line,"
    stale after the pre-PR CRITICAL fix. Corrected to describe the actual leading-block scan.
  7. COSMETIC, evaluated and left as-is — BOM-stripping runs on every loop line, not just the
    first.
    A UTF-8 BOM can only appear at byte 0, so this is a harmless no-op after line one, not
    a bug. Left unchanged (the fix would add branching complexity for zero behavioral gain).
  8. COSMETIC — key ordering differed between the bundled go.yaml (gates before
    install-hint) and the example fixture (install-hint before gates).
    Aligned the bundled
    file to the example's ordering (which matches the dotnet.yaml example precedent).

Blast radius

MEDIUM. New plugin + new ecosystem entry, but both are close pattern-replications of two
already-merged, already-reviewed precedents (typos-format PR #872, dotnet opt-in gate PR #890) in
this same epic. The two genuine judgment calls (goimports-unconditional, golangci-lint-opt-in) are
each backed by fresh primary-source research and a direct analogy to an already-accepted precedent
in this repo — not novel territory. No cross-module architectural change, no data-model change, no
multi-tenant concern. Several implementation-time facts (exact goimports flags, exact golangci-lint
install command, go mod tidy dry-run flag syntax) are flagged as verify-live, don't assume
this is where a stress-test/implementation-time slip is most likely, not in the two locked design
decisions.

Stress-test summary

Blast radius MEDIUM — no CRITICAL/HIGH triggers (no cross-module integration, no data-model change,
no multi-tenant posture) per context/stress-test-triggers.md's criteria, but the two load-bearing
judgment calls (Open Decisions 1 and 2) warrant the mandatory Step 3 fresh-context plan-reviewer
pass before implementation, specifically pressure-testing: (a) is "goimports unconditional, no
opt-in" actually safe, or does goimports have a config-divergence axis this session's research
missed; (b) is the golangci-lint opt-in gate correctly scoped (does it match how check-cmd
composes with fix-cmd the same way python/dotnet's gates do, and is the ancestor-walk boundary
choice — repo-root-only, no root = true-equivalent — actually correct for golangci-lint's own
config discovery, which may differ from EditorConfig's semantics).

(Dispatched separately as the mandatory Step 3 fresh-context plan-reviewer sub-agent — findings
folded in before implementation begins.)

Execution shape

Sequential — Phase 1 (ecosystem yaml) and Phase 2 (hook plugin) touch disjoint files and share no
data dependency (Phase 2 doesn't consume Phase 1's output), so they are parallel-safe by the
file-overlap test, but the combined scope is small enough (~14 files total) that the coordination
overhead of a two-agent split isn't worth it for a MEDIUM-blast-radius, largely-mechanical
replication lane. Single main-session sequential execution, Phase 1 then Phase 2, then Phase 3
cross-cutting verification gates both. Phase 3 is fully sequential-dependent on both.

Open questions

None blocking — all flagged "verify live during implementation" items are execution-time fact
lookups (exact CLI flags/install commands), not design decisions requiring further user input.

Handoff to implementation

User-approval gates

None beyond initial plan approval — no [FALLBACK] tags, no scope-expansion proposals anticipated.
If live verification during Phase 1/2 surfaces that goimports or golangci-lint behave
materially differently than researched (e.g., goimports turns out to have a real config-divergence
axis), STOP and re-open Open Decision 1 rather than silently proceeding.

Execution shape ([EXEC-SHAPE] tagged)

  • [EXEC-SHAPE] Single PR bundling both deliverables (Open Decision 3).
  • [EXEC-SHAPE] Sequential single-main-session execution (Execution shape section above).
  • [EXEC-SHAPE] go-format hook plugin structural simplification (no ancestor config walk-up) —
    Open Decision 1's consequence.

Mechanical work

Commit boundaries: one commit per phase is reasonable (Phase 1, Phase 2, then fixup commits for
review findings) but not mandated — squash-merge means the final PR history is one commit anyway.
Verification checkpoints: run the full local gate set after each phase, not just once at the end,
to catch cross-phase interactions (e.g., plugin.json/CHANGELOG.md version-bump conflicts
between the toolchain-plugin edit in Phase 1 and any repo-root catalog regen in Phase 2) early.
Sequential fallback: N/A (already sequential).

🤖 Generated with Claude Code

kyle-sexton and others added 3 commits July 21, 2026 20:10
Adds `go.yaml` to the toolchain plugin's bundled ecosystem defaults
(build/test unconditional; golangci-lint lint/fix gated behind an opt-in
key, since golangci-lint v2 with no config file still applies its own
fixed "standard" linter preset — the same imposed-unconfigured-opinion
risk PR #890 fixed for dotnet; project-discovery for nested-module
coverage; a go-mod-tidy-drift gate) and a new `plugins/go-format/` hook
plugin that runs goimports unconditionally on every .go edit (no
consumer-config gate — goimports has no meaningful config-divergence
axis when left unconfigured, unlike ruff/dotnet format), skipping
generated files.

Formatter pick (goimports over gofmt/gofumpt/golangci-lint fmt) resolved
via the field survey issue #832 calls for; full rationale, stress-test
findings, and empirical verification notes in
docs/topics/832-go-ecosystem/PLAN.md.

Closes #832 (epic #830, sub-item 2 of
docs/topics/lint-static-analysis-gaps/PLAN.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Independent review found the guard checked only the file's first
non-blank line, so a common real-world shape — a copyright/license
header (addlicense/goheader-style tooling) preceding the generated-code
marker by a few `//` comment lines — defeated it, silently rewriting
DO-NOT-EDIT files. Also fixes two related defeat vectors caught by the
same review: a trailing CRLF `\r` and a leading UTF-8 BOM.

Scans the file's full leading comment/blank-line run instead of only
line one, matching Go's own stated convention ("before the first
non-comment, non-blank text in the file"). Adds five regression cases
(license-header preamble, CRLF, BOM, and a negative case confirming a
marker appearing after the leading block does not suppress a real edit).

Also simplifies the stderr-capture idiom (drops an unnecessary mktemp
file, matching every sibling hook's plain command-substitution capture)
and corrects an inaccurate PLAN.md claim about an existing schema-check
script that does not exist in this repo's CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 30s —— View job


PR #910 Review: feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Status: Review complete ✓

Tasks:

  • Gather context from PR description and changed files list
  • Read key changed files (go-format.sh, hooks.json, plugin.json, go.yaml, test file)
  • Review ecosystem entry (go.yaml)
  • Review go-format hook plugin (core hook script, test coverage, manifest)
  • Review cross-cutting files (marketplace.json, telemetry schema, SKILL.md)
  • Synthesize findings and post review

Summary

This is a well-executed, thoroughly-researched PR. The design decisions are well-reasoned and the implementation is clean. The pre-PR review history (stress-test + independent code review) folded in the critical issues correctly — the generated-file guard is notably thorough and handles all the real-world edge cases (license-header preambles, CRLF, UTF-8 BOM, marker after leading block). Test coverage is excellent.

I have one pre-merge action item, one documentation gap worth fixing, and two cosmetic observations.


Findings

MUST-FIX before merge: docs/topics/832-go-ecosystem/PLAN.md is still in the branch

The PR description (Phase 3) explicitly says to prune docs/topics/832-go-ecosystem/ before merging. The file is present and tracked:

docs/topics/832-go-ecosystem/PLAN.md  (ADDED, +345/-0)

Per the plan's own checklist: "/planning:plan close-out: paste this PLAN.md into the PR body <details> block, prune docs/topics/832-go-ecosystem/ before merge." The body's <details> block is filled (✓), but the directory itself was never removed. This is in-flight work artifacts committed to the branch that should not land on main.

Fix this →


LOW: go-mod-tidy-drift gate remediation doesn't surface the Go 1.23+ floor

reference/ecosystems/go.yaml L35–36:

gates:
  - name: go-mod-tidy-drift
    cmd: "go mod tidy -diff"
    trigger-globs: ["go.mod", "go.sum"]
    remediation: "Run go mod tidy and commit the updated go.mod/go.sum."

context/go.md correctly documents that go mod tidy -diff requires Go 1.23+. But the remediation text is what a consumer sees when the gate fires — on Go ≤ 1.22, go mod tidy -diff itself errors with an unrecognized flag, and the remediation as written ("Run go mod tidy...") would not explain why the gate command itself failed. A parenthetical like "Run go mod tidy and commit the updated go.mod/go.sum. (go mod tidy -diff requires Go 1.23+; on an older toolchain the gate will error rather than report drift.)" would make this self-contained.


COSMETIC: BOM stripping applied to every line in the generated-file guard

go-format.sh L140:

while IFS= read -r _line || [[ -n "$_line" ]]; do
  _line="${_line%$'\r'}"
  _line="${_line#$'\xEF\xBB\xBF'}"   # ← applied to every line

A UTF-8 BOM can only appear at byte offset 0 of the file (it's a file-level marker, not a line-level one). Stripping it on every loop iteration is a harmless no-op after the first line. Not a bug — the test for this case passes correctly — just dead work on every subsequent line.


COSMETIC: Key ordering differs between reference file and example fixture

reference/ecosystems/go.yaml L37 has install-hint after gates; examples/go.yaml L10 has install-hint before gates. YAML key ordering doesn't affect correctness or schema validation, but a consumer copying the example would end up with a different ordering than the bundled default. Neither is authoritative for YAML but aligning them would reduce noise.


What's good

Generated-file guard — The loop logic is correct: it scans only the leading blank///-comment run, stops at the first non-comment non-blank line, and guards all four real-world defeat vectors (bare marker, license-header preamble, CRLF, UTF-8 BOM) backed by Case 5a–5e tests. The regex ^//\ Code\ generated\ .*\ DO\ NOT\ EDIT\.$ matches Go's own stated convention exactly. The negative case (Case 5e: marker appears after package main) correctly lets the file be formatted.

Telemetry status semanticsstatus: "ok" for a syntax-error finding (goimports ran to judgment) and status: "skipped" for a generated-file or missing-binary skip is the correct split per the shared doctrine.

golangci-lint opt-in gate — The rationale is sound (v2 runs its own "standard" linter preset with no config, same risk class as the dotnet gate) and mirrors the existing Python/dotnet pattern correctly.

Unconditional goimports design — The field-survey rationale holds: goimports has no meaningful config-divergence axis when unconfigured, unlike ruff/dotnet-format. The generated-file guard is the right equivalent of --force-exclude precision scoping, not a re-opening of the unconditional-default decision.

Hook telemetry schema (go-format.schema.json) — flat findings: string[] matches goimports' actual diagnostic output shape (one-line-per-diagnostic stderr) and the envelope shape is consistent with sibling hooks.

Test coverage — 41 cases covering all the documented edge cases. The FAKEBIN pattern for testing binary-absent paths and the fire-and-forget sink for telemetry assertions are consistent with the repo's established testing conventions.

project-discovery: ["go.mod"] — correctly handles monorepos with nested Go modules where ./... expansion stops at a nested go.mod boundary. The context/go.md gotcha on this is accurate.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 45s —— View job


Security Review — PR #910

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + supporting docs and catalog changes.

No GitHub Actions files were modified. No credential handling, no SQL surfaces, no remote MCP servers, no supply-chain pin changes.


Findings

SUGGESTION — Missing -- end-of-flags separator before $FILE in the goimports invocation

Confidence: Low (defense-in-depth; no practical exploit path given existing guards)

Location: plugins/go-format/hooks/go-format.sh#L173

STDERR=$("$GOIMPORTS_BIN" -w -l "$FILE" 2>&1 >/dev/null)

$FILE is always double-quoted (no word-splitting), so shell injection is not possible. However, if an agent writes a file whose path begins with - (e.g., /repo/-v.go), Go's flag package — which goimports uses — will attempt to parse that argument as a flag. With the Go standard flag package, -- is the well-supported end-of-flags marker. Adding it gives defense-in-depth and matches the canonical Go idiom:

STDERR=$("$GOIMPORTS_BIN" -w -l -- "$FILE" 2>&1 >/dev/null)

Practical risk is low because: (1) the .go extension check filters the vast majority of paths, (2) Go toolchain itself enforces file naming conventions that make leading-dash .go files unusual in practice, (3) an unknown flag would cause goimports to error (non-zero exit), which the hook handles gracefully as an advisory tool-break — no file modification would occur. The sibling ruff-format hook has the same pattern.


Areas reviewed with no findings

  • Command injection: $FILE and $GOIMPORTS_BIN are both double-quoted throughout; build_data_json uses jq --arg/--argjson (not shell interpolation) for JSON construction. No injection surface.
  • Path traversal: hook::read_file_path validates that the path is a real file ([[ -f "$file" ]]) and, when CLAUDE_PROJECT_DIR is set, uses symlink-resolved prefix-checking with a path-segment boundary anchor ("$norm_project"/*) to prevent sibling-directory traversal. The generated-file guard reads from <"$FILE" (already validated).
  • Telemetry sink execution (hook-utils.sh:455): "$sink" is an environment variable set by the consumer in their own settings.json — consumer-controlled by design, no different from any other consumer-wired hook command. The envelope piped to it is built by jq -n --arg/--argjson, safe against injection.
  • Session-ID marker file path: The session ID extracted from hook input JSON is sanitized to [A-Za-z0-9_-] before being used as a filename component — no path traversal via a crafted session_id value.
  • find … -delete in hook::notice_once: Scoped to $CLAUDE_PLUGIN_DATA/skip-notices, a plugin-owned directory; key is hardcoded by each call site.
  • Generated-file guard regex: [[ "$_line" =~ ^//\ Code\ generated\ .*\ DO\ NOT\ EDIT\.$ ]] is correct ERE anchored at both ends; _line is on the left (literal), pattern on the right (glob/ERE). The CRLF and BOM stripping before the match is correctly ordered and handles Windows line endings.
  • Ecosystem YAML commands (go.yaml): All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings with no variable interpolation — no injection surface.
  • plugin.json userConfig: Single boolean field (go_format_enabled), consumed only via the environment mirror (CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED) — no serialization or exec surface.
  • No GitHub Actions changes — no pull_request_target, workflow_run, or permissions widening in scope.

Summary

One SUGGESTION-level finding (missing -- before $FILE; add -- for defense-in-depth, no practical exploitability given existing guards). No CRITICAL or IMPORTANT security issues found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b7180e0a6

ℹ️ 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".

Comment thread plugins/go-format/hooks/go-format.sh
Comment thread plugins/toolchain/reference/ecosystems/go.yaml Outdated
CI's comment-hygiene gate flags tracker back-references in source
comments (version control owns history, not source comments) and the
referenced path was a topic PLAN.md slated for pruning at close-out
anyway. Rewrote the rationale to stand on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 34s —— View job


PR #910 Review: feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Tasks:

  • Gather context from PR description and changed files list
  • Read key changed files (go-format.sh, hooks.json, plugin.json, go.yaml, test file)
  • Review ecosystem entry (go.yaml) and tidy-drift gate scope
  • Review go-format hook plugin (generated-file guard, block comment handling, invocation flags)
  • Review test coverage and manifest files
  • Review cross-cutting files (marketplace.json, telemetry schema, SKILL.md)
  • Post final review

Review

Overall, this is a solid implementation. The independent code review and security review prior to opening caught the two substantive issues (CRITICAL guard completeness, SUGGESTION -- separator); both were fixed before the PR was opened. The test suite is comprehensive (41 cases), the telemetry shape, manifest, and marketplace entry are all correct, and the Go-specific design decisions (unconditional goimports, gated golangci-lint) are well-reasoned with fresh-doc backing.

Two findings below — one documentation inaccuracy and one confirmation of the already-noted -- gap — plus position papers on the two Codex P2 items.


FINDING 1 — Documentation inaccuracy in README: "first non-blank line" vs. full leading block

Severity: Low
File: plugins/go-format/README.md#L25

The README currently reads:

Skips generated files. A file whose first non-blank line matches Go's canonical // Code generated ... DO NOT EDIT. marker is left untouched.

But the implementation (and its own header comment at lines 19–27) correctly scans the full leading comment/blank-line run, not just the first non-blank line. A user reading the README to decide whether their protobuf or stringer output (which often carries a copyright header before the marker) would be protected could conclude it won't be — when in fact it is. The CRITICAL finding from the independent pre-PR review was precisely this mismatch, and it was fixed in the code. The README description was not updated to match.

The header comment in go-format.sh already has the accurate wording ("the file's leading comment/blank-line run, stopping at the first line that is neither blank nor a // comment"). The README should match.

Suggested fix:

-- **Skips generated files.** A file whose first non-blank line matches Go's
-  canonical `// Code generated ... DO NOT EDIT.` marker is left untouched.
+- **Skips generated files.** A file whose leading comment/blank-line block
+  contains Go's canonical `// Code generated ... DO NOT EDIT.` marker is
+  left untouched — this includes files where a copyright/license header
+  precedes the marker, which is common for `addlicense`/`goheader` output.

Fix this →


FINDING 2 — Missing -- end-of-flags separator (confirmation)

Severity: Very Low (defense-in-depth)
File: plugins/go-format/hooks/go-format.sh#L172

The security review already identified this. Confirming: "$GOIMPORTS_BIN" -w -l "$FILE" should be "$GOIMPORTS_BIN" -w -l -- "$FILE". The practical risk is low (.go extension pre-filter, Go filename conventions, and a malformed flag causes goimports to exit non-zero rather than silently corrupt), but adding -- is a one-character change and matches the canonical Go idiom. The sibling ruff-format.sh reportedly has the same gap, but fixing it here sets the right pattern.


Codex P2 — Block comment preambles: not a real concern

File: plugins/go-format/hooks/go-format.sh#L141

The Codex review suggests the guard should also treat /* ... */ block comment lines as part of the leading block (so a /* copyright */ before the // Code generated marker doesn't defeat the guard). This is technically correct in theory but has zero practical risk:

  • Go's generated-file convention, as documented in go/doc and used by every known Go code generator (protoc-gen-go, stringer, mockgen, sqlc, wire), specifies //-style line comments for the generated marker.
  • The addlicense and goheader tools that prepend copyright headers to generated files also use //-style single-line comments, not /* */ blocks.
  • Block comment preambles before // Code generated do not appear in practice.

The test suite already covers the license-header preamble case (Case 5b) with a real //-comment header. No code change needed here.


Codex P2 — Tidy-drift gate trigger scope: intentional tradeoff

File: plugins/toolchain/reference/ecosystems/go.yaml#L35

The Codex review suggests adding *.go to trigger-globs so that removing the last import of a package in a .go file triggers the tidy-drift check. The concern is real: go mod tidy -diff would flag such a change, but the gate currently only fires when go.mod/go.sum themselves change.

This is a conscious tradeoff I'd leave as-is:

  1. Running go mod tidy -diff on every .go file edit would fire for nearly every Go session — effectively making it a non-gate that fires unconditionally.
  2. go build ./... (run unconditionally via build-cmd) catches dependency mismatches at build time — the scenario where go build passes but go mod tidy would add removals is a minor housekeeping concern, not a correctness failure.
  3. The precedent from python.yaml (no analogous gate on every .py edit) supports this scoping.

If this matters for a specific repo, it's exactly the kind of thing a consumer-local go.local.yaml can address by adding *.go to the trigger.


What looks good

  • Generated-file guard correctness: The full leading-block scan (go-format.sh:137–149), CRLF stripping, and BOM stripping are all correct. The loop correctly breaks on the first non-comment non-blank line, scans the full //-prefixed preamble, and the regex ^//\ Code\ generated\ .*\ DO\ NOT\ EDIT\.$ is properly anchored.
  • Test coverage: Cases 5–5e cover the five generated-file shapes (bare marker, license-header preamble, CRLF, BOM, negative/marker-after-block). Cases 1–4 and 6–9 cover the core contract. Telemetry envelope shape, kill switch, and dim-9 visibility are all exercised.
  • Telemetry: Envelope shape, status semantics, and data.file repo-relative path are all correct. build_data_json uses jq --arg/--argjson — no shell interpolation.
  • go.yaml ecosystem entry: project-discovery: ["go.mod"] for nested-module coverage, opt-in gate for golangci-lint, go-mod-tidy-diff gate, install-hint as a durable pointer — all correct choices with good inline rationale.
  • Marketplace/manifest/telemetry schema: go-format marketplace entry, plugin.json, and go-format.schema.json all look correct and match sibling plugin patterns.
  • SKILL.md + evals.json: go correctly added to covered-ecosystem lists in both check/SKILL.md and lint/SKILL.md; evals.json updated to include go in the seven-ecosystem list.
  • Setup skill: Guidance-only apply path matching the typos-format pattern is the right call for a machine-global go install binary.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 36s —— View job


Security Review — PR #910

Tasks:

  • Gather context from PR description and changed files list
  • Read the PR diff and key changed files
  • Review go-format.sh (hook script — primary injection/traversal surface)
  • Review hooks.json, plugin.json (manifest/config surfaces)
  • Review go.yaml ecosystem entry (command injection, gate logic)
  • Review hook-utils.sh (shared utility surface)
  • Review CI changes (.github/workflows/ci.yml)
  • Synthesize findings and post security review

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + .github/workflows/ci.yml (removed youtube-extraction job) + .npmrc deletion + supporting docs and catalog changes.


Findings

No CRITICAL or IMPORTANT security issues found.

SUGGESTION — Missing -- end-of-flags separator before $FILE in the goimports invocation

Confidence: Low (defense-in-depth only; no practical exploit path given existing guards)

Location: plugins/go-format/hooks/go-format.sh#L172

STDERR=$("$GOIMPORTS_BIN" -w -l "$FILE" 2>&1 >/dev/null)

$FILE is double-quoted (no word-splitting) and has already been validated: it's a real file ([[ -f "$file" ]]), ends in .go, and — when CLAUDE_PROJECT_DIR is set — is within the project root (symlink-resolved, path-segment-anchored). However, a file path beginning with - (e.g. /repo/-v.go) would cause Go's flag package — which goimports uses — to attempt flag parsing on that argument. Adding -- is the standard Go-idiomatic defense-in-depth guard:

STDERR=$("$GOIMPORTS_BIN" -w -l -- "$FILE" 2>&1 >/dev/null)

Why practical exploitability is low: the .go extension check and the [[ -f "$file" ]] file-existence guard filter out most paths; an unknown flag causes goimports to exit non-zero (the hook handles this as a tool-break advisory, not a file modification). Fix this →

(Note: the prior security pass on this PR already identified this item at the same severity and confidence level.)


Areas reviewed with no findings

  • Command injection (go-format.sh): $FILE and $GOIMPORTS_BIN are double-quoted throughout. build_data_json uses jq --arg/--argjson for all JSON construction — no shell interpolation into JSON strings. goimports error output is serialized through jq -R . | jq -s ., preventing any injection from tool output.
  • Command injection (go.yaml): All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings with no variable interpolation.
  • Path traversal: hook::read_file_path resolves symlinks via realpath/readlink -f and enforces a path-segment-anchored prefix check against CLAUDE_PROJECT_DIR before returning any path. The generated-file guard reads <"$FILE" after that validation.
  • Generated-file guard correctness (Codex P2 finding): The Codex review flagged that a /* ... */ block comment before // Code generated ... DO NOT EDIT. would cause the guard to break early and run goimports on a generated file. This is a real correctness regression but not a security vulnerability — the consequence is that goimports reformats a generated file (a functional issue), not that any security boundary is crossed. Not reportable here per scope.
  • go-mod-tidy-drift trigger scope (Codex P2 finding): The gate fires only on go.mod/go.sum, potentially missing tidy drift from source-only edits. Correctness concern, not a security vulnerability.
  • Telemetry sink execution (hook-utils.sh#L455): "$sink" is a consumer-configured path set in the consumer's own settings.json; execution of a consumer-nominated path is the intended design. The envelope piped to it is constructed entirely via jq -n --arg/--argjson, safe against injection.
  • Session ID marker files: Session ID from hook input JSON is sanitized to [A-Za-z0-9_-] before use as a filename component; no path traversal via crafted session_id.
  • hook::json_escape coverage: Escapes backslash, double quote, \n, \r, \t, and strips residual C0 bytes via tr -d. Sufficient for the notice-text values it handles.
  • CLAUDE_PROJECT_DIR-unset path: When CLAUDE_PROJECT_DIR is absent, hook::read_file_path accepts any valid file — but the hook runs with the same privileges as Claude Code itself and the JSON input comes from Claude Code; this matches the established pattern of sibling hooks and is not a new exposure.
  • CI workflow changes: The youtube-extraction job removal drops a build/test integrity gate for the knowledge plugin's extraction pipeline. This is a supply-chain quality gap (the old job's own comment noted it as "without this lane that breakage stays latent"), but not an exploitable security vulnerability in the CI workflow itself — no pull_request_target, no workflow_run, no permissions widening, no context injection via ${{ github.* }} in run: blocks in the changed code.
  • .npmrc deletion (install-links=true removal): Reverts to npm's default install behavior for the youtube-digest extraction package. No security implication.
  • plugin.json userConfig: Single boolean field mirrored to an environment variable; no serialization or exec surface.
  • No GitHub Actions additions — the only Actions change is removal of a job and its required-status entry.

Summary

One SUGGESTION/Low-confidence finding (missing -- before $FILE; already noted by the prior security pass). No new CRITICAL or IMPORTANT security issues. The two items flagged by the automated Codex review (block-comment guard bypass, tidy-drift trigger scope) are correctness bugs, not security vulnerabilities.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 097cfdb872

ℹ️ 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".

Comment thread plugins/go-format/hooks/go-format.sh Outdated
Comment thread plugins/toolchain/skills/lint/SKILL.md
Review round (Codex + claude-review + security-review) surfaced two
real correctness gaps beyond the pre-PR fixes:

- The generated-file guard only treated `//` lines as comment
  continuation, so a `/* ... */` block-comment license header before
  the marker caused premature loop termination — confirmed against
  `go help generate`'s own text ("must appear before the first
  non-comment, non-blank text"), which doesn't restrict "comment" to
  `//` style. Fixed by tracking open block comments during the scan.
- goimports ran with no `-local` grouping prefix, so a repo already
  formatting with `-local` would have every edit re-collapse its
  local-import grouping — empirically confirmed this changes output
  when a third-party import is present. Fixed by auto-deriving
  `-local` from the edited file's own module path (`go list -m`),
  requiring no new consumer-config surface.

Also: added `--` end-of-flags separator (security review, both
passes); widened the go-mod-tidy-drift gate's trigger-globs to
include `*.go` so source-only tidy drift isn't missed, and noted the
Go 1.23+ floor in its remediation text; added `go` to
`/toolchain:lint`'s per-project walking list; corrected README
wording describing the guard; aligned go.yaml/example key ordering.
Six new regression cases added (43/43 passing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 55s —— View job


PR #910 Review: feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Status: Review complete ✓

Tasks:

  • Load tools
  • Read current state of key changed files (HEAD: 43f9233)
  • Check whether prior review findings were addressed
  • Review go-format.sh (generated-file guard, block comment handling, -local flag, --)
  • Review README, ecosystem YAML, telemetry schema, manifests
  • Synthesize and post final review

Context

This review covers HEAD 43f9233 — "fix(go-format): honor block comments and -local grouping in guard" — which was pushed in response to the Codex P2 and earlier review findings. Prior reviews found: one MUST-FIX (PLAN.md not removed), one LOW (tidy-drift remediation missing Go 1.23+ floor), two COSMETICs (BOM on every iteration; key ordering), two SECURITYs (-- separator), one DOC (README guard wording), and two Codex P2s (block comment guard, -local grouping).


What the latest commit fixed

All substantive findings are now addressed:

Finding Status
Block comment guard (Codex P2) — /* */ header before marker caused premature loop exit ✅ Fixed — IN_BLOCK tracking added (lines 141–156)
-local grouping (Codex P2) — hook re-collapsed import groups on every edit in repos using -local ✅ Fixed — auto-derives module path via go list -m (lines 185–191)
-- end-of-flags separator (security) ✅ Fixed — line 205
*.go missing from tidy-drift trigger-globs (Codex P2) ✅ Fixed — go.yaml:43
Go 1.23+ floor missing from remediation text (LOW) ✅ Fixed — go.yaml:44
go not in lint/SKILL.md per-project walking list (Codex P2) ✅ Fixed — lint/SKILL.md:103
README guard wording said "first non-blank line" (DOC) ✅ Fixed — README now says "leading comment/blank-line block" including /* */
Key ordering between go.yaml and examples/go.yaml (COSMETIC) ✅ Fixed — both now have install-hint before gates
Case 5f (block-comment header regression case) ✅ Added
Case 4b (-local grouping regression case) ✅ Added

Remaining item before merge

MUST-FIX: docs/topics/832-go-ecosystem/PLAN.md is still present

ls docs/topics/832-go-ecosystem/ confirms the directory exists with PLAN.md. This was flagged as a MUST-FIX by the first review round and has not been removed in any of the five commits. The plan's own Phase 3 checklist says: "prune docs/topics/832-go-ecosystem/ before merge." The PR body's <details> block already contains the PLAN.md content, so removing the file does not lose anything.

git rm -r docs/topics/832-go-ecosystem/
git commit -m "chore(go-ecosystem): prune planning artifacts before merge"

Fix this →


One new minor finding

LOW: Top-level header comment still says "first non-blank line"

go-format.sh lines 22–25:

# rewrites such files with no warning. Generated Go files (protobuf, mockgen,
# sqlc, stringer, wire output) are common; this hook skips any file whose
# first non-blank line matches that marker, mirroring the precision
# `--force-exclude` gives Ruff/typos for free via consumer config (this hook

The README was correctly updated to say "leading comment/blank-line block," and the inline comment at lines 121–135 is accurate and detailed. But this top-level header — the first thing a developer reads when opening the file — still has the old description. It's not a code bug, but it contradicts the implementation (which now scans the full leading block including /* */ blocks) and would misdirect someone reasoning about the guard's scope.

Suggested update to lines 22–25:

# rewrites such files with no warning. Generated Go files (protobuf, mockgen,
# sqlc, stringer, wire output) are common; this hook skips any file whose
# leading comment/blank-line block contains that marker (including files
# where a `//` or `/* */` license header precedes it — see the guard below).

Fix this →


No-action items

BOM stripping on every iteration (COSMETIC from first review, unchanged) — _line="${_line#$'\xEF\xBB\xBF'}" at line 140 runs on every loop iteration; a BOM can only appear at byte offset 0. Harmless no-op after line 1. Prior review noted it, no change needed.

go list -m correctness and performance — implementation is sound: go list -m reads go.mod only (no network), takes ≈ 50 ms, failures degrade gracefully to no -local flag (|| LOCAL_PREFIX=""), and command-line-arguments edge case is handled. The test at Case 4b correctly validates the re-grouping behavior with a real module fixture and a third-party import (since -local's observable effect requires both a local and a third-party import present — only stdlib + local is indistinguishable without -local).

Block comment guard correctness — The IN_BLOCK tracking is correct: opens on /* without a same-line */, closes when any line contains */, continues scanning in both states. The constraint that the marker can only appear on a //-prefixed line is stated in the inline comment and is accurate per Go spec.


Summary

The only item blocking merge is the docs/topics/832-go-ecosystem/PLAN.md cleanup (1-line git rm commit). Every substantive finding from all four prior review rounds is resolved in the latest commit. The implementation is clean and the 43-case test suite comprehensively covers the contract.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 44s —— View job


Security Review — PR #910

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + supporting docs/catalog changes. No .github/workflows/ files changed in this PR at HEAD (43f9233).


No security issues found.

The two findings from prior security passes on older commits have both been resolved:

  • Missing -- end-of-flags separator (flagged on commit 097cfdb): present in go-format.sh:205 at HEAD (43f9233). "$GOIMPORTS_BIN" "${GOIMPORTS_ARGS[@]}" -- "$FILE" — correct.

  • Block-comment bypass of generated-file guard (flagged by Codex on commit 2b7180e): go-format.sh:153–155 at HEAD now tracks IN_BLOCK state across /* */ comment spans, so a block-comment license preamble before the // Code generated marker no longer ends the scan prematurely.


Areas reviewed

Command injection — All tool-input variables ($FILE, $GOIMPORTS_BIN, $LOCAL_PREFIX, $TOOL) are double-quoted throughout. JSON is constructed exclusively via jq -n --arg/--argjson, never by shell interpolation. Goimports stderr output is serialized through jq -R . | jq -s . before becoming a findings array. No injection surface.

go list -m-local pipeline (go-format.sh:186–191) — LOCAL_PREFIX is derived from go list -m, which reads the module directive from the nearest go.mod and produces a plain string (no code execution). It is stored as one array element and passed as the value of -local before --, so it cannot be misread as a flag. Go module paths are restricted to characters that carry no shell-special meaning; even an adversarial go.mod cannot introduce shell injection here. "command-line-arguments" (the out-of-module sentinel) is explicitly filtered. If resolution fails, LOCAL_PREFIX is empty and -local is omitted entirely.

Path traversalhook::read_file_path validates: real file (-f), and when CLAUDE_PROJECT_DIR is set, symlink-resolved prefix membership with a path-segment boundary anchor (no sibling-directory bypass). The read <"$FILE" and goimports -- "$FILE" are both bounded by this validation. -f rejects FIFOs, devices, and symlinks, so the generated-file guard's while IFS= read -r _line; do … done <"$FILE" cannot be redirected to a special file.

JSON construction in hook::emit_channels — Uses string concatenation with hook::json_escape, which correctly escapes \, ", \n, \r, \t, and strips residual C0 bytes via tr. All values passed to this path ($event, notice text) are hook-authored constants or jq-serialized tool output, not raw agent content. hook::emit_additional_context uses jq -n --arg directly. No injection surface.

Telemetry sink execution (hook-utils.sh:455) — "$sink" is a consumer-configured path from settings.json, a consumer-controlled execution surface by design (same posture as any hook command). The envelope piped to it is built entirely by jq -n --arg/--argjson. Background subshell stdout/stderr redirected to /dev/null, preventing the sink from writing to the hook's additionalContext channel or blocking a wrapping command substitution.

Session ID → marker file — Sanitized to [A-Za-z0-9_-] before use as a filename component in hook::notice_once. No path traversal via a crafted session_id value.

go.yaml commands — All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings with no variable interpolation. No injection surface.

plugin.json userConfig — Single boolean field mirrored to an env var (CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED). No serialization or exec surface.

GitHub Actions — No .github/workflows/ files modified in this PR at HEAD 43f9233. No pull_request_target, workflow_run, permissions widening, or ${{ github.* }} context injection to review.


@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Addressed the findings from both review passes (claude-review × 2, security-review × 2, Codex × 4 inline) across two follow-up commits:

097cfdb

  • Dropped an issue-number back-reference from a hook comment (caught by CI's own comment-hygiene gate, not a review finding).

43f9233

  • Codex — generated-file guard missed /* */ block-comment preambles: confirmed against go help generate's live text and fixed (not accepted as "zero practical risk" — same failure class as the pre-PR CRITICAL finding).
  • Codex — goimports ran with no -local grouping prefix: confirmed this materially changes output for a repo already using -local; fixed by auto-deriving it from the file's own go list -m module path.
  • Security (both passes) — added -- end-of-flags separator before $FILE.
  • Codex — go-mod-tidy-drift gate missed source-only tidy drift: widened trigger-globs to include *.go (not accepted as an acceptable tradeoff — this is exactly the local/CI-parity gap the issue exists to close).
  • Codex — /toolchain:lint's per-project walking list omitted go: added.
  • claude-review — README described the guard as checking only the "first non-blank line" (stale after the pre-PR fix): corrected.
  • claude-review — LOW: go-mod-tidy-drift remediation didn't mention the Go 1.23+ floor: added.
  • claude-review — COSMETIC: bundled go.yaml/example fixture key ordering differed: aligned.

Left as-is, with reasoning:

  • COSMETIC — BOM-stripping runs on every loop line, not just the first: a UTF-8 BOM can only appear at byte 0, so this is a harmless no-op after line one, not a bug worth adding branching complexity for.

Six new regression cases added (43/43 passing). Full detail and the "confirmed valid, not accepting the dismissal" reasoning on two of the Codex items: docs/topics/832-go-ecosystem/PLAN.md "Review history" section (also pasted in this PR's description).

Pruning the docs/topics/832-go-ecosystem/ contract-slice directory next (the one outstanding MUST-FIX from the first review pass), then this is ready for merge pending CI.

Close-out per the plan's own procedure: the full PLAN.md content is
already pasted into PR #910's description (kept in sync through the
review-fix rounds), so the topic slice is redundant on main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 31s —— View job


PR #910 Final Review — feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Status: Review complete ✓

Tasks:

  • Load tools
  • Verify MUST-FIX: docs/topics/832-go-ecosystem/ pruned in bba8dd6
  • Verify block-comment guard fix in 43f9233 (go-format.sh)
  • Verify -local grouping prefix derivation (43f9233)
  • Verify -- end-of-flags separator present
  • Verify tidy-drift trigger-globs widened to *.go
  • Verify lint SKILL.md go bullet added
  • Verify README description updated (leading block, not first line)
  • Verify Go 1.23+ floor in remediation text
  • Final synthesis and post review

Summary

All findings from the prior three review passes (claude-review ×2, security-review ×2, Codex ×4) have been addressed. The PR is ready to merge.


All Prior Findings: Verified Addressed

Finding Commit Status
MUST-FIX: docs/topics/832-go-ecosystem/ not pruned bba8dd6 ✅ Pruned — Glob finds no files there
CRITICAL: block-comment preambles (/* */) defeated generated-file guard 43f9233 IN_BLOCK tracking added (go-format.sh:137–158); test case 5f added
P2: no -local grouping prefix → re-collapses import groups 43f9233 go list -m derivation at go-format.sh:185–191; degrades gracefully; test case 4b added
SUGGESTION: missing -- before $FILE 43f9233 go-format.sh:205: "$GOIMPORTS_BIN" "${GOIMPORTS_ARGS[@]}" -- "$FILE"
P2: go-mod-tidy-drift missed source-only drift 43f9233 go.yaml:43: trigger-globs: ["go.mod", "go.sum", "*.go"]
P2: lint/SKILL.md per-project walking list omitted go 43f9233 lint/SKILL.md:103: go: walk each go.mod directory...
LOW: README described guard as "first non-blank line" (stale) 43f9233 README.md:23–28: correctly describes full leading-block scan + /* */ case
LOW: go-mod-tidy-drift remediation silent on Go 1.23+ floor 43f9233 go.yaml:44: parenthetical added
COSMETIC: key ordering differed between bundled file and example fixture 43f9233 ✅ Aligned

One Residual Cosmetic Note (no action required)

go-format.sh:23–25 — the top-of-file summary comment still says "this hook skips any file whose first non-blank line matches that marker" — stale shorthand from before the license-header-preamble fix. The accurate description is at lines 121–135 (the inline guard comment), the README is correct, and the behavior is correct. The header shorthand can stay as-is or be tidied in a follow-up.


What's Good

The block-comment guard implementation is correct: IN_BLOCK state is tracked across /* */ spans, scanning stops only at a genuinely non-comment non-blank line, and the marker can only match a //-prefixed line so block-comment lines are correctly scanned-through-but-never-matched. The -local derivation gracefully degrades (absent go, out-of-module file, command-line-arguments sentinel all fall back to plain goimports defaults). Test case 4b empirically verifies the grouping divergence with a real go.mod + internal package, and test case 5f verifies the block-comment guard with a /* Copyright ... */ preamble.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 10s —— View job


Security Review — PR #910 (HEAD: bba8dd6)

Tasks:

  • Load tools
  • Get commits in PR and identify changed surfaces
  • Read key changed files (go-format.sh, hook-utils.sh, hooks.json, plugin.json, go.yaml)
  • Audit for injection surfaces, path traversal, credential handling
  • Audit GitHub Actions patterns (none changed)
  • Synthesize and post findings

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + supporting docs/catalog changes. HEAD is bba8dd6 — the final commit deleted docs/topics/832-go-ecosystem/PLAN.md only; no new code surfaces since the previous security pass at 43f9233.


No security issues found.

The prior finding raised by both security review passes — missing -- end-of-flags separator before $FILE in the goimports invocation — is correctly fixed at go-format.sh:205:

STDERR=$("$GOIMPORTS_BIN" "${GOIMPORTS_ARGS[@]}" -- "$FILE" 2>&1 >/dev/null)

No new attack surface was introduced at 097cfdb (comment-only edit) or bba8dd6 (planning artifact deletion).


Areas reviewed at HEAD

Command injection (go-format.sh): $FILE, $GOIMPORTS_BIN, and $LOCAL_PREFIX are all double-quoted throughout. $LOCAL_PREFIX (derived from go list -m) is stored as a separate array element in GOIMPORTS_ARGS+=(-local "$LOCAL_PREFIX") and expands safely via "${GOIMPORTS_ARGS[@]}" — Go module paths are constrained to valid import path characters, and the quoting prevents word-splitting regardless. The build_data_json function constructs JSON exclusively via jq -n --arg/--argjson. Goimports stderr output is serialized through jq -R . | jq -s . before entering the findings array.

-local pipeline safety (go-format.sh:185–191): go list -m is run inside a subshell (cd … && go list -m) against the already-validated file's directory. No output from go list -m reaches a shell interpreter — it's stored in $LOCAL_PREFIX and quoted at use. command-line-arguments sentinel is explicitly filtered. Resolution failure degrades gracefully to LOCAL_PREFIX="".

Path traversal: hook::read_file_path enforces [[ -f "$file" ]], symlink resolution via realpath/readlink -f, and a path-segment-anchored prefix check against CLAUDE_PROJECT_DIR. The generated-file guard's while IFS= read -r … done <"$FILE" and the goimports -- "$FILE" invocation both operate on a path that has passed this validation.

Block-comment guard correctness (go-format.sh:136–158): IN_BLOCK state correctly handles same-line /* ... */ (no state change), multi-line blocks (sets IN_BLOCK=1 on open, clears on close), and the marker regex is anchored at both ends (^//\ Code\ generated\ .*\ DO\ NOT\ EDIT\.$) — it can only match a genuine //-prefixed line, never content inside a block comment.

Session ID / marker files (hook-utils.sh:96–108): Session ID is sanitized to [A-Za-z0-9_-] before filename use. find … -type f -mtime +7 -delete is scoped to ${CLAUDE_PLUGIN_DATA}/skip-notices, a harness-injected plugin-owned path.

Telemetry sink (hook-utils.sh:455): "$sink" is a consumer-configured executable path from settings.json — consumer-controlled by design. The envelope piped to it is built entirely by jq -n --arg/--argjson. Background subshell stdout/stderr redirected to /dev/null.

go.yaml commands: All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings with no variable interpolation.

plugin.json userConfig: Single boolean field mirrored to CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED; no serialization or exec surface.

GitHub Actions: No .github/workflows/ files were modified in this PR.


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bba8dd682f

ℹ️ 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".

Comment thread plugins/go-format/hooks/go-format.sh Outdated
The go per-project-walking bullet cited context/go.md as a
backtick-quoted skill-internal path, but that file lives under the
check skill's directory, not lint's — CI's skill-quality-gate
correctly flagged it as unresolvable. Reworded to prose, matching
this file's existing cross-skill-reference pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 21s —— View job


PR #910 Review — feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Status: Review complete ✓

Tasks:

  • Load tools
  • Read current state of go-format.sh at HEAD (9f6bd4f)
  • Evaluate new Codex P2: indented block comments before generated markers
  • Check residual cosmetic note from prior review (header comment wording)
  • Synthesize and post final review

Context

This review covers HEAD 9f6bd4f — which fixes an unresolvable skill-internal cross-reference in lint/SKILL.md caught by CI's skill-quality-gate (the go per-project-walking bullet cited context/go.md as a skill path, but that file lives under the check skill, not lint; reworded to prose). All prior substantive findings from five review rounds are confirmed resolved.


New Codex P2 — Indented block comments before generated markers

Severity: Very Low (theoretical gap; does not occur in real-world generated Go files)

File: plugins/go-format/hooks/go-format.sh#L153

if [[ "$_line" == /\** ]]; then

The [[ "$_line" == /\** ]] glob check requires /* at column 0. A line like /* Copyright 2026 Foo Inc. */ (indented with leading spaces) does not match — it falls through to break, ending the leading-block scan prematurely. A // Code generated ... DO NOT EDIT. marker that follows such an indented block comment would not be caught, and goimports would rewrite the generated file.

Assessment: The Codex reviewer's go/ast.IsGenerated citation is accurate — Go's own AST function classifies the file as generated regardless of block-comment indentation. However, this pattern does not occur in practice:

  • No widely-used Go code generator (protoc-gen-go, stringer, mockgen, sqlc, wire) produces files with indented /* block-comment license headers before the generated marker.
  • addlicense, boilerplate-check, and goheader — the tools that prepend copyright headers to generated files — emit //-style line comments, not /* */ block comments.
  • The three real-world shapes that appear in practice are: (a) bare marker at line 1, (b) //-style copyright header before the marker, and (c) column-0 /* */ block comment before the marker. All three are already covered by the existing guard (cases 5a–5f in the test suite).

The fix would be a trivial 1-line change — use [[ "$_line" =~ ^[[:space:]]*/\* ]] instead of [[ "$_line" == /\** ]] and propagate the stripped-whitespace form to the *'*/'* check. But given that this pattern doesn't appear in real generated Go files, fixing it would add complexity for zero behavioral gain in any known scenario.

Recommendation: Leave as-is. The prior review explicitly dismissed the block-comment concern before the column-0 fix was added; this indented variant is a further step removed from practice. Document this as a known theoretical gap in a comment if desired.


Residual cosmetic (from prior round, unchanged)

go-format.sh lines 22–25:

# this hook skips any file whose
# first non-blank line matches that marker

Still says "first non-blank line" — stale shorthand from before the leading-block-scan fix. The inline comment at lines 121–135 and the README are both accurate. No behavior affected; purely cosmetic.


Latest commit (9f6bd4f) — confirmed correct

The lint/SKILL.md fix at line 103 correctly rewrites the cross-skill reference as prose:

- go: walk each `go.mod` directory and run check/fix from there (a root `./...` invocation stops at a nested module boundary — see `/toolchain:check`'s per-ecosystem context file)

This matches the existing cross-skill-reference pattern in the file and eliminates the unresolvable skill-internal backtick path that CI flagged.


All prior findings — confirmed resolved

Finding Round Status
MUST-FIX: docs/topics/832-go-ecosystem/ not pruned claude-review 1 bba8dd6
CRITICAL: /* */ block-comment preamble defeated guard Codex P2 43f9233 (IN_BLOCK tracking)
P2: no -local grouping prefix Codex P2 43f9233 (go list -m derivation)
SUGGESTION: missing -- before $FILE security × 2 43f9233
P2: tidy-drift gate missed source-only drift Codex P2 43f9233 (*.go added to trigger-globs)
P2: lint/SKILL.md per-project walking list omitted go Codex P2 43f9233 (prose bullet added)
LOW: README described guard as "first non-blank line" claude-review 2 43f9233
LOW: tidy-drift remediation silent on Go 1.23+ floor claude-review 1 43f9233
COSMETIC: key ordering differed between files claude-review 1 43f9233
CI: unresolvable skill-internal path in lint/SKILL.md CI gate 9f6bd4f

Verdict

Ready to merge. The two remaining items are cosmetic (stale header comment) and theoretical (indented /* edge case with no real-world occurrence). Neither is a behavioral bug. All substantive findings from five review rounds are resolved.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 22s —— View job


Security Review — PR #910 (HEAD: 9f6bd4f)

Tasks:

  • Load tools
  • Read PR diff and identify changed surfaces
  • Read key changed files (go-format.sh, hook-utils.sh, go.yaml, hooks.json, plugin.json)
  • Audit injection surfaces, path traversal, credential handling, GitHub Actions patterns
  • Evaluate Codex P2: indented block comments before generated markers
  • Synthesize and post findings

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + supporting docs/catalog changes. No .github/workflows/ files appear in the changed-files list at HEAD 9f6bd4f.


No security issues found.

All prior security findings have been resolved:

  • Missing -- end-of-flags separator (raised by both prior security passes): fixed at go-format.sh:205"$GOIMPORTS_BIN" "${GOIMPORTS_ARGS[@]}" -- "$FILE".
  • Block-comment preamble bypass (Codex P2, raised on commit bba8dd6): fixed at go-format.sh:137–158IN_BLOCK tracking correctly handles /* */ spans in the generated-file guard.

On the new Codex P2 (commit bba8dd6): indented block comments

Finding: The guard at go-format.sh:153 matches [[ "$_line" == /\** ]] — a glob that only matches /* at column 0. A generated file whose block-comment license header is indented (e.g. /* Copyright ... */) would not be recognized as a comment continuation, causing the loop to break early and run goimports on the generated file.

Security verdict: Not a security issue. The consequence — goimports reformats a generated file — is a correctness/quality concern, not an exploitable security vulnerability. There is no code injection path, no privilege escalation, no path traversal, and no credential exposure. This is appropriately tracked as a correctness finding by the code review pass, not this security review.


Areas reviewed with no findings

Command injection (go-format.sh): $FILE, $GOIMPORTS_BIN, and $LOCAL_PREFIX are double-quoted throughout. LOCAL_PREFIX is derived from go list -m (Go module paths are constrained to [a-zA-Z0-9._/-] and carry no shell-special meaning), stored as a separate array element (GOIMPORTS_ARGS+=(-local "$LOCAL_PREFIX")), and expanded via "${GOIMPORTS_ARGS[@]}" — no injection surface. build_data_json uses jq -n --arg/--argjson exclusively. Goimports stderr is serialized through jq -R . | jq -s . before entering the findings array.

Path traversal (hook::read_file_path): Validates [[ -f "$file" ]] (regular file only, no FIFOs or devices), resolves symlinks via realpath/readlink -f, and enforces a path-segment-anchored prefix check against CLAUDE_PROJECT_DIR — sibling-directory traversal is prevented. The generated-file guard's read … <"$FILE" and the goimports -- "$FILE" invocation are both bounded by this validation.

hook::emit_channels / hook::json_escape: String concatenation path in hook::emit_channels is guarded by hook::json_escape (escapes \, ", \n, \r, \t, strips C0). The hook::ctx_flush path uses hook::emit_additional_context, which routes through jq -n --arg — completely safe regardless of content.

Telemetry sink execution (hook-utils.sh:455): "$sink" is consumer-configured in settings.json — consumer-controlled by design. Envelope piped to it is built by jq -n --arg/--argjson. Background subshell stdout/stderr redirected to /dev/null, preventing the sink from writing to the hook's additionalContext channel.

Session ID → marker files: Sanitized to [A-Za-z0-9_-] before use as a filename component. find … -type f -mtime +7 -delete scoped to ${CLAUDE_PLUGIN_DATA}/skip-notices.

go.yaml commands: All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings with no variable interpolation.

plugin.json userConfig: Single boolean field mirrored to CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED; no serialization or exec surface.

hooks.json: Hook command is "${CLAUDE_PLUGIN_ROOT}"/hooks/go-format.sh — references the plugin-root env var as documented; no injection surface in the manifest itself.

GitHub Actions: No .github/workflows/ files appear in the changed-files list. No pull_request_target, workflow_run, permissions widening, or ${{ github.* }} context injection to review.


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f6bd4f535

ℹ️ 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".

Comment thread plugins/toolchain/reference/ecosystems/go.yaml
Comment thread plugins/go-format/hooks/go-format.sh
Codex caught that comment-shape classification (// and /* checks)
matched only column-0, so a generated file with an indented block-
comment license header before the marker would defeat the guard —
same failure class as the two prior block-comment findings, just
indented. Fixed by trimming leading whitespace before classifying a
line as comment-continuation; the marker regex itself stays
column-0-anchored, matching Go's own convention exactly. New
regression case (5g) added, 44/44 passing.

Also corrects the top-of-file summary comment, which still described
the pre-fix "first non-blank line" behavior.

A third Codex finding (a marker literally nested inside an open /* */
span, e.g. "// Code generated..." as text within a block comment) was
evaluated and declined — no real Go generator or license-header tool
produces that shape.

A fourth finding (the ecosystem-commands `gates` schema array has no
execution path in check/SKILL.md or lint/SKILL.md, for any ecosystem)
is a real, pre-existing, cross-cutting toolchain-plugin gap unrelated
to Go specifically — filed as #926 rather than folded into this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

One more review round landed on `225fee6` (Codex, post-prune):

  • Indented block-comment license headers (a further variant of the block-comment finding) also defeated the guard — fixed, comment-shape classification now trims leading whitespace before matching. New regression case (5g), 44/44 passing.
  • A marker nested literally inside an open `/* */` span — evaluated and declined (no real Go generator or license-header tool produces that shape; unlike the other block-comment cases, this one requires deliberately mixing comment styles around the same marker).
  • The ecosystem-commands `gates` schema array has no execution path in `check/SKILL.md`/`lint/SKILL.md`, for any ecosystem, not just Go — a real, pre-existing, cross-cutting gap. Filed fix(toolchain): wire the ecosystem-commands gates schema array into an actual execution step #926 rather than folding a toolchain-plugin-wide fix into this PR; the `go-mod-tidy-drift` entry stays in `go.yaml` (schema-correct, matches the existing example-fixture precedent) and will activate once fix(toolchain): wire the ecosystem-commands gates schema array into an actual execution step #926 lands.

All threads replied to, reacted, and resolved. Waiting on this commit's CI before merge.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 19s —— View job


Claude finished @kyle-sexton's task ——


PR #910 Review — feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Status: Review complete ✓

Tasks:

  • Gather context from PR description, commit history, and prior review comments
  • Read HEAD commit (225fee6) changes — indented block-comment guard fix
  • Verify all prior findings remain addressed at HEAD
  • Inspect generated-file guard correctness at HEAD
  • Final synthesis and verdict

Context

This review covers HEAD 225fee6 — "fix(go-format): recognize indented comments in generated-file guard" — the latest commit pushed after the final review round flagged that an indented /* block-comment preamble would defeat the guard. All prior findings from seven review rounds have been addressed across the eight commits on this branch.


225fee6 change: indented block-comment guard fix

Correct. The prior commit (43f9233) added IN_BLOCK tracking for /* */ preambles but used [[ "$_line" == /\** ]] — a glob that only matches /* at column 0. An indented /* Copyright ... */ block caused premature break and missed the generated marker that followed.

The fix at 225fee6:

_trimmed="${_line#"${_line%%[![:space:]]*}"}"
...
if [[ "$_trimmed" == /\** ]]; then
    [[ "$_trimmed" == *'*/'* ]] || IN_BLOCK=1
    continue
fi

_trimmed strips all leading whitespace (spaces and tabs) via the standard ${var#"${var%%[![:space:]]*}"} idiom. This correctly handles:

  • Space-indented /*_trimmed = /* → enters block-tracking
  • Tab-indented \t/*_trimmed = /* → enters block-tracking
  • Column-0 /*_trimmed = /* (same as _line) → same path as before

Case 5g in the test suite exercises /*\n Copyright...\n */\n\n// Code generated ... and confirms the generated file is left untouched.

The guard logic trace for Case 5g:

  • /* → blank check passes → _trimmed = /*[[ "$_trimmed" == /\** ]] true → same-line close test: /* doesn't contain */IN_BLOCK=1 → continue
  • Copyright...IN_BLOCK=1*'*/'*? no → continue
  • */IN_BLOCK=1*'*/'*? yes → IN_BLOCK=0 → continue
  • `` (blank) → blank check → continue
  • // Code generated by protoc-gen-go. DO NOT EDIT.[[ "$_line" == //* ]] → regex matches → GENERATED=1 → break
  • emit_skipped → file left untouched ✓

Top-level header comment: The stale "first non-blank line" wording flagged at 9f6bd4f is now correct at HEAD (lines 22–26 read "leading comment/blank-line block (scanned below) contains that marker"). Confirmed fixed.


All prior findings — confirmed resolved at HEAD

Finding Round Fixed in
MUST-FIX: docs/topics/832-go-ecosystem/ not pruned claude-review 1 bba8dd6
CRITICAL: /* */ block-comment preamble defeats guard Codex P2 43f9233
P2: no -local grouping prefix Codex P2 43f9233
SUGGESTION: missing -- before $FILE security × 2 43f9233
P2: tidy-drift gate missed source-only drift Codex P2 43f9233
P2: lint/SKILL.md per-project walking list omitted go Codex P2 43f9233
LOW: README described guard as "first non-blank line" claude-review 2 43f9233
LOW: tidy-drift remediation silent on Go 1.23+ floor claude-review 1 43f9233
COSMETIC: key ordering between reference/example YAML claude-review 1 43f9233
CI: unresolvable skill-internal path in lint/SKILL.md CI gate 9f6bd4f
P2: indented /* block-comment preamble defeats guard Codex P2 225fee6
LOW: header comment still said "first non-blank line" claude-review 5 225fee6

No-action items


Verdict

Ready to merge. All blocking and substantive findings across seven prior review rounds are resolved. The generated-file guard now correctly handles all four real-world leading-block shapes: bare marker, //-style license-header preamble, column-0 /* */ block-comment preamble, and indented /* */ block-comment preamble. The 44-case test suite covers all shapes with regression cases. The implementation is clean.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 33s —— View job


Security Review — PR #910 (HEAD: 225fee6)

Tasks:

  • Load tools
  • Get PR diff and identify changed surfaces
  • Read go-format.sh at HEAD — primary injection/traversal surface
  • Audit indented block-comment guard fix (225fee6)
  • Audit all other changed files for new security surfaces
  • Synthesize and post findings

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + supporting docs/catalog changes. No .github/workflows/ files changed in this PR at HEAD 225fee6.


No security issues found.

All security findings raised across five prior review rounds are confirmed resolved. The latest commit (225fee6) introduces no new attack surface.


Security assessment of 225fee6 — indented block-comment guard

The change introduces _trimmed, derived by stripping leading whitespace from _line:

go-format.sh:150

_trimmed="${_line#"${_line%%[![:space:]]*}"}"

_trimmed is used exclusively in [[ ... ]] glob tests (== //*, == /\**, == *'*/'*) — no subprocess, no eval, no command substitution, no injection surface of any kind. Bash [[ ]] is not subject to word-splitting or glob expansion on the left-hand side.

For column-0 /* lines (previously matched by [[ "$_line" == /\** ]], now removed): when _line has no leading whitespace, _trimmed == _line, so the [[ "$_trimmed" == /\** ]] branch matches identically. The refactoring is semantically equivalent and introduces no behavioral or security regression.


All prior findings — confirmed resolved at HEAD

Finding Commit Status
Missing -- before $FILE in goimports invocation 43f9233 go-format.sh:213
/* */ block-comment preamble defeated generated-file guard 43f9233 IN_BLOCK tracking, go-format.sh:142–144
Indented ////* lines defeated guard (column-0 only match) 225fee6 _trimmed classification, go-format.sh:150–163

Areas reviewed with no findings

Command injection (go-format.sh): $FILE, $GOIMPORTS_BIN, and $LOCAL_PREFIX are double-quoted throughout. LOCAL_PREFIX (from go list -m) is stored as a separate GOIMPORTS_ARGS array element and expanded via "${GOIMPORTS_ARGS[@]}" — Go module path characters carry no shell-special meaning and the quoting prevents word-splitting regardless. build_data_json uses jq -n --arg/--argjson exclusively. Goimports stderr is serialized through jq -R . | jq -s . before entering the findings array.

go list -m-local pipeline: Executed in a subshell (cd "$(dirname "$FILE")" && go list -m) against the already-validated file's directory. Output stored in $LOCAL_PREFIX, quoted at use. command-line-arguments sentinel is explicitly filtered. Resolution failure degrades to LOCAL_PREFIX="".

Path traversal (hook::read_file_path): Validates [[ -f "$file" ]] (regular file, no FIFOs/devices), resolves symlinks via realpath/readlink -f, enforces a path-segment-anchored prefix check against CLAUDE_PROJECT_DIR. The generated-file guard's read <"$FILE" loop and goimports -- "$FILE" both operate on paths that have passed this validation.

_trimmed derivation in generated-file guard: Pure bash parameter expansion — ${_line%%[![:space:]]*} captures leading whitespace, ${_line#...} strips it. No subprocess, no eval. Safe regardless of _line content.

Telemetry sink execution (hook-utils.sh:455): "$sink" is a consumer-configured path from settings.json — consumer-controlled by design. Envelope built by jq -n --arg/--argjson. Background subshell stdout/stderr redirected to /dev/null.

Session ID → marker files: Sanitized to [A-Za-z0-9_-] before filename use. find … -type f -mtime +7 -delete scoped to ${CLAUDE_PLUGIN_DATA}/skip-notices.

go.yaml commands: All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings — no variable interpolation, no injection surface.

plugin.json userConfig: Single boolean field mirrored to CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED. No serialization or exec surface.

GitHub Actions: No .github/workflows/ files changed in this PR.


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 225fee627e

ℹ️ 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".

Comment thread plugins/toolchain/reference/ecosystems/go.yaml
Comment thread plugins/go-format/hooks/go-format.sh Outdated
The blank-line check (${_line// /}) only stripped literal spaces, so
a tab-only separator line between a comment header and the marker
fell through to the "first non-comment, non-blank line" break case,
defeating the guard — Codex caught this against a real ast.IsGenerated
comparison. Fixed: strips all [[:space:]], not just spaces. New
regression case (5h) added, 45/45 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 14s —— View job


PR #910 Review — feat(go): Go coverage — batch ecosystem entry + go-format hook plugin

Status: Review complete ✓

Tasks:

  • Load tools
  • Read go-format.sh at HEAD — verify tab-only blank-line fix (396587a)
  • Verify all prior findings remain addressed
  • Synthesize and post final review

Context

This review covers HEAD 396587a — "fix(go-format): treat tab-only lines as blank in generated-file guard" — addressing the final Codex P2 from the 225fee6 round. All prior substantive findings across seven review rounds were confirmed resolved in the 225fee6 review; the stale "first non-blank line" header comment noted as a residual cosmetic at that round was also corrected before this commit.


396587a — Tab-only blank-line fix: correct

The change is a one-line diff in the generated-file guard:

-  [[ -n "${_line// /}" ]] || continue # blank line: keep scanning the leading block
+  [[ -n "${_line//[[:space:]]/}" ]] || continue # blank line (any whitespace, incl. tabs): keep scanning the leading block

${_line// /} only removes literal space characters. A line consisting solely of a tab character (\t) produced a non-empty result and fell through to break, ending the leading-block scan prematurely — a // Code generated ... marker following a tab-only separator would not be caught, and goimports would silently rewrite the generated file.

${_line//[[:space:]]/} removes all POSIX whitespace (space, tab, carriage return, etc.). A tab-only line correctly collapses to empty, triggering || continue to keep scanning. The CRLF and BOM stripping earlier in the loop means \r-only lines are also handled correctly.

The fix is minimal and precise. Case 5h in the test suite (lines 271–282) exercises the exact shape (/*\nCopyright.\n*/\n\t\n// Code generated ...) and confirms the file is left untouched.

Header comment: go-format.sh lines 22–26 now correctly reads "leading comment/blank-line block (scanned below) contains that marker" — the stale "first non-blank line" wording from the prior cosmetic note is gone. ✅


All prior findings — confirmed resolved at HEAD

Finding Fixed in
MUST-FIX: docs/topics/832-go-ecosystem/ not pruned bba8dd6
CRITICAL: /* */ block-comment preamble defeated guard 43f9233
P2: no -local grouping prefix → re-collapsed import groups 43f9233
SUGGESTION: missing -- before $FILE (security) 43f9233
P2: tidy-drift gate missed source-only drift 43f9233
P2: lint/SKILL.md per-project walking list omitted go 43f9233
LOW: README guard wording said "first non-blank line" 43f9233
LOW: tidy-drift remediation silent on Go 1.23+ floor 43f9233
COSMETIC: key ordering between bundled/example YAML 43f9233
CI: unresolvable skill-internal ref in lint/SKILL.md 9f6bd4f
P2: indented /* block-comment preamble defeated guard 225fee6
COSMETIC: top-level header said "first non-blank line" 225fee6/396587a
Codex P2: tab-only separator line defeated guard 396587a

No-action items


Verdict

Ready to merge. The generated-file guard now correctly handles all real-world leading-block shapes: bare marker, //-style license-header preamble, column-0 /* */ block-comment preamble, indented /* */ block-comment preamble, and tab-only separator lines between any of the above. The 45-case test suite covers all shapes with regression cases. All substantive findings from eight prior review rounds are resolved.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 22s —— View job


Security Review — PR #910 (HEAD: 396587a)

Tasks:

  • Load tools
  • Get PR diff and identify changed surfaces at HEAD
  • Read go-format.sh at HEAD — primary injection/traversal surface
  • Audit 396587a change — tab-only blank-line classification
  • Audit all other changed files for new security surfaces
  • Synthesize and post findings

Scope: plugins/go-format/ (new hook plugin) + plugins/toolchain/reference/ecosystems/go.yaml (new ecosystem entry) + supporting docs/catalog changes. No .github/workflows/ files changed in this PR.


No security issues found.

All security findings raised across five prior review rounds are confirmed resolved at HEAD. The latest commit (396587a) introduces no new attack surface.


Assessment of 396587a — tab-only blank-line check

The change at go-format.sh:146:

# Before (225fee6):
[[ -n "${_line// /}" ]] || continue

# After (396587a):
[[ -n "${_line//[[:space:]]/}" ]] || continue

This is a pure bash parameter expansion — substituting [[:space:]] (all horizontal whitespace including tabs) for a literal space in the blank-line test. No subprocess, no eval, no command execution. No security implication.


All prior security findings — confirmed resolved at HEAD

Finding Fixed in Verified at HEAD
Missing -- before $FILE in goimports invocation 43f9233 go-format.sh:213: "$GOIMPORTS_BIN" "${GOIMPORTS_ARGS[@]}" -- "$FILE"
/* */ block-comment preamble bypassed generated-file guard 43f9233 IN_BLOCK tracking at go-format.sh:142–144
Indented ////* lines bypassed guard (column-0 match only) 225fee6 _trimmed whitespace-strip at go-format.sh:150
Tab-only separator line bypassed blank-line check 396587a go-format.sh:146: [[:space:]]

Areas reviewed with no findings

Command injection: $FILE, $GOIMPORTS_BIN, and $LOCAL_PREFIX are double-quoted throughout. LOCAL_PREFIX is derived from go list -m (Go module path characters are syntactically restricted and carry no shell-special meaning), stored as a separate array element (GOIMPORTS_ARGS+=(-local "$LOCAL_PREFIX")), and expanded via "${GOIMPORTS_ARGS[@]}". build_data_json uses jq -n --arg/--argjson exclusively. Goimports stderr is serialized through jq -R . | jq -s . before entering the findings array.

go list -m-local pipeline (go-format.sh:194–197): Executed in a subshell (cd "$(dirname "$FILE")" && go list -m) against the already-validated file's directory. dirname "$FILE" is safe given $FILE is validated. No output reaches a shell interpreter — stored in $LOCAL_PREFIX and quoted at use. command-line-arguments sentinel is explicitly filtered; resolution failure degrades to LOCAL_PREFIX="".

Generated-file guard (go-format.sh:137–166): Entirely pure bash string operations — [[ ... ]] tests, parameter expansions, =~ regex against a hard-coded pattern. No subprocess, no eval. The _trimmed derivation at line 150 (${_line#"${_line%%[![:space:]]*}"}) is textbook leading-whitespace strip with no subprocess. Safe regardless of _line content.

Path traversal (hook::read_file_path): Validates [[ -f "$file" ]] (regular file, not FIFO/device), resolves symlinks via hook::physical_path, and enforces a path-segment-anchored prefix check against CLAUDE_PROJECT_DIR. The guard's read <"$FILE" loop and goimports -- "$FILE" both operate on a path that has passed this validation.

Telemetry sink (hook-utils.sh:455): "$sink" is consumer-configured in settings.json — consumer-controlled by design. Envelope built entirely by jq -n --arg/--argjson. Background subshell stdout/stderr redirected to /dev/null.

Session ID / marker files: Sanitized to [A-Za-z0-9_-] before filename use. find … -type f -mtime +7 -delete scoped to ${CLAUDE_PLUGIN_DATA}/skip-notices.

go.yaml commands: All build-cmd, test-cmd, check-cmd, fix-cmd, and gates.cmd values are fixed strings — no variable interpolation, no injection surface.

plugin.json userConfig: Single boolean field mirrored to CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED. No serialization or exec surface.

hooks.json: Hook command is "${CLAUDE_PLUGIN_ROOT}"/hooks/go-format.sh — references the plugin-root env var as documented; no injection surface in the manifest itself.

GitHub Actions: No .github/workflows/ files changed in this PR.


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 396587aa9f

ℹ️ 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".

Comment thread plugins/go-format/hooks/go-format.sh
@kyle-sexton
kyle-sexton merged commit 8cd3e1d into main Jul 22, 2026
24 checks passed
@kyle-sexton
kyle-sexton deleted the feat/832-go-ecosystem branch July 22, 2026 01:18
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
…Generated gaps (#937)

## Summary

`plugins/go-format/hooks/go-format.sh`'s generated-file guard is a bash
string-match approximation of Go's own `ast.IsGenerated` (a parsed-AST
classifier via `go/parser`). Across PR #910's review, an automated Codex
pass
progressively surfaced — and each time got a real fix for — a bare
marker, a
`//`-header before the marker, a `/* */` block-comment header, an
*indented*
block-comment header, and a tab-only blank separator (now covered by
regression
cases 5–5h in `go-format.test.sh`).

That is the expected shape of chasing string-match parity with an
AST-based
detector: the gap is **structural, not a single fixable bug**, so
further narrow
edge cases can keep surfacing indefinitely. The guard is advisory only —
the
hook never blocks an edit; the worst case for an unhandled shape is
goimports
reformatting a generated file, visible in the diff, no gate broken. This
PR
documents that deliberate scope boundary at the guard's location so the
known-limitation is explicit rather than implicit, and re-litigating the
tradeoff isn't needed each time a bot or human surfaces a new exotic
shape.

## Fix

Documentation-only. No behavior change, no logic change, no test change.

- **`plugins/go-format/hooks/go-format.sh`** — extended the existing
`GENERATED-FILE GUARD` inline comment block (at the guard's scan logic)
with
an `APPROXIMATION:` note capturing: (1) the scan is a deliberate
string-match
stand-in for `ast.IsGenerated`, which can't be made equal by adding
patterns
because the gap is structural; (2) that's tolerable because the guard is
advisory (worst case = a generated file gets reformatted, visible in the
diff); (3) no further pattern patches unless a **real-world** generated
file
is observed defeating it (a synthetic counter-example is not enough);
(4) the
  actual structural fix — a `go`-toolchain shell-out (`go/parser` + real
`ast.IsGenerated`) — is deliberately not taken, to avoid per-edit
latency and
  a hard `go` dependency this hook otherwise keeps optional. Placed as a
maintainer-facing code comment (single home) rather than duplicated into
the
user-facing README, whose "Skips generated files" bullet already covers
the
behavior. The comment references no issue/PR number — it stands on its
own.
- **`plugins/go-format/CHANGELOG.md`** — new `## [0.1.1]` `### Added`
entry.
- **`plugins/go-format/.claude-plugin/plugin.json`** — patch bump
`0.1.0` → `0.1.1`.

## Verification

Docs-only: verification is "no code/behavior change; existing suite
still
passes." Ran the full go-format hook test suite (a real `goimports`
v0.48.0 on
PATH) before and after the comment edit:

```
$ GOIMPORTS_TEST_BIN="$HOME/go/bin/goimports.exe" bash plugins/go-format/hooks/go-format.test.sh
...
PASS=45 FAIL=0
```

`shellcheck plugins/go-format/hooks/go-format.sh` → clean.

Closes #927

## Related

- #910 — the PR whose review (a progressive Codex edge-case pass)
produced this
  issue and the 5a–5h regression cases.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
#867)

## Summary

Adds `lychee-offline` to the `toolchain` plugin's bundled
`cross-cutting.yaml` ecosystem
default, alongside the existing `typos`/`gitleaks`/editorconfig-checker
tools — on-disk
link/anchor integrity checking with no network dependency.

## Fix

- `reference/ecosystems/cross-cutting.yaml`: `check-cmd` gains
`lychee --offline --no-progress './**/*.md'` (`--offline` = "Only check
local files and
block network requests" per lychee's own CLI help — external URLs are
skipped, only local
file and fragment targets are verified); `opt-in` and `install-hint`
documented following
the same per-tool-config pattern as the existing tools (optional
`lychee.toml` at repo
  root customizes the ruleset; absent means lychee's own defaults).
- `skills/setup/SKILL.md`: cross-cutting inference-candidate line
mentions `lychee-offline`,
  and all four cross-cutting tool names backtick-wrapped consistently.
- `skills/lint/evals/evals.json`: eval 3's `expected_output` updated to
include
  `lychee-offline` in the enumerated cross-cutting tool set.
- `plugins/toolchain/.claude-plugin/plugin.json` + `CHANGELOG.md`:
version bump.

**Version note**: sibling PR #859 (issue #834) landed first and took the
intervening
`0.6.0`/`0.7.0` versions; PR #910 (issue #832) then landed `0.8.0`.
Rebased onto current
`main` and renumbered to `0.9.0`, sequential CHANGELOG history intact.
Collision with #859
is resolved (merged); `do-not-merge` label removed.

## Verification

- Confirmed `--offline` and `--no-progress` are real lychee CLI flags,
and the winget
package id (`lycheeverse.lychee`) / brew formula (`lychee`) are correct,
against lychee's
own upstream README (`lycheeverse/lychee`, `master` branch, Installation
and Commandline
  usage sections).
- Smoke-tested the actual `check-cmd` (`lychee --offline --no-progress
'./**/*.md'`) against
  this repo: 0 errors across 2645 links (1079 unique, 1056 excluded).
- `evals.json` still parses as valid JSON; structural shape (required
`id`/`prompt` fields)
  unchanged — only `expected_output` prose edited.
- `skills/setup/SKILL.md` re-checked against the 500-line skill-quality
hard cap.
- Rebased onto current `origin/main`; `claude plugin validate --strict`,
`node scripts/validate-plugin-contracts.mjs`, and full `bash
scripts/run-plugin-tests.sh`
  all green.

Closes #833

## Related

- Epic: #830
- Brief item 3, `docs/topics/lint-static-analysis-gaps/PLAN.md`
- Sibling PR on the same plugin: #859 (issue #834, pyright) — merged,
serialization resolved

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 25, 2026
#926) (#1020)

## Summary

`docs/conventions/ecosystem-commands/ecosystem.schema.json` defines a
structured `gates` array (`name`/`cmd`/`trigger-globs`/`remediation`)
that any ecosystem config — bundled default or consumer-tracked — can
declare, and both `plugins/toolchain/reference/ecosystems/go.yaml`
(`go-mod-tidy-drift`) and
`docs/conventions/ecosystem-commands/examples/dotnet.yaml`
(`nuget-lockfile-drift`) ship one. The array was resolved as part of
each ecosystem's command surface (`check/SKILL.md` §1.5) but **never
invoked by any workflow step** — so every declared gate was inert. The
only gate-execution mechanism `check/SKILL.md` documented was a prose,
consumer-CLAUDE.md-only step whose closing sentence ("this plugin ships
none of its own") was itself stale once `go.yaml` shipped a bundled
gate.

## Fix

Scoped to `plugins/toolchain/skills/check/SKILL.md`, precise and
additive (no restructuring of unrelated parts):

- **New structured gate-execution step** in §2 "Run checks", replacing
the stale prose step at the old `:121`. After an affected ecosystem's
build → test → lint, iterate its resolved `gates` array (§1.5 ladder —
bundled default or consumer file). Per gate:
- **Fire condition** — `trigger-globs` present → run only when ≥1
changed file matches, against the **full** changed-files set (a gate's
trigger files need not classify into the ecosystem's own `globs`);
omitted → run whenever the ecosystem runs; present-but-no-match (e.g.
`check all` on a clean tree) → does not fire.
- **Independent of the build/test/lint short-circuit** — a fired gate
runs even when an earlier phase failed and stopped (a lockfile / `go mod
tidy` gate is meaningful regardless of build success).
- **Tool presence** — missing tool → `skip` with the ecosystem's
`install-hint`, never `FAIL`.
- **Outcome** — `pass`/`FAIL` by name; on `FAIL` surface
`gate.remediation`; a fired-and-failed gate counts toward the run's FAIL
verdict.
- **Overall verdict** now covers gate-only failures — a failed fired
gate flips Overall to `FAIL` and is counted even when every ecosystem's
build/test/lint cell passed (e.g. `Overall: FAIL (0 of 2 ecosystems
failed, 1 gate failed)`).
- **Stale sentence removed** — the "this plugin ships none of its own"
framing is replaced with an accurate ladder pointer (bundled default
e.g. `go.yaml`'s `go-mod-tidy-drift`; consumer-declared in
`.claude/ecosystems/<ecosystem>.yaml`).
- **Cross-reference reconciled** at the old `:119` ("CI-parity gates …
run independent of `check-cmd`") and the report scaffold (`:140`).

**`lint/SKILL.md` deliberately unchanged** (the one maintainer-vetoable
fork, per the planning comment): every schema-described gate and both
shipped examples are CI-parity checks beyond build/test/lint (lockfile
drift, module drift, generated-artifact freshness) — none is a
lint/format op. They belong with the heavier `check` step and
`/verification:confirm` (the sanctioned pre-commit gate); adding them to
`/toolchain:lint` would break its "fast" contract. Veto before merge if
a maintainer wants gates on the lint path.

**Version bump (rule 6d):** fresh `origin/main` had `plugins/toolchain`
at `0.9.0` (claimed by the lychee PR #867); this is a new executable
behavior, so bumped to `0.10.0` with a matching `## [0.10.0]` → `###
Added` CHANGELOG entry. (The planning comment's `0.8.0 → 0.9.0` predated
#867 landing on main.)

## Verification

No skill-behavior test harness exists (`find plugins/toolchain -iname
'*test*'` and a repo-level search for `toolchain:check`/`gates` tests
both empty), so verification is static gate + hand-trace:

- **`/skill-quality:check` on the edited `check` skill:** PASS — 0
errors, markdownlint clean, 165/500 lines, all base-ref trigger phrases
preserved (frontmatter `description` untouched). The lone WARN
(description lacks `Use when:` phrasing) is pre-existing and out of
scope.
- **Hand-trace — bundled default (`go.yaml`, a real rung-4 default that
runs for this repo):** a change set including `go.sum` (or any `*.go`)
makes `go` affected; §1.5 resolves `go.yaml`; the new §2 step reaches
its `gates` and matches `go-mod-tidy-drift`'s `trigger-globs:
["go.mod","go.sum","*.go"]` against the full changed-files set → gate
fires → runs `go mod tidy -diff` from `$REPO_ROOT`. Drift → `FAIL` +
remediation + Overall FAIL; clean → `pass`; missing Go toolchain →
`skip` with `install-hint`.
- **Hand-trace — consumer-declared shape (`examples/dotnet.yaml`, an
example fixture under `docs/`, not a bundled default):** for a consumer
whose tracked `.claude/ecosystems/dotnet.yaml` carries the
`nuget-lockfile-drift` gate, a changed `*.csproj` matches
`trigger-globs:
["*.csproj","Directory.Packages.props","packages.lock.json"]` → gate
fires → runs `dotnet restore --locked-mode`. Same pass/FAIL/skip
semantics.

Both gate entries are invoked by the new step given a matching
changed-files set; neither required a per-ecosystem change.

Closes #926

## Related

- PR #910 (Go ecosystem) — **merged** `2026-07-22`; added `go.yaml`'s
`go-mod-tidy-drift` gate whose Codex review surfaced this wiring gap.
This change is the cross-cutting follow-up that makes that gate (and
every other declared gate) actually run — additive, no `go.yaml` change.
- Planning-subagent comment on #926 (`2026-07-22T03:10:48Z`) — the
primary implementation plan followed here; the only deviation is the
version target (`0.10.0`, re-derived from fresh `origin/main` per rule
6d, since `0.9.0` was taken by #867 after the plan was written).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019xSPACJvbsqGsjYCP91DoX

Work-class: C3 (bug-fix-shaped) — attended triage 2026-07-23,
operator-ratified. 🤖

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(go): Go coverage — batch ecosystem entry + format hook plugin

1 participant