fix(import): redaction config + provenance privacy for imported checkpoints · Entire
fix(import): redaction config + provenance privacy for imported checkpoints
1747e42→main·
computermode·3w ago·7 files·+105 added/-23 removed
Address review findings on imported-checkpoint history:
- Call strategy.EnsureRedactionConfigured() before import writes so imported transcripts honor opt-in PII, custom_redactions, and redactor packs, not just always-on secret scanning. Covered by a custom-redaction test.
- Store only the transcript basename (transcript_file) in provenance instead of the absolute path, which leaked local usernames/repo layout onto the shared v1 branch.
- Hash the redacted turn slice instead of raw bytes, closing a confirmation attack against guessed raw prompt/output content.
- Gate the explain "--generate" hint on summary.Imported, since imported history is read-only and --generate is refused.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
3e645399c3f3View transcript
Changes
7
api/checkpoint
Mmetadata.go+2/-2
- Mmetadata_test.go+1/-1
cmd/entire/cli
agentimport
Magentimport.go+30/-6
- Magentimport_test.go+52
- Mclaude.go+8/-14
Mexplain.go+5
- Mimport_cmd.go+7
// Only set for imported (Kind == "imported") checkpoints.
type Provenance struct {
Source string `json:`"source"` // e.g. "claude-code"
TranscriptPath string `json:`"transcript_path"` // absolute path at import time
TranscriptFile string `json:`"transcript_file"` // basename of the source transcript (no absolute path: avoid leaking local usernames/repo layout)
SessionID string `json:`"session_id"` // agent session id
TurnUUID string `json:`"turn_uuid"` // uuid of the user-prompt line that starts this turn
ParentUUID string `json:`"parent_uuid,omitempty"` // parent uuid of that line
LineStart int `json:`"line_start"` // 0-indexed start line of this turn in the source
LineEnd int `json:`"line_end"` // exclusive end line (next turn start or EOF)
ContentHash string `json:`"content_hash"` // hash of the turn slice
ContentHash string `json:`"content_hash"` // hash of the redacted turn slice (never raw content)
ImportVersion int `json:`"import_version"` // importer schema version
}
Test cases
package agentimport
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
)
func TestDeriveCheckpointID_StableAndDistinct(t *testing.T) {
}
func TestRun_AppliesConfiguredCustomRedaction(t *testing.T) {
// A benign marker word that always-on secret scanning would never flag, so
// redacting it can only be the configured custom rule's doing.
const secret = "bananaphone-marker-word"
redact.ConfigureCustomRules(redact.CustomRulesConfig{
Inline: map[string]string{"acme-token": secret},
})
t.Cleanup(func() { redact.ConfigureCustomRules(redact.CustomRulesConfig{}) })
repo, repoDir := initRepoWithCommit(t)
claudeDir := t.TempDir()
content := strings.Join([]string{
`{"type":"user","uuid":"u1","timestamp":"2026-06-20T00:00:00Z","message":{"role":"user","content":"use ` + secret + ` please"}}`,
`{"type":"assistant","uuid":"a1","message":{"id":"m1","model":"claude-x","content":[{"type":"text","text":"ok"}],"usage":{"output_tokens":5}}}`,
}, "\n") + "\n"
if err := os.WriteFile(filepath.Join(claudeDir, "sess1.jsonl"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
res, err := Run(context.Background(), repo, claudeImporter{}, Options{
RepoRoot: repoDir, OverridePath: claudeDir,
Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC),
})
if err != nil {
t.Fatal(err)
}
if res.TurnsImported != 1 {
t.Fatalf("want 1 imported, got %+v", res)
}
stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{})
if err != nil {
t.Fatal(err)
}
cid := DeriveCheckpointID("sess1", "u1")
sc, err := stores.Persistent.ReadSessionContent(context.Background(), cid, 0)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(sc.Transcript), secret) {
t.Fatalf("custom-configured secret was not redacted from imported transcript")
}
if !strings.Contains(string(sc.Transcript), redact.RedactedPlaceholder) {
t.Fatalf("expected %q in redacted transcript, got: %s", redact.RedactedPlaceholder, sc.Transcript)
}
}
func TestRun_DryRunWritesNothing(t *testing.T) {
t.Parallel()
repo, repoDir := initRepoWithCommit(t)
}`