Skip to content

feat(cli): appmap sanitize — equality-preserving value tokenization#2375

Merged
kgilpin merged 22 commits into
mainfrom
feat/sanitize
Jul 11, 2026
Merged

feat(cli): appmap sanitize — equality-preserving value tokenization#2375
kgilpin merged 22 commits into
mainfrom
feat/sanitize

Conversation

@kgilpin

@kgilpin kgilpin commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

A new appmap sanitize command that makes committed AppMaps structurally incapable of carrying a secret, plus this repo adopting it end to end.

  1. appmap sanitize <files..> (4b2cd63d2) — replaces every captured runtime value string with a per-file, equality-preserving token (<v1>, <v2>, …) assigned by first appearance. Equality within one AppMap still reads as data-flow; zero content characters are retained. Details:
    • Positional, not content-sniffing: only the format's captured-data fields are touched (parameter/receiver/return/message values, exception messages, header values with names kept, concrete paths with route templates kept, client URLs). Code-object names, classes, labels are schema and survive.
    • Kind-annotated tokens (<uuid:v3>, <int:v7>) — recognition informs the label, never exemption (session tokens are UUIDs).
    • SQL parameterized, fail-closed: literals stripped via normalizeSQL keeping statement shape; a statement it can't reliably strip (it returns the original — fail-open for its display use) is downgraded to a whole-statement token.
    • Allowlist: built-in enumerables (true/false/null…) plus curated vocabularies via --allow/--allow-file, exact whole-value match only.
    • Idempotent; atomic writes; malformed files skipped, not fatal. 9-test unit spec.
  2. All 20 committed gold-trace baselines sanitized retroactively (234284a8c) — verified against the gold-traces engine's own pipeline: bless digests identical before/after for all 20 (values are outside the digest by design), so no false re-bless ever. 17% smaller as a side effect.
  3. Review workflow → released review-action@v1 (b3484ec51), dropping the merged skills-ref override; v1's per-matrix sticky comments fix this repo's two-package matrix.
  4. AppMap skills vendored as a submodule (491e32dcb) at .claude/appland-skills with committed relative symlinks, replacing machine-local links.

Why

Gold traces are committed, cloneable, and diff-invisible (*.appmap.json binary) — the one place a captured secret would live forever. Truncation caps leakage but retains whole short secrets (PINs, passwords ≤ cap); digests of low-entropy values are crackable offline. Tokenization retains zero bits while preserving the equality structure reviews actually use.

Review

A behavioral review of this branch ran against the sanitized baselines (asymmetric inputs: sanitized head vs trimmed base): compare clean 20/20, one 🟡 follow-up — unrecognized format fields currently pass through silently; a spec-driven audit + warn-on-unknown-keys mode is the fast follow.

Follow-ups (not this PR)

  • Gold-traces skill switch trimsanitize at bless (drafted, gated on this shipping in a release).
  • Gold-trace manifest entry for the sanitize command itself.
  • Format-version field audit (the 🟡 above).

Examples

Screenshot 2026-07-08 at 10 45 11 AM

kgilpin and others added 5 commits July 7, 2026 16:52
…tion

Replaces every captured runtime value string with a per-AppMap token
(<v1>, <v2>, ...) assigned by first appearance, so equality within one
AppMap is preserved (data-flow still reads) while zero content
characters are retained — the committed file is structurally incapable
of carrying a secret. Kind recognizers annotate token labels
(<uuid:v3>) without ever exempting a value: a UUID is exactly the shape
session tokens take.

Sanitization is positional (the format's captured-data fields only):
parameter/receiver/return/message values, exception messages, HTTP
header values (names kept), concrete request paths (route templates
kept), client URLs. SQL is parameterized via normalizeSQL to keep the
statement shape, wrapped fail-closed: a statement the obfuscator can't
reliably strip (it returns the original — fail-open for its display
use) is downgraded to a whole-statement token.

Allowlist: built-in enumerables (true/false/null/...) plus curated
project vocabularies via --allow / --allow-file, exact whole-value
match only. Idempotent; atomic writes; a malformed file is skipped,
not fatal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skills pin via a submodule at .claude/appland-skills (the pattern used
in nova-2), with the project's .claude/skills entries symlinked into
it — replacing machine-local symlinks into a sibling checkout that only
resolved on one workstation. The submodule SHA records which skills
revision the repo's gold-traces baselines were maintained with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rride

The action shipped as v1 (usage reporting, cached toolchain install,
live agent progress, per-matrix sticky comments via comment-tag — which
this two-package matrix needs). The skills feature branch this workflow
pinned is merged to skills main, the action's default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retroactively apply the new 'appmap sanitize' tokenization to all 20
committed baselines: every captured value string becomes a per-file
equality-preserving token, so the committed traces are structurally
incapable of carrying a secret. 17% smaller as a side effect
(1,027,715 -> 852,294 bytes).

Verified digest-safe against the gold-traces engine's own pipeline:
the bless digest (sha256 over the exported sequence diagram's
rootActions subtreeDigests) is identical before and after for all 20
baselines, so no re-bless and no false drift. Sanitization verified
idempotent on every file.

Also removes a stale manifest comment referencing the pre-engine
gold_traces/trim.mts script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

AppMap Behavioral Review — appmap sanitize value-redaction command

Revisions: b81492f4 vs main (33d2543e) · Date: 2026-07-11 ·
Commits: 4b2cd63d add appmap sanitize — equality-preserving value tokenization · 77ce035e sanitize exits non-zero when any file in the batch is skipped · 0cc5b94f sanitize eventUpdates too · 1157abc9 provenance markers for sanitize and trim · 5a414d98 normalize AppMaps as the first step of sanitize · e280c560 the masking walk sanitizes metadata itself · f76cd708 normalize HTTP routes in sanitize instead of tokenizing (plus 6b74f59b rename/PR-feedback, 45d73bf9 eslint style, and out-of-scope ci(appmap-review), chore(gold-traces), and .claude submodule commits)

Summary

Severity Findings Action required
🔴 High 0
🟡 Medium 0
🟢 Low 2 Optional: one documented residual to stay aware of, one small test-coverage gap.

Not merge-blocking. This is a clean, purely additive change: a new sanitize
command and its redaction library, plus provenance bookkeeping on trim. None of the
14 pre-existing behavioral baselines drifted — the only change the compare sees is the
two new traces the feature adds for its own coverage. The single most useful
follow-up is a unit test for the new trim "skip already-trimmed" branch (#2).

Findings

1 · 🟢 LOW — A template-less HTTP path keeps purely-alphabetic segments verbatim

File: packages/cli/src/lib/sanitizeAppMap.ts:237 · Context: normalizeSegment · Evidence: unit + gold test sanitizeAppMap sanitizes a composite AppMap end-to-end asserts /users/123/friends/alice/users/:int/friends/alice

When an HTTP server request has no route template (normalized_path_info), its
concrete path_info is normalized to a route template rather than tokenized (so the
route stays stable across recordings, since it feeds the compare digest). A segment
that looks like an id becomes a :kind placeholder, but a purely-alphabetic segment
is kept as-is — so a username in /users/alice survives, indistinguishable from a
route word without the application's route table. This is explicitly documented in the
source and asserted by the test; it is a deliberate, accepted limitation, not a
regression.

Risk: A template-less path could retain a low-entropy identifier (e.g. a
username). In this repository the exposure is nil — the CLI gold traces contain no HTTP
server endpoints (OpenAPI is empty on both revisions), so the branch is never reached
here. Fix: No change recommended (the tradeoff is documented). If a future traced
surface serves real HTTP requests, prefer emitting normalized_path_info route
templates so the concrete path is fully tokenized instead.

2 · 🟢 LOW — New trim skip/provenance branch has no test

File: packages/cli/src/cmds/trim/trim.ts:120 · Context: trim handler (metadata.trimmed marker) · Evidence: source diff only — no gold trace and no unit test exercises this branch

trim now writes a metadata.trimmed provenance marker and skips a file already
trimmed by the same CLI version to the same cap (warning that a larger cap cannot
restore truncated values). The gold trace guards only trimAppMap — the value
truncation transform — which is why the trim baseline did not drift. The new
handler-level skip/marker logic is not covered by any test (trim.spec.ts was not
touched in this change).

Risk: Low — a bug in the skip guard would at worst re-trim (idempotent) or wrongly
skip a file, not leak data. Fix: Add a unit test that runs trim twice and asserts
the second run is skipped, and that a smaller cap on a re-run records
Math.min(prior, new):

// tests/unit/trim.spec.ts
it('skips a file already trimmed by this version to this cap', () => { /* run trim twice, assert "already trimmed, skipping" + unchanged output */ });

Checks performed

Check Result Note
Behavioral compare ✅ clean 2 new traces (the feature's own coverage); 0 of 14 existing traces changed or removed. Captured values, object ids, and timing are excluded from the digest, so every non-change is real.
Changes outside the PR's scope (Step 5) ✅ none No trace in any untouched subsystem drifted; the change is purely additive (two new files + one command registration) plus provenance-only edits to trim.
Missing guards (Step 6) The security.sanitization boundary is fail-closed by construction and is exercised by both new traces, including the negative batch path.
Test/recording coverage (Step 2) ❌ 1 gap → #2 Redaction boundary is well covered; the new trim skip/marker branch is not.
SQL (Step 4b) ✅ clean No SQL diff. sanitizeSql parameterizes literals (excluded from the digest) and masks the whole statement when the obfuscator leaves residue (fail-closed); the SQL-bearing traces did not drift.
HTTP (Step 4c) ✅ clean No HTTP endpoints in the traced surface (OpenAPI empty on both sides). Path/URL normalization is covered by the unit suite and the composite trace, not by a live server request.
Intended changes verified sanitize command + sanitizeAppMap library added; trim/sanitize provenance markers added; the two new gold traces establish the sanitize baseline. No pre-existing behavior moved.
Review detail — features, coverage, labels, drift

Feature List

  1. appmap sanitize command — replaces captured AppMap values with per-file, equality-preserving tokens (<v1>, <v2>, …); writes each output atomically; supports --allow/--allow-file verbatim allowlists and --output-dir.
  2. sanitizeAppMap redaction library — the security boundary (security.sanitization): tokenizes parameters, return values, receivers, headers, and exception/test-failure messages; normalizes HTTP paths and client URLs to lossy route templates; parameterizes SQL and masks the whole statement when it can't be reliably obfuscated; strips credentials from metadata.git.repository.
  3. Normalize-then-mask pipeline — sanitize runs buildAppMap().normalize() first, so masking walks the canonical form (eventUpdates merged, ids re-indexed, unreturned calls balanced).
  4. Fail-closed batch semantics — exit code 0 means every file was sanitized; a batch with any unprocessable file still sanitizes the good files but exits non-zero so a skipped, possibly secret-bearing file never looks like success.
  5. Provenance markersmetadata.sanitized (version + allow-values) and metadata.trimmed (version + max_length); both commands skip a file already processed by the same version/options and warn that masking/truncation is one-way.
  6. Command registrationSanitizeCommand wired into the yargs CLI (cli.ts).

Only features 1–5 have runtime coverage, via the two new traces below; nothing here produced drift in a pre-existing trace.

Coverage Matrix

Feature Covered by Status
Redaction boundary (composite walk) sanitizeAppMap sanitizes a composite AppMap end-to-end (gold) + rich sanitize.spec.ts unit suite
Batch fail-closed (negative branch) sanitize command handler fails when any file is skipped, while still sanitizing the rest (gold)
SQL parameterize / fail-closed composite gold + parameterizes SQL literals… / fails closed on SQL… unit tests
HTTP path & client-URL normalization composite gold + unit tests (no live HTTP-server trace exists in this surface) ✅ / —
Idempotency & token-reservation on re-runs is idempotent, never assigns a token a marked document already contains unit tests
trim skip-already-trimmed / marker branch no test — the trim gold trace covers trimAppMap (the transform) only uncovered#2

The redaction boundary is thoroughly guarded, including its fail-closed and negative
paths. The one gap is the new trim handler branch (#2); it is best closed by a unit
test rather than a gold trace (a warn-and-continue path does not warrant a committed
recording).

Suggested Labels

None required. The redaction boundary already carries @label security.sanitization
(ValueMasker.mask, sanitizeSql, normalizePath, normalizeUrl, sanitizeAppMap),
and no unlabeled function changed in the compare. The sanitize handler itself is a
CLI entrypoint, for which no application-code label in the taxonomy fits.

Behavioral Drift

Every one of the 14 pre-existing gold AppMaps changed on disk in this revision — they
were re-recorded and then blessed (sanitized and trimmed), gaining metadata.sanitized
and metadata.trimmed markers. Yet the behavioral digest — which ignores captured
values, object ids, and timing — reports zero changed traces. That is the expected
and reassuring outcome: the sanitize feature is additive (two new source files plus one
command registration), and the trim edit changed only provenance bookkeeping, not the
truncation transform the gold trace fingerprints. The only drift is the two new
traces (sanitizeAppMap … and sanitize command handler …), which are the feature's
own coverage and have no baseline to compare against. No pre-existing subsystem moved;
the SQL and OpenAPI surfaces are identical across revisions; the scanner found nothing
on either side.


Agent usage

As reported by Claude Code; the cost is computed by the agent from billed API usage.

Run Model Cost Tokens read Tokens written Turns Time
update claude-haiku-4-5-20251001, claude-opus-4-8[1m] $2.61 3M (98% from cache) 28.2k 54 9m 33s
review claude-haiku-4-5-20251001, claude-opus-4-8[1m] $2.04 1.7M (95% from cache) 27.9k 30 7m 6s
total $4.65 4.8M (97% from cache) 56k 84 16m 39s

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new appmap sanitize capability to the CLI to replace captured runtime value strings with equality-preserving tokens, and updates committed gold-trace AppMaps/workflows to use sanitized baselines and the released review action.

Changes:

  • Added sanitizeAppMap + ValueTokenizer library to tokenize captured value slots (with SQL normalization + fail-closed behavior).
  • Introduced a new appmap sanitize CLI command (with allowlist support and atomic writes) and wired it into the CLI entrypoint.
  • Updated gold-trace baselines/manifests and the AppMap review workflow (review-action @v1, in-tree appmap on PATH), plus added an AppMap skills submodule.

Reviewed changes

Copilot reviewed 30 out of 34 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/sequence-diagram/gold_traces/manifest.yaml Updates gold-trace recording notes for sequence-diagram package.
packages/sequence-diagram/gold_traces/baseline/appmaps/jest/Specification/includes_all_relevant_actors.appmap.json Sanitizes committed sequence-diagram baseline AppMap values.
packages/sequence-diagram/gold_traces/baseline/appmaps/jest/Sequence_diagram/HTTP_server_request/is_recorded.appmap.json Sanitizes committed sequence-diagram HTTP baseline AppMap values.
packages/cli/tests/unit/cmds/sanitize.spec.ts Adds unit coverage for sanitization behavior (tokenization, allowlist, SQL, idempotency, etc.).
packages/cli/src/lib/sanitizeAppMap.ts Implements AppMap value tokenization + SQL normalization/fail-closed logic.
packages/cli/src/cmds/sanitize/sanitize.ts Adds appmap sanitize command with allowlist + output-dir support and atomic writes.
packages/cli/src/cli.ts Registers the new sanitize command in the CLI.
packages/cli/gold_traces/manifest.yaml Adds gold-trace entries covering sanitize behavior (including SQL fail-closed branch).
packages/cli/gold_traces/baseline/appmaps/jest/trim_command/trimAppMap/truncates_values_across_every_captured_slot_while_preserving_structure.appmap.json Sanitizes committed CLI trim baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/tree_--filter/returns_only_sql_events_when_filter=sql.appmap.json Sanitizes committed CLI query/tree baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/stats_subcommand/analyzes_a_directory.appmap.json Sanitizes committed CLI stats baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/sequence_diagram_command/JSON_format/is_valid.appmap.json Sanitizes committed CLI sequence-diagram baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/sanitizeAppMap/shares_the_token_namespace_across_all_value_positions.appmap.json Adds new sanitize gold-trace baseline (shared token namespace across slots).
packages/cli/gold_traces/baseline/appmaps/jest/sanitizeAppMap/parameterizes_SQL_literals_but_keeps_the_statement_shape.appmap.json Adds new sanitize gold-trace baseline (SQL normalized branch).
packages/cli/gold_traces/baseline/appmaps/jest/sanitizeAppMap/fails_closed_on_SQL_it_cannot_reliably_parameterize.appmap.json Adds new sanitize gold-trace baseline (SQL fail-closed branch).
packages/cli/gold_traces/baseline/appmaps/jest/prune_subcommand/correctly_reduces_the_size_of_an_appmap_from_a_base64url-encoded_filter.appmap.json Sanitizes committed CLI prune baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/index_command/fingerprinting_a_directory/fingerprints_AppMaps_and_writes_an_index.appmap.json Sanitizes committed CLI index baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/importAppmap/imports_an_end-to-end_recording_into_all_tables.appmap.json Sanitizes committed CLI import baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/functionHotspots/--class_filters_by_defined_class.appmap.json Sanitizes committed CLI hotspots baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/findCalls/--label_filters_via_the_labels_table.appmap.json Sanitizes committed CLI findCalls baseline AppMap values.
packages/cli/gold_traces/baseline/appmaps/jest/endpoints/sorts_by_the_requested_key.appmap.json Sanitizes committed CLI endpoints baseline AppMap values.
.gitmodules Adds .claude/appland-skills as a git submodule.
.github/workflows/appmap-review.yml Updates workflow to use review-action @v1 and expose in-tree appmap on PATH.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/lib/sanitizeAppMap.ts Outdated
Comment thread packages/cli/tests/unit/cmds/sanitize.spec.ts

@dividedmind dividedmind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My biggest worry are the event updates; we need to also sanitize them, or (preferably) normalize the appmap before sanitization.

Comment thread packages/cli/src/lib/sanitizeAppMap.ts
Comment thread packages/cli/src/lib/sanitizeAppMap.ts
Comment thread packages/cli/src/lib/sanitizeAppMap.ts
Comment thread packages/cli/src/lib/sanitizeAppMap.ts Outdated
Comment thread packages/cli/src/lib/sanitizeAppMap.ts Outdated
Comment thread packages/cli/tests/unit/cmds/sanitize.spec.ts
Comment thread packages/sequence-diagram/gold_traces/manifest.yaml
Comment thread packages/cli/src/lib/sanitizeAppMap.ts
kgilpin and others added 7 commits July 9, 2026 08:16
- ValueTokenizer -> ValueMasker, tokenize() -> mask(): the operation is
  masking; 'tokenizer' reads as lexing in a codebase even though
  data-security tokenization is the operation's established name.
- 'undefined' joins the built-in allowlist — JS recorders emit it
  constantly and it has no secret capacity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three gold traces for one subsystem multiplies review and bless cost
without strengthening the baseline (the skill's own guidance: keep
one). A new composite unit test walks every sanitization branch in a
single AppMap — shared-namespace masking, headers/paths, SQL both
parseable and not — and its recording replaces the three baselines.
The recording is sanitized, matching what the engine will do once it
switches from trim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A skipped file keeps its original, possibly secret-bearing contents, so
a partial batch must not look like success — a CI gate trusting the
exit code could otherwise commit an unsanitized AppMap. All files are
still processed before failing (stopping at the first bad file would
leave the rest untouched); best-effort callers can append '|| true'.
Handler-level tests cover both outcomes; the gold trace is unchanged
(one composite trace per subsystem).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…push

A review costs a few dollars of agent time; pushes no longer trigger
one. Add the appmap-review label to review a PR (remove and re-add to
review again). The per-package change check and the fork guard stay;
workflow_dispatch stays as the manual path and the way to refresh the
baseline on main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
workflow_dispatch gains head and base inputs (defaults: the picked
branch, main). The checkout, the per-package change check, and the
review's base/head revisions all honor them, so any two refs can be
re-reviewed against each other — and a dispatch against an explicit
base now also skips packages the head didn't change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

AppMap Behavioral Review — sanitize baselines (@appland/sequence-diagram)

Revisions: bf932868 vs main (33d2543e) · Date: 2026-07-09 ·
Commits: 234284a8 chore(gold-traces): sanitize committed baselines — the only
commit touching this package (the appmap sanitize feature itself and its other
sanitize/gold-trace commits all live in packages/cli, out of scope here).

Summary

Severity Findings Action required
🔴 High 0
🟡 Medium 0
🟢 Low 0

Nothing blocks merge. The packages/sequence-diagram source is untouched, and the
behavioral compare of its six gold traces is clean: every recorded interaction ran
identically on both revisions. The baseline files changed on disk, but only because
their captured parameter values were replaced with sanitize tokens — a change the
comparison ignores by design.

Findings

No findings. No behavior changed between the two revisions.

Checks performed

Check Result Note
Behavioral compare ✅ clean 0 new / 0 removed / 0 changed traces across all 6 gold traces; elapsed time and captured values are excluded from the fingerprint, so a clean result means behavior really is identical.
Changes outside the PR's scope (Step 5) ✅ none The baseline files were rewritten (values tokenized) but no interaction changed — mechanical propagation of sanitization, blast radius confirmed (see drift note).
Missing guards (Step 6) No security-labeled function changed; nothing to guard.
Test/recording coverage (Step 2) All 6 manifest features are exercised by a committed gold trace.
SQL (Step 4b) ✅ clean No query added, removed, or changed; no new tables.
HTTP (Step 4c) ✅ clean OpenAPI diff empty — no breaking or non-breaking request/response changes.
Scanner findings 0 findings on both revisions.
Intended changes verified The committed baselines were sanitized (parameter values → <v1>/<v2> tokens); structure, ordering, and event count are unchanged.
Review detail — features, coverage, labels, drift

Feature List

No application-code changes in packages/sequence-diagramsrc/ is byte-identical
between main and bf932868. The single in-package change is a maintenance edit:

  1. Sanitized committed baselines — the six gold-trace recordings were rewritten so
    each captured parameter value becomes an equality-preserving token (<v1>, <v2>);
    identical values map to the same token. No recorded behavior changes.

Coverage Matrix

Feature Covered by Status
specification (actor/package selection) Specification › includes all relevant actors
labels (labels carried onto a diagram action) Sequence diagram › labels › are reported on a labeled function action
protocols-http (HTTP request as an action) Sequence diagram › HTTP server request › is recorded
protocols-sql (SQL query as an action) Sequence diagram › SQL query › is recorded
diff (two diagrams → PlantUML) Sequence diagram diff › PlantUML › user found vs not found › UML matches expectation
formatter-text (diff → Text output) Sequence diagram diff › PlantUML › user found vs not found › Text matches expectation

No coverage gaps — no new behavior was introduced in this package that would need a
new trace.

Suggested Labels

None. No function changed in the compare, so there is nothing newly worth labeling.

Behavioral Drift

The six baseline .appmap.json files differ from main at the file level, but the
comparison reports zero behavioral change. The only content that differs is the
parameters[*].value strings, now replaced with sanitize tokens (e.g. a node-3
parameter block → <v1>, an HTTP-callee block → <v2>), with equal originals mapping
to the same token. Event count (14 in the HTTP trace, unchanged), call ordering, the
class map, and every non-value field are identical. Because the change-detection
fingerprint deliberately excludes captured parameter and return values, this
value-only rewrite is invisible to it — exactly the outcome the sanitize feature is
meant to produce. This is acceptable mechanical propagation; the blast radius is the
committed baselines only, and it is confirmed intended.


Agent usage

As reported by Claude Code; the cost is computed by the agent from billed API usage.

Run Model Cost Tokens read Tokens written Turns Time
update claude-haiku-4-5-20251001, claude-opus-4-8[1m] $2.09 2.2M (97% from cache) 25.1k 46 9m 3s
review claude-haiku-4-5-20251001, claude-opus-4-8[1m] $1.26 1.2M (96% from cache) 16.7k 31 4m 33s
total $3.35 3.4M (97% from cache) 41.8k 77 13m 36s

kgilpin and others added 8 commits July 9, 2026 09:04
AppMaps carry captured values in a second place: the top-level
eventUpdates map (late updates to events, e.g. a promise's return value
filled in after the call event was written). The walk now sanitizes
those entries in place — same fields, same token namespace — closing a
gap where 6 committed baselines still held raw value strings. The
updates are deliberately not merged into their events: sanitize changes
values only, never structure.

New tokens are now numbered above any token already in the document, so
re-sanitizing a file that gained coverage (like these baselines) can
never assign a token some other value already holds. All affected
baselines re-sanitized; verified no raw eventUpdates values remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each command now records what it did in the file's metadata:

  metadata.sanitized: { version, allow_values }
  metadata.trimmed:   { version, max_length }

Behavior on a marked file: skip when the same CLI version already ran
with the same options; re-apply otherwise. Re-applying only tightens —
masking and truncation are one-way — so the marker records what still
holds: the allowlist intersection of all runs, and the smallest cap.
Both commands warn when options loosen (re-record to actually loosen).

The marker also replaces in-band token detection: in a marked file the
tokens are ours and pass through; in a fresh file a string that merely
looks like a token is application data and is masked — closing the
last gap where a lookalike value could slip through or alias a token.

All 22 committed baselines are marked (they were sanitized by this
code); no values changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The review agent blessed it via the skills-main engine, which still
trims; sanitize it and give it the provenance marker like the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The handler now runs buildAppMap().source().normalize().build() before
masking — the same transform every AppMap viewer applies on load. What
that adds: credentials are stripped from metadata.git.repository (a
capture surface the masking walk didn't cover), eventUpdates are merged
into their events, event ids are re-indexed, and unreturned calls are
balanced. Masking then runs over the canonical form.

Note: normalize() does NOT parameterize SQL or group routes — sanitize's
own fail-closed SQL handling remains the only thing scrubbing SQL.

All 24 committed baselines re-processed through the full pipeline;
bless digests verified identical for every one.

Co-Authored-By: Claude Fable 5 <noreply@appland.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
metadata carries captured data too: git identity (user_name /
user_email), credentials embedded in the repository URL, and exception
/ test-failure messages. The command pipeline's normalize step also
strips repository credentials, but the exported sanitizeAppMap must
not depend on the command wrapping it — the security guarantee lives
in the walk, alone. An unparseable repository URL is masked whole
rather than risking an embedded credential.

Branch, commit, and code locations stay verbatim: repo state, not
captured data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sanitize tokenizes every captured value with a per-file `<vN>`
placeholder. Two fields, though, feed the `appmap compare`
sequence-diagram digest through the request route: an HTTP client
`url`, and a server `path_info` with no route template. A per-file
token there varies between recordings, so two otherwise-identical
AppMaps would compare as changed.

Normalize those fields to a deterministic, lossy route template
instead: strip userinfo, drop the query string and fragment, keep the
host and static path words, and parameterize value-shaped segments
(`/users/123` -> `/users/:int`). Deterministic output keeps the digest
stable across files; erasing the id rather than encoding it keeps the
redaction non-reversible -- a content hash would give the same
cross-file stability but is reversible for low-entropy PII by
guess-and-check.

Server path_info keeps full `<vN>` redaction when a route template is
present, since the concrete path is not in the digest then.

Extend the composite gold trace to cover a client URL and a
template-less path, and re-bless both sanitize baselines.

Assisted-by: Claude:claude-opus-4-8
@kgilpin kgilpin added the appmap-review Run the AppMap behavioral review on this PR label Jul 11, 2026
@kgilpin kgilpin merged commit f18e14f into main Jul 11, 2026
7 checks passed
@kgilpin kgilpin deleted the feat/sanitize branch July 11, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

appmap-review Run the AppMap behavioral review on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants