Skip to content

Repository files navigation

TraceSmith

TraceSmith is a command-line tool that turns the local conversation logs left behind by AI coding agents into clean, redacted, fine-tuning-ready datasets. It runs a four-stage pipeline — extract → redact → export → manifest — over agent history from eight supported sources, replaces every secret, path, email, and identifier with a stable placeholder, and emits DistillKit-ready JSONL plus a provenance manifest. Nothing about your identity is ever baked into the tool; the output is safe to publish straight to the HuggingFace Hub.

The pipeline is reproducible and privacy-first: the rule-based redaction layer is always on, with optional privacy-filter, gitleaks, and LLM residue passes layered on top. Each source contributes its own file at every stage, so datasets can be sliced or dropped per-source.


Status

v0.1.0 — extract (8 sources), layered redaction, two export variants, MANIFEST provenance, verify, stats, sample, publish. The first publishable milestone.


Install

Requires Python ≥ 3.10.

# Core (click only).
pip install -e .

# Optional extras:
pip install -e ".[publish]"          # huggingface_hub, for `tracesmith publish`
pip install -e ".[privacy-filter]"   # transformers + torch, for the optional privacy filter
pip install -e ".[dev]"              # pytest + pytest-cov, for running the suite

The optional redaction layers have extra, non-Python dependencies:

  • gitleaks — the external gitleaks binary (the --gitleaks / --gitleaks-fix flags). Install separately, e.g. brew install gitleaks.
  • LLM residue pass — any OpenAI-compatible endpoint, passed via --llm-residue http://localhost:8000.

These are all opt-in. The default redaction path needs nothing but click.


Quick start

One command runs the whole pipeline end to end:

# Extract from ~, redact, export both variants, write MANIFEST.json.
tracesmith run --out ./output --user "$USER"

This produces:

output/
├── raw_extracted/<source>.jsonl   # normalized conversations, one per source
├── redacted/<source>.jsonl        # PII/secrets replaced with placeholders
├── REDACTION_REPORT.json          # per-source redaction counts + config snapshot
├── export/
│   ├── messages.jsonl             # DistillKit "messages" variant
│   └── sharegpt.jsonl             # DistillKit "ShareGPT" variant
└── MANIFEST.json                  # provenance: counts, config, file hashes

Then inspect before you publish:

tracesmith verify --in ./output/redacted     # scan for leftover PII (exits 1 on findings)
tracesmith stats  --in ./output/export       # corpus metrics as JSON
tracesmith sample --in ./output/export -n 5 --out ./preview   # random 5-row preview

Happy? Publish:

tracesmith publish --repo youruser/my-traces --in ./output/export --variant both

Supported sources

Eight extractors, all auto-discovered from the local home directory:

Source Key
Claude Code claude_code
Codex (OpenAI) codex
Continue continue
Cursor cursor
Gemini CLI gemini
OpenCode opencode
Trae trae
Windsurf windsurf

Each source contributes its own <source>.jsonl in every stage, so datasets can be sliced or dropped per-source (see --drop-sources under export).


CLI reference

All eight commands. Defaults are shown.

tracesmith run — the whole pipeline

tracesmith run [--sources a,b] [--root ~] [--out ./output]
               [--variant messages|sharegpt|both] [--user NAME] [--home DIR]

Chains extract → redact → export → MANIFEST.json, calling the stage functions directly so the per-stage counts feed the manifest. Use --user and --home so the redactor knows which username/home path to scrub as private identifiers (defaults to $USER / $HOME).

tracesmith extract

tracesmith extract [--sources a,b] [--root ~] [--out ./output]

Scan --root for installed agents and write raw_extracted/<source>.jsonl. Sources with no installation contribute an empty file (downstream stages always find a file). With no --sources, all 8 are tried.

tracesmith redact

tracesmith redact [--in ./output/raw_extracted] [--out ./output/redacted]
                  [--allow-public-urls] [--allow-domain D]... [--private-term T]...
                  [--private-domain D]... [--user NAME] [--home DIR]
                  [--privacy-filter] [--privacy-filter-device auto|cpu|cuda|cuda:N]
                  [--gitleaks] [--gitleaks-fix]
                  [--llm-residue URL]

Applies the layered redactor (rules → optional privacy filter → optional gitleaks fix → optional LLM residue → optional final gitleaks scan) and writes redacted/<source>.jsonl plus a sibling REDACTION_REPORT.json.

tracesmith export

tracesmith export [--in ./output/redacted] [--out ./output/export]
                  [--variant messages|sharegpt|both]
                  [--min-turns N] [--max-turns N] [--min-assistant-chars N]
                  [--include-source GLOB]... [--drop-sources a,b]
                  [--project GLOB]... [--model GLOB]... [--status STATUS]...
                  [--since ISO_TIME] [--until ISO_TIME]
                  [--require-tools] [--require-diffs] [--dedup]

Flatten redacted conversations into DistillKit-ready JSONL (see Output format below). Quality filters drop conversations with no assistant turn, apply message-count bounds, require a minimum assistant message length, filter by normalized source/project/model/status/time metadata, require tool calls or diffs, and optionally deduplicate. Filter summaries include drop reasons.

tracesmith verify

tracesmith verify [--in ./output/redacted]

Scan the redacted tree for leftover PII/secrets that survived the pipeline. Exits 0 if clean, 1 if anything is found (prints up to 20 findings).

tracesmith publish

tracesmith publish --repo USER/DATASET [--in ./output/export]
                   [--variant messages|sharegpt|both] [--private]

Upload the chosen variant(s) to the HuggingFace Hub with an auto-generated dataset card (provenance, redaction summary, attribution). Requires the [publish] extra and a logged-in huggingface_hub token.

tracesmith stats

tracesmith stats [--in ./output/export]

Print corpus metrics (conversation counts, token/char totals, per-source and per-variant breakdowns) as JSON.

tracesmith sample

tracesmith sample [--in ./output/export] [-n 10] [--seed 0] [--out DIR]

Sample n random conversations for pre-publish inspection. With --out, also writes sample.jsonl. Seeded for reproducibility.


Output format

TraceSmith writes two interchangeable variants of the same flattened data, so you can point DistillKit at whichever one your recipe expects.

messages variant — export/messages.jsonl

One JSON object per conversation, DistillKit's flat messages schema. Each message's rich fields (tool_use, tool_results, suggested_diffs, …) are flattened into a single string content, and the original role is preserved.

{"messages": [
  {"role": "user", "content": "add a greeting function to the project"},
  {"role": "assistant", "content": "I'll create it.\n\n<tool_use name=\"write_file\">\n{...}\n</tool_use>"}
], "metadata": {
  "schema_version": "1.0",
  "trace_id": "ts_...",
  "source": "claude_code",
  "models": ["claude-sonnet-4-5"],
  "counts": {"messages": 2, "tool_calls": 1, "tool_results": 0, "diffs": 0}
}}

sharegpt variant — export/sharegpt.jsonl

One JSON object per user→assistant instruction pair (independent pairs, no rolling context). A leading system message, if present, is attached to the first pair. Uses the classic from/value shape:

{"conversations": [
  {"from": "human", "value": "add a greeting function to the project"},
  {"from": "gpt",   "value": "I'll create it.\n\n<tool_use name=\"write_file\">\n{...}\n</tool_use>"}
], "metadata": {
  "schema_version": "1.0",
  "trace_id": "ts_...",
  "source": "claude_code",
  "pair": {"index": 0, "count": 1}
}}

Metadata contract

Provider adapters normalize structured source fields into the Conversation and Message types before export. The metadata exporter reads only that declared internal contract. It does not search message text, infer a project from shell commands, or probe provider-specific aliases.

Trace and project IDs are keyed HMAC pseudonyms. Set TRACESMITH_METADATA_KEY when identities must remain stable across separate runs. Real timestamps are reduced to UTC day precision in exported metadata; filters still use the normalized pre-export timestamp. Raw session IDs, paths, titles, message IDs, tool payloads, and source files are not copied into public metadata.

The adapters and fixtures were checked against a key-only inventory of recent local Claude Code, Codex, Gemini CLI, and OpenCode records. The inventory recorded paths, types, discriminator values, and frequency only; it did not copy field values or personal trace content into the repository.

Worked, redacted examples of both variants live in examples/sample_input.jsonl (raw) plus the golden messages_expected.jsonl and sharegpt_expected.jsonl the pipeline produces from it. Run python examples/regenerate.py to reproduce them with the fixed, public fixture key documented in examples/README.md.

MANIFEST.json

Provenance for reproducibility: a UTC created_at, the tool_version, the metadata_schema_version, the config snapshot used, per-source raw and export counts, and a files block with sha256/bytes/rows for each export artifact. Generated automatically by tracesmith run.


DistillKit integration

Point DistillKit at the exported JSONL and pick the matching variant. Minimal config snippet:

# distillkit.yaml
dataset:
  # Use exactly one of the two below.
  path_messages: ./output/export/messages.jsonl   # flat messages variant
  path_sharegpt: ./output/export/sharegpt.jsonl   # from/value pairs

  # Quality knobs that mirror `tracesmith export` so train/eval stay consistent.
  min_turns: 2
  min_assistant_chars: 50
  dedup: true

output:
  dir: ./distilled

The two variants are drop-in for DistillKit's expected schemas: messages.jsonl → the {"messages": [{"role","content"}, ...]} chat format; sharegpt.jsonl → the {"conversations": [{"from","value"}, ...]} instruction-pair format.


Privacy model

Redaction is the core safety boundary. It is layered and opt-in, with the rule layer always on:

  1. Rule layer (always on). Regex-driven, ported from the upstream agent-trace-redaction-methodology and split by family — identifiers, secrets, paths, URLs, plus your private terms/domains. Every match becomes a stable, reversible placeholder like [GENERIC_HOME_PATH:0001:58c69070ae] (family + index + short hash) so structure is preserved but identity is not.
  2. Privacy filter (optional, --privacy-filter). A transformers NER model that catches names/PII the regex layer misses. Needs the [privacy-filter] extra.
  3. gitleaks fix (optional, --gitleaks-fix). Iteratively redacts findings from a live gitleaks scan, in place, before the final pass.
  4. LLM residue pass (optional, --llm-residue URL). An OpenAI-compatible model call that re-reads the redacted text for anything still leaking.
  5. Final gitleaks scan (optional, --gitleaks). A read-only last line of defense that fails loudly if anything remains.

Always run tracesmith verify on the redacted tree before publishing — it's a cheap, dependency-free safety net that exits non-zero on any leftover.

You control your own scope: --user and --home tell the redactor which username and home directory to treat as yours; --private-term / --private-domain add custom scrub targets; --allow-public-urls / --allow-domain loosen URL scrubbing for domains you've decided are fine.


Known limitations (v0.1.0)

These are documented gaps in the initial release. None affects redaction safety or the DistillKit output shape; they're coverage/attribution/convenience items tracked for a follow-up.

  • Timezone-city strings are not auto-discovered. discover_private_terms derives private terms from the username, home dir, hostname, git identity, SSH config, and Pi session paths — but it does not scan for timezone-city strings (e.g. Europe/Zurich, America/New_York) the way the upstream redactor does. If your locale city appears in traces, add it explicitly via --private-term (e.g. --private-term Zurich).
  • --purge-raw is deferred to v0.1.1. Raw extraction is always retained under <out>/raw_extracted/ so the redaction step can be re-run with different config without re-extracting. There is no flag to auto-delete it yet; delete the directory manually if you don't need it.
  • MANIFEST does not report per-source export attribution. The export pipeline does not thread source tags through to the emitted rows, so a per-source "exported messages / pairs" count would be misleading (an earlier draft stamped the GLOBAL row count onto every source — see Fix 1 in the pre-publish touch-ups). v0.1.0 carries global counts in the top-level export_summary block of MANIFEST.json; the per-source block reports only raw_records and redaction_counts.
  • tracesmith stats / tracesmith sample source breakdown works best on redacted/. Exported rows (export/messages.jsonl, export/sharegpt.jsonl) lose the top-level source field during export, so a source breakdown run against export/ cannot attribute rows back to a source. Point these commands at <out>/redacted/ for an accurate per-source view.
  • Cursor inline-storage branch doesn't capture toolResults. The Cursor extractor's inline-storage path omits tool-result bubbles. This is an upstream bug ported verbatim (see tracesmith/extract/cursor.py); conversations extracted from Cursor's inline storage may be missing tool outputs. The standard Cursor storage path is unaffected.

Attribution

This project builds on two upstream MIT-licensed projects, vendored and refactored in place:

  • 0xSero/ai-data-extraction — the eight source extractors.
  • RodriMora/agent-trace-redaction-methodology — the layered redactor and canary test suite.

File-by-file provenance (upstream path, commit SHA, license, and a summary of changes) is recorded in ATTRIBUTION.md. Licensed under the MIT License.

About

Extract, redact, and export AI coding-agent traces for DistillKit. Combines 0xSero/ai-data-extraction (8 extractors) and RodriMora/agent-trace-redaction-methodology into one tool.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages