[object Object]

← back to Cli Printing Press

fix(cli): add scoped govulncheck gates (#699)

ac75b83ccb7804433579d163d0798774c1c7051b · 2026-05-07 22:51:53 -0700 · Trevin Chow

Files touched

Diff

commit ac75b83ccb7804433579d163d0798774c1c7051b
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Thu May 7 22:51:53 2026 -0700

    fix(cli): add scoped govulncheck gates (#699)
---
 AGENTS.md                                          |  3 +-
 README.md                                          | 14 ++-----
 docs/GLOSSARY.md                                   |  2 +-
 docs/PIPELINE.md                                   |  4 +-
 go.mod                                             |  4 +-
 internal/cli/publish.go                            | 40 +++++++++++++++-----
 internal/cli/publish_test.go                       | 43 +++++++++++++++++++++-
 internal/cli/verify_skill.go                       | 18 +--------
 internal/cli/verify_skill_test.go                  | 18 ++++-----
 internal/generator/generator_test.go               |  6 +--
 internal/generator/install_section.go              | 19 +++-------
 internal/generator/install_section_test.go         |  6 +--
 internal/generator/templates/go.mod.tmpl           |  4 +-
 internal/generator/templates/readme.md.tmpl        |  2 +-
 internal/generator/templates/skill.md.tmpl         |  2 +-
 internal/generator/validate.go                     |  8 ++++
 internal/generator/validate_test.go                | 38 ++++++++++++++++++-
 internal/govulncheck/govulncheck.go                | 14 +++++++
 internal/govulncheck/govulncheck_test.go           | 21 +++++++++++
 internal/pipeline/fullrun.go                       |  8 ++--
 internal/pipeline/fullrun_test.go                  | 20 +++++++++-
 internal/pipeline/planner.go                       |  2 +-
 internal/pipeline/planner_artifacts_test.go        |  2 +
 internal/pipeline/seeds.go                         |  4 +-
 internal/pipeline/seeds_test.go                    |  5 +++
 skills/printing-press-catalog/SKILL.md             |  2 +-
 skills/printing-press-publish/SKILL.md             | 20 +++++++++-
 skills/printing-press-score/SKILL.md               |  2 +-
 skills/printing-press/SKILL.md                     | 21 +++++------
 skills/printing-press/references/setup-checks.md   |  6 +--
 .../printing-press-rich-auth/README.md             |  2 +-
 .../printing-press-rich-auth/SKILL.md              |  2 +-
 .../printing-press-golden/README.md                |  2 +-
 .../printing-press-golden/SKILL.md                 |  2 +-
 .../printing-press-golden/go.mod                   |  2 +-
 .../public-param-golden/README.md                  |  2 +-
 .../public-param-golden/SKILL.md                   |  2 +-
 37 files changed, 259 insertions(+), 113 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index c27ea8f5..3edabac0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -147,7 +147,8 @@ Add tests for new non-trivial logic. Match the package's existing style (typical
 Run `go test ./...` before considering your work done.
 
 ## Quality Gates
-Generated CLIs must pass 7 gates: `go mod tidy`, `go vet`, `go build`, binary build, `--help`, `version`, and `doctor`.
+Generated CLIs must pass 8 gates: `go mod tidy`, `govulncheck`, `go vet`, `go build`, binary build, `--help`, `version`, and `doctor`.
+Run `govulncheck` in default mode only, scoped to the generated or publishing CLI module (`./...` from that CLI directory). Do not use `-show verbose` or a whole public-library scan as a blocking gate; the public library is a historical collection, so its blocking CI should scan only added or changed CLI modules and leave whole-library sweeps to scheduled/reporting workflows.
 
 ## Local Artifacts
 Generated artifacts live under `~/printing-press/`, not in this repo: `library/<api-slug>/`, `manuscripts/<api-slug>/`, and `.runstate/<scope>/`. The API slug is derived by the generator from the spec title (`cleanSpecName`), and the binary name is `<api-slug>-pp-cli`. Never hardcode an API slug when the generator can derive it. See [`docs/ARTIFACTS.md`](docs/ARTIFACTS.md) for local-vs-public flow and divergence rules.
diff --git a/README.md b/README.md
index 1b64da6f..19fda7b1 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ You need both the **binary** and the **Claude Code skills**. The skills (`/print
 
 The binary alone works (research, generation, verification, scoring) but skips the curated agent loop. The skills alone have nothing to call. Install both.
 
-**Prerequisites:** [Go 1.26+](https://go.dev/dl/) and [Claude Code](https://claude.ai/code). The skills are tested with Claude Code; other harnesses like Codex may work but aren't tested. **Use Claude Code for the best experience.**
+**Prerequisites:** [Go 1.26.3 or newer](https://go.dev/dl/) and [Claude Code](https://claude.ai/code). The skills are tested with Claude Code; other harnesses like Codex may work but aren't tested. **Use Claude Code for the best experience.**
 
 ### 1. Install the binary
 
@@ -32,7 +32,7 @@ The binary alone works (research, generation, verification, scoring) but skips t
 go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
 ```
 
-Verify with `printing-press --version`. If `go install` fails with `410 Gone` or an auth error, see [Troubleshooting](#troubleshooting).
+Verify with `printing-press --version`. If `go install` fails, confirm Go 1.26.3 or newer is installed and `$GOPATH/bin` is on your `PATH`.
 
 ### 2. Install the skills — pick one path
 
@@ -463,14 +463,6 @@ Each published CLI ships a research manuscript, verification proofs, and a `.pri
 
 ## Troubleshooting
 
-**`go install` returns `410 Gone` or an auth error.** The repo is currently private. Set `GOPRIVATE` so Go fetches directly from GitHub instead of the public module proxy, and ensure you have GitHub access (SSH key or `gh auth login`):
-
-```bash
-GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
-```
-
-After the public launch, the plain command works for everyone.
-
 **`/printing-press` slash command doesn't appear in Claude Code.** Restart your Claude Code session after installing the skills. If you cloned the repo, confirm `claude --plugin-dir .` was run from the cloned repo root. If you installed skills only, run `gh skill list` (or `npx skills list`) to verify the install.
 
 **`printing-press: command not found` after a successful `go install`.** `$GOPATH/bin` (default `~/go/bin`) isn't on your `PATH`. Add it to your shell profile.
@@ -483,7 +475,7 @@ After the public launch, the plain command works for everyone.
 
 ## Limitations
 
-- **Requires Go 1.26+ and Claude Code.** No standalone distribution today; the slash command is the supported entry point.
+- **Requires Go 1.26.3 or newer and Claude Code.** No standalone distribution today; the slash command is the supported entry point.
 - **Generated CLIs are domain-shaped, not vendor-replacements.** A `<api>-pp-cli` covers the agent power-user surface, not every back-office knob a vendor's official CLI ships.
 - **Browser-sniff requires manual capture.** You point a browser at the site (or import a HAR); the press doesn't crawl autonomously.
 - **Live verify is read-only.** Phase 5 runs GET only and never mutates. Real write-path coverage lives in unit tests and the dogfood structural checks.
diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md
index 802f5973..c6f588da 100644
--- a/docs/GLOSSARY.md
+++ b/docs/GLOSSARY.md
@@ -29,7 +29,7 @@ Default disambiguation conventions:
 | **emboss** | A second-pass improvement cycle for an already-printed CLI. Audits baseline, re-researches, identifies top improvements, rebuilds, re-verifies, reports delta. Subcommand: `printing-press emboss <api>`. Still active — not deprecated. |
 | **polish** | Targeted fix-up of a printed CLI (distinct from emboss's full cycle). Skill: `/printing-press-polish`. The retro improves the machine; polish improves the printed CLI. |
 | **retro** / **retrospective** | Post-generation analysis of *the machine itself* — not the printed CLI. Identifies systemic improvements to templates, the Go binary, skill instructions, or catalog. Output goes to `docs/retros/` and `manuscripts/<api>/<run>/proofs/`. |
-| **quality gates** | 7 mechanical static checks every printed CLI must pass: go mod tidy, go vet, go build, binary build, `--help`, version, doctor. These are build-time checks — see **verify** for runtime testing. |
+| **quality gates** | 8 mechanical static checks every printed CLI must pass: go mod tidy, default-mode govulncheck, go vet, go build, binary build, `--help`, version, doctor. These are build-time checks — see **verify** for runtime testing. |
 | **verify** | Runtime behavioral testing of a printed CLI — runs every command against the real API (read-only) or a mock server. Produces PASS/WARN/FAIL verdicts. Has `--fix` mode for auto-patching. Distinct from quality gates (static) and dogfood (structural). |
 | **dogfood** | Generation-time structural validation of a printed CLI against its source spec. Catches dead flags, invalid API paths, auth mismatches, and MCP surface parity drift. Subcommand: `printing-press dogfood`. Compare with **doctor** (shipped in the CLI for end-users) and **verify** (runtime behavioral). |
 | **cliutil** | The generator-owned Go package emitted into every printed CLI at `internal/cliutil/`. Houses shared helpers meant for agent-authored novel code to import: `cliutil.FanoutRun` for aggregation commands (per-source error collection, bounded concurrency, source-order output), `cliutil.CleanText` for HTML/JSON-LD text normalization, `cliutil.IsVerifyEnv()` for the side-effect short-circuit (see **side-effect command convention**). **Generator-reserved namespace** — agents authoring novel code in Phase 3 must not put their code in `internal/cliutil/` or name their own helpers that collide with cliutil's exports. |
diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md
index 35d7fbc4..55835f3b 100644
--- a/docs/PIPELINE.md
+++ b/docs/PIPELINE.md
@@ -107,7 +107,7 @@ Freshness ownership:
 - Freshness metadata belongs in the existing JSON provenance envelope at `meta.freshness`. It describes current-cache freshness for the covered path only; it must not be described as full historical backfill or API-specific enrichment.
 
 Gates:
-- All seven generator quality gates pass: `go mod tidy`, `go vet`, `go build`, binary build, `--help`, version, `doctor`
+- All eight generator quality gates pass: `go mod tidy`, default-mode `govulncheck`, `go vet`, `go build`, binary build, `--help`, version, `doctor`
 
 Artifacts:
 - Full CLI source tree in the output directory
@@ -146,7 +146,7 @@ Outputs:
 
 Gates:
 - Overlay merge completes without conflicts
-- All seven generator quality gates pass again after regeneration
+- All eight generator quality gates pass again after regeneration, including default-mode `govulncheck`
 
 Artifacts:
 - Merged spec (format follows the source spec)
diff --git a/go.mod b/go.mod
index ac886e31..36c2ff64 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/mvanhorn/cli-printing-press/v4
 
-go 1.26.1
+go 1.26.3
 
 require (
 	github.com/dave/dst v0.27.4
@@ -11,6 +11,7 @@ require (
 	github.com/stretchr/testify v1.11.1
 	golang.org/x/mod v0.35.0
 	golang.org/x/sync v0.20.0
+	golang.org/x/sys v0.43.0
 	golang.org/x/text v0.36.0
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -54,6 +55,5 @@ require (
 	go.uber.org/mock v0.6.0 // indirect
 	golang.org/x/crypto v0.50.0 // indirect
 	golang.org/x/net v0.53.0 // indirect
-	golang.org/x/sys v0.43.0 // indirect
 	golang.org/x/tools v0.44.0 // indirect
 )
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 18e671a4..cfc40deb 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	catalogpkg "github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+	"github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
 	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
 	"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
 	"github.com/spf13/cobra"
@@ -19,6 +20,7 @@ import (
 
 const (
 	goCommandTimeout   = 2 * time.Minute
+	vulnCheckTimeout   = 5 * time.Minute
 	binaryCheckTimeout = 15 * time.Second
 )
 
@@ -546,21 +548,31 @@ func runValidation(dir string) ValidateResult {
 	}
 	result.Checks = append(result.Checks, tidyCheck)
 
-	// 4. go vet check
+	// 4. govulncheck catches reachable vulnerable code in this one CLI module
+	// before publish. Keep this scoped to dir; the public library may contain
+	// stale historical CLIs, and whole-library vulnerability enforcement
+	// belongs in changed-module CI or a report-only scheduled sweep.
+	vulnCheck := runGoVulnCheck(dir)
+	if !vulnCheck.Passed {
+		allPassed = false
+	}
+	result.Checks = append(result.Checks, vulnCheck)
+
+	// 5. go vet check
 	vetCheck := runGoCheck(dir, "vet", "./...")
 	if !vetCheck.Passed {
 		allPassed = false
 	}
 	result.Checks = append(result.Checks, vetCheck)
 
-	// 5. go build check
+	// 6. go build check
 	buildCheck := runGoCheck(dir, "build", "./...")
 	if !buildCheck.Passed {
 		allPassed = false
 	}
 	result.Checks = append(result.Checks, buildCheck)
 
-	// 6. --help / --version checks use a dedicated temp binary so validation
+	// 7. --help / --version checks use a dedicated temp binary so validation
 	// exercises current source without depending on or mutating source-tree artifacts.
 	binaryPath, cleanupBinary, err := buildValidationBinary(dir, cliName)
 	if cleanupBinary != nil {
@@ -588,7 +600,7 @@ func runValidation(dir string) ValidateResult {
 			result.HelpOutput = string(helpOut)
 		}
 
-		// 7. --version check
+		// 8. --version check
 		verCtx, verCancel := context.WithTimeout(context.Background(), binaryCheckTimeout)
 		defer verCancel()
 		versionCmd := exec.CommandContext(verCtx, binaryPath, "--version")
@@ -605,7 +617,7 @@ func runValidation(dir string) ValidateResult {
 		}
 	}
 
-	// 8. SKILL.md verification — fail if the agent-facing skill advertises
+	// 9. SKILL.md verification — fail if the agent-facing skill advertises
 	// commands, flags, or arguments the shipped CLI source does not provide.
 	skillCheck := checkVerifySkill(dir)
 	if !skillCheck.Passed {
@@ -613,7 +625,7 @@ func runValidation(dir string) ValidateResult {
 	}
 	result.Checks = append(result.Checks, skillCheck)
 
-	// 9. Manuscripts check (warn-only)
+	// 10. Manuscripts check (warn-only)
 	// Try CLI name first (new convention), then API name, then fuzzy resolve
 	apiName := result.APIName
 	if apiName == "" {
@@ -701,8 +713,11 @@ func checkVerifySkill(dir string) CheckResult {
 }
 
 func runGoCheck(dir string, args ...string) CheckResult {
-	name := "go " + args[0]
-	ctx, cancel := context.WithTimeout(context.Background(), goCommandTimeout)
+	return runGoCommandCheck(dir, "go "+args[0], goCommandTimeout, args...)
+}
+
+func runGoCommandCheck(dir, name string, timeout time.Duration, args ...string) CheckResult {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 	cmd := exec.CommandContext(ctx, "go", args...)
 	cmd.Dir = dir
@@ -710,7 +725,7 @@ func runGoCheck(dir string, args ...string) CheckResult {
 	if err != nil {
 		errMsg := strings.TrimSpace(string(output))
 		if ctx.Err() == context.DeadlineExceeded {
-			errMsg = fmt.Sprintf("timed out after %s", goCommandTimeout)
+			errMsg = fmt.Sprintf("timed out after %s", timeout)
 		} else if errMsg == "" {
 			errMsg = err.Error()
 		}
@@ -719,6 +734,13 @@ func runGoCheck(dir string, args ...string) CheckResult {
 	return CheckResult{Name: name, Passed: true}
 }
 
+func runGoVulnCheck(dir string) CheckResult {
+	if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil {
+		return CheckResult{Name: govulncheck.Name, Passed: false, Error: "go.mod not found"}
+	}
+	return runGoCommandCheck(dir, govulncheck.Name, vulnCheckTimeout, govulncheck.GoRunArgs("./...")...)
+}
+
 func checkGoModTidy(dir string) CheckResult {
 	modPath := filepath.Join(dir, "go.mod")
 	sumPath := filepath.Join(dir, "go.sum")
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 04c787a3..b97bdc08 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -4,9 +4,11 @@ import (
 	"encoding/json"
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
+	"github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
 	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
 	"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
 	"github.com/stretchr/testify/assert"
@@ -151,7 +153,7 @@ func TestPublishValidateJSONHasAllChecks(t *testing.T) {
 	}
 
 	// All checks should be present (they may fail in test env, but must exist)
-	expectedChecks := []string{"manifest", "transcendence", "phase5", "go mod tidy", "go vet", "go build", "--help", "--version", "verify-skill", "manuscripts"}
+	expectedChecks := []string{"manifest", "transcendence", "phase5", "go mod tidy", "govulncheck", "go vet", "go build", "--help", "--version", "verify-skill", "manuscripts"}
 	for _, name := range expectedChecks {
 		assert.True(t, checkNames[name], "should have %q check", name)
 	}
@@ -244,6 +246,43 @@ func TestPublishValidateExitCode(t *testing.T) {
 	assert.Equal(t, ExitPublishError, exitErr.Code, "should use ExitPublishError exit code")
 }
 
+func TestRunGoVulnCheckRequiresGoMod(t *testing.T) {
+	result := runGoVulnCheck(t.TempDir())
+	assert.False(t, result.Passed)
+	assert.Equal(t, govulncheck.Name, result.Name)
+	assert.Equal(t, "go.mod not found", result.Error)
+}
+
+func TestRunGoVulnCheckUsesPinnedDefaultCommand(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("fake shell go binary is Unix-only")
+	}
+	dir := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n\ngo 1.26.3\n"), 0o644))
+
+	fakeBin := t.TempDir()
+	callsPath := filepath.Join(t.TempDir(), "go-calls.txt")
+	fakeGo := filepath.Join(fakeBin, "go")
+	require.NoError(t, os.WriteFile(fakeGo, []byte(`#!/bin/sh
+printf '%s\n' "$*" >> "$FAKE_GO_CALLS"
+echo "fake govulncheck failure" >&2
+exit 42
+`), 0o755))
+	t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"))
+	t.Setenv("FAKE_GO_CALLS", callsPath)
+
+	result := runGoVulnCheck(dir)
+	assert.False(t, result.Passed)
+	assert.Equal(t, govulncheck.Name, result.Name)
+	assert.Contains(t, result.Error, "fake govulncheck failure")
+
+	calls, err := os.ReadFile(callsPath)
+	require.NoError(t, err)
+	assert.Equal(t, "run "+govulncheck.ToolModule+" ./...\n", string(calls))
+	assert.NotContains(t, string(calls), "-show")
+	assert.NotContains(t, string(calls), "verbose")
+}
+
 func TestPublishPackageMissingDirFlag(t *testing.T) {
 	cmd := newPublishCmd()
 	cmd.SetArgs([]string{"package", "--json"})
@@ -855,7 +894,7 @@ func newInsightCmd() *cobra.Command {
 	return &cobra.Command{Use: "insight", Short: "Show test insight"}
 }
 `), 0o644))
-	skillInstall := generator.CanonicalSkillInstallSection("test", "", false)
+	skillInstall := generator.CanonicalSkillInstallSection("test", "")
 	require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("# Test CLI\n\n"+skillInstall+"\n## Command Reference\n\n- `test-pp-cli insight` — Show test insight\n\n## Usage\n\n```bash\ntest-pp-cli insight --agent\n```\n"), 0o644))
 
 	writeTestManifest(t, dir, pipeline.CLIManifest{
diff --git a/internal/cli/verify_skill.go b/internal/cli/verify_skill.go
index d61d28f5..1f26e771 100644
--- a/internal/cli/verify_skill.go
+++ b/internal/cli/verify_skill.go
@@ -8,7 +8,6 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"regexp"
 	"strings"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
@@ -116,9 +115,6 @@ type pythonReport struct {
 	Findings       []canonicalFinding `json:"findings"`
 }
 
-// goModVersionRE matches the `go X.Y` directive at the top of go.mod.
-var goModVersionRE = regexp.MustCompile(`(?m)^go\s+(\d+)\.(\d+)`)
-
 // runCanonicalSectionsCheck verifies that the install/prerequisites section
 // of dir/SKILL.md matches what the generator would emit for this CLI today.
 // Detects post-publish edits to a generator-owned section (the failure mode
@@ -140,19 +136,9 @@ func runCanonicalSectionsCheck(dir string) (finding canonicalFinding, hasFinding
 		return canonicalFinding{}, false, true, nil
 	}
 
-	goModBytes, gErr := os.ReadFile(filepath.Join(dir, "go.mod"))
-	if gErr != nil {
+	if _, gErr := os.Stat(filepath.Join(dir, "go.mod")); gErr != nil {
 		return canonicalFinding{}, false, true, nil
 	}
-	usesBrowserHTTP := false
-	if m := goModVersionRE.FindSubmatch(goModBytes); m != nil {
-		// browser-HTTP transport raises the floor to 1.25; treat any
-		// 1.25+ go directive as the browser-HTTP signal. CLIs not on
-		// browser-HTTP stay on 1.23.
-		if string(m[1]) == "1" && string(m[2]) >= "25" {
-			usesBrowserHTTP = true
-		}
-	}
 
 	skillBytes, sErr := os.ReadFile(filepath.Join(dir, "SKILL.md"))
 	if sErr != nil {
@@ -160,7 +146,7 @@ func runCanonicalSectionsCheck(dir string) (finding canonicalFinding, hasFinding
 	}
 	skill := string(skillBytes)
 
-	expected := generator.CanonicalSkillInstallSection(name, manifest.Category, usesBrowserHTTP)
+	expected := generator.CanonicalSkillInstallSection(name, manifest.Category)
 	got, ok := ExtractInstallSectionForTest(skill)
 	if !ok {
 		return canonicalFinding{
diff --git a/internal/cli/verify_skill_test.go b/internal/cli/verify_skill_test.go
index 72bfe811..2e09876e 100644
--- a/internal/cli/verify_skill_test.go
+++ b/internal/cli/verify_skill_test.go
@@ -332,7 +332,7 @@ func Execute() error {
 func TestVerifySkill_CanonicalSectionsPassesOnFreshFixture(t *testing.T) {
 	t.Parallel()
 	bin := buildPrintingPressBinary(t)
-	dir := writeCanonicalFixture(t, "myapi", "productivity", false, "")
+	dir := writeCanonicalFixture(t, "myapi", "productivity", "")
 	out, err := exec.Command(bin, "verify-skill", "--dir", dir, "--only", "canonical-sections").CombinedOutput()
 	require.NoError(t, err, "fresh fixture must pass canonical-sections: %s", string(out))
 	require.Contains(t, string(out), "canonical-sections passed")
@@ -347,10 +347,10 @@ func TestVerifySkill_CanonicalSectionsCatchesFlagStrip(t *testing.T) {
 	t.Parallel()
 	bin := buildPrintingPressBinary(t)
 	tampered := strings.Replace(
-		generator.CanonicalSkillInstallSection("myapi", "productivity", false),
+		generator.CanonicalSkillInstallSection("myapi", "productivity"),
 		" --cli-only", "", 1,
 	)
-	dir := writeCanonicalFixture(t, "myapi", "productivity", false, tampered)
+	dir := writeCanonicalFixture(t, "myapi", "productivity", tampered)
 	out, err := exec.Command(bin, "verify-skill", "--dir", dir, "--only", "canonical-sections").CombinedOutput()
 	require.Error(t, err, "stripping --cli-only must fail canonical-sections: %s", string(out))
 	require.Contains(t, string(out), "canonical-sections")
@@ -367,7 +367,7 @@ func TestVerifySkill_CanonicalSectionsCatchesFabricatedInstall(t *testing.T) {
 	t.Parallel()
 	bin := buildPrintingPressBinary(t)
 	fabricated := "## Prerequisites: Install the CLI\n\nInstall via the Printing Press Library plugin (`/ppl install myapi` from Claude Code).\n\nIf `--version` reports \"command not found\" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.\n"
-	dir := writeCanonicalFixture(t, "myapi", "productivity", false, fabricated)
+	dir := writeCanonicalFixture(t, "myapi", "productivity", fabricated)
 	out, err := exec.Command(bin, "verify-skill", "--dir", dir, "--only", "canonical-sections").CombinedOutput()
 	require.Error(t, err, "fabricated install instructions must fail canonical-sections: %s", string(out))
 	require.Contains(t, string(out), "drift")
@@ -398,7 +398,7 @@ func Execute() error { return (&cobra.Command{Use: "fixture-pp-cli"}).Execute()
 // for exercising the canonical-sections check end-to-end. When skillBody is
 // "", the canonical install section is used verbatim; pass a tampered body
 // to simulate hand-editing of the install section.
-func writeCanonicalFixture(t *testing.T, name, category string, usesBrowserHTTP bool, skillBody string) string {
+func writeCanonicalFixture(t *testing.T, name, category, skillBody string) string {
 	t.Helper()
 	dir := t.TempDir()
 	cliDir := filepath.Join(dir, "internal", "cli")
@@ -408,19 +408,15 @@ import "github.com/spf13/cobra"
 func Execute() error { return (&cobra.Command{Use: "`+name+`-pp-cli"}).Execute() }
 `), 0o644))
 
-	goVersion := "1.23"
-	if usesBrowserHTTP {
-		goVersion = "1.25"
-	}
 	require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"),
-		[]byte("module github.com/example/"+name+"-pp-cli\n\ngo "+goVersion+"\n"), 0o644))
+		[]byte("module github.com/example/"+name+"-pp-cli\n\ngo 1.26.3\n"), 0o644))
 
 	manifest := fmt.Sprintf(`{"api_name":%q,"cli_name":%q,"category":%q}`,
 		name, name+"-pp-cli", category)
 	require.NoError(t, os.WriteFile(filepath.Join(dir, ".printing-press.json"), []byte(manifest), 0o644))
 
 	if skillBody == "" {
-		skillBody = generator.CanonicalSkillInstallSection(name, category, usesBrowserHTTP)
+		skillBody = generator.CanonicalSkillInstallSection(name, category)
 	}
 	skill := "---\nname: pp-" + name + "\ndescription: \"fixture\"\n---\n\n# " + name + "\n\n" + skillBody + "\nFixture body.\n"
 	require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(skill), 0o644))
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 99a34792..2b469101 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1180,7 +1180,7 @@ func TestGenerateBrowserChromeTransport(t *testing.T) {
 
 	gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
 	require.NoError(t, err)
-	assert.Contains(t, string(gomod), "go 1.25")
+	assert.Contains(t, string(gomod), "go 1.26.3")
 	assert.Contains(t, string(gomod), "github.com/enetx/surf")
 
 	clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
@@ -1338,7 +1338,7 @@ func TestGenerateHTMLExtractionEndpoint(t *testing.T) {
 	require.FileExists(t, filepath.Join(outputDir, "internal", "cli", "html_extract.go"))
 	gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
 	require.NoError(t, err)
-	assert.Contains(t, string(gomod), "golang.org/x/net")
+	assert.Contains(t, string(gomod), "golang.org/x/net v0.53.0")
 
 	runGoCommand(t, outputDir, "mod", "tidy")
 	binaryPath := filepath.Join(outputDir, "webhtml-pp-cli")
@@ -1735,7 +1735,7 @@ func TestGenerateStandardTransportForOfficialAPI(t *testing.T) {
 
 	gomod, err := os.ReadFile(filepath.Join(outputDir, "go.mod"))
 	require.NoError(t, err)
-	assert.Contains(t, string(gomod), "go 1.23")
+	assert.Contains(t, string(gomod), "go 1.26.3")
 	assert.NotContains(t, string(gomod), "github.com/enetx/surf")
 
 	clientGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
diff --git a/internal/generator/install_section.go b/internal/generator/install_section.go
index 6e5166e6..b8c223aa 100644
--- a/internal/generator/install_section.go
+++ b/internal/generator/install_section.go
@@ -20,8 +20,7 @@ const SkillInstallSectionEndSubstr = "Do not proceed with skill commands until v
 // emits into a printed CLI's SKILL.md install section. Indexed verbs:
 //
 //	%[1]s — CLI slug (e.g. "linear" — produces linear-pp-cli)
-//	%[2]s — Go-fallback floor ("Go 1.23+" or "Go 1.25+")
-//	%[3]s — catalog category (or "other" when empty)
+//	%[2]s — catalog category (or "other" when empty)
 //
 // Stays in lockstep with internal/generator/templates/skill.md.tmpl via
 // TestCanonicalSkillInstallSectionMatchesTemplate.
@@ -36,33 +35,27 @@ const canonicalSkillInstallSectionFormat = "## Prerequisites: Install the CLI\n"
 	"2. Verify: `%[1]s-pp-cli --version`\n" +
 	"3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.\n" +
 	"\n" +
-	"If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires %[2]s):\n" +
+	"If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):\n" +
 	"\n" +
 	"```bash\n" +
-	"go install github.com/mvanhorn/printing-press-library/library/%[3]s/%[1]s/cmd/%[1]s-pp-cli@latest\n" +
+	"go install github.com/mvanhorn/printing-press-library/library/%[2]s/%[1]s/cmd/%[1]s-pp-cli@latest\n" +
 	"```\n" +
 	"\n" +
 	"If `--version` reports \"command not found\" after install, the install step did not put the binary on `$PATH`. Do not proceed with skill commands until verification succeeds.\n"
 
 // CanonicalSkillInstallSection returns the exact text of the install/
 // prerequisites section that the generator emits into a printed CLI's
-// SKILL.md, given the CLI slug, the catalog category (empty -> "other"),
-// and whether the CLI uses the browser HTTP transport (raises the Go-
-// fallback floor from 1.23 to 1.25).
+// SKILL.md, given the CLI slug and catalog category (empty -> "other").
 //
 // The verify-skill canonical-sections check uses this function to detect
 // post-publish edits to the install instructions. The function is the
 // authoritative source post-generation; the template stays in sync via
 // TestCanonicalSkillInstallSectionMatchesTemplate.
-func CanonicalSkillInstallSection(name, category string, usesBrowserHTTP bool) string {
+func CanonicalSkillInstallSection(name, category string) string {
 	if category == "" {
 		category = "other"
 	}
-	goFloor := "Go 1.23+"
-	if usesBrowserHTTP {
-		goFloor = "Go 1.25+"
-	}
-	return fmt.Sprintf(canonicalSkillInstallSectionFormat, name, goFloor, category)
+	return fmt.Sprintf(canonicalSkillInstallSectionFormat, name, category)
 }
 
 // ExtractSkillInstallSection slices the install/prerequisites block out of
diff --git a/internal/generator/install_section_test.go b/internal/generator/install_section_test.go
index 00fb9a9d..e2b80c18 100644
--- a/internal/generator/install_section_test.go
+++ b/internal/generator/install_section_test.go
@@ -15,7 +15,7 @@ import (
 // CanonicalSkillInstallSection and skill.md.tmpl. The two are parallel
 // renderings of the same canonical install block — one Go literal, one
 // Go template — and they must produce byte-identical output for any
-// (name, category, usesBrowserHTTP) tuple. If either drifts, this test
+// (name, category) tuple. If either drifts, this test
 // fails before any printed CLI ships with a desynced install section.
 //
 // The verify-skill canonical-sections check enforces this contract at
@@ -33,7 +33,7 @@ func TestCanonicalSkillInstallSectionMatchesTemplate(t *testing.T) {
 	}{
 		{"empty category and standard transport", "myapi", "", false},
 		{"explicit category", "myapi", "productivity", false},
-		{"browser transport raises Go floor", "myapi", "productivity", true},
+		{"browser transport uses same Go floor", "myapi", "productivity", true},
 		{"slug with hyphens", "trigger-dev", "developer-tools", false},
 	}
 
@@ -57,7 +57,7 @@ func TestCanonicalSkillInstallSectionMatchesTemplate(t *testing.T) {
 			extracted, ok := ExtractSkillInstallSection(string(rendered))
 			require.True(t, ok, "extractor must find the install section in a freshly-rendered SKILL.md")
 
-			expected := CanonicalSkillInstallSection(tc.apiName, tc.category, tc.usesBrowserHTTP)
+			expected := CanonicalSkillInstallSection(tc.apiName, tc.category)
 			require.Equal(t, expected, extracted,
 				"template-rendered install section must equal CanonicalSkillInstallSection output for %s", tc.label)
 		})
diff --git a/internal/generator/templates/go.mod.tmpl b/internal/generator/templates/go.mod.tmpl
index 3f759389..675a549c 100644
--- a/internal/generator/templates/go.mod.tmpl
+++ b/internal/generator/templates/go.mod.tmpl
@@ -1,6 +1,6 @@
 module {{modulePath}}
 
-go {{if .UsesBrowserHTTPTransport}}1.25{{else}}1.23{{end}}
+go 1.26.3
 
 require (
 {{- if .UsesBrowserHTTPTransport}}
@@ -8,7 +8,7 @@ require (
 {{- end}}
 	github.com/spf13/cobra v1.9.1
 {{- if .HasHTMLExtraction}}
-	golang.org/x/net v0.43.0
+	golang.org/x/net v0.53.0
 {{- end}}
 {{- if eq .Config.Format "toml"}}
 	github.com/pelletier/go-toml/v2 v2.2.4
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index 05b10094..be54695c 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -32,7 +32,7 @@ npx -y @mvanhorn/printing-press install {{.Name}} --cli-only
 
 ### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires {{if .UsesBrowserHTTPTransport}}Go 1.25+{{else}}Go 1.23+{{end}}):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 6ec07e55..3032ed40 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -29,7 +29,7 @@ This skill drives the `{{.Name}}-pp-cli` binary. **You must verify the CLI is in
 2. Verify: `{{.Name}}-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires {{if .UsesBrowserHTTPTransport}}Go 1.25+{{else}}Go 1.23+{{end}}):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{.Category}}{{else}}other{{end}}/{{.Name}}/cmd/{{.Name}}-pp-cli@latest
diff --git a/internal/generator/validate.go b/internal/generator/validate.go
index 427a84df..4908db7c 100644
--- a/internal/generator/validate.go
+++ b/internal/generator/validate.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/artifacts"
+	"github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
 	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
 )
 
@@ -46,6 +47,13 @@ func (g *Generator) Validate() error {
 				return err
 			},
 		},
+		{
+			name: "govulncheck ./...",
+			run: func() error {
+				_, err := runCommand(g.OutputDir, qualityGateTimeout, "go", govulncheck.GoRunArgs("./...")...)
+				return err
+			},
+		},
 		{
 			name: "go vet ./...",
 			run: func() error {
diff --git a/internal/generator/validate_test.go b/internal/generator/validate_test.go
index 3d44c127..2f89b011 100644
--- a/internal/generator/validate_test.go
+++ b/internal/generator/validate_test.go
@@ -3,8 +3,10 @@ package generator
 import (
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 
+	"github.com/mvanhorn/cli-printing-press/v4/internal/govulncheck"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -13,7 +15,7 @@ func TestGoBuildCacheDirIsShared(t *testing.T) {
 	t.Setenv("GOCACHE", "")
 
 	// Two different project directories should get the same cache dir.
-	// This is critical for CI performance — shared cache avoids each
+	// This is critical for CI performance because the shared cache avoids each
 	// parallel test recompiling the Go standard library from scratch.
 	dir1, err := goBuildCacheDir("/tmp/project-a")
 	require.NoError(t, err)
@@ -47,3 +49,37 @@ func TestGoBuildCacheDirHonorsExplicitGOCACHE(t *testing.T) {
 	assert.Equal(t, cacheDir, dir)
 	assert.DirExists(t, cacheDir)
 }
+
+func TestValidateRunsPinnedDefaultGovulncheckGate(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("fake shell go binary is Unix-only")
+	}
+	outputDir := filepath.Join(t.TempDir(), "validate-pp-cli")
+	gen := New(minimalSpec("validate"), outputDir)
+	require.NoError(t, gen.Generate())
+
+	fakeBin := t.TempDir()
+	callsPath := filepath.Join(t.TempDir(), "go-calls.txt")
+	fakeGo := filepath.Join(fakeBin, "go")
+	require.NoError(t, os.WriteFile(fakeGo, []byte(`#!/bin/sh
+printf '%s\n' "$*" >> "$FAKE_GO_CALLS"
+if [ "$1" = "run" ]; then
+  echo "fake govulncheck failure" >&2
+  exit 42
+fi
+exit 0
+`), 0o755))
+	t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"))
+	t.Setenv("FAKE_GO_CALLS", callsPath)
+
+	err := gen.Validate()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), `gate "govulncheck ./..." failed`)
+
+	calls, err := os.ReadFile(callsPath)
+	require.NoError(t, err)
+	assert.Contains(t, string(calls), "mod tidy\n")
+	assert.Contains(t, string(calls), "run "+govulncheck.ToolModule+" ./...\n")
+	assert.NotContains(t, string(calls), "-show")
+	assert.NotContains(t, string(calls), "verbose")
+}
diff --git a/internal/govulncheck/govulncheck.go b/internal/govulncheck/govulncheck.go
new file mode 100644
index 00000000..35a4f2ad
--- /dev/null
+++ b/internal/govulncheck/govulncheck.go
@@ -0,0 +1,14 @@
+package govulncheck
+
+const (
+	Name        = "govulncheck"
+	ToolVersion = "v1.3.0"
+	ToolModule  = "golang.org/x/vuln/cmd/govulncheck@" + ToolVersion
+)
+
+// Default mode keeps blocking focused on reachable findings, not dependency
+// presence alone.
+func GoRunArgs(args ...string) []string {
+	goArgs := []string{"run", ToolModule}
+	return append(goArgs, args...)
+}
diff --git a/internal/govulncheck/govulncheck_test.go b/internal/govulncheck/govulncheck_test.go
new file mode 100644
index 00000000..6a2dce3a
--- /dev/null
+++ b/internal/govulncheck/govulncheck_test.go
@@ -0,0 +1,21 @@
+package govulncheck
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestToolModuleIsPinned(t *testing.T) {
+	assert.NotContains(t, ToolModule, "@latest")
+	assert.True(t, strings.HasPrefix(ToolVersion, "v"))
+	assert.Equal(t, "golang.org/x/vuln/cmd/govulncheck@"+ToolVersion, ToolModule)
+}
+
+func TestGoRunArgsUsesDefaultMode(t *testing.T) {
+	args := GoRunArgs("./...")
+	assert.Equal(t, []string{"run", ToolModule, "./..."}, args)
+	assert.NotContains(t, args, "-show")
+	assert.NotContains(t, args, "verbose")
+}
diff --git a/internal/pipeline/fullrun.go b/internal/pipeline/fullrun.go
index 35c82d28..ca6c44ae 100644
--- a/internal/pipeline/fullrun.go
+++ b/internal/pipeline/fullrun.go
@@ -17,6 +17,8 @@ import (
 	"gopkg.in/yaml.v3"
 )
 
+const fullRunQualityGateCount = 8
+
 // FullRunResult holds everything the press produced for one API.
 type FullRunResult struct {
 	APIName string
@@ -421,7 +423,7 @@ func PrintComparisonTable(results []*FullRunResult) string {
 
 	writeComparisonRows(&b, results, []comparisonRow{
 		{"Quality Gates", func(r *FullRunResult) string {
-			return fmt.Sprintf("%d/7 PASS", r.GatesPassed)
+			return fmt.Sprintf("%d/%d PASS", r.GatesPassed, fullRunQualityGateCount)
 		}},
 		{"Commands", func(r *FullRunResult) string {
 			return fmt.Sprintf("%d", r.CommandCount)
@@ -570,8 +572,8 @@ func GenerateLearningsPlan(results []*FullRunResult, outputPath string) error {
 		if len(r.Errors) > 0 {
 			status = fmt.Sprintf("%d errors", len(r.Errors))
 		}
-		fmt.Fprintf(&b, "- **%s** (%s) - Gates %d/7, Duration %s, Status: %s\n",
-			r.APIName, r.Level, r.GatesPassed, r.Duration.Round(time.Second), status)
+		fmt.Fprintf(&b, "- **%s** (%s) - Gates %d/%d, Duration %s, Status: %s\n",
+			r.APIName, r.Level, r.GatesPassed, fullRunQualityGateCount, r.Duration.Round(time.Second), status)
 	}
 	b.WriteString("\n")
 
diff --git a/internal/pipeline/fullrun_test.go b/internal/pipeline/fullrun_test.go
index 2cb4d11e..beb54c08 100644
--- a/internal/pipeline/fullrun_test.go
+++ b/internal/pipeline/fullrun_test.go
@@ -44,7 +44,7 @@ func TestFullRun(t *testing.T) {
 			result := MakeBestCLI(api.name, api.level, api.flag, api.url, outputDir, pressBinary)
 			results = append(results, result)
 
-			assert.Equal(t, 7, result.GatesPassed, "%s: all 7 gates should pass", api.name)
+			assert.Equal(t, fullRunQualityGateCount, result.GatesPassed, "%s: all quality gates should pass", api.name)
 			assert.True(t, result.CommandCount > 0, "%s: should have commands", api.name)
 			assert.NotNil(t, result.Scorecard, "%s: should have scorecard", api.name)
 		})
@@ -71,6 +71,22 @@ func TestFullRunQualitySpecPath(t *testing.T) {
 	assert.Equal(t, "", fullRunQualitySpecPath("--docs", "https://example.com/docs"))
 }
 
+func TestGenerateLearningsPlanUsesQualityGateCount(t *testing.T) {
+	t.Parallel()
+
+	outputPath := filepath.Join(t.TempDir(), "learnings.md")
+	require.NoError(t, GenerateLearningsPlan([]*FullRunResult{{
+		APIName:     "demo",
+		Level:       "EASY",
+		GatesPassed: 6,
+	}}, outputPath))
+
+	content, err := os.ReadFile(outputPath)
+	require.NoError(t, err)
+	assert.Contains(t, string(content), "Gates 6/8")
+	assert.NotContains(t, string(content), "Gates 6/7")
+}
+
 func TestPrintComparisonTableRows(t *testing.T) {
 	t.Parallel()
 
@@ -113,7 +129,7 @@ func TestPrintComparisonTableRows(t *testing.T) {
 		last = idx
 	}
 
-	assert.Contains(t, table, "6/7 PASS")
+	assert.Contains(t, table, "6/8 PASS")
 	assert.Contains(t, table, "72/80 (72%)")
 	assert.Contains(t, table, "1/2")
 	assert.Contains(t, table, "PASS 100%")
diff --git a/internal/pipeline/planner.go b/internal/pipeline/planner.go
index bfe4df47..e0b49f93 100644
--- a/internal/pipeline/planner.go
+++ b/internal/pipeline/planner.go
@@ -104,7 +104,7 @@ func generateScaffoldPlan(ctx PlanContext) (string, error) {
 
 	b.WriteString("## What This Phase Must Produce\n\n")
 	fmt.Fprintf(&b, "- Generated CLI source tree in %s\n", ctx.SeedData.OutputDir)
-	b.WriteString("- All seven generator quality gates passing\n")
+	b.WriteString("- All eight generator quality gates passing, including default-mode govulncheck\n")
 	fmt.Fprintf(&b, "- Working CLI binary for %s\n\n", ctx.SeedData.APIName)
 
 	b.WriteString("## Codebase Pointers\n\n")
diff --git a/internal/pipeline/planner_artifacts_test.go b/internal/pipeline/planner_artifacts_test.go
index 279a41bc..2ba727b2 100644
--- a/internal/pipeline/planner_artifacts_test.go
+++ b/internal/pipeline/planner_artifacts_test.go
@@ -42,6 +42,8 @@ func TestGenerateNextPlanLoadsArtifactsFromRunstateDirs(t *testing.T) {
 	scaffoldPlan, err := GenerateNextPlan(state, PhaseScaffold)
 	require.NoError(t, err)
 	assert.Contains(t, scaffoldPlan, "Novelty score:** 8/10 (proceed)")
+	assert.Contains(t, scaffoldPlan, "All eight")
+	assert.Contains(t, scaffoldPlan, "govulncheck")
 
 	comparativePlan, err := GenerateNextPlan(state, PhaseComparative)
 	require.NoError(t, err)
diff --git a/internal/pipeline/seeds.go b/internal/pipeline/seeds.go
index f6eddfaf..869cb57e 100644
--- a/internal/pipeline/seeds.go
+++ b/internal/pipeline/seeds.go
@@ -175,7 +175,7 @@ Generate the first working {{.APIName}} CLI from the validated OpenAPI spec.
 ## What This Phase Must Produce
 
 - Generated CLI source tree in {{.OutputDir}}
-- All seven generator quality gates passing
+- All eight generator quality gates passing, including default-mode govulncheck
 - Working CLI binary for {{.APIName}}
 
 ## Prior Phase Outputs
@@ -250,7 +250,7 @@ Merge the enrichments into the source spec and regenerate the CLI without losing
 
 - Re-generated CLI in {{.OutputDir}} using the merged overlay
 - Merged spec artifact suitable for regeneration
-- All seven quality gates still passing after regeneration
+- All eight quality gates still passing after regeneration, including default-mode govulncheck
 
 ## Prior Phase Outputs
 
diff --git a/internal/pipeline/seeds_test.go b/internal/pipeline/seeds_test.go
index 51307cc1..be259a48 100644
--- a/internal/pipeline/seeds_test.go
+++ b/internal/pipeline/seeds_test.go
@@ -33,6 +33,11 @@ func TestRenderSeedProducesThinSeedTemplates(t *testing.T) {
 			require.Equal(t, "---", lines[0])
 			require.Contains(t, rendered, "type: feat")
 			require.Contains(t, rendered, "date: ")
+			if phase == PhaseScaffold || phase == PhaseRegenerate {
+				require.Contains(t, rendered, "All eight")
+				require.Contains(t, rendered, "govulncheck")
+				require.NotContains(t, rendered, "All seven")
+			}
 		})
 	}
 }
diff --git a/skills/printing-press-catalog/SKILL.md b/skills/printing-press-catalog/SKILL.md
index 661dbb09..f71fcf5e 100644
--- a/skills/printing-press-catalog/SKILL.md
+++ b/skills/printing-press-catalog/SKILL.md
@@ -30,7 +30,7 @@ Browse and install pre-built Go CLIs for popular APIs.
 
 ## Prerequisites
 
-- Go 1.21+ installed
+- Go 1.26.3 or newer installed
 - `printing-press` binary on PATH (install with `go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest`)
 
 ## Setup
diff --git a/skills/printing-press-publish/SKILL.md b/skills/printing-press-publish/SKILL.md
index 25aea36d..f7e15aa3 100644
--- a/skills/printing-press-publish/SKILL.md
+++ b/skills/printing-press-publish/SKILL.md
@@ -194,6 +194,12 @@ Run:
 printing-press publish validate --dir <cli-dir> --json
 ```
 
+`govulncheck` in this step is intentionally scoped to `<cli-dir>` only. It
+uses the default `govulncheck ./...` mode so reachable symbol findings block
+publish, while merely-required vulnerable modules without a call path do not
+become release blockers. Do not replace this with a full public-library scan or
+`govulncheck -show verbose`.
+
 Parse the JSON result. Display each check result to the user:
 
 ```
@@ -201,6 +207,7 @@ Validating <api-slug>...
   manifest        PASS
   phase5          PASS
   go mod tidy     PASS
+  govulncheck     PASS
   go vet          PASS
   go build        PASS
   --help          PASS
@@ -398,10 +405,18 @@ if [ -f "$PUBLISH_REPO_DIR/tools/generate-skills/main.go" ]; then
   (cd "$PUBLISH_REPO_DIR" && go run ./tools/generate-skills/main.go)
 fi
 
-# Verify it builds from the publish repo
-cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" && go build ./...
+# Verify this changed/new CLI builds and has no reachable Go vulnerabilities from the publish repo
+cd "$PUBLISH_REPO_DIR/library/<category>/<api-slug>" \
+  && go build ./... \
+  && go run golang.org/x/vuln/cmd/govulncheck@v1.3.0 ./...
 ```
 
+Keep vulnerability verification scoped to `library/<category>/<api-slug>` in
+publish PRs. The public library is a historical collection and cannot be kept
+fully current on every unrelated PR; whole-library govulncheck sweeps belong in
+a scheduled/reporting workflow, while blocking CI should scan only added or
+changed CLI modules.
+
 After the publish repo copy and build verification are complete, remove the staging
 directory:
 
@@ -735,6 +750,7 @@ $ <cli-name> --help
 | Manifest | PASS/FAIL |
 | Phase 5 | PASS/FAIL |
 | go mod tidy | PASS/FAIL |
+| govulncheck (this CLI only, reachable findings) | PASS/FAIL |
 | go vet | PASS/FAIL |
 | go build | PASS/FAIL |
 | --help | PASS/FAIL |
diff --git a/skills/printing-press-score/SKILL.md b/skills/printing-press-score/SKILL.md
index 9af4c181..d76f3519 100644
--- a/skills/printing-press-score/SKILL.md
+++ b/skills/printing-press-score/SKILL.md
@@ -26,7 +26,7 @@ Score generated CLIs against the Steinberger bar. Supports rescoring, scoring by
 
 ## Prerequisites
 
-- Go 1.21+ installed
+- Go 1.26.3 or newer installed
 - `printing-press` binary on PATH (install with `go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest`)
 
 ## Step 0: Setup
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 06ff89b6..e95d1f0f 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -136,19 +136,17 @@ elif ! command -v printing-press >/dev/null 2>&1; then
   else
     # Refuse: the printing-press binary is required and we will not auto-install
     # it. The README's two-step install (binary + plugin) is the source of truth;
-    # silent auto-install hides failure modes (GOPRIVATE auth, network, wrong
-    # GOPATH) inside an opaque skill invocation.
+    # silent auto-install hides failure modes (network, wrong GOPATH) inside an
+    # opaque skill invocation.
     echo ""
     echo "[setup-error] printing-press binary not found."
     echo ""
     if command -v go >/dev/null 2>&1; then
       echo "Install it in your terminal:"
-      echo "  GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
-      echo ""
-      echo "(GOPRIVATE is required while the repo is private. The plain command works after the public launch.)"
+      echo "  go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
     else
-      echo "Go 1.22+ is also not installed. Install Go from https://go.dev/dl/, then:"
-      echo "  GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
+      echo "Go 1.26.3 or newer is also not installed. Install Go from https://go.dev/dl/, then:"
+      echo "  go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest"
     fi
     echo ""
     echo "Verify with: printing-press --version"
@@ -173,10 +171,9 @@ mkdir -p "$PRESS_RUNSTATE" "$PRESS_LIBRARY" "$PRESS_MANUSCRIPTS" "$PRESS_CURRENT
 
 # --- Latest-version advisory (throttled, fail-open) ---
 # Once per 24h, check whether a newer printing-press release exists and print a
-# one-line notice if so. Uses `go list` so it works for private modules via
-# GOPRIVATE and public modules via the proxy. Runs in every context — devs
-# ahead of latest stay silent (comparison handles it), devs behind latest get
-# the same nudge anyone else does.
+# one-line notice if so. Uses `go list` through the public module proxy. Runs in
+# every context — devs ahead of latest stay silent (comparison handles it), devs
+# behind latest get the same nudge anyone else does.
 PRESS_VERCHECK_FILE="$PRESS_HOME/.version-check"
 PRESS_VERCHECK_TTL=86400
 _now_ts=$(date +%s)
@@ -190,7 +187,7 @@ fi
 
 if [ "$_should_check" = "true" ] && command -v go >/dev/null 2>&1; then
   _installed=$(printing-press version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
-  _latest=$(GOPRIVATE=github.com/mvanhorn/* go list -m -versions github.com/mvanhorn/cli-printing-press/v4 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
+  _latest=$(go list -m -versions github.com/mvanhorn/cli-printing-press/v4 2>/dev/null | awk '{print $NF}' | sed 's/^v//')
 
   if [ -n "$_installed" ] && [ -n "$_latest" ] && [ "$_installed" != "$_latest" ]; then
     # sort -V is not fully semver-aware: it ranks "3.0.0-rc1" above "3.0.0" instead of below.
diff --git a/skills/printing-press/references/setup-checks.md b/skills/printing-press/references/setup-checks.md
index 944fd811..14dba379 100644
--- a/skills/printing-press/references/setup-checks.md
+++ b/skills/printing-press/references/setup-checks.md
@@ -8,9 +8,9 @@ Apply these in order. Each section is conditional — do nothing if its trigger
 
 If the setup contract output contains a line starting with `[setup-error]`, the printing-press binary is not installed and the contract has already exited non-zero.
 
-**Stop the skill immediately.** Do not proceed to research, generation, or any other work. Surface the message the contract printed (it includes the exact `go install` command and `GOPRIVATE` guidance) verbatim to the user.
+**Stop the skill immediately.** Do not proceed to research, generation, or any other work. Surface the message the contract printed (it includes the exact `go install` command) verbatim to the user.
 
-The user must install the binary in their terminal before re-running. Do not offer to auto-install — the README's two-step install is the source of truth, and silent auto-install hides failure modes (GOPRIVATE auth, network, wrong GOPATH) inside an opaque skill invocation.
+The user must install the binary in their terminal before re-running. Do not offer to auto-install — the README's two-step install is the source of truth, and silent auto-install hides failure modes (network, wrong GOPATH) inside an opaque skill invocation.
 
 ## 2. Interactive upgrade prompt
 
@@ -31,7 +31,7 @@ Then ask the user via `AskUserQuestion` before continuing setup:
 If the user picks **Yes**, run:
 
 ```bash
-GOPRIVATE=github.com/mvanhorn/* go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
+go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest
 ```
 
 After it completes, confirm with `printing-press version --json` and tell the user `"Upgraded to v<new>."` Then continue setup.
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
index 2d657580..9d1e5277 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
@@ -18,7 +18,7 @@ npx -y @mvanhorn/printing-press install printing-press-rich --cli-only
 
 ### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.23+):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
index f7d73a4f..d7dececc 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
@@ -29,7 +29,7 @@ This skill drives the `printing-press-rich-pp-cli` binary. **You must verify the
 2. Verify: `printing-press-rich-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.23+):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/other/printing-press-rich/cmd/printing-press-rich-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
index 1d1c4af9..fde3b361 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
@@ -18,7 +18,7 @@ npx -y @mvanhorn/printing-press install printing-press-golden --cli-only
 
 ### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.23+):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
index e1d91b0b..bd0b2761 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
@@ -29,7 +29,7 @@ This skill drives the `printing-press-golden-pp-cli` binary. **You must verify t
 2. Verify: `printing-press-golden-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.23+):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden/cmd/printing-press-golden-pp-cli@latest
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod b/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
index 4dd91bda..909013bc 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
@@ -1,6 +1,6 @@
 module printing-press-golden-pp-cli
 
-go 1.23
+go 1.26.3
 
 require (
 	github.com/spf13/cobra v1.9.1
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
index 172237ae..60acc282 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
@@ -18,7 +18,7 @@ npx -y @mvanhorn/printing-press install public-param-golden --cli-only
 
 ### Without Node (Go fallback)
 
-If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.23+):
+If `npx` isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli@latest
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
index aabdf89e..cfa65c8c 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/SKILL.md
@@ -29,7 +29,7 @@ This skill drives the `public-param-golden-pp-cli` binary. **You must verify the
 2. Verify: `public-param-golden-pp-cli --version`
 3. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
 
-If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.23+):
+If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.3 or newer):
 
 ```bash
 go install github.com/mvanhorn/printing-press-library/library/other/public-param-golden/cmd/public-param-golden-pp-cli@latest

← af70886c chore(main): release 4.0.4 (#692)  ·  back to Cli Printing Press  ·  chore(main): release 4.0.5 (#700) e601b211 →