Skip to content

Commit 3ef7d42

Browse files
gtrrz-victorclaude
andcommitted
redact: preserve thinking-block signatures (fix omp replay 400)
shouldSkipJSONLField only skipped a field named exactly "signature" (Claude Code). Oh My Pi stores the extended-thinking signature under "thinkingSignature", which slipped through to the entropy scanner. Its value is high-entropy base64, so redaction replaced segments with "REDACTED", corrupting the signature. When such a redacted transcript is replayed, the Anthropic API rejects it with "messages.N.content.0: Invalid `signature` in `thinking` block" (400) — it validates signatures on every history turn, not just the last. Skip any field whose lowercased name ends in "signature" (covers "signature" and "thinkingSignature"). "signatures" (plural) still redacts. Add field-skip and end-to-end preservation tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 11fbdc6 commit 3ef7d42

2 files changed

Lines changed: 30 additions & 5 deletions

File tree

redact/redact.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -827,12 +827,19 @@ func collectJSONLReplacements(v any, redactor func(string) string) []jsonReplace
827827
}
828828

829829
// shouldSkipJSONLField returns true if a JSON key should be excluded from scanning/redaction.
830-
// Skips "signature" (exact), ID fields (ending in "id"/"ids"), and common path/directory fields.
830+
// Skips signature fields (any key ending in "signature"), ID fields (ending in "id"/"ids"),
831+
// and common path/directory fields.
831832
func shouldSkipJSONLField(key string) bool {
832-
if key == "signature" {
833+
lower := strings.ToLower(key)
834+
835+
// Skip signature fields: cryptographic attestations, not secrets. Covers
836+
// "signature" (Claude Code) and provider variants like "thinkingSignature"
837+
// (Oh My Pi). Their values are high-entropy base64, so the entropy scanner
838+
// would otherwise redact them — corrupting extended-thinking signatures and
839+
// breaking transcript replay ("Invalid `signature` in `thinking` block").
840+
if strings.HasSuffix(lower, "signature") {
833841
return true
834842
}
835-
lower := strings.ToLower(key)
836843

837844
// Skip ID fields
838845
if strings.HasSuffix(lower, "id") || strings.HasSuffix(lower, "ids") {

redact/redact_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,10 @@ func TestShouldSkipJSONLField(t *testing.T) {
224224
{"ids", true},
225225
{"session_ids", true},
226226
{"userIds", true},
227-
// Exact match "signature" should be skipped.
227+
// Signature fields should be skipped (any key ending in "signature").
228228
{"signature", true},
229+
{"thinkingSignature", true},
230+
{"thinking_signature", true},
229231
// Path-related fields should be skipped.
230232
{"filePath", true},
231233
{"file_path", true},
@@ -245,7 +247,7 @@ func TestShouldSkipJSONLField(t *testing.T) {
245247
{"args", false},
246248
{"video", false}, // ends in "o", not "id"
247249
{"identify", false}, // ends in "ify", not "id"
248-
{"signatures", false}, // not exact match "signature"
250+
{"signatures", false}, // does not end in "signature"
249251
{"signal_data", false},
250252
{"consideration", false}, // contains "id" but doesn't end with it
251253
}
@@ -292,6 +294,22 @@ func TestJSONLContent_SkippedFieldValueCollision(t *testing.T) {
292294
}
293295
}
294296

297+
func TestJSONLContent_PreservesThinkingSignature(t *testing.T) {
298+
t.Parallel()
299+
// Oh My Pi stores extended-thinking signatures under "thinkingSignature".
300+
// Their base64 value is high-entropy; redacting it corrupts the signature and
301+
// breaks replay with "Invalid `signature` in `thinking` block".
302+
input := `{"type":"thinking","thinking":"plan","thinkingSignature":"` + highEntropySecret + `"}`
303+
304+
result, err := JSONLContent(input)
305+
if err != nil {
306+
t.Fatalf("unexpected error: %v", err)
307+
}
308+
if !strings.Contains(result, `"thinkingSignature":"`+highEntropySecret+`"`) {
309+
t.Fatalf("expected thinkingSignature to be preserved verbatim, got: %s", result)
310+
}
311+
}
312+
295313
func TestString_PatternDetection(t *testing.T) {
296314
// These secrets have entropy below 4.5 so entropy-only detection misses them.
297315
// Betterleaks pattern matching should catch them.

0 commit comments

Comments
 (0)