[object Object]

← back to Cli Printing Press

fix(cli): preserve browser-sniffed request defaults (#737)

8721df4bf310cac7b0619747af9db366508583c3 · 2026-05-08 11:58:39 -0700 · Trevin Chow

Files touched

Diff

commit 8721df4bf310cac7b0619747af9db366508583c3
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri May 8 11:58:39 2026 -0700

    fix(cli): preserve browser-sniffed request defaults (#737)
---
 internal/catalog/catalog.go                        |  3 +-
 internal/cli/generate_test.go                      | 29 +++++++-
 internal/cli/root.go                               | 10 +--
 internal/generator/generator_test.go               | 80 ++++++++++++++++++++++
 internal/generator/templates/auth_browser.go.tmpl  |  4 +-
 internal/generator/templates/client.go.tmpl        | 13 +++-
 internal/generator/templates/doctor.go.tmpl        | 10 ++-
 internal/generator/templates/readme.md.tmpl        |  7 +-
 internal/generator/templates/skill.md.tmpl         |  7 +-
 internal/spec/spec.go                              | 34 ++++++---
 internal/spec/spec_test.go                         | 13 ++++
 skills/printing-press/references/spec-format.md    |  6 ++
 .../internal/client/client.go                      |  4 +-
 .../internal/client/client.go                      |  4 +-
 .../tier-routing-golden/internal/client/client.go  |  4 +-
 15 files changed, 205 insertions(+), 23 deletions(-)

diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
index 85494bfa..46aa05e1 100644
--- a/internal/catalog/catalog.go
+++ b/internal/catalog/catalog.go
@@ -64,6 +64,7 @@ var validClientPatterns = map[string]struct{}{
 
 var validHTTPTransports = map[string]struct{}{
 	"standard":          {}, // Plain net/http, default for official APIs
+	"browser-http":      {}, // Plain net/http with HTTP/2 disabled for browser-facing websites
 	"browser-chrome":    {}, // Browser-compatible transport for web-discovered/non-official APIs
 	"browser-chrome-h3": {}, // Chrome-compatible HTTP transport forced through HTTP/3
 }
@@ -294,7 +295,7 @@ func (e *Entry) Validate() error {
 	}
 	if e.HTTPTransport != "" {
 		if _, ok := validHTTPTransports[e.HTTPTransport]; !ok {
-			return fmt.Errorf("http_transport must be one of: standard, browser-chrome, browser-chrome-h3")
+			return fmt.Errorf("http_transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3")
 		}
 	}
 	if err := validateBearerRefresh(e.BearerRefresh); err != nil {
diff --git a/internal/cli/generate_test.go b/internal/cli/generate_test.go
index 72e6483d..f4067740 100644
--- a/internal/cli/generate_test.go
+++ b/internal/cli/generate_test.go
@@ -471,12 +471,39 @@ func TestMergeSpecsPrefersReplayableBrowserTransportOverUnshippablePageContext(t
 
 	mergedRuntimeLast := mergeSpecs([]*spec.APISpec{chromeSpec, runtimeSpec}, "merged")
 	assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedRuntimeLast.HTTPTransport)
+
+	httpSpec := &spec.APISpec{
+		Name:          "http",
+		Version:       "0.1.0",
+		BaseURL:       "https://http.example.com",
+		HTTPTransport: spec.HTTPTransportBrowserHTTP,
+		Resources:     map[string]spec.Resource{},
+		Types:         map[string]spec.TypeDef{},
+	}
+	standardSpec := &spec.APISpec{
+		Name:          "standard",
+		Version:       "0.1.0",
+		BaseURL:       "https://standard.example.com",
+		HTTPTransport: spec.HTTPTransportStandard,
+		Resources:     map[string]spec.Resource{},
+		Types:         map[string]spec.TypeDef{},
+	}
+
+	mergedHTTP := mergeSpecs([]*spec.APISpec{standardSpec, httpSpec}, "merged")
+	assert.Equal(t, spec.HTTPTransportBrowserHTTP, mergedHTTP.HTTPTransport)
+
+	mergedChrome := mergeSpecs([]*spec.APISpec{httpSpec, chromeSpec}, "merged")
+	assert.Equal(t, spec.HTTPTransportBrowserChrome, mergedChrome.HTTPTransport)
 }
 
 func TestNormalizeHTTPTransportAllowsBrowserChromeH3(t *testing.T) {
 	t.Parallel()
 
-	got, err := normalizeHTTPTransport(spec.HTTPTransportBrowserChromeH3)
+	got, err := normalizeHTTPTransport(spec.HTTPTransportBrowserHTTP)
+	require.NoError(t, err)
+	assert.Equal(t, spec.HTTPTransportBrowserHTTP, got)
+
+	got, err = normalizeHTTPTransport(spec.HTTPTransportBrowserChromeH3)
 	require.NoError(t, err)
 	assert.Equal(t, spec.HTTPTransportBrowserChromeH3, got)
 
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 04e88dd9..7eff875d 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -383,7 +383,7 @@ func newGenerateCmd() *cobra.Command {
 	cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Parse spec and show what would be generated without writing files (remote specs are still fetched)")
 	cmd.Flags().StringVar(&specSource, "spec-source", "", "Spec provenance: official, community, sniffed/browser-sniffed, docs (affects generated client defaults like rate limiting)")
 	cmd.Flags().StringVar(&clientPattern, "client-pattern", "", "HTTP client pattern: rest (default), proxy-envelope (wraps requests in POST envelope)")
-	cmd.Flags().StringVar(&httpTransport, "transport", "", "HTTP transport: standard, browser-chrome, or browser-chrome-h3 (defaults based on spec provenance and reachability)")
+	cmd.Flags().StringVar(&httpTransport, "transport", "", "HTTP transport: standard, browser-http, browser-chrome, or browser-chrome-h3 (defaults based on spec provenance and reachability)")
 	cmd.Flags().StringVar(&researchDir, "research-dir", "", "Pipeline directory containing research.json and discovery/ for README source credits")
 	cmd.Flags().IntVar(&maxResources, "max-resources", 0, "Maximum resource groups to generate (default 500, raise for enormous APIs)")
 	cmd.Flags().IntVar(&maxEndpointsPerResource, "max-endpoints-per-resource", 0, "Maximum endpoints per resource (default 50, raise for large APIs)")
@@ -505,10 +505,10 @@ func normalizeClientPattern(value string) (string, error) {
 
 func normalizeHTTPTransport(value string) (string, error) {
 	switch value {
-	case "", spec.HTTPTransportStandard, spec.HTTPTransportBrowserChrome, spec.HTTPTransportBrowserChromeH3:
+	case "", spec.HTTPTransportStandard, spec.HTTPTransportBrowserHTTP, spec.HTTPTransportBrowserChrome, spec.HTTPTransportBrowserChromeH3:
 		return value, nil
 	default:
-		return "", fmt.Errorf("--transport must be one of: standard, browser-chrome, browser-chrome-h3 (got %q)", value)
+		return "", fmt.Errorf("--transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3 (got %q)", value)
 	}
 }
 
@@ -751,8 +751,10 @@ func strongerHTTPTransport(current, candidate string) string {
 func httpTransportPriority(value string) int {
 	switch value {
 	case spec.HTTPTransportBrowserChromeH3:
-		return 3
+		return 4
 	case spec.HTTPTransportBrowserChrome:
+		return 3
+	case spec.HTTPTransportBrowserHTTP:
 		return 2
 	case spec.HTTPTransportStandard:
 		return 1
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 4a187684..16d4ba34 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -1295,6 +1295,47 @@ func TestGenerateBrowserChromeH3Transport(t *testing.T) {
 	runGoCommand(t, outputDir, "test", "./internal/client")
 }
 
+func TestGenerateBrowserHTTPTransportDisablesHTTP2(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := &spec.APISpec{
+		Name:          "websurfacehttp",
+		Version:       "0.1.0",
+		BaseURL:       "https://www.example.com",
+		HTTPTransport: spec.HTTPTransportBrowserHTTP,
+		Auth:          spec.AuthConfig{Type: "none"},
+		Config: spec.ConfigSpec{
+			Format: "toml",
+			Path:   "~/.config/websurfacehttp-pp-cli/config.toml",
+		},
+		Resources: map[string]spec.Resource{
+			"posts": {
+				Description: "Browse posts",
+				Endpoints: map[string]spec.Endpoint{
+					"list": {
+						Method:      "GET",
+						Path:        "/",
+						Description: "List posts",
+					},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "websurfacehttp-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	clientGo := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+	assert.Contains(t, clientGo, `"crypto/tls"`)
+	assert.Contains(t, clientGo, `transport := http.DefaultTransport.(*http.Transport).Clone()`)
+	assert.Contains(t, clientGo, `transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)`)
+	assert.NotContains(t, clientGo, `"github.com/enetx/surf"`)
+	assert.NotContains(t, clientGo, `Impersonate()`)
+
+	gomod := readGeneratedFile(t, outputDir, "go.mod")
+	assert.NotContains(t, gomod, "github.com/enetx/surf")
+}
+
 func TestGenerateHTMLExtractionEndpoint(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -4333,6 +4374,22 @@ func TestGeneratedDoctor_AuthVerifyPathProbesEndpoint(t *testing.T) {
 	assert.NotContains(t, content, "inconclusive (HTTP %d from base URL")
 }
 
+func TestGeneratedDoctor_HealthCheckPathProbesEndpoint(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("healthdoc")
+	apiSpec.HealthCheckPath = "api/marketStatus"
+
+	outputDir := filepath.Join(t.TempDir(), "healthdoc-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	doctorSrc := readGeneratedFile(t, outputDir, "internal", "cli", "doctor.go")
+	assert.Contains(t, doctorSrc, `healthPath := "api/marketStatus"`)
+	assert.Contains(t, doctorSrc, `if !strings.HasPrefix(healthPath, "/") {`)
+	assert.Contains(t, doctorSrc, `reachBody, reachErr := c.Get(healthPath, nil)`)
+	assert.NotContains(t, doctorSrc, `reachBody, reachErr := c.Get("/", nil)`)
+}
+
 func TestGeneratedDoctor_InterstitialMarkersAreTitleAnchored(t *testing.T) {
 	t.Parallel()
 
@@ -4639,6 +4696,29 @@ func TestGenerate_UserAgentOverrideGatedByBrowserTransport(t *testing.T) {
 	assert.True(t, os.IsNotExist(err), "auth.go should not be emitted for auth.type:none specs")
 }
 
+func TestGenerateRequiredUserAgentHeaderBeatsDefaultUserAgent(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("browserheaders")
+	apiSpec.RequiredHeaders = []spec.RequiredHeader{
+		{Name: "User-Agent", Value: "Mozilla/5.0 Browser Sniff"},
+		{Name: "Referer", Value: "https://www.example.com/"},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "browserheaders-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	clientSrc := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+	require.Contains(t, clientSrc, `req.Header.Set("User-Agent", "Mozilla/5.0 Browser Sniff")`)
+	require.Contains(t, clientSrc, `req.Header.Set("Referer", "https://www.example.com/")`)
+	assert.Contains(t, clientSrc, `if req.Header.Get("User-Agent") == "" {`)
+	assert.Contains(t, clientSrc, `req.Header.Set("User-Agent", "browserheaders-pp-cli/0.1.0")`)
+
+	doctorSrc := readGeneratedFile(t, outputDir, "internal", "cli", "doctor.go")
+	require.Contains(t, doctorSrc, `authHeaders["User-Agent"] = "Mozilla/5.0 Browser Sniff"`)
+	assert.NotContains(t, doctorSrc, `authHeaders["User-Agent"] = "browserheaders-pp-cli"`)
+}
+
 func TestGenerateObjectBodyDefaultsAreParsedAsJSON(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index bfbebef9..a23b4f42 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -1282,7 +1282,9 @@ func validateComposedAuth(authHeader string) error {
 	req.Header.Set("{{.Name}}", "{{.Value}}")
 {{- end}}
 {{- if not .UsesBrowserManagedUserAgent}}
-	req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+	if req.Header.Get("User-Agent") == "" {
+		req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+	}
 {{- end}}
 
 	client := &http.Client{Timeout: 5 * time.Second}
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index f3b58cdd..dc2fd59b 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -5,6 +5,9 @@ package client
 
 import (
 	"bytes"
+{{- if .UsesHTTP2DisabledTransport}}
+	"crypto/tls"
+{{- end}}
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/json"
@@ -259,7 +262,11 @@ func (e *APIError) Error() string {
 }
 
 func newHTTPClient(timeout time.Duration, jar http.CookieJar) *http.Client {
-{{- if .UsesBrowserHTTPTransport}}
+{{- if .UsesHTTP2DisabledTransport}}
+	transport := http.DefaultTransport.(*http.Transport).Clone()
+	transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
+	return &http.Client{Timeout: timeout, Jar: jar, Transport: transport}
+{{- else if .UsesBrowserHTTPTransport}}
 	builder := surf.NewClient().
 		Builder().
 		Impersonate().
@@ -668,7 +675,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 			req.Header.Set(k, v)
 		}
 {{- if not .UsesBrowserManagedUserAgent}}
-		req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+		if req.Header.Get("User-Agent") == "" {
+			req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+		}
 {{- end}}
 
 		resp, err := c.HTTPClient.Do(req)
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index ebcb9a87..9d403d58 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -333,7 +333,15 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 					report["api"] = fmt.Sprintf("client init error: %s", clientErr)
 				} else {
 					// Step 1: Basic reachability via the configured transport.
+{{- if .HealthCheckPath}}
+					healthPath := "{{.HealthCheckPath}}"
+					if !strings.HasPrefix(healthPath, "/") {
+						healthPath = "/" + healthPath
+					}
+					reachBody, reachErr := c.Get(healthPath, nil)
+{{- else}}
 					reachBody, reachErr := c.Get("/", nil)
+{{- end}}
 					var reachAPIErr *client.APIError
 					switch {
 					case reachErr == nil:
@@ -396,7 +404,7 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
 {{- range .RequiredHeaders}}
 						authHeaders["{{.Name}}"] = "{{.Value}}"
 {{- end}}
-{{- if not .UsesBrowserManagedUserAgent}}
+{{- if and (not .UsesBrowserManagedUserAgent) (not (.HasRequiredHeader "User-Agent"))}}
 						authHeaders["User-Agent"] = "{{.Name}}-pp-cli"
 {{- end}}
 						_, authErr := c.GetWithHeaders(verifyPath, authParams, authHeaders)
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index be54695c..b246711f 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -515,7 +515,12 @@ Environment variables:
 - **{{.Symptom}}** — {{.Fix}}
 {{- end}}
 {{- end}}
-{{- if .UsesBrowserHTTPTransport}}
+{{- if .UsesHTTP2DisabledTransport}}
+
+## HTTP Transport
+
+This CLI uses standard HTTP transport with HTTP/2 disabled for browser-facing endpoints. It does not require a resident browser process for normal API calls.
+{{- else if .UsesBrowserHTTPTransport}}
 
 ## HTTP Transport
 
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 3032ed40..92d52654 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -95,7 +95,12 @@ These capabilities aren't available in any other tool for this API.
 {{- end}}
 {{- end}}
 {{- end}}
-{{- if .UsesBrowserHTTPTransport}}
+{{- if .UsesHTTP2DisabledTransport}}
+
+## HTTP Transport
+
+This CLI uses standard HTTP transport with HTTP/2 disabled for browser-facing endpoints. It does not require a resident browser process for normal API calls.
+{{- else if .UsesBrowserHTTPTransport}}
 
 ## HTTP Transport
 
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 9dd81554..2b4776d1 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -25,6 +25,7 @@ const (
 
 const (
 	HTTPTransportStandard        = "standard"          // default for official API clients
+	HTTPTransportBrowserHTTP     = "browser-http"      // stdlib transport with HTTP/2 disabled for browser-facing web surfaces
 	HTTPTransportBrowserChrome   = "browser-chrome"    // Chrome-impersonated transport for browser-facing web surfaces
 	HTTPTransportBrowserChromeH3 = "browser-chrome-h3" // Chrome-impersonated transport forced through HTTP/3 for stricter bot screens
 )
@@ -109,11 +110,12 @@ type APISpec struct {
 	Kind                 string              `yaml:"kind,omitempty" json:"kind,omitempty"`                     // "rest" (default) or "synthetic" — synthetic CLIs aggregate multiple sources beyond the spec; dogfood's path-validity check is relaxed accordingly
 	SpecSource           string              `yaml:"spec_source,omitempty" json:"spec_source,omitempty"`       // official, community, sniffed, docs — affects generated client defaults
 	ClientPattern        string              `yaml:"client_pattern,omitempty" json:"client_pattern,omitempty"` // rest (default), proxy-envelope — affects generated HTTP client
-	HTTPTransport        string              `yaml:"http_transport,omitempty" json:"http_transport,omitempty"` // standard (default for official APIs), browser-chrome, or browser-chrome-h3
-	ProxyRoutes          map[string]string   `yaml:"proxy_routes,omitempty" json:"proxy_routes,omitempty"`     // path prefix → service name for proxy-envelope routing
-	BearerRefresh        BearerRefreshConfig `yaml:"bearer_refresh,omitempty" json:"bearer_refresh,omitzero"`  // live-source metadata for rotating public client bearer tokens
-	WebsiteURL           string              `yaml:"website_url,omitempty" json:"website_url,omitempty"`       // product/company website (not the API base URL)
-	Category             string              `yaml:"category,omitempty" json:"category,omitempty"`             // catalog category (e.g., productivity, developer-tools) — used for library install path
+	HTTPTransport        string              `yaml:"http_transport,omitempty" json:"http_transport,omitempty"` // standard (default for official APIs), browser-http, browser-chrome, or browser-chrome-h3
+	HealthCheckPath      string              `yaml:"health_check_path,omitempty" json:"health_check_path,omitempty"`
+	ProxyRoutes          map[string]string   `yaml:"proxy_routes,omitempty" json:"proxy_routes,omitempty"`    // path prefix → service name for proxy-envelope routing
+	BearerRefresh        BearerRefreshConfig `yaml:"bearer_refresh,omitempty" json:"bearer_refresh,omitzero"` // live-source metadata for rotating public client bearer tokens
+	WebsiteURL           string              `yaml:"website_url,omitempty" json:"website_url,omitempty"`      // product/company website (not the API base URL)
+	Category             string              `yaml:"category,omitempty" json:"category,omitempty"`            // catalog category (e.g., productivity, developer-tools) — used for library install path
 	Auth                 AuthConfig          `yaml:"auth" json:"auth"`
 	TierRouting          TierRoutingConfig   `yaml:"tier_routing,omitempty" json:"tier_routing,omitzero"`
 	RequiredHeaders      []RequiredHeader    `yaml:"required_headers,omitempty" json:"required_headers,omitempty"`
@@ -290,7 +292,7 @@ func (s *APISpec) EffectiveHTTPTransport() string {
 		return HTTPTransportStandard
 	}
 	switch s.HTTPTransport {
-	case HTTPTransportStandard, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
+	case HTTPTransportStandard, HTTPTransportBrowserHTTP, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
 		return s.HTTPTransport
 	}
 	switch s.SpecSource {
@@ -314,6 +316,10 @@ func (s *APISpec) UsesBrowserHTTP3Transport() bool {
 	return s.EffectiveHTTPTransport() == HTTPTransportBrowserChromeH3
 }
 
+func (s *APISpec) UsesHTTP2DisabledTransport() bool {
+	return s.EffectiveHTTPTransport() == HTTPTransportBrowserHTTP
+}
+
 func (s *APISpec) UsesBrowserManagedUserAgent() bool {
 	switch s.EffectiveHTTPTransport() {
 	case HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
@@ -323,6 +329,18 @@ func (s *APISpec) UsesBrowserManagedUserAgent() bool {
 	}
 }
 
+func (s *APISpec) HasRequiredHeader(name string) bool {
+	if s == nil {
+		return false
+	}
+	for _, header := range s.RequiredHeaders {
+		if strings.EqualFold(strings.TrimSpace(header.Name), name) {
+			return true
+		}
+	}
+	return false
+}
+
 func (s *APISpec) HasHTMLExtraction() bool {
 	if s == nil {
 		return false
@@ -1514,9 +1532,9 @@ func (s *APISpec) Validate() error {
 		return fmt.Errorf("at least one resource is required")
 	}
 	switch s.HTTPTransport {
-	case "", HTTPTransportStandard, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
+	case "", HTTPTransportStandard, HTTPTransportBrowserHTTP, HTTPTransportBrowserChrome, HTTPTransportBrowserChromeH3:
 	default:
-		return fmt.Errorf("http_transport must be one of: standard, browser-chrome, browser-chrome-h3")
+		return fmt.Errorf("http_transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3")
 	}
 	if err := validateExtraCommands(s.ExtraCommands); err != nil {
 		return err
diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go
index 8ce18939..0a724dca 100644
--- a/internal/spec/spec_test.go
+++ b/internal/spec/spec_test.go
@@ -2454,6 +2454,14 @@ func TestHTTPTransportValidationAndDefaults(t *testing.T) {
 	assert.Equal(t, HTTPTransportBrowserChrome, sniffed.EffectiveHTTPTransport())
 	require.NoError(t, sniffed.Validate())
 
+	browserHTTP := base
+	browserHTTP.HTTPTransport = HTTPTransportBrowserHTTP
+	assert.Equal(t, HTTPTransportBrowserHTTP, browserHTTP.EffectiveHTTPTransport())
+	assert.False(t, browserHTTP.UsesBrowserHTTPTransport())
+	assert.True(t, browserHTTP.UsesHTTP2DisabledTransport())
+	assert.False(t, browserHTTP.UsesBrowserManagedUserAgent())
+	require.NoError(t, browserHTTP.Validate())
+
 	community := base
 	community.SpecSource = "community"
 	assert.Equal(t, HTTPTransportBrowserChrome, community.EffectiveHTTPTransport())
@@ -2477,6 +2485,11 @@ func TestHTTPTransportValidationAndDefaults(t *testing.T) {
 	assert.False(t, runtime.UsesBrowserManagedUserAgent())
 	require.ErrorContains(t, runtime.Validate(), "http_transport must be one of")
 
+	requiredUA := base
+	requiredUA.RequiredHeaders = []RequiredHeader{{Name: "user-agent", Value: "Mozilla/5.0"}}
+	assert.True(t, requiredUA.HasRequiredHeader("User-Agent"))
+	assert.False(t, requiredUA.HasRequiredHeader("Referer"))
+
 	invalid := base
 	invalid.HTTPTransport = "lynx"
 	require.ErrorContains(t, invalid.Validate(), "http_transport must be one of")
diff --git a/skills/printing-press/references/spec-format.md b/skills/printing-press/references/spec-format.md
index 2ddd4f48..5425986a 100644
--- a/skills/printing-press/references/spec-format.md
+++ b/skills/printing-press/references/spec-format.md
@@ -10,6 +10,11 @@ name: my-api                     # string (REQUIRED) CLI binary prefix, e.g. "my
 description: "My API CLI"       # string shown in command help
 version: "0.1.0"                # string baked into generated binary
 base_url: "https://api.example.com/v1" # string (REQUIRED) default base API URL
+http_transport: standard          # string optional: standard | browser-http | browser-chrome | browser-chrome-h3
+health_check_path: "/health"      # string optional doctor reachability path; defaults to /
+required_headers:                 # []RequiredHeader optional headers sent on every request
+  - name: User-Agent
+    value: "Mozilla/5.0 ..."
 
 auth:                             # object (AuthConfig)
   type: api_key                   # string: api_key | oauth2 | bearer_token | none
@@ -79,6 +84,7 @@ name: stytch # CLI binary prefix => stytch-cli
 description: "Stytch authentication API CLI" # Root help text and README summary
 version: "0.1.0" # Printed by `stytch-cli version`
 base_url: "https://api.stytch.com/v1" # Default base URL endpoint paths are joined against
+health_check_path: "/sessions" # Optional doctor reachability path; omit to probe /
 
 auth:
   type: api_key # Uses API key style auth
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
index f3297a7f..6966ce01 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
@@ -231,7 +231,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)
 		}
-		req.Header.Set("User-Agent", "printing-press-oauth2-pp-cli/1.0.0")
+		if req.Header.Get("User-Agent") == "" {
+			req.Header.Set("User-Agent", "printing-press-oauth2-pp-cli/1.0.0")
+		}
 
 		resp, err := c.HTTPClient.Do(req)
 		if err != nil {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
index b9cddae2..52c5cced 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
@@ -226,7 +226,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)
 		}
-		req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
+		if req.Header.Get("User-Agent") == "" {
+			req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
+		}
 
 		resp, err := c.HTTPClient.Do(req)
 		if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
index 396a5931..4f13cb33 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
@@ -329,7 +329,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)
 		}
-		req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
+		if req.Header.Get("User-Agent") == "" {
+			req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
+		}
 
 		resp, err := c.HTTPClient.Do(req)
 		if err != nil {

← 1d26ae77 fix(cli): guard generated CLI build safety (#736)  ·  back to Cli Printing Press  ·  fix(cli): hydrate promote state across scopes (#738) dfdf1831 →