[object Object]

← back to Cli Printing Press

fix(cli): preserve operation server host overrides (#834)

1c0d110bb6ef79822ed226d6e961682d27b03c0a · 2026-05-09 12:31:43 -0700 · Trevin Chow

Files touched

Diff

commit 1c0d110bb6ef79822ed226d6e961682d27b03c0a
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 9 12:31:43 2026 -0700

    fix(cli): preserve operation server host overrides (#834)
---
 docs/SPEC-EXTENSIONS.md                            |  4 +-
 internal/generator/generator.go                    | 90 ++++++++++++----------
 internal/generator/generator_test.go               | 37 +++++++++
 internal/generator/templates/client.go.tmpl        |  8 +-
 .../generator/templates/command_endpoint.go.tmpl   |  2 +-
 .../generator/templates/command_promoted.go.tmpl   |  2 +-
 internal/openapi/parser.go                         | 19 +++++
 internal/openapi/parser_test.go                    | 41 ++++++++++
 internal/spec/spec.go                              | 25 ++++--
 skills/printing-press/references/spec-format.md    |  6 +-
 10 files changed, 174 insertions(+), 60 deletions(-)

diff --git a/docs/SPEC-EXTENSIONS.md b/docs/SPEC-EXTENSIONS.md
index ce4b4a47..d29933e3 100644
--- a/docs/SPEC-EXTENSIONS.md
+++ b/docs/SPEC-EXTENSIONS.md
@@ -164,8 +164,8 @@ Rules:
 - Credential-bearing tier `base_url` values must be HTTPS and cannot point at
   loopback, private, link-local, or unrelated hosts unless
   `allow_cross_host_auth: true` documents explicit review.
-- Incompatible with `client_pattern: proxy-envelope` and with resource-level
-  `base_url` overrides when any tier declares its own `base_url`.
+- Incompatible with `client_pattern: proxy-envelope` and with resource- or
+  endpoint-level `base_url` overrides when any tier declares its own `base_url`.
 - Tier credential env vars are read from the environment at request time; they
   are not serialized into generated config files.
 
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index fe26eaba..b9f8bc95 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -657,9 +657,9 @@ type configTemplateData struct {
 
 // endpointTemplateData is the data passed to command_endpoint.go.tmpl
 // for both top-level resource endpoints and sub-resource endpoints.
-// ResourceBaseURL carries the resource's BaseURL override (or its
-// inherited parent override for sub-resources); the template prepends
-// it to Endpoint.Path so per-resource hosts produce absolute URLs.
+// ResourceBaseURL carries the endpoint's effective BaseURL override. The
+// template prepends it to Endpoint.Path so per-request hosts produce absolute
+// URLs.
 type endpointTemplateData struct {
 	ResourceName    string
 	ResourceBaseURL string
@@ -1732,7 +1732,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 			asyncInfo, isAsync := g.AsyncJobs[name+"/"+eName]
 			epData := endpointTemplateData{
 				ResourceName:    name,
-				ResourceBaseURL: strings.TrimRight(resource.BaseURL, "/"),
+				ResourceBaseURL: effectiveEndpointBaseURL(resource, endpoint),
 				EffectivePath:   effectiveEndpointPath(resource, endpoint),
 				EffectiveTier:   g.Spec.EffectiveTier(resource, endpoint),
 				FuncPrefix:      name,
@@ -1773,17 +1773,6 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 				return fmt.Errorf("rendering sub-parent %s/%s: %w", name, subName, err)
 			}
 
-			// Sub-resources inherit the parent's BaseURL override; an
-			// explicit sub_resource.base_url wins. Falls through to the
-			// spec-level BaseURL when both are empty. Trailing slash is
-			// trimmed so the template's `path := <base><endpoint.path>`
-			// concat doesn't produce `https://x.com/v1//search` when the
-			// override and endpoint path both carry slashes.
-			subResourceBaseURL := subResource.BaseURL
-			if subResourceBaseURL == "" {
-				subResourceBaseURL = resource.BaseURL
-			}
-			subResourceBaseURL = strings.TrimRight(subResourceBaseURL, "/")
 			for eName, endpoint := range subResource.Endpoints {
 				subKey := subName + "/" + eName
 				asyncInfo, isAsync := g.AsyncJobs[subKey]
@@ -1793,7 +1782,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 				}
 				epData := endpointTemplateData{
 					ResourceName:    subName,
-					ResourceBaseURL: subResourceBaseURL,
+					ResourceBaseURL: effectiveSubEndpointBaseURL(resource, subResource, endpoint),
 					EffectivePath:   effectiveSubEndpointPath(resource, subResource, endpoint),
 					EffectiveTier:   g.Spec.EffectiveTier(effectiveResource, endpoint),
 					FuncPrefix:      name + "-" + subName,
@@ -2476,34 +2465,36 @@ func (g *Generator) renderPromotedCommandFiles(promotedCommands []PromotedComman
 	for _, pc := range promotedCommands {
 		// Look up the full resource to pass sibling endpoints/sub-resources.
 		// Trim trailing slash on BaseURL so the promoted handler's
-		// `path := <Resource.BaseURL><Endpoint.Path>` concat doesn't
+		// `path := <ResourceBaseURL><Endpoint.Path>` concat doesn't
 		// produce `https://x.com/v1//search`.
 		resource := g.Spec.Resources[pc.ResourceName]
-		resource.BaseURL = strings.TrimRight(resource.BaseURL, "/")
+		resourceBaseURL := effectiveEndpointBaseURL(resource, pc.Endpoint)
 		promotedData := struct {
-			PromotedName  string
-			ResourceName  string
-			EndpointName  string
-			EffectivePath string
-			Endpoint      spec.Endpoint
-			EffectiveTier string
-			HasStore      bool
-			Resource      spec.Resource
-			FuncPrefix    string
-			IsReadOnly    bool
+			PromotedName    string
+			ResourceName    string
+			EndpointName    string
+			ResourceBaseURL string
+			EffectivePath   string
+			Endpoint        spec.Endpoint
+			EffectiveTier   string
+			HasStore        bool
+			Resource        spec.Resource
+			FuncPrefix      string
+			IsReadOnly      bool
 			*spec.APISpec
 		}{
-			PromotedName:  pc.PromotedName,
-			ResourceName:  pc.ResourceName,
-			EndpointName:  pc.EndpointName,
-			EffectivePath: effectiveEndpointPath(resource, pc.Endpoint),
-			Endpoint:      pc.Endpoint,
-			EffectiveTier: g.Spec.EffectiveTier(resource, pc.Endpoint),
-			HasStore:      g.VisionSet.Store,
-			Resource:      resource,
-			FuncPrefix:    pc.ResourceName,
-			IsReadOnly:    endpointIsReadCommand(pc.Endpoint, pc.EndpointName),
-			APISpec:       g.Spec,
+			PromotedName:    pc.PromotedName,
+			ResourceName:    pc.ResourceName,
+			EndpointName:    pc.EndpointName,
+			ResourceBaseURL: resourceBaseURL,
+			EffectivePath:   effectiveEndpointPath(resource, pc.Endpoint),
+			Endpoint:        pc.Endpoint,
+			EffectiveTier:   g.Spec.EffectiveTier(resource, pc.Endpoint),
+			HasStore:        g.VisionSet.Store,
+			Resource:        resource,
+			FuncPrefix:      pc.ResourceName,
+			IsReadOnly:      endpointIsReadCommand(pc.Endpoint, pc.EndpointName),
+			APISpec:         g.Spec,
 		}
 		promotedPath := filepath.Join("internal", "cli", "promoted_"+pc.PromotedName+".go")
 		if err := g.renderTemplate("command_promoted.go.tmpl", promotedPath, promotedData); err != nil {
@@ -3843,15 +3834,30 @@ func lookupEndpointForTemplate(api *spec.APISpec, ref string) (templateEndpoint,
 }
 
 func effectiveEndpointPath(resource spec.Resource, endpoint spec.Endpoint) string {
-	return endpointPathWithBase(resource.BaseURL, endpoint.Path)
+	return endpointPathWithBase(effectiveEndpointBaseURL(resource, endpoint), endpoint.Path)
 }
 
 func effectiveSubEndpointPath(parent spec.Resource, sub spec.Resource, endpoint spec.Endpoint) string {
-	baseURL := sub.BaseURL
+	return endpointPathWithBase(effectiveSubEndpointBaseURL(parent, sub, endpoint), endpoint.Path)
+}
+
+func effectiveEndpointBaseURL(resource spec.Resource, endpoint spec.Endpoint) string {
+	baseURL := endpoint.BaseURL
+	if baseURL == "" {
+		baseURL = resource.BaseURL
+	}
+	return strings.TrimRight(baseURL, "/")
+}
+
+func effectiveSubEndpointBaseURL(parent spec.Resource, sub spec.Resource, endpoint spec.Endpoint) string {
+	baseURL := endpoint.BaseURL
+	if baseURL == "" {
+		baseURL = sub.BaseURL
+	}
 	if baseURL == "" {
 		baseURL = parent.BaseURL
 	}
-	return endpointPathWithBase(baseURL, endpoint.Path)
+	return strings.TrimRight(baseURL, "/")
 }
 
 func endpointPathWithBase(baseURL, path string) string {
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 2a1e87d8..4f371b98 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -6869,6 +6869,43 @@ func TestGenerateSubResourceInheritsParentBaseURL(t *testing.T) {
 	runGoCommand(t, outputDir, "build", "./...")
 }
 
+func TestGenerateEndpointBaseURLOverrideRoutesToOverrideHost(t *testing.T) {
+	t.Parallel()
+	apiSpec := minimalSpec("endpoint-multihost")
+	apiSpec.BaseURL = "https://api.example.com/v1"
+	apiSpec.Resources["forecast"] = spec.Resource{
+		Description: "Weather forecast",
+		Endpoints: map[string]spec.Endpoint{
+			"now": {Method: "GET", Path: "/forecast", Description: "Current"},
+			"geocode": {
+				Method:      "GET",
+				Path:        "/search",
+				BaseURL:     "https://geocoding-api.example.com/v1",
+				Description: "Geocode",
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	geoHandler, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "forecast_geocode.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(geoHandler), `path := "https://geocoding-api.example.com/v1/search"`,
+		"endpoint override must emit the absolute URL into path")
+
+	fcHandler, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "forecast_now.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(fcHandler), `path := "/forecast"`,
+		"sibling endpoint without override must keep the relative path")
+
+	mcpTools, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "tools.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(mcpTools), `makeAPIHandler("GET", "https://geocoding-api.example.com/v1/search"`,
+		"typed MCP endpoint tool must use the endpoint override URL")
+}
+
 // TestGenerateNoResourceBaseURLOverrideByteCompat — specs without any
 // per-resource BaseURL override must regenerate without the
 // isAbsoluteURL helper or the absolute-URL branch in client.do().
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 0140677c..4e4268eb 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -489,7 +489,7 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 {{- else}}
 {{- if .EndpointTemplateVars}}
 {{- if .HasResourceBaseURLOverride}}
-	// Per-resource BaseURL overrides flow through as absolute paths
+	// Resource and endpoint BaseURL overrides flow through as absolute paths
 	// (https:// or http://); buildURL still substitutes any
 	// {placeholder} markers, but the relative-path concat with
 	// c.BaseURL must be skipped for absolute paths.
@@ -511,7 +511,7 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 {{- end}}
 {{- else}}
 {{- if .HasResourceBaseURLOverride}}
-	// Per-resource BaseURL overrides flow through as absolute paths
+	// Resource and endpoint BaseURL overrides flow through as absolute paths
 	// (https:// or http://); the resource template emits the full URL
 	// in `path` and we skip the c.BaseURL concat for those.
 	var targetURL string
@@ -1185,8 +1185,8 @@ func (c *Client) refreshAccessToken() error {
 {{- end}}
 
 {{if .HasResourceBaseURLOverride -}}
-// isAbsoluteURL reports whether path is already a full URL (per-resource
-// BaseURL override emits the full URL into path; the relative-path
+// isAbsoluteURL reports whether path is already a full URL (resource or
+// endpoint BaseURL override emits the full URL into path; the relative-path
 // concat with c.BaseURL must be skipped for those calls).
 func isAbsoluteURL(path string) bool {
 	return strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://")
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index e7505acc..27e5ccb5 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -120,7 +120,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 			c = c.WithTier({{printf "%q" .EffectiveTier}})
 {{- end}}
 
-			path := "{{.ResourceBaseURL}}{{.Endpoint.Path}}"{{/* ResourceBaseURL is empty unless the resource declares an override; when set it makes path absolute and the client skips the c.BaseURL concat. */}}
+			path := "{{.ResourceBaseURL}}{{.Endpoint.Path}}"{{/* ResourceBaseURL is empty unless the resource or endpoint declares an override; when set it makes path absolute and the client skips the c.BaseURL concat. */}}
 {{- range $i, $p := .Endpoint.Params}}
 {{- if .Positional}}
 {{- if gt $i 0}}
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index dc13877c..3a9bab1e 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -88,7 +88,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 			c = c.WithTier({{printf "%q" .EffectiveTier}})
 {{- end}}
 
-			path := "{{.Resource.BaseURL}}{{.Endpoint.Path}}"{{/* Resource.BaseURL is empty unless the resource declares an override; when set it makes path absolute and the client skips the c.BaseURL concat. */}}
+			path := "{{.ResourceBaseURL}}{{.Endpoint.Path}}"{{/* ResourceBaseURL is empty unless the resource or endpoint declares an override; when set it makes path absolute and the client skips the c.BaseURL concat. */}}
 {{- range $i, $p := .Endpoint.Params}}
 {{- if .Positional}}
 			if len(args) < {{add $i 1}} {
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index f0e8b4f3..e7f75398 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -1671,6 +1671,7 @@ func mapResources(doc *openapi3.T, out *spec.APISpec, basePath string) {
 			endpoint := spec.Endpoint{
 				Method:      strings.ToUpper(method),
 				Path:        path,
+				BaseURL:     operationServerBaseURL(out.BaseURL, pathItem, op),
 				Description: description,
 				Params:      params,
 				Body:        body,
@@ -1722,6 +1723,24 @@ func mapResources(doc *openapi3.T, out *spec.APISpec, basePath string) {
 	filterGlobalParams(out.Resources)
 }
 
+func operationServerBaseURL(specBaseURL string, pathItem *openapi3.PathItem, op *openapi3.Operation) string {
+	var servers openapi3.Servers
+	if pathItem != nil {
+		servers = pathItem.Servers
+	}
+	if op != nil && op.Servers != nil {
+		servers = *op.Servers
+	}
+	if len(servers) == 0 || servers[0] == nil {
+		return ""
+	}
+	baseURL, _ := resolveServerURL(servers[0])
+	if baseURL == "" || baseURL == strings.TrimRight(specBaseURL, "/") {
+		return ""
+	}
+	return baseURL
+}
+
 // operationAllowsAnonymous checks whether an operation can be called without
 // authentication. Returns true when:
 //   - The operation has security: [] (explicit opt-out)
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index fd6c5e8c..3c43e5b0 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -4100,6 +4100,47 @@ paths:
 	assert.Equal(t, "https://global.example.com", parsed.BaseURL)
 }
 
+func TestParseOperationServersBecomeEndpointBaseURLOverrides(t *testing.T) {
+	t.Parallel()
+
+	specYAML := `openapi: "3.0.3"
+info:
+  title: Multi Host Test
+  version: "1.0"
+servers:
+  - url: https://api.open-meteo.com/v1
+paths:
+  /forecast:
+    get:
+      operationId: forecast
+      responses:
+        '200': {description: OK}
+  /search:
+    get:
+      operationId: geocoding
+      servers:
+        - url: https://geocoding-api.open-meteo.com/v1
+      responses:
+        '200': {description: OK}
+`
+	parsed, err := Parse([]byte(specYAML))
+	require.NoError(t, err)
+	assert.Equal(t, "https://api.open-meteo.com/v1", parsed.BaseURL)
+
+	var search spec.Endpoint
+	found := false
+	for _, resource := range parsed.Resources {
+		for _, endpoint := range resource.Endpoints {
+			if endpoint.Path == "/search" {
+				search = endpoint
+				found = true
+			}
+		}
+	}
+	require.True(t, found, "expected /search endpoint to be parsed")
+	assert.Equal(t, "https://geocoding-api.open-meteo.com/v1", search.BaseURL)
+}
+
 func TestParseMCPExtensionFromRoot(t *testing.T) {
 	t.Parallel()
 	data := []byte(`
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 6745d439..8e0cd6ae 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -883,10 +883,9 @@ type Resource struct {
 	SubResources map[string]Resource `yaml:"sub_resources,omitempty" json:"sub_resources,omitempty"`
 }
 
-// HasResourceBaseURLOverride reports whether any resource (top-level or
-// nested sub-resource) declares a BaseURL override. Used by the client
-// template to gate the absolute-URL detection branch — specs that don't
-// opt in regenerate byte-identically.
+// HasResourceBaseURLOverride reports whether any resource or endpoint declares
+// a BaseURL override. Used by the client template to gate the absolute-URL
+// detection branch — specs that don't opt in regenerate byte-identically.
 func (s *APISpec) HasResourceBaseURLOverride() bool {
 	if s == nil {
 		return false
@@ -903,6 +902,11 @@ func resourceHasBaseURLOverride(resource Resource) bool {
 	if resource.BaseURL != "" {
 		return true
 	}
+	for _, endpoint := range resource.Endpoints {
+		if endpoint.BaseURL != "" {
+			return true
+		}
+	}
 	for _, sub := range resource.SubResources {
 		if resourceHasBaseURLOverride(sub) {
 			return true
@@ -914,6 +918,7 @@ func resourceHasBaseURLOverride(resource Resource) bool {
 type Endpoint struct {
 	Method          string            `yaml:"method" json:"method"`
 	Path            string            `yaml:"path" json:"path"`
+	BaseURL         string            `yaml:"base_url,omitempty" json:"base_url,omitempty"`
 	Description     string            `yaml:"description" json:"description"`
 	Params          []Param           `yaml:"params" json:"params"`
 	Body            []Param           `yaml:"body" json:"body"`
@@ -1578,7 +1583,7 @@ func (s *APISpec) Validate() error {
 		return err
 	}
 	if s.ClientPattern == "proxy-envelope" && s.HasResourceBaseURLOverride() {
-		return fmt.Errorf("resource base_url overrides are incompatible with client_pattern=proxy-envelope; the proxy POSTs every request to the spec-level BaseURL, so per-resource overrides would be silently ignored")
+		return fmt.Errorf("resource or endpoint base_url overrides are incompatible with client_pattern=proxy-envelope; the proxy POSTs every request to the spec-level BaseURL, so per-request overrides would be silently ignored")
 	}
 	for name, r := range s.Resources {
 		if len(r.Endpoints) == 0 && len(r.SubResources) == 0 {
@@ -1854,7 +1859,7 @@ func validateTierRouting(s *APISpec) error {
 		}
 	}
 	if anyTierBaseURL && s.HasResourceBaseURLOverride() {
-		return fmt.Errorf("resource base_url overrides are incompatible with tier_routing tier base_url overrides; choose one routing source")
+		return fmt.Errorf("resource or endpoint base_url overrides are incompatible with tier_routing tier base_url overrides; choose one routing source")
 	}
 	for name, resource := range s.Resources {
 		if err := validateTierRoutingResource(s, name, resource, "", ""); err != nil {
@@ -2026,6 +2031,10 @@ func validateTierRoutingResource(s *APISpec, resourcePath string, resource Resou
 	effectiveResource := resource
 	effectiveResource.Tier = resourceTier
 	for endpointName, endpoint := range resource.Endpoints {
+		endpointBaseURL := strings.TrimSpace(endpoint.BaseURL)
+		if endpointBaseURL == "" {
+			endpointBaseURL = resourceBaseURL
+		}
 		tierName, tier, ok := s.EffectiveTierConfig(effectiveResource, endpoint)
 		if tierName == "" {
 			continue
@@ -2036,9 +2045,9 @@ func validateTierRoutingResource(s *APISpec, resourcePath string, resource Resou
 		if endpoint.NoAuth && tierAuthRequiresCredential(tier.Auth) {
 			return fmt.Errorf("resource %q endpoint %q declares no_auth but tier %q requires credentials", resourcePath, endpointName, tierName)
 		}
-		if tierAuthRequiresCredential(tier.Auth) && strings.TrimSpace(tier.BaseURL) == "" && resourceBaseURL != "" {
+		if tierAuthRequiresCredential(tier.Auth) && strings.TrimSpace(tier.BaseURL) == "" && endpointBaseURL != "" {
 			resourceTier := tier
-			resourceTier.BaseURL = resourceBaseURL
+			resourceTier.BaseURL = endpointBaseURL
 			if err := validateCredentialTierBaseURL(tierName, resourceTier, s.BaseURL); err != nil {
 				return fmt.Errorf("resource %q endpoint %q: %w", resourcePath, endpointName, err)
 			}
diff --git a/skills/printing-press/references/spec-format.md b/skills/printing-press/references/spec-format.md
index 5425986a..65b98995 100644
--- a/skills/printing-press/references/spec-format.md
+++ b/skills/printing-press/references/spec-format.md
@@ -37,6 +37,7 @@ resources:                        # map[string]Resource (REQUIRED: at least one
       list:                       # endpoint key becomes subcommand: users list
         method: GET               # string (REQUIRED) must be one of GET | POST | PUT | DELETE
         path: "/users"           # string (REQUIRED) API path; supports {param} placeholders
+        base_url: "https://search.example.com/v1" # string optional override for this endpoint only
         description: "List users" # string endpoint help text
         params:                   # []Param query/path parameters
           - name: limit           # string upstream wire key; request serialization always uses this value
@@ -259,7 +260,8 @@ Validation in `spec.Validate()` enforces:
 - every resource must have at least one endpoint
 - every endpoint must have both `method` and `path`
 - `resources.<name>.base_url` is allowed for resources that live on another host; sub-resources inherit the parent override unless they set their own `base_url`
-- resource `base_url` overrides cannot be combined with `client_pattern: proxy-envelope`, because proxy-envelope clients POST every request to the root `base_url`
+- `resources.<name>.endpoints.<name>.base_url` is allowed for a single endpoint that lives on another host; it wins over resource and sub-resource overrides
+- resource or endpoint `base_url` overrides cannot be combined with `client_pattern: proxy-envelope`, because proxy-envelope clients POST every request to the root `base_url`
 - `flag_name` and `aliases` must be lowercase kebab-case, non-empty when present, and collision-free within their command surface
 
 ## 5. Common Mistakes
@@ -272,7 +274,7 @@ These commonly cause generation/build failures or incorrect CLI behavior:
 - Defining `body` params on `GET` endpoints (allowed in YAML, but ignored by GET command generation)
 - Forgetting `positional: true` for params used in `/{path_placeholders}`
 - Using parameter types outside supported scalar set: `string`, `int`, `bool`, `float`
-- Putting a website/feed host in the root `base_url` just to make one resource work; keep the root API host as the default and use a resource `base_url` override for the outlier surface
+- Putting a website/feed host in the root `base_url` just to make one endpoint work; keep the root API host as the default and use a resource or endpoint `base_url` override for the outlier surface
 
 ## 5. Type Mapping
 

← dfb879de chore(main): release 4.2.1 (#805)  ·  back to Cli Printing Press  ·  Reject stale publish manifests before packaging (#836) 5155331c →