"""Sanitize a Codex JSONL trace for sharing. This is a best-effort helper, not a privacy guarantee. Always manually review the sanitized output before publishing it publicly. """ from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any SENSITIVE_PATTERN_SPECS = ( ("openai_key", r"sk-[A-Za-z0-9_-]{20,}", "[REDACTED_OPENAI_KEY]", 0), ("huggingface_token", r"hf_[A-Za-z0-9_-]{20,}", "[REDACTED_HF_TOKEN]", 0), ("github_fine_grained_pat", r"github_pat_[A-Za-z0-9_]{20,}", "[REDACTED_GITHUB_TOKEN]", 0), ("github_classic_token", r"gh[pousr]_[A-Za-z0-9]{20,}", "[REDACTED_GITHUB_TOKEN]", 0), ("google_api_key", r"AIza[0-9A-Za-z_-]{30,}", "[REDACTED_GOOGLE_API_KEY]", 0), ("aws_access_key", r"\b(?:AKIA|ASIA)[A-Z0-9]{16}\b", "[REDACTED_AWS_ACCESS_KEY]", 0), ("slack_token", r"xox[baprs]-[A-Za-z0-9-]{20,}", "[REDACTED_SLACK_TOKEN]", 0), ( "private_key_block", r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", "[REDACTED_PRIVATE_KEY]", re.DOTALL, ), ("bearer_token", r"\bbearer\s+[A-Za-z0-9._~+/-]{20,}", "Bearer [REDACTED_BEARER_TOKEN]", re.IGNORECASE), ) SENSITIVE_ASSIGNMENT_PATTERN = re.compile( r"\b([A-Z0-9_.-]*(?:api[_-]?key|token|secret|password|authorization)[A-Z0-9_.-]*\s*[:=]\s*)([\"']?)([^\"'\s,}]+)", re.IGNORECASE, ) SESSION_META_STRIP_KEYS = { "base_instructions", } def build_replacements(workspace: Path, manual_terms: list[str]) -> list[tuple[str, re.Pattern[str], str]]: replacements: list[tuple[str, re.Pattern[str], str]] = [] for name, pattern, replacement, flags in SENSITIVE_PATTERN_SPECS: replacements.append((name, re.compile(pattern, flags), replacement)) paths = [workspace.resolve(), Path.home().resolve()] for path in paths: value = str(path) if value: replacement = "[WORKSPACE]" if path == workspace.resolve() else "[HOME]" replacements.append((f"path:{value}", re.compile(re.escape(value)), replacement)) home_parent = Path.home().resolve().parent replacements.append( ( "user_home_parent", re.compile(re.escape(str(home_parent)) + r"/[^/\"'\s]+"), str(home_parent) + "/[USER]", ) ) for index, term in enumerate(manual_terms, start=1): if term: replacements.append( ( f"manual_term_{index}", re.compile(re.escape(term), re.IGNORECASE), "[REDACTED_PERSONAL_DETAIL]", ) ) return replacements def redact_string(value: str, replacements: list[tuple[str, re.Pattern[str], str]], stats: dict[str, int]) -> str: redacted = value for name, pattern, replacement in replacements: redacted, count = pattern.subn(replacement, redacted) if count: stats[name] = stats.get(name, 0) + count redacted, count = SENSITIVE_ASSIGNMENT_PATTERN.subn(r"\1\2[REDACTED_SECRET]", redacted) if count: stats["sensitive_assignment"] = stats.get("sensitive_assignment", 0) + count return redacted def redact_value(value: Any, replacements: list[tuple[str, re.Pattern[str], str]], stats: dict[str, int]) -> Any: if isinstance(value, str): return redact_string(value, replacements, stats) if isinstance(value, list): return [redact_value(item, replacements, stats) for item in value] if isinstance(value, dict): redacted: dict[str, Any] = {} for key, item in value.items(): redacted_key = redact_string(key, replacements, stats) redacted[redacted_key] = ( "[REDACTED_SESSION_METADATA]" if key in SESSION_META_STRIP_KEYS else redact_value(item, replacements, stats) ) return redacted return value def should_drop_event(event: dict[str, Any], keep_reasoning: bool) -> bool: if keep_reasoning: return False payload = event.get("payload") if event.get("type") == "response_item" and isinstance(payload, dict): return payload.get("type") == "reasoning" return False def sanitize_trace( input_path: Path, output_path: Path, workspace: Path, manual_terms: list[str], keep_reasoning: bool, ) -> dict[str, int]: replacements = build_replacements(workspace, manual_terms) stats: dict[str, int] = { "input_lines": 0, "output_lines": 0, "dropped_reasoning_lines": 0, } output_path.parent.mkdir(parents=True, exist_ok=True) with input_path.open("r", encoding="utf-8") as source, output_path.open( "w", encoding="utf-8", ) as destination: for line_number, line in enumerate(source, start=1): if not line.strip(): continue stats["input_lines"] += 1 try: event = json.loads(line) except json.JSONDecodeError as exc: raise ValueError(f"{input_path}:{line_number}: invalid JSONL: {exc}") from exc if isinstance(event, dict) and should_drop_event(event, keep_reasoning): stats["dropped_reasoning_lines"] += 1 continue redacted_event = redact_value(event, replacements, stats) destination.write(json.dumps(redacted_event, sort_keys=False) + "\n") stats["output_lines"] += 1 return stats def default_output_path(input_path: Path) -> Path: if input_path.name.endswith(".jsonl"): return input_path.with_name(input_path.name.removesuffix(".jsonl") + ".sanitized.jsonl") return input_path.with_name(input_path.name + ".sanitized.jsonl") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "Sanitize a Codex JSONL trace for sharing. This is best-effort only; " "manually review output before publishing." ) ) parser.add_argument("input", type=Path, help="Path to the raw Codex JSONL trace.") parser.add_argument( "-o", "--output", type=Path, help="Path for the sanitized JSONL trace. Defaults to INPUT.sanitized.jsonl.", ) parser.add_argument( "--workspace", type=Path, default=Path.cwd(), help="Workspace path to redact. Defaults to the current directory.", ) parser.add_argument( "--redact-term", action="append", default=[], help="Additional case-insensitive phrase to replace with [REDACTED_PERSONAL_DETAIL]. Repeat as needed.", ) parser.add_argument( "--keep-reasoning", action="store_true", help="Keep Codex reasoning trace items. By default they are dropped.", ) return parser.parse_args() def main() -> None: args = parse_args() input_path = args.input.expanduser().resolve() output_path = (args.output.expanduser() if args.output else default_output_path(input_path)).resolve() workspace = args.workspace.expanduser().resolve() if not input_path.exists(): raise FileNotFoundError(input_path) if input_path == output_path: raise ValueError("Output path must be different from input path.") stats = sanitize_trace(input_path, output_path, workspace, args.redact_term, args.keep_reasoning) print(f"Wrote {output_path}") print(f"Input lines: {stats['input_lines']}") print(f"Output lines: {stats['output_lines']}") print(f"Dropped reasoning lines: {stats['dropped_reasoning_lines']}") print("Redactions:") hidden_stats = {"input_lines", "output_lines", "dropped_reasoning_lines"} for name in sorted(key for key in stats if key not in hidden_stats): print(f" {name}: {stats[name]}") if __name__ == "__main__": main()