Skip to main content

Module session_redact

Module session_redact 

Source
Expand description

Session-persistence → stream-json transformer + PII redactor.

Claude Code persists each session as JSONL under $CLAUDE_CONFIG_DIR/projects/<slug>/<session-id>.jsonl, overlapping the wire protocol in broad shape but adding camelCase persistence-only fields and dropping wire-only frames. transform_persistence_line converts one such line to wire shape; both it and WireRedactor then redact.

Most rules are SPELLING rules - home paths, .claude-<profile> directories (.claude-plugin excepted, it is a plugin manifest), -Users-<name>- project slugs, and the home-directory owner as a bare word. Those recognise nothing by meaning, so a check written from them can only confirm they ran, and prose in a captured tool result is not redacted at all. Read a new capture before committing it.

Four rules are instead value-blind, replacing a field wholesale rather than rewriting spellings inside it: everything non-categorical under account, the body of a hook_response frame, the command / skill inventory, and an mcpServers[].config blob. Each is a field where no spelling rule could ever be enough - a hook is an arbitrary local program, and its output on this repo’s own captures was the author’s entire cross-project memory index, in prose, which every spelling rule passed through untouched.

Prefer that shape when adding a rule. A fixed-point gate over this module can only ever check that a capture agrees with these rules, so its reach is bounded by them: it is strengthened by a rule that recognises nothing, never by a longer list of things to recognise.

Two rules are entry-point specific: the owner rule needs a whole trace to discover the name, so only WireRedactor has it; message bodies, tool inputs and results are stubbed only by transform_persistence_line, which also drops the persistence-only fields rather than tokenising them.

Deterministic: one input line always gives the same output line.

Re-exports§

use std::collections::HashMap; 🔒
use std::str::FromStr; 🔒
use std::sync::OnceLock; 🔒
use serde_json::Value; 🔒

Structs§

RedactState
Per-run state so that each distinct session_id / uuid gets a stable opaque token. Reused across every line in the same transformation pass so references inside a single session stay consistent.
WireRedactor
Redacts one captured wire trace. Whole-trace rather than per-line because the identity also leaks as a bare name in prose, which is only recognisable once a home path elsewhere in the file supplies it - from the trace, so the same file redacts identically on any machine.

Constants§

ACCOUNT_CATEGORICAL_KEYS 🔒
Account keys whose value is a categorical CLI label, not personal data: the corpus records two distinct subscriptionType labels and collapsing them would delete a wire fact the fixture exists to record. Only keys observed nested under an account object belong here - an exemption that never fires just risks leaving a real value.
COMMAND_ENTRY_KEYS 🔒
Per-entry fields of a command inventory entry. argumentHint is only stubbed when already present and non-empty: the parser collapses an empty hint to None, so inventing one would record a wire fact that never happened.
CONTROL_INVENTORY_KEYS 🔒
Inventory arrays a control_response can carry, and what each holds: commands from the command-list reply, skillFrontmatter from the context-usage reply. Both are keyed inside that frame rather than globally, which is what keeps a same-named field elsewhere on the wire out of scope.
HOOK_BODY_KEYS 🔒
Body fields of a hook_response frame. A hook is an arbitrary local program, so these carry whatever the capture machine happened to print, which on this repo’s own captures was the author’s entire cross-project memory index.
HOOK_BODY_PLACEHOLDER 🔒
What replaces a hook body. Non-empty, so a capture cannot pass the fixed-point gate by having merely been emptied by hand.
MCP_CONFIG_CATEGORICAL_KEYS 🔒
Keys inside an mcpServers[].config blob whose value is a CLI label rather than local configuration. type is the transport discriminant (stdio / http / sse / claudeai-proxy), which the fixture exists to record.
MCP_CONFIG_PLACEHOLDER 🔒
What replaces a value inside an mcpServers[].config blob.
MIN_OWNER_LEN 🔒
Shortest owner name scrubbed as a bare token; a shorter one is an error, not a skip. This does not make token scrubbing safe - assistant and permission clear it - but a collision above the floor fails loudly on replay, and below it would not.
PLUGIN_MANIFEST_DIR 🔒
A plugin manifest directory, not a profile - profile_regex would otherwise take it for one.

Functions§

collect_owners 🔒
Collect owner names from both spellings of a home path in s: the path and the project slug.
collect_owners_recursive 🔒
Collect owner names from every string a parsed line holds. Reading parsed values rather than raw text matters: over raw text {"cwd":"/Users/ada","type":"system"} captures a name running to end-of-line, which then matches nothing.
needs_scrub 🔒
True when any string-level rule would rewrite s.
number_token_counts_match 🔒
True when both lines carry the same NUMBER of number literals. A count change means the re-encode lost or gained structure - duplicate keys collapse on parse - and that still refuses; equal counts pass, accepting serde_json’s canonical re-spelling.
number_tokens 🔒
Number literals appearing outside string literals, in order. With serde_json’s preserve_order, two serialisations of the same Value can only differ in these, so the number of these tokens is what decides whether a re-encode lost structure. Values are parsed to f64 only to keep the sequence numbers, not compared: a 17-digit float’s parse drifts by one ulp, so equality would reject the very re-spelling this gate exists to allow.
owner_regex 🔒
Captures the home-directory OWNER name out of an absolute home path. Narrower than path_regex in two ways: no /Volumes/ arm, because a volume name is not a user name, and a user-name character class, because stderr puts : or a quote straight after the path and a captured fragment then matches nothing.
path_regex 🔒
Home-path prefixes, replaced by <redacted-home> with the path tail kept. The classes must exclude ": these patterns also run over raw line text, where a class that swallows the delimiter eats the field that follows. That is also what keeps the , they DO match harmless - drop the " again and the comma inclusion becomes a field-eater.
profile_regex 🔒
A claude profile directory name, which path_regex cannot reach: it replaces only the home prefix, and ~/.claude-alt has no prefix at all. Bare .claude is the default config dir, so it is not matched.
redact_account_field 🔒
Redact one value under an account field. key names whatever it hung off, so a bare "account": "…" and a nested "account": {"profile": {"email": …}} both get a naming placeholder.
redact_account_recursive 🔒
Placeholder every non-categorical string under account / accounts, keeping the keys so the fixture still records the CLI’s shape. Structural rather than value-matching, so a field the CLI adds later is redacted before anyone notices it leaked.
redact_command_inventory_recursive 🔒
Stub the command / skill inventory the CLI reports for whatever is installed on the capture machine: slash_commands and skills on system/init, commands on system/commands_changed, and the commands reply carried by a control_response.
redact_content_block 🔒
redact_hook_body_recursive 🔒
Stub the body of every hook_response frame, keeping the keys and every other field so the fixture still records the CLI’s shape.
redact_mcp_config_recursive 🔒
Stub the config blob on every mcpServers[] entry, keeping the keys and every sibling field.
redact_session_file
Transform + redact an entire persistence .jsonl file into stream-json-shaped output. One TraceLog.entries item per decodable line, all tagged "in" (CLI → SDK direction).
redact_session_path
Handy bundle: given a persistence file path, produce its redacted stream-json lines plus a small summary string for logs.
replace_owners 🔒
Apply every owner pattern to s.
scrub_owner_tokens_recursive 🔒
Replace each discovered owner name with <redacted-user> in string VALUES only. Keys are left alone: an owner name is never a wire key, and rewriting one would change the shape rather than a value.
scrub_paths_recursive 🔒
Recursively walk a JSON value and rewrite every absolute home path segment to <redacted-home>. Defence-in-depth on top of the structural redaction (which strips text / tool_use_input / tool_result_content); catches any path leak that survives in less-common fields.
scrub_text 🔒
Apply every string-level rule to s. Ordered widest-first so an absolute path loses its home prefix before its profile segment.
slug_regex 🔒
The project-slug spelling of a home path, which Claude Code mints by turning / into - (-Users-<name>-Projects-forge). Captures the name so discovery can harvest it as well as redact it.
stub_inventory 🔒
Replace each entry of one inventory array, whichever of the two shapes it uses: a bare name string, or an object carrying a name plus local prose about it. A non-array is left alone.
stub_mcp_config 🔒
Replace every non-categorical string inside one config blob, at any depth, keeping array lengths and object keys.
stub_nested_commands 🔒
Find every inventory array under a control_response and stub it. Scoped to that frame by the caller.
transform_persistence_line
Transform one persistence-format JSONL line into a stream-json wire-shape line. Returns Ok(None) for entries that don’t map (persistence-only types like file-history-snapshot, attachment, last-prompt).