[object Object]

← back to Cli Printing Press

fix(cli): harden browser-sniff PII placeholders (#1639)

7eee7dfcb1d294feeeae6a013b4fd9d5bcaaec61 · 2026-05-18 16:46:18 -0700 · Trevin Chow

Files touched

Diff

commit 7eee7dfcb1d294feeeae6a013b4fd9d5bcaaec61
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Mon May 18 16:46:18 2026 -0700

    fix(cli): harden browser-sniff PII placeholders (#1639)
---
 internal/artifacts/pii.go                          |  36 +++++-
 internal/artifacts/pii_test.go                     |  45 +++++++-
 internal/artifacts/secrets.go                      | 127 +++++++++++++++++++--
 internal/artifacts/secrets_test.go                 |  56 +++++++++
 internal/browsersniff/fixtures.go                  | 108 ++++++++++++++++--
 internal/browsersniff/fixtures_test.go             |  62 ++++++++++
 internal/cli/publish.go                            |  25 +++-
 internal/cli/publish_test.go                       |  25 ++++
 internal/generator/captured_fixture_test.go        |  50 ++++++++
 internal/generator/example_value_test.go           |   9 ++
 internal/generator/generator.go                    |   4 +
 internal/generator/placeholders.go                 |  27 +++++
 internal/generator/templates/captured_test.go.tmpl |  12 ++
 internal/mcpdesc/params.go                         |   7 +-
 internal/mcpdesc/params_test.go                    |  22 ++++
 internal/piiplaceholders/placeholders.go           |  92 +++++++++++++++
 internal/piiplaceholders/placeholders_test.go      |  22 ++++
 internal/pipeline/lock.go                          |   2 +-
 internal/pipeline/toolsmanifest.go                 |   2 +
 internal/pipeline/toolsmanifest_test.go            |   6 +-
 20 files changed, 709 insertions(+), 30 deletions(-)

diff --git a/internal/artifacts/pii.go b/internal/artifacts/pii.go
index b3aa3478..a0850690 100644
--- a/internal/artifacts/pii.go
+++ b/internal/artifacts/pii.go
@@ -16,6 +16,8 @@ import (
 	"sort"
 	"strings"
 	"time"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/piiplaceholders"
 )
 
 // PII gate implementation following the Deterministic Inventory +
@@ -38,6 +40,8 @@ const (
 // changing a value is a backward-incompatible ledger format change.
 const (
 	PIIKindCardLast4     = "card-last-4"
+	PIIKindOrderID       = "order-id"
+	PIIKindASIN          = "asin"
 	PIIKindEmail         = "email"
 	PIIKindPhoneUS       = "phone-us"
 	PIIKindZipPlus4      = "zip-plus-4"
@@ -100,6 +104,19 @@ var piiDetectors = []piiDetector{
 		// \b but their length and shape are unambiguous.
 		pattern: regexp.MustCompile(`(?i)(?:\b(?:card|visa|mastercard|amex|ending in|last\s+4)|x{4,}|\*{4,})[\s:.\-]{0,5}\d{4}`),
 	},
+	{
+		kind: PIIKindOrderID,
+		// Physical and digital order IDs observed in browser-sniff
+		// captures. The canonical synthetic placeholders are filtered
+		// after matching.
+		pattern: piiplaceholders.OrderIDPattern(),
+	},
+	{
+		kind: PIIKindASIN,
+		// ASIN-shaped product IDs from browser-sniff captures. The
+		// canonical B0EXAMPLE* placeholders are filtered after matching.
+		pattern: piiplaceholders.ASINPattern(),
+	},
 	{
 		kind: PIIKindEmail,
 		// Standard email shape with a TLD of 2+ chars. Word boundaries
@@ -250,6 +267,19 @@ var skippedDirs = map[string]bool{
 	"build":        true,
 }
 
+func isSyntheticPIIPlaceholder(kind, matched string) bool {
+	switch kind {
+	case PIIKindOrderID:
+		return piiplaceholders.IsSyntheticOrderID(matched)
+	case PIIKindASIN:
+		return piiplaceholders.IsSyntheticASIN(matched)
+	case PIIKindPostalAddress:
+		return piiplaceholders.IsSyntheticPostalAddress(matched)
+	default:
+		return false
+	}
+}
+
 // FindPII walks root, applies the file-scoping rules, and returns all
 // PII-shape matches. Ordering is stable (file, line, column, kind) so
 // the JSON output and ledger reconcile cleanly across runs.
@@ -386,12 +416,16 @@ func scanPIIFile(root, path string) ([]PIIFinding, error) {
 		lineNumber++
 		for _, det := range piiDetectors {
 			for _, match := range det.pattern.FindAllStringIndex(line, -1) {
+				matchedSpan := line[match[0]:match[1]]
+				if isSyntheticPIIPlaceholder(det.kind, matchedSpan) {
+					continue
+				}
 				findings = append(findings, PIIFinding{
 					Kind:        det.kind,
 					File:        relSlash,
 					Line:        lineNumber,
 					Column:      match[0] + 1, // 1-based
-					MatchedSpan: line[match[0]:match[1]],
+					MatchedSpan: matchedSpan,
 				})
 			}
 		}
diff --git a/internal/artifacts/pii_test.go b/internal/artifacts/pii_test.go
index 17de3d41..e3fe2fd3 100644
--- a/internal/artifacts/pii_test.go
+++ b/internal/artifacts/pii_test.go
@@ -23,9 +23,12 @@ func TestFindPII_CardLast4(t *testing.T) {
 		expectKinds []string
 	}{
 		{name: "ending-in", line: `"display": "card ending in 4242"`, expectKinds: []string{PIIKindCardLast4}},
-		{name: "mask-asterisks", line: `"masked": "****-****-****-1234"`, expectKinds: []string{PIIKindCardLast4}},
+		{name: "mask-asterisks", line: `"masked": "****-****-****-4321"`, expectKinds: []string{PIIKindCardLast4}},
 		{name: "mask-x", line: `"masked": "xxxx-xxxx-xxxx-9999"`, expectKinds: []string{PIIKindCardLast4}},
-		{name: "visa-context", line: `"brand": "visa 5678"`, expectKinds: []string{PIIKindCardLast4}},
+		{name: "visa-context", line: `"brand": "visa 9876"`, expectKinds: []string{PIIKindCardLast4}},
+		{name: "numeric-placeholder-a-still-flags", line: `"brand": "visa 1234"`, expectKinds: []string{PIIKindCardLast4}},
+		{name: "numeric-placeholder-b-still-flags", line: `"brand": "amex 5678"`, expectKinds: []string{PIIKindCardLast4}},
+		{name: "non-numeric-last4-placeholder", line: `"brand": "visa LAST4"`, expectKinds: nil},
 		{name: "no-context-bare-4digits", line: `* 1234 changelog bullet`, expectKinds: nil},
 		{name: "no-context-year", line: `"version": "2024"`, expectKinds: nil},
 		{name: "no-context-port", line: `"port": "8080"`, expectKinds: nil},
@@ -42,6 +45,43 @@ func TestFindPII_CardLast4(t *testing.T) {
 	}
 }
 
+func TestFindPII_OrderIDs(t *testing.T) {
+	tests := []struct {
+		name        string
+		line        string
+		expectKinds []string
+	}{
+		{name: "physical-order", line: `"order_id": "123-4567890-1234567"`, expectKinds: []string{PIIKindOrderID}},
+		{name: "digital-order", line: `"order_id": "D01-4567890-1234567"`, expectKinds: []string{PIIKindOrderID}},
+		{name: "synthetic-physical-order", line: `"order_id": "111-1111111-1111111"`, expectKinds: nil},
+		{name: "synthetic-digital-order", line: `"order_id": "D01-1111111-1111111"`, expectKinds: nil},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := scanLine(t, tt.line, "test.json")
+			assertKinds(t, got, tt.expectKinds, PIIKindOrderID)
+		})
+	}
+}
+
+func TestFindPII_ASINs(t *testing.T) {
+	tests := []struct {
+		name        string
+		line        string
+		expectKinds []string
+	}{
+		{name: "real-asin", line: `"asin": "B012345678"`, expectKinds: []string{PIIKindASIN}},
+		{name: "synthetic-asin", line: `"asin": "B0EXAMPLE1"`, expectKinds: nil},
+		{name: "not-asin", line: `"sku": "A012345678"`, expectKinds: nil},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := scanLine(t, tt.line, "test.json")
+			assertKinds(t, got, tt.expectKinds, PIIKindASIN)
+		})
+	}
+}
+
 func TestFindPII_Email(t *testing.T) {
 	tests := []struct {
 		name        string
@@ -126,6 +166,7 @@ func TestFindPII_PostalAddress(t *testing.T) {
 		{name: "main-street-title", line: `"address": "1234 Main Street"`, expectKinds: []string{PIIKindPostalAddress}},
 		{name: "ave-title", line: `"line1": "567 Park Ave"`, expectKinds: []string{PIIKindPostalAddress}},
 		{name: "drive-lowercase-suffix", line: `"line1": "890 Sunset drive"`, expectKinds: []string{PIIKindPostalAddress}},
+		{name: "synthetic-address", line: `"address": "123 Test St, Anytown, ST 12345"`, expectKinds: nil},
 		{name: "no-number", line: `"page": "SEE README.MD"`, expectKinds: nil},
 		{name: "no-suffix", line: `"line": "1234 MAIN"`, expectKinds: nil},
 		// Regression guards: conversational prose where the name words
diff --git a/internal/artifacts/secrets.go b/internal/artifacts/secrets.go
index dcdaa7dd..0ff5490d 100644
--- a/internal/artifacts/secrets.go
+++ b/internal/artifacts/secrets.go
@@ -10,6 +10,8 @@ import (
 	"path/filepath"
 	"regexp"
 	"strings"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/piiplaceholders"
 )
 
 type secretReplacement struct {
@@ -96,7 +98,72 @@ var vendorPrefixSecretPatterns = []vendorPrefixSecretPattern{
 	},
 }
 
+const structuredCookieLineWindow = 5
+
+var structuredCookieValueLineRE = regexp.MustCompile(`(?i)["']value["']\s*:\s*["']([^"']{8,})["']`)
+
 func FindVendorPrefixSecrets(root string) ([]VendorPrefixSecretFinding, error) {
+	return findSecrets(root, vendorPrefixSecretPatterns)
+}
+
+func FindPackageSecrets(root string, cookieNames []string) ([]VendorPrefixSecretFinding, error) {
+	patterns := append([]vendorPrefixSecretPattern(nil), vendorPrefixSecretPatterns...)
+	patterns = append(patterns, cookieSecretPatterns(cookieNames)...)
+	return findSecrets(root, patterns)
+}
+
+func FindSpecDeclaredCookieSecrets(root string, cookieNames []string) ([]VendorPrefixSecretFinding, error) {
+	return findSecrets(root, cookieSecretPatterns(cookieNames))
+}
+
+func cookieSecretPatterns(cookieNames []string) []vendorPrefixSecretPattern {
+	if len(cookieNames) == 0 {
+		return nil
+	}
+
+	patterns := make([]vendorPrefixSecretPattern, 0, len(cookieNames)*3)
+	for _, name := range cookieNames {
+		name = strings.TrimSpace(name)
+		if name == "" {
+			continue
+		}
+		quotedName := regexp.QuoteMeta(name)
+		patterns = append(patterns, vendorPrefixSecretPattern{
+			kind:    "cookie-value:" + name,
+			pattern: regexp.MustCompile("(?:^|[\\s\"'`;,{:])" + quotedName + "=([^\\s;\"'&,}]{8,})"),
+			accept: func(candidate string) bool {
+				_, value, ok := strings.Cut(candidate, "=")
+				return ok && !piiplaceholders.IsSyntheticCookieValue(value)
+			},
+		})
+		patterns = append(patterns,
+			structuredCookieSecretPattern(name, regexp.MustCompile(`(?i)["']name["']\s*:\s*["']`+quotedName+`["'][^{}\n]*["']value["']\s*:\s*["']([^"']{8,})["']`)),
+			structuredCookieSecretPattern(name, regexp.MustCompile(`(?i)["']value["']\s*:\s*["']([^"']{8,})["'][^{}\n]*["']name["']\s*:\s*["']`+quotedName+`["']`)),
+		)
+	}
+	return patterns
+}
+
+func structuredCookieSecretPattern(name string, pattern *regexp.Regexp) vendorPrefixSecretPattern {
+	return vendorPrefixSecretPattern{
+		kind:    "cookie-value:" + name,
+		pattern: pattern,
+		accept: func(candidate string) bool {
+			matches := pattern.FindStringSubmatch(candidate)
+			return len(matches) == 2 && !piiplaceholders.IsSyntheticCookieValue(matches[1])
+		},
+	}
+}
+
+func FormatVendorPrefixSecretFindings(findings []VendorPrefixSecretFinding) string {
+	lines := make([]string, 0, len(findings))
+	for _, finding := range findings {
+		lines = append(lines, fmt.Sprintf("%s:%d %s", finding.Path, finding.Line, finding.Kind))
+	}
+	return strings.Join(lines, "\n")
+}
+
+func findSecrets(root string, patterns []vendorPrefixSecretPattern) ([]VendorPrefixSecretFinding, error) {
 	var findings []VendorPrefixSecretFinding
 	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
 		if walkErr != nil {
@@ -105,7 +172,7 @@ func FindVendorPrefixSecrets(root string) ([]VendorPrefixSecretFinding, error) {
 		if entry.IsDir() {
 			return nil
 		}
-		fileFindings, err := scanVendorPrefixSecretFile(root, path)
+		fileFindings, err := scanSecretFile(root, path, patterns)
 		if err != nil {
 			return err
 		}
@@ -115,15 +182,7 @@ func FindVendorPrefixSecrets(root string) ([]VendorPrefixSecretFinding, error) {
 	return findings, err
 }
 
-func FormatVendorPrefixSecretFindings(findings []VendorPrefixSecretFinding) string {
-	lines := make([]string, 0, len(findings))
-	for _, finding := range findings {
-		lines = append(lines, fmt.Sprintf("%s:%d %s", finding.Path, finding.Line, finding.Kind))
-	}
-	return strings.Join(lines, "\n")
-}
-
-func scanVendorPrefixSecretFile(root, path string) ([]VendorPrefixSecretFinding, error) {
+func scanSecretFile(root, path string, patterns []vendorPrefixSecretPattern) ([]VendorPrefixSecretFinding, error) {
 	file, err := os.Open(path)
 	if err != nil {
 		return nil, err
@@ -147,6 +206,8 @@ func scanVendorPrefixSecretFile(root, path string) ([]VendorPrefixSecretFinding,
 
 	var findings []VendorPrefixSecretFinding
 	lineNumber := 0
+	cookieNames := declaredCookieNamesFromPatterns(patterns)
+	pendingCookieNames := map[string]int{}
 	for {
 		line, readErr := reader.ReadString('\n')
 		if readErr != nil && readErr != io.EOF {
@@ -156,7 +217,7 @@ func scanVendorPrefixSecretFile(root, path string) ([]VendorPrefixSecretFinding,
 			break
 		}
 		lineNumber++
-		for _, pattern := range vendorPrefixSecretPatterns {
+		for _, pattern := range patterns {
 			if vendorPrefixSecretLineMatch(pattern, line) {
 				findings = append(findings, VendorPrefixSecretFinding{
 					Path: rel,
@@ -165,6 +226,29 @@ func scanVendorPrefixSecretFile(root, path string) ([]VendorPrefixSecretFinding,
 				})
 			}
 		}
+		for name, nameLine := range pendingCookieNames {
+			if lineNumber-nameLine > structuredCookieLineWindow {
+				delete(pendingCookieNames, name)
+			}
+		}
+		for _, name := range cookieNames {
+			if structuredCookieNameLineMatch(name, line) {
+				pendingCookieNames[name] = lineNumber
+			}
+		}
+		if matches := structuredCookieValueLineRE.FindStringSubmatch(line); len(matches) == 2 && !piiplaceholders.IsSyntheticCookieValue(matches[1]) {
+			for name, nameLine := range pendingCookieNames {
+				if nameLine == lineNumber {
+					continue
+				}
+				findings = append(findings, VendorPrefixSecretFinding{
+					Path: rel,
+					Line: lineNumber,
+					Kind: "cookie-value:" + name,
+				})
+				delete(pendingCookieNames, name)
+			}
+		}
 		if readErr == io.EOF {
 			break
 		}
@@ -172,6 +256,27 @@ func scanVendorPrefixSecretFile(root, path string) ([]VendorPrefixSecretFinding,
 	return findings, nil
 }
 
+func declaredCookieNamesFromPatterns(patterns []vendorPrefixSecretPattern) []string {
+	seen := map[string]struct{}{}
+	for _, pattern := range patterns {
+		name, ok := strings.CutPrefix(pattern.kind, "cookie-value:")
+		if !ok || name == "" {
+			continue
+		}
+		seen[name] = struct{}{}
+	}
+	names := make([]string, 0, len(seen))
+	for name := range seen {
+		names = append(names, name)
+	}
+	return names
+}
+
+func structuredCookieNameLineMatch(name, line string) bool {
+	pattern := regexp.MustCompile(`(?i)["']name["']\s*:\s*["']` + regexp.QuoteMeta(name) + `["']`)
+	return pattern.MatchString(line)
+}
+
 func vendorPrefixSecretLineMatch(pattern vendorPrefixSecretPattern, line string) bool {
 	for _, candidate := range pattern.pattern.FindAllString(line, -1) {
 		if pattern.accept == nil || pattern.accept(candidate) {
diff --git a/internal/artifacts/secrets_test.go b/internal/artifacts/secrets_test.go
index e3e67ce3..a5a63408 100644
--- a/internal/artifacts/secrets_test.go
+++ b/internal/artifacts/secrets_test.go
@@ -82,6 +82,62 @@ func TestFindVendorPrefixSecretsIgnoresPlaceholdersAndBinaryFiles(t *testing.T)
 	require.Empty(t, findings)
 }
 
+func TestFindSpecDeclaredCookieSecretsReportsCookieNameOnly(t *testing.T) {
+	root := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(root, "README.md"), []byte("Cookie:session-id=actuallyrealcookievaluexyz; x-main=your-cookie-here; y-main=not-an-example-real-value\n"), 0o644))
+
+	findings, err := FindSpecDeclaredCookieSecrets(root, []string{"session-id", "x-main", "y-main"})
+	require.NoError(t, err)
+	require.Len(t, findings, 2)
+	byKind := map[string]VendorPrefixSecretFinding{}
+	for _, finding := range findings {
+		byKind[finding.Kind] = finding
+	}
+	require.Equal(t, "README.md", byKind["cookie-value:session-id"].Path)
+	require.Equal(t, 1, byKind["cookie-value:session-id"].Line)
+	require.Equal(t, "README.md", byKind["cookie-value:y-main"].Path)
+	require.Equal(t, 1, byKind["cookie-value:y-main"].Line)
+}
+
+func TestFindSpecDeclaredCookieSecretsReportsStructuredNameValueCookies(t *testing.T) {
+	root := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(root, "cookies.json"), []byte(`{"name":"session-id","value":"actuallyrealcookievaluexyz"}`+"\n"), 0o644))
+	require.NoError(t, os.WriteFile(filepath.Join(root, "cookies-reversed.json"), []byte(`{"value":"anotherrealcookievaluexyz","name":"x-main"}`+"\n"), 0o644))
+	require.NoError(t, os.WriteFile(filepath.Join(root, "cookies-pretty.json"), []byte("{\n  \"name\": \"y-main\",\n  \"value\": \"prettyrealcookievaluexyz\"\n}\n"), 0o644))
+
+	findings, err := FindSpecDeclaredCookieSecrets(root, []string{"session-id", "x-main", "y-main"})
+	require.NoError(t, err)
+	require.Len(t, findings, 3)
+	byKind := map[string]VendorPrefixSecretFinding{}
+	for _, finding := range findings {
+		byKind[finding.Kind] = finding
+	}
+	require.Equal(t, "cookies.json", byKind["cookie-value:session-id"].Path)
+	require.Equal(t, 1, byKind["cookie-value:session-id"].Line)
+	require.Equal(t, "cookies-reversed.json", byKind["cookie-value:x-main"].Path)
+	require.Equal(t, 1, byKind["cookie-value:x-main"].Line)
+	require.Equal(t, "cookies-pretty.json", byKind["cookie-value:y-main"].Path)
+	require.Equal(t, 3, byKind["cookie-value:y-main"].Line)
+}
+
+func TestFindPackageSecretsCombinesVendorPrefixAndDeclaredCookies(t *testing.T) {
+	root := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(root, "README.md"), []byte("Cookie: session-id=actuallyrealcookievaluexyz\n"), 0o644))
+	require.NoError(t, os.WriteFile(filepath.Join(root, "spec.json"), []byte("\"token\":\""+testSecret("sk", "-or-v1-", "abcdefghijklmnopqrstuvwxyz1234567890")+"\"\n"), 0o644))
+
+	findings, err := FindPackageSecrets(root, []string{"session-id"})
+	require.NoError(t, err)
+	require.Len(t, findings, 2)
+	byKind := map[string]VendorPrefixSecretFinding{}
+	for _, finding := range findings {
+		byKind[finding.Kind] = finding
+	}
+	require.Equal(t, "README.md", byKind["cookie-value:session-id"].Path)
+	require.Equal(t, 1, byKind["cookie-value:session-id"].Line)
+	require.Equal(t, "spec.json", byKind["openrouter-api-key"].Path)
+	require.Equal(t, 1, byKind["openrouter-api-key"].Line)
+}
+
 func testSecret(parts ...string) string {
 	return strings.Join(parts, "")
 }
diff --git a/internal/browsersniff/fixtures.go b/internal/browsersniff/fixtures.go
index c5306995..7a3284ad 100644
--- a/internal/browsersniff/fixtures.go
+++ b/internal/browsersniff/fixtures.go
@@ -2,19 +2,29 @@ package browsersniff
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/url"
+	"regexp"
 	"sort"
 	"strings"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/discovery"
+	"github.com/mvanhorn/cli-printing-press/v4/internal/piiplaceholders"
 )
 
+type FixtureValue struct {
+	Name  string
+	Value string
+}
+
 type TestFixture struct {
 	EndpointName string
 	Method       string
 	Path         string
 	ParamNames   []string
 	BodyFields   []string
+	ParamSamples []FixtureValue
+	BodySamples  []FixtureValue
 	HasAuth      bool
 }
 
@@ -58,6 +68,8 @@ func GenerateFixtures(capture *EnrichedCapture) *FixtureSet {
 
 		paramNames := make(map[string]struct{})
 		bodyFields := make(map[string]struct{})
+		paramSamples := make(map[string]string)
+		bodySamples := make(map[string]string)
 
 		for _, entry := range group.Entries {
 			entryFixture := SanitizeForFixture(entry)
@@ -75,10 +87,14 @@ func GenerateFixtures(capture *EnrichedCapture) *FixtureSet {
 				bodyFields[name] = struct{}{}
 				paramNames[name] = struct{}{}
 			}
+			mergeFixtureSamples(paramSamples, entryFixture.ParamSamples)
+			mergeFixtureSamples(bodySamples, entryFixture.BodySamples)
 		}
 
 		fixture.ParamNames = sortedKeys(paramNames)
 		fixture.BodyFields = sortedKeys(bodyFields)
+		fixture.ParamSamples = sortedFixtureValues(paramSamples)
+		fixture.BodySamples = sortedFixtureValues(bodySamples)
 		fixtureSet.Fixtures = append(fixtureSet.Fixtures, fixture)
 	}
 
@@ -94,51 +110,127 @@ func SanitizeForFixture(entry EnrichedEntry) TestFixture {
 	parsedURL, err := url.Parse(entry.URL)
 	if err == nil {
 		queryNames := make(map[string]struct{}, len(parsedURL.Query()))
-		for name := range parsedURL.Query() {
+		querySamples := make(map[string]string, len(parsedURL.Query()))
+		for name, values := range parsedURL.Query() {
 			queryNames[name] = struct{}{}
+			if len(values) > 0 {
+				querySamples[name] = syntheticFixtureValue(name, values[0])
+			}
 		}
 		fixture.ParamNames = sortedKeys(queryNames)
+		fixture.ParamSamples = sortedFixtureValues(querySamples)
 	}
 
 	contentType := strings.ToLower(getHeaderValue(entry.RequestHeaders, "Content-Type"))
-	bodyFields := extractBodyFieldNames(entry.RequestBody, contentType)
+	bodyFields, bodySamples := extractBodyFieldSamples(entry.RequestBody, contentType)
 	fixture.BodyFields = bodyFields
+	fixture.BodySamples = sortedFixtureValues(bodySamples)
 	fixture.ParamNames = mergeSortedNames(fixture.ParamNames, bodyFields)
+	mergeFixtureSamplesMap := map[string]string{}
+	mergeFixtureSamples(mergeFixtureSamplesMap, fixture.ParamSamples)
+	mergeFixtureSamples(mergeFixtureSamplesMap, fixture.BodySamples)
+	fixture.ParamSamples = sortedFixtureValues(mergeFixtureSamplesMap)
 	fixture.HasAuth = hasAuthHeaders(entry.RequestHeaders)
 
 	return fixture
 }
 
-func extractBodyFieldNames(body string, contentType string) []string {
+func extractBodyFieldSamples(body string, contentType string) ([]string, map[string]string) {
 	body = strings.TrimSpace(body)
 	if body == "" {
-		return nil
+		return nil, nil
 	}
 
 	switch {
 	case strings.Contains(contentType, "json"):
 		var value any
 		if err := json.Unmarshal([]byte(body), &value); err != nil {
-			return nil
+			return nil, nil
 		}
 
 		root := topLevelObject(value)
 		if root == nil {
-			return nil
+			return nil, nil
 		}
 
 		fields := make([]string, 0, len(root))
+		samples := make(map[string]string, len(root))
 		for key := range root {
 			fields = append(fields, key)
+			samples[key] = syntheticFixtureValue(key, root[key])
 		}
 		sort.Strings(fields)
-		return fields
+		return fields, samples
 	case strings.Contains(contentType, "form-urlencoded"):
 		values := ParseFormBody(body)
-		return sortedKeysFromMap(values)
+		samples := make(map[string]string, len(values))
+		for key, value := range values {
+			samples[key] = syntheticFixtureValue(key, value)
+		}
+		return sortedKeysFromMap(values), samples
+	default:
+		return nil, nil
+	}
+}
+
+var (
+	moneyValueRE     = regexp.MustCompile(`^\$?\d+(?:\.\d{2})?$`)
+	dateValueRE      = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
+	cardLast4ValueRE = regexp.MustCompile(`^\d{4}$`)
+)
+
+func syntheticFixtureValue(name string, raw any) string {
+	value := strings.TrimSpace(fmt.Sprint(raw))
+	nameLower := strings.ToLower(name)
+	switch {
+	case piiplaceholders.OrderIDPattern().MatchString(value):
+		if strings.HasPrefix(strings.ToUpper(value), "D01-") {
+			return piiplaceholders.SyntheticDigitalOrderID
+		}
+		return piiplaceholders.SyntheticOrderID
+	case piiplaceholders.ASINPattern().MatchString(value):
+		return piiplaceholders.PrimarySyntheticASIN
+	case cardLast4ValueRE.MatchString(value) && strings.Contains(nameLower, "last"):
+		return piiplaceholders.SyntheticCardLast4
+	case moneyValueRE.MatchString(value) && (strings.Contains(nameLower, "money") || strings.Contains(nameLower, "amount") || strings.Contains(nameLower, "price") || strings.Contains(nameLower, "total")):
+		return piiplaceholders.SyntheticMoney
+	case dateValueRE.MatchString(value) || isDateFixtureField(nameLower):
+		return piiplaceholders.SyntheticDate
 	default:
+		return "example-value"
+	}
+}
+
+func isDateFixtureField(nameLower string) bool {
+	normalized := strings.ReplaceAll(nameLower, "-", "_")
+	return normalized == "date" || strings.HasSuffix(normalized, "_date")
+}
+
+func mergeFixtureSamples(samples map[string]string, values []FixtureValue) {
+	for _, value := range values {
+		if value.Name == "" {
+			continue
+		}
+		if _, exists := samples[value.Name]; !exists {
+			samples[value.Name] = value.Value
+		}
+	}
+}
+
+func sortedFixtureValues(samples map[string]string) []FixtureValue {
+	if len(samples) == 0 {
 		return nil
 	}
+	names := make([]string, 0, len(samples))
+	for name := range samples {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	values := make([]FixtureValue, 0, len(names))
+	for _, name := range names {
+		values = append(values, FixtureValue{Name: name, Value: samples[name]})
+	}
+	return values
 }
 
 func hasAuthHeaders(headers map[string]string) bool {
diff --git a/internal/browsersniff/fixtures_test.go b/internal/browsersniff/fixtures_test.go
index 6ee49416..d0ea09b3 100644
--- a/internal/browsersniff/fixtures_test.go
+++ b/internal/browsersniff/fixtures_test.go
@@ -151,6 +151,62 @@ func TestSanitizeForFixture(t *testing.T) {
 				BodyFields: []string{"client_id", "grant_type"},
 			},
 		},
+		{
+			name: "captured value shapes become synthetic samples",
+			entry: EnrichedEntry{
+				Method: "POST",
+				URL:    "https://api.example.com/orders?order_id=123-4567890-1234567&asin=B012345678",
+				RequestHeaders: map[string]string{
+					"Content-Type": "application/json",
+				},
+				RequestBody: `{"card_last4":"4242","amount":"99.95","purchased_date":"2026-05-01"}`,
+			},
+			want: TestFixture{
+				Method:     "POST",
+				Path:       "/orders",
+				ParamNames: []string{"amount", "asin", "card_last4", "order_id", "purchased_date"},
+				BodyFields: []string{"amount", "card_last4", "purchased_date"},
+				ParamSamples: []FixtureValue{
+					{Name: "amount", Value: "12.34"},
+					{Name: "asin", Value: "B0EXAMPLE1"},
+					{Name: "card_last4", Value: "LAST4"},
+					{Name: "order_id", Value: "111-1111111-1111111"},
+					{Name: "purchased_date", Value: "2026-01-15"},
+				},
+				BodySamples: []FixtureValue{
+					{Name: "amount", Value: "12.34"},
+					{Name: "card_last4", Value: "LAST4"},
+					{Name: "purchased_date", Value: "2026-01-15"},
+				},
+			},
+		},
+		{
+			name: "date substring in unrelated field does not become date sample",
+			entry: EnrichedEntry{
+				Method: "POST",
+				URL:    "https://api.example.com/check",
+				RequestHeaders: map[string]string{
+					"Content-Type": "application/json",
+				},
+				RequestBody: `{"validate":"pending","candidate":"alice","update":"later"}`,
+			},
+			want: TestFixture{
+				Method:     "POST",
+				Path:       "/check",
+				ParamNames: []string{"candidate", "update", "validate"},
+				BodyFields: []string{"candidate", "update", "validate"},
+				ParamSamples: []FixtureValue{
+					{Name: "candidate", Value: "example-value"},
+					{Name: "update", Value: "example-value"},
+					{Name: "validate", Value: "example-value"},
+				},
+				BodySamples: []FixtureValue{
+					{Name: "candidate", Value: "example-value"},
+					{Name: "update", Value: "example-value"},
+					{Name: "validate", Value: "example-value"},
+				},
+			},
+		},
 	}
 
 	for _, tt := range tests {
@@ -162,6 +218,12 @@ func TestSanitizeForFixture(t *testing.T) {
 			assert.Equal(t, tt.want.Path, fixture.Path)
 			assert.Equal(t, tt.want.ParamNames, fixture.ParamNames)
 			assert.Equal(t, tt.want.BodyFields, fixture.BodyFields)
+			if tt.want.ParamSamples != nil {
+				assert.Equal(t, tt.want.ParamSamples, fixture.ParamSamples)
+			}
+			if tt.want.BodySamples != nil {
+				assert.Equal(t, tt.want.BodySamples, fixture.BodySamples)
+			}
 		})
 	}
 }
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 1e2dfe8e..7e6660dd 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -408,10 +408,15 @@ func newPublishPackageCmd() *cobra.Command {
 				fmt.Fprintln(os.Stderr, "warning: no manuscripts found, packaging without them")
 			}
 
-			findings, err := artifacts.FindVendorPrefixSecrets(outCLIDir)
+			cookieNames, err := stagedPackageCookieNames(outCLIDir)
 			if err != nil {
 				cleanupOnFailure()
-				return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("scanning staged package for vendor-prefix tokens: %w", err)}
+				return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("reading staged package cookie auth metadata: %w", err)}
+			}
+			findings, err := artifacts.FindPackageSecrets(outCLIDir, cookieNames)
+			if err != nil {
+				cleanupOnFailure()
+				return &ExitError{Code: ExitPublishError, Err: fmt.Errorf("scanning staged package for secret tokens: %w", err)}
 			}
 
 			piiResult, piiErr := artifacts.RunPIIAudit(outCLIDir)
@@ -1160,6 +1165,22 @@ func hasContent(dir string) bool {
 	return false
 }
 
+func stagedPackageCookieNames(dir string) ([]string, error) {
+	manifest, err := pipeline.ReadToolsManifest(dir)
+	if err != nil {
+		if errors.Is(err, fs.ErrNotExist) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	switch strings.ToLower(strings.TrimSpace(manifest.Auth.Type)) {
+	case "cookie", "composed":
+		return manifest.Auth.Cookies, nil
+	default:
+		return nil, nil
+	}
+}
+
 // formatCombinedScanError composes the publish-time error message from
 // both scanners. Sections appear in fixed order: vendor-prefix tokens,
 // then PII pending findings, then PII gate failures. Returns nil when
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index e0fc5dcd..eecef4f6 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -706,6 +706,31 @@ func TestPublishPackageRejectsVendorPrefixSecretsInStagedCLI(t *testing.T) {
 	assert.ErrorIs(t, statErr, os.ErrNotExist, "failed packaging should clean up the staging target")
 }
 
+func TestPublishPackageRejectsSpecDeclaredCookieValuesInStagedCLI(t *testing.T) {
+	for _, authType := range []string{"cookie", "composed"} {
+		t.Run(authType, func(t *testing.T) {
+			home := setLibraryTestEnv(t)
+			cliDir := filepath.Join(home, "library", "test-pp-cli")
+			writePublishableTestCLI(t, cliDir)
+			require.NoError(t, os.WriteFile(filepath.Join(cliDir, "tools-manifest.json"), []byte(`{"auth":{"type":"`+authType+`","cookies":["session-id"]}}`+"\n"), 0o644))
+			require.NoError(t, os.WriteFile(filepath.Join(cliDir, "README.md"), []byte("Cookie: session-id=actuallyrealcookievaluexyz\n"), 0o644))
+
+			target := filepath.Join(t.TempDir(), "staging")
+			cmd := newPublishCmd()
+			cmd.SetArgs([]string{"package", "--dir", cliDir, "--category", "other", "--target", target, "--json"})
+
+			err := cmd.Execute()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), "vendor-prefix tokens detected")
+			assert.Contains(t, err.Error(), "README.md:1 cookie-value:session-id")
+			assert.NotContains(t, err.Error(), "actuallyrealcookievaluexyz")
+
+			_, statErr := os.Stat(target)
+			assert.ErrorIs(t, statErr, os.ErrNotExist, "failed packaging should clean up the staging target")
+		})
+	}
+}
+
 func TestPublishPackageRejectsPIIInStagedCLI(t *testing.T) {
 	home := setLibraryTestEnv(t)
 	cliDir := filepath.Join(home, "library", "test-pp-cli")
diff --git a/internal/generator/captured_fixture_test.go b/internal/generator/captured_fixture_test.go
new file mode 100644
index 00000000..d133f33c
--- /dev/null
+++ b/internal/generator/captured_fixture_test.go
@@ -0,0 +1,50 @@
+package generator
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/browsersniff"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGenerateCapturedFixtureUsesSyntheticSamples(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("captured-placeholders")
+	outputDir := filepath.Join(t.TempDir(), "captured-placeholders-pp-cli")
+	gen := New(apiSpec, outputDir)
+	gen.FixtureSet = &browsersniff.FixtureSet{
+		Fixtures: []browsersniff.TestFixture{
+			{
+				EndpointName: "get_order",
+				Method:       "GET",
+				Path:         "/orders",
+				ParamSamples: []browsersniff.FixtureValue{
+					{Name: "amount", Value: "12.34"},
+					{Name: "asin", Value: "B0EXAMPLE1"},
+					{Name: "card_last4", Value: "LAST4"},
+					{Name: "order_id", Value: "111-1111111-1111111"},
+					{Name: "purchased_date", Value: "2026-01-15"},
+				},
+			},
+		},
+	}
+
+	require.NoError(t, gen.Generate())
+	data, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client_captured_test.go"))
+	require.NoError(t, err)
+	src := string(data)
+
+	require.Contains(t, src, `"order_id"`)
+	require.Contains(t, src, `"111-1111111-1111111"`)
+	require.Contains(t, src, `"asin"`)
+	require.Contains(t, src, `"B0EXAMPLE1"`)
+	require.Contains(t, src, `"card_last4"`)
+	require.Contains(t, src, `"LAST4"`)
+	require.Contains(t, src, `"amount"`)
+	require.Contains(t, src, `"12.34"`)
+	require.Contains(t, src, `"purchased_date"`)
+	require.Contains(t, src, `"2026-01-15"`)
+}
diff --git a/internal/generator/example_value_test.go b/internal/generator/example_value_test.go
index 5159ac00..b2306709 100644
--- a/internal/generator/example_value_test.go
+++ b/internal/generator/example_value_test.go
@@ -56,6 +56,15 @@ func TestExampleValueIDRecognition(t *testing.T) {
 		{"camel movieId uuid type", spec.Param{Name: "movieId", Type: "uuid"}, uuid},
 		{"camel personId guid type", spec.Param{Name: "personId", Type: "guid"}, uuid},
 
+		// Browser-sniff customer-data shapes route to canonical synthetic placeholders.
+		{"asin", spec.Param{Name: "asin", Type: "string"}, "B0EXAMPLE1"},
+		{"card last four", spec.Param{Name: "card_last4", Type: "string"}, "LAST4"},
+		{"recipient", spec.Param{Name: "recipient_name", Type: "string"}, "Test User"},
+		{"address", spec.Param{Name: "shipping_address", Type: "string"}, "123 Test St, Anytown, ST 12345"},
+		{"generic order id stays uuid", spec.Param{Name: "orderId", Type: "string"}, uuid},
+		{"address id stays uuid", spec.Param{Name: "shipping_address_id", Type: "string"}, uuid},
+		{"recipient id stays uuid", spec.Param{Name: "recipient_id", Type: "string"}, uuid},
+
 		// Negative — does not end in `id`.
 		{"userIdentifier", spec.Param{Name: "userIdentifier", Type: "string"}, "example-value"},
 		{"empty name", spec.Param{Name: "", Type: "string"}, "example-value"},
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 43627132..3d26ebfb 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -4275,6 +4275,10 @@ func (g *Generator) mcpParamDescription(p spec.Param) string {
 }
 
 func exampleValue(p spec.Param) string {
+	if value, ok := syntheticExampleValue(p.Name); ok {
+		return value
+	}
+
 	nameLower := strings.ToLower(p.Name)
 
 	// camelCase `*Id` carries an exclusion fence so bool/numeric params
diff --git a/internal/generator/placeholders.go b/internal/generator/placeholders.go
new file mode 100644
index 00000000..f50923b2
--- /dev/null
+++ b/internal/generator/placeholders.go
@@ -0,0 +1,27 @@
+package generator
+
+import (
+	"strings"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/piiplaceholders"
+)
+
+// syntheticExampleValue returns canonical fake values for high-risk browser-
+// sniff shapes so generated examples do not echo captured customer data.
+func syntheticExampleValue(paramName string) (string, bool) {
+	nameLower := strings.ToLower(paramName)
+	isIDField := nameLower == "id" || strings.HasSuffix(nameLower, "_id") ||
+		strings.HasSuffix(nameLower, "id")
+	switch {
+	case strings.Contains(nameLower, "asin"):
+		return piiplaceholders.PrimarySyntheticASIN, true
+	case (strings.Contains(nameLower, "card") || strings.Contains(nameLower, "payment")) &&
+		(strings.Contains(nameLower, "last4") || strings.Contains(nameLower, "last_4") || strings.Contains(nameLower, "last-four")):
+		return piiplaceholders.SyntheticCardLast4, true
+	case strings.Contains(nameLower, "recipient") && !isIDField:
+		return piiplaceholders.SyntheticRecipient, true
+	case strings.Contains(nameLower, "address") && !isIDField:
+		return piiplaceholders.SyntheticAddress, true
+	}
+	return "", false
+}
diff --git a/internal/generator/templates/captured_test.go.tmpl b/internal/generator/templates/captured_test.go.tmpl
index 39514208..6350bf39 100644
--- a/internal/generator/templates/captured_test.go.tmpl
+++ b/internal/generator/templates/captured_test.go.tmpl
@@ -10,6 +10,18 @@ import (
 
 {{range .Fixtures}}
 func TestCaptured_{{.EndpointName}}_RequestShape(t *testing.T) {
+	sampleParams := map[string]string{
+{{- range .ParamSamples}}
+		{{printf "%q" .Name}}: {{printf "%q" .Value}},
+{{- end}}
+	}
+	sampleBody := map[string]string{
+{{- range .BodySamples}}
+		{{printf "%q" .Name}}: {{printf "%q" .Value}},
+{{- end}}
+	}
+	_, _ = sampleParams, sampleBody
+
 	var capturedReq *http.Request
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		capturedReq = r
diff --git a/internal/mcpdesc/params.go b/internal/mcpdesc/params.go
index e44346dd..6493f0f3 100644
--- a/internal/mcpdesc/params.go
+++ b/internal/mcpdesc/params.go
@@ -5,6 +5,7 @@ import (
 	"unicode/utf8"
 
 	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
+	"github.com/mvanhorn/cli-printing-press/v4/internal/piiplaceholders"
 	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
 )
 
@@ -59,12 +60,12 @@ func NewParamDescriptionCompactorForEndpoints(endpoints []spec.Endpoint) *ParamD
 func (c *ParamDescriptionCompactor) Description(p spec.Param) string {
 	description := naming.OneLineNormalize(p.Description)
 	if c == nil || description == "" {
-		return description
+		return piiplaceholders.SanitizeCapturedExamples(description)
 	}
 	if compacted, ok := c.compacted[keyForParamDescription(p, description)]; ok {
-		return compacted
+		description = compacted
 	}
-	return description
+	return piiplaceholders.SanitizeCapturedExamples(description)
 }
 
 func appendResourceEndpoints(endpoints []spec.Endpoint, resource spec.Resource) []spec.Endpoint {
diff --git a/internal/mcpdesc/params_test.go b/internal/mcpdesc/params_test.go
index 4328c9cb..28673c85 100644
--- a/internal/mcpdesc/params_test.go
+++ b/internal/mcpdesc/params_test.go
@@ -105,3 +105,25 @@ func TestParamDescriptionCompactorTruncatesUnicodeSafely(t *testing.T) {
 	assert.LessOrEqual(t, utf8.RuneCountInString(got), sharedParamDescriptionMax)
 	assert.True(t, strings.HasSuffix(got, sharedParamDescriptionTail))
 }
+
+func TestParamDescriptionCompactorSanitizesCapturedExamples(t *testing.T) {
+	description := "Order ID, format XXX-XXXXXXX-XXXXXXX (e.g. 123-4567890-1234567), ASIN B012345678, card ending in 4242."
+	api := &spec.APISpec{
+		Resources: map[string]spec.Resource{
+			"orders": {
+				Endpoints: map[string]spec.Endpoint{
+					"get": {Params: []spec.Param{{Name: "order_id", Type: "string", Description: description}}},
+				},
+			},
+		},
+	}
+
+	got := NewParamDescriptionCompactor(api).Description(spec.Param{Name: "order_id", Type: "string", Description: description})
+
+	assert.Contains(t, got, "e.g. 111-1111111-1111111")
+	assert.Contains(t, got, "ASIN B0EXAMPLE1")
+	assert.Contains(t, got, "card ending in LAST4")
+	assert.NotContains(t, got, "123-4567890-1234567")
+	assert.NotContains(t, got, "B012345678")
+	assert.NotContains(t, got, "4242")
+}
diff --git a/internal/piiplaceholders/placeholders.go b/internal/piiplaceholders/placeholders.go
new file mode 100644
index 00000000..838cf4dd
--- /dev/null
+++ b/internal/piiplaceholders/placeholders.go
@@ -0,0 +1,92 @@
+package piiplaceholders
+
+import (
+	"regexp"
+	"strings"
+)
+
+const (
+	SyntheticOrderID        = "111-1111111-1111111"
+	SyntheticDigitalOrderID = "D01-1111111-1111111"
+	SyntheticCardLast4      = "LAST4"
+	SyntheticRecipient      = "Test User"
+	SyntheticAddress        = "123 Test St, Anytown, ST 12345"
+	SyntheticMoney          = "12.34"
+	SyntheticDate           = "2026-01-15"
+	SyntheticCookieValue    = "your-cookie-here"
+	PrimarySyntheticASIN    = "B0EXAMPLE1"
+)
+
+var syntheticASINs = [...]string{
+	"B0EXAMPLE1",
+	"B0EXAMPLE2",
+	"B0EXAMPLE3",
+	"B0EXAMPLE4",
+	"B0EXAMPLE5",
+	"B0EXAMPLE6",
+	"B0EXAMPLE7",
+	"B0EXAMPLE8",
+}
+
+var (
+	orderIDExampleRE   = regexp.MustCompile(`\b(?:\d{3}|D01)-\d{7}-\d{7}\b`)
+	asinExampleRE      = regexp.MustCompile(`\bB0[A-Z0-9]{8}\b`)
+	cardLast4ExampleRE = regexp.MustCompile(`(?i)((?:card|visa|mastercard|amex|ending in|last\s+4)[^0-9]{0,10})\d{4}`)
+	placeholderRE      = regexp.MustCompile(`^your-[a-z0-9-]+-here$`)
+	xPlaceholderRE     = regexp.MustCompile(`^x+$`)
+)
+
+func OrderIDPattern() *regexp.Regexp {
+	return orderIDExampleRE
+}
+
+func ASINPattern() *regexp.Regexp {
+	return asinExampleRE
+}
+
+// SanitizeCapturedExamples replaces captured-value examples in user-authored
+// parameter descriptions with canonical synthetic values.
+func SanitizeCapturedExamples(description string) string {
+	out := orderIDExampleRE.ReplaceAllStringFunc(description, func(match string) string {
+		if strings.HasPrefix(strings.ToUpper(match), "D01-") {
+			return SyntheticDigitalOrderID
+		}
+		return SyntheticOrderID
+	})
+	out = asinExampleRE.ReplaceAllString(out, PrimarySyntheticASIN)
+	out = cardLast4ExampleRE.ReplaceAllString(out, "${1}"+SyntheticCardLast4)
+	return out
+}
+
+func IsSyntheticOrderID(value string) bool {
+	normalized := strings.ToLower(strings.TrimSpace(value))
+	return normalized == strings.ToLower(SyntheticOrderID) ||
+		normalized == strings.ToLower(SyntheticDigitalOrderID)
+}
+
+func IsSyntheticASIN(value string) bool {
+	for _, candidate := range syntheticASINs {
+		if strings.EqualFold(candidate, strings.TrimSpace(value)) {
+			return true
+		}
+	}
+	return false
+}
+
+func IsSyntheticPostalAddress(value string) bool {
+	return strings.HasPrefix(strings.ToLower(SyntheticAddress), strings.ToLower(strings.TrimSpace(value)))
+}
+
+// IsSyntheticCookieValue reports whether a spec-declared cookie assignment
+// uses placeholder text rather than captured session material.
+func IsSyntheticCookieValue(value string) bool {
+	normalized := strings.ToLower(strings.Trim(strings.TrimSpace(value), `"'`))
+	if normalized == "" {
+		return true
+	}
+	switch normalized {
+	case "example", "example-cookie", SyntheticCookieValue, "<redacted>", "redacted":
+		return true
+	}
+	return placeholderRE.MatchString(normalized) || xPlaceholderRE.MatchString(normalized)
+}
diff --git a/internal/piiplaceholders/placeholders_test.go b/internal/piiplaceholders/placeholders_test.go
new file mode 100644
index 00000000..0243a8da
--- /dev/null
+++ b/internal/piiplaceholders/placeholders_test.go
@@ -0,0 +1,22 @@
+package piiplaceholders
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSanitizeCapturedExamples(t *testing.T) {
+	description := "order 123-4567890-1234567, digital D01-4567890-1234567, ASIN B012345678, card ending in 4242"
+
+	got := SanitizeCapturedExamples(description)
+
+	assert.Contains(t, got, SyntheticOrderID)
+	assert.Contains(t, got, SyntheticDigitalOrderID)
+	assert.Contains(t, got, PrimarySyntheticASIN)
+	assert.Contains(t, got, "card ending in "+SyntheticCardLast4)
+	assert.NotContains(t, got, "123-4567890-1234567")
+	assert.NotContains(t, got, "D01-4567890-1234567")
+	assert.NotContains(t, got, "B012345678")
+	assert.NotContains(t, got, "4242")
+}
diff --git a/internal/pipeline/lock.go b/internal/pipeline/lock.go
index f5f3586b..8b280799 100644
--- a/internal/pipeline/lock.go
+++ b/internal/pipeline/lock.go
@@ -447,7 +447,7 @@ func validatePIIGateForPromote(workingDir string) error {
 		msg += "gate failures:\n" + artifacts.FormatPIIGateFailures(result.Completion) + "\n"
 	}
 	msg += fmt.Sprintf("ledger: %s\n", ledgerPath)
-	msg += "scope: phase-1 detectors (card-last-4, email, phone, ZIP+4, postal-address); order-IDs, ASINs, and standalone names are a future detector class.\n"
+	msg += "scope: phase-1 detectors (order-id, ASIN, card-last-4, email, phone, ZIP+4, postal-address); standalone names are a future detector class.\n"
 	msg += "run `printing-press pii-audit <dir>` and follow skills/printing-press-polish/references/pii-polish.md"
 	return fmt.Errorf("%s", msg)
 }
diff --git a/internal/pipeline/toolsmanifest.go b/internal/pipeline/toolsmanifest.go
index f505c495..3a0a4875 100644
--- a/internal/pipeline/toolsmanifest.go
+++ b/internal/pipeline/toolsmanifest.go
@@ -47,6 +47,7 @@ type ManifestAuth struct {
 	EnvVarSpecs                    []spec.AuthEnvVar `json:"env_var_specs,omitempty"`
 	KeyURL                         string            `json:"key_url,omitempty"`
 	CookieDomain                   string            `json:"cookie_domain,omitempty"`
+	Cookies                        []string          `json:"cookies,omitempty"`
 	RequiresBrowserSession         bool              `json:"requires_browser_session,omitempty"`
 	BrowserSessionValidationPath   string            `json:"browser_session_validation_path,omitempty"`
 	BrowserSessionValidationMethod string            `json:"browser_session_validation_method,omitempty"`
@@ -230,6 +231,7 @@ func manifestAuth(auth spec.AuthConfig) ManifestAuth {
 		EnvVarSpecs:                    auth.EnvVarSpecs,
 		KeyURL:                         auth.KeyURL,
 		CookieDomain:                   auth.CookieDomain,
+		Cookies:                        auth.Cookies,
 		RequiresBrowserSession:         auth.RequiresBrowserSession,
 		BrowserSessionValidationPath:   auth.BrowserSessionValidationPath,
 		BrowserSessionValidationMethod: auth.BrowserSessionValidationMethod,
diff --git a/internal/pipeline/toolsmanifest_test.go b/internal/pipeline/toolsmanifest_test.go
index b225f2df..d6c01ac0 100644
--- a/internal/pipeline/toolsmanifest_test.go
+++ b/internal/pipeline/toolsmanifest_test.go
@@ -559,7 +559,7 @@ func TestWriteToolsManifest_CookieAuthOnlyNoAuthEndpoints(t *testing.T) {
 	parsed := &spec.APISpec{
 		Name:    "cookie-api",
 		BaseURL: "https://api.example.com",
-		Auth:    spec.AuthConfig{Type: "cookie", EnvVars: []string{"COOKIE"}},
+		Auth:    spec.AuthConfig{Type: "cookie", EnvVars: []string{"COOKIE"}, Cookies: []string{"session-id", "x-main"}},
 		Resources: map[string]spec.Resource{
 			"Items": {
 				Endpoints: map[string]spec.Endpoint{
@@ -585,6 +585,7 @@ func TestWriteToolsManifest_CookieAuthOnlyNoAuthEndpoints(t *testing.T) {
 	assert.Equal(t, "items_public_count", got.Tools[0].Name)
 	assert.Equal(t, "items_public_list", got.Tools[1].Name)
 	assert.Equal(t, "partial", got.MCPReady)
+	assert.Equal(t, []string{"session-id", "x-main"}, got.Auth.Cookies)
 }
 
 func TestWriteToolsManifest_ComposedAuthOnlyNoAuthEndpoints(t *testing.T) {
@@ -592,7 +593,7 @@ func TestWriteToolsManifest_ComposedAuthOnlyNoAuthEndpoints(t *testing.T) {
 	parsed := &spec.APISpec{
 		Name:    "composed-api",
 		BaseURL: "https://api.example.com",
-		Auth:    spec.AuthConfig{Type: "composed", EnvVars: []string{"AUTH_TOKEN"}},
+		Auth:    spec.AuthConfig{Type: "composed", EnvVars: []string{"AUTH_TOKEN"}, Cookies: []string{"session-id"}},
 		Resources: map[string]spec.Resource{
 			"Items": {
 				Endpoints: map[string]spec.Endpoint{
@@ -614,6 +615,7 @@ func TestWriteToolsManifest_ComposedAuthOnlyNoAuthEndpoints(t *testing.T) {
 
 	require.Len(t, got.Tools, 1)
 	assert.Equal(t, "items_public_list", got.Tools[0].Name)
+	assert.Equal(t, []string{"session-id"}, got.Auth.Cookies)
 }
 
 func TestWriteToolsManifest_EmptyDescription(t *testing.T) {

← 8c955701 fix(cli): correct live-check and helper emission (#1637)  ·  back to Cli Printing Press  ·  fix(cli): gate ship plans on agent readiness (#1638) ed17d302 →