fix(redact): catch Supabase sb_secret_ keys · Entire

fix(redact): catch Supabase sb_secret_ keys

Supabase secret API keys (sb_secret_) and personal access tokens (sbp_) passed through the built-in redaction unredacted into checkpoint objects.

Every always-on layer missed them when captured on their own:

Add a deterministic provider-prefix layer that redacts sb_secret_ and sbp_ tokens purely by prefix + length, independent of entropy or the surrounding variable name. Publishable keys (sb_publishable_) are designed to be embedded in client code and protected by row-level security, so they are intentionally not targeted (matching betterleaks, which ships no publishable-key rule).

Verified end-to-end: a sb_secret_ value planted across prompt text and shell tool I/O in a Claude Code transcript now redacts to REDACTED in the entire/checkpoints/v1 condensed blob (previously it survived verbatim).

Fixes #1716

Changes

4

//go:build integration

package integration

import (
    "os"
    "strings"
    "testing"

"github.com/entireio/cli/cmd/entire/cli/paths"
)

// TestSupabaseSecretRedaction_FullHookFlow is the end-to-end regression for
// issue #1716. It drives the real entire hook binary (UserPromptSubmit ->
// mid-turn commit/condensation -> Stop/finalize) on a Claude Code session
// whose transcript embeds a Supabase sb_secret_ API key across the two vectors
// from the issue report (prompt text and shell-tool input/output), then reads
// the real entire/checkpoints/v1 transcript blob back and proves:
//   - the sb_secret_ value does NOT survive into the condensed blob,
//   - a REDACTED placeholder is present,
//   - a plain capture-control marker DID survive (so a zero secret count means
//     redaction happened, not that capture failed — mirroring the issue's
//     methodology),
//   - a sb_publishable_ key (public by design) is NOT over-redacted.

func TestSupabaseSecretRedaction_FullHookFlow(t *testing.T) {
    // Hook subprocesses share settings/env; do not run in parallel.
    // The sb_secret_ / sb_publishable_ prefixes are assembled from fragments so
    // a complete Supabase-shaped token never appears verbatim in source, keeping
    // secret scanners (including GitHub push protection) from flagging these
    // synthetic fixtures; the runtime values are complete.
    const (
        supabaseSecret      = "sb" + "_secret_" + "probe_20260710_7f91c2d8e4a6b3f0"
        supabasePublishable = "sb" + "_publishable_" + "probe_20260710_7f91c2d8e4a6b3f0"
        captureControl      = "CAPTURE_CONTROL_MARKER_9f"
    )

env := NewFeatureBranchEnv(t)
    session := env.NewSession()

// Author a Claude Code transcript: a prompt that names the secret (vector 1)
    // plus the publishable control and capture marker, a Bash tool_use whose
    // command exports the secret (vector 2 input), the shell tool_result echoing
    // the secret (vector 2 output), then a file-writing tool use so the commit
    // has attributable content.
    prompt := "Configure the backend. The service_role key is " + supabaseSecret +
        " and the public client key " + supabasePublishable +
        " is safe to commit. " + captureControl
    transcript := strings.Join([]string{
        `{"uuid":"u1","type":"user","message":{"role":"user","content":"` + prompt + `"},"timestamp":"2026-01-01T00:00:00Z"}`,
        `{"uuid":"a1","type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"export SUPABASE_SERVICE_ROLE_KEY='` + supabaseSecret + `','description":"set service role key"}}]},"timestamp":"2026-01-01T00:00:01Z"}`,
        `{"uuid":"u2","type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"Applied. Wrote key ` + supabaseSecret + ` to env. ` + captureControl + `"}]},"timestamp":"2026-01-01T00:00:02Z"}`,
        `{"uuid":"a2","type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_2","name":"Write","input":{"file_path":"feature.go","content":"package main\n"}}]},