// Package anthropicfp provides pure helpers for suppressing client-side // fingerprints that would otherwise be visible to upstream Anthropic when a // forwarding gateway sits between the client and api.anthropic.com. // // Currently exposes NormalizeDateline: it rewrites the "Today's date is // YYYY-MM-DD." sentence inside a request body back to a canonical ASCII form, // erasing three bits of steganographic signal (four apostrophe code points and // a date-separator variant) that some clients embed in that sentence when // they detect a non-official base URL. package anthropicfp import ( "fmt" "regexp" "sort" "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // datelineRegexes matches the fingerprinted sentence with any of the four // apostrophe code points seen in the wild and either separator. Two regexes // are used because Go's RE2-based regexp package does not support // backreferences: matching `-` and `/` in two passes keeps the two separators // inside YYYY?MM?DD forced to agree, so mixed-separator strings like // "Today's date is 2026-07/01." never match. This is what filters out // user-authored prose like "Today is foo." or "His date is 2026-06-30." from // being touched. var ( datelineRegexHyphen = regexp.MustCompile(`Today(['’ʼʹ])s date is (\d{4})-(\d{2})-(\d{2})\.`) datelineRegexSlash = regexp.MustCompile(`Today(['’ʼʹ])s date is (\d{4})/(\d{2})/(\d{2})\.`) ) // systemReminderRegex matches a block. The dateline lives in // this block once the conversation has advanced past the first turn (system // prompt caching hides the top-level system block for subsequent turns), so // the messages[].content[] scan is confined to what lives inside these tags. var systemReminderRegex = regexp.MustCompile(`(?s).*?`) // DatelineHit records what a single rewrite normalized, for observability. type DatelineHit struct { // ApostropheVariant is one of "ascii" (U+0027), "u2019", "u02bc", "u02b9". ApostropheVariant string // DateSeparator is either "-" or "/" as seen before normalization. DateSeparator string } // canonicalize returns the canonical form of a matched dateline sentence. // The output always uses ASCII apostrophe and hyphen separators. func canonicalize(year, month, day string) string { return fmt.Sprintf("Today's date is %s-%s-%s.", year, month, day) } func apostropheVariant(r rune) string { switch r { case '’': return "u2019" case 'ʼ': return "u02bc" case 'ʹ': return "u02b9" default: return "ascii" } } type datelineMatch struct { start, end int apoRune rune sep string year, month, day string } func collectMatches(text string, re *regexp.Regexp, sep string) []datelineMatch { locs := re.FindAllStringSubmatchIndex(text, -1) if len(locs) == 0 { return nil } out := make([]datelineMatch, 0, len(locs)) for _, m := range locs { var apoRune rune for _, r := range text[m[2]:m[3]] { apoRune = r break } out = append(out, datelineMatch{ start: m[0], end: m[1], apoRune: apoRune, sep: sep, year: text[m[4]:m[5]], month: text[m[6]:m[7]], day: text[m[8]:m[9]], }) } return out } // NormalizeText replaces every fingerprinted dateline sentence in text with // its canonical form. It returns the possibly-rewritten text and the list of // hits observed. When no match is found the original string is returned // verbatim (byte-identical), and the returned hit slice is nil. func NormalizeText(text string) (string, []DatelineHit) { if !strings.Contains(text, "date is ") { return text, nil } matches := collectMatches(text, datelineRegexHyphen, "-") matches = append(matches, collectMatches(text, datelineRegexSlash, "/")...) if len(matches) == 0 { return text, nil } sort.Slice(matches, func(i, j int) bool { return matches[i].start < matches[j].start }) var b strings.Builder b.Grow(len(text)) prev := 0 hits := make([]DatelineHit, 0, len(matches)) changed := false for _, m := range matches { full := text[m.start:m.end] canonical := canonicalize(m.year, m.month, m.day) if canonical == full { // Already canonical: no rewrite, no hit. continue } _, _ = b.WriteString(text[prev:m.start]) _, _ = b.WriteString(canonical) prev = m.end changed = true hits = append(hits, DatelineHit{ ApostropheVariant: apostropheVariant(m.apoRune), DateSeparator: m.sep, }) } if !changed { return text, nil } _, _ = b.WriteString(text[prev:]) return b.String(), hits } // normalizeSystemReminderScopedText scans only the blocks // inside text and normalizes datelines inside them. Text outside the blocks is // preserved byte-for-byte, so user prose, tool_result content, code blocks, // or shell commands that happen to contain an apostrophe or a slash date are // never touched. func normalizeSystemReminderScopedText(text string) (string, []DatelineHit) { if !strings.Contains(text, "") { return text, nil } locs := systemReminderRegex.FindAllStringIndex(text, -1) if len(locs) == 0 { return text, nil } var b strings.Builder b.Grow(len(text)) prev := 0 var hits []DatelineHit changed := false for _, loc := range locs { _, _ = b.WriteString(text[prev:loc[0]]) block := text[loc[0]:loc[1]] normalized, blockHits := NormalizeText(block) if normalized != block { changed = true } _, _ = b.WriteString(normalized) hits = append(hits, blockHits...) prev = loc[1] } if !changed { return text, nil } _, _ = b.WriteString(text[prev:]) return b.String(), hits } // NormalizeDateline scans an Anthropic /v1/messages request body and rewrites // every fingerprinted dateline sentence back to its canonical ASCII form. // // Scope (mirroring where genuine clients place the sentence): // - `system` string, or `.text` field of each text-typed block in `system`. // - Text bodies inside `messages[i].content` — but ONLY the substrings that // appear inside `...` tags. Free user // prose, tool_use.input, tool_result.content, and other block types are // never scanned, guaranteeing that legitimate text like a code block, a // shell command, or a chat message that mentions today's date is never // accidentally rewritten. // // The function is a pure transform: it never modifies the input slice, and if // no rewrite is needed it returns the original slice (identity), a nil hit // slice, and changed=false. func NormalizeDateline(body []byte) ([]byte, []DatelineHit, bool) { if len(body) == 0 { return body, nil, false } out := body var hits []DatelineHit changed := false sys := gjson.GetBytes(out, "system") if sys.Exists() { switch { case sys.Type == gjson.String: normalized, sysHits := NormalizeText(sys.String()) if normalized != sys.String() { if next, err := sjson.SetBytes(out, "system", normalized); err == nil { out = next changed = true hits = append(hits, sysHits...) } } case sys.IsArray(): idx := 0 sys.ForEach(func(_, item gjson.Result) bool { if item.Get("type").String() == "text" { t := item.Get("text") if t.Exists() && t.Type == gjson.String { normalized, textHits := NormalizeText(t.String()) if normalized != t.String() { path := fmt.Sprintf("system.%d.text", idx) if next, err := sjson.SetBytes(out, path, normalized); err == nil { out = next changed = true hits = append(hits, textHits...) } } } } idx++ return true }) } } messages := gjson.GetBytes(out, "messages") if messages.IsArray() { msgIdx := -1 messages.ForEach(func(_, msg gjson.Result) bool { msgIdx++ content := msg.Get("content") if !content.Exists() { return true } switch { case content.Type == gjson.String: normalized, contentHits := normalizeSystemReminderScopedText(content.String()) if normalized != content.String() { path := fmt.Sprintf("messages.%d.content", msgIdx) if next, err := sjson.SetBytes(out, path, normalized); err == nil { out = next changed = true hits = append(hits, contentHits...) } } case content.IsArray(): contentIdx := -1 content.ForEach(func(_, block gjson.Result) bool { contentIdx++ if block.Get("type").String() != "text" { return true } t := block.Get("text") if !t.Exists() || t.Type != gjson.String { return true } normalized, textHits := normalizeSystemReminderScopedText(t.String()) if normalized != t.String() { path := fmt.Sprintf("messages.%d.content.%d.text", msgIdx, contentIdx) if next, err := sjson.SetBytes(out, path, normalized); err == nil { out = next changed = true hits = append(hits, textHits...) } } return true }) } return true }) } if !changed { return body, nil, false } return out, hits, true }