Skip to content

fix(telemetry): require base64-run evidence before labelling a match blob-embedded - #2707

Merged
devantler merged 7 commits into
mainfrom
claude/cred-table-blob-run-evidence-2522
Aug 7, 2026
Merged

fix(telemetry): require base64-run evidence before labelling a match blob-embedded#2707
devantler merged 7 commits into
mainfrom
claude/cred-table-blob-run-evidence-2522

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

The leak table is how we notice a credential reaching a transcript. Today it can't separate a real one from noise: a token-shaped string that occurs by chance inside an encoded blob lands in the same rows as a genuinely exposed credential. Measured last month, 35 of 113 GitHub-token-shaped matches were chance substrings of encoded data — so roughly a third of the table is noise nobody can tell apart from the thing it exists to catch.

An earlier attempt at this was closed rather than shipped, because it would have made the detector worse in the one direction that matters: it could have marked a genuinely exposed credential as "probably just encoding noise", quietly lowering triage priority on a real leak.

What

Separates the two, but only on positive evidence that the match really sits inside encoded data — never on a guess from a single character, which is what sank the earlier attempt. Anything ambiguous stays in the plain high-signal row.

Nothing is suppressed or filtered out: a labelled row keeps its shape and its count, so the numbers are unchanged and only the triage order improves. The scan cost is unchanged too.

Part of #2522 — this delivers the classification half. The issue's remaining item (having the locator and concentration lines derive from the same filtered input as the table, so the three surfaces cannot contradict each other) is untouched and keeps the issue open.

Blocked — GitHub Actions outage

GitHub reported Actions: major_outage (verified live at 2026-08-06T22:03Z), so CI cannot run at this head. Validated locally instead: the full contract suite is 520 passed / 0 failed (baseline 515 + 5 new), with the new behaviour proven RED before GREEN and two ablations each firing their own named assertion.

…blob-embedded

A token shape occurring by chance inside an encoded blob passes the leak
table's boundary anchor, because the base64 characters + / = all fall
outside the anchor class. Measured 2026-07-25: 35 of 113 GitHub-token
shaped matches were chance substrings inside Codex encrypted_content
blobs, sitting in the same rows as real credentials.

Label them from evidence of a surrounding base64 run, never from the
boundary character alone. #2520 tried the latter and was closed: only
+ and / CAN match mid-blob, but that does not make / MEAN mid-blob, and
a JWT in a URL path segment takes / as its boundary too — so the label
would have downgraded a genuinely exposed credential.

The label never suppresses: the row keeps its shape and count, and any
value without positive run evidence falls through to the plain
high-signal row.

The blob set is partitioned from the SAME single decode pass as the
table, so an excluded image payload cannot manufacture a blob label,
and the credential-table jq startup count is unchanged.

Part of #2522

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Generated by the Agentic Engineer

Self-review (cross-instance, tick 1005)

Reviewed commit 558299c37b2db7fff74a22d1590fb26a950d521d.

One finding, in the direction the PR explicitly says it must never fail in. Raised as a thread rather than pushed, because a live sibling session currently owns this branch's worktree and the two-writer rule says stand down rather than write through.

Verdict: 1 finding (P1: 1).

Comment thread .claude/scripts/agent-telemetry.sh Outdated
= is both a base64 padding character and the assignment operator, so an
assignment whose key is a long unbroken alphanumeric run cleared the run
threshold on its own. myverylongsecrettoken=ghp_... satisfied the run AND
the token shape, and was labelled blob-embedded — downgrading the most
common real leak form to encoding noise, the one direction this detector
must never fail in.

The boundary class is now [+/]. The trade is deliberate and cheap in the
safe direction: a token sitting immediately after base64 padding stays a
plain high-signal row, costing one extra triage, where the alternative
buries a live credential. = stays in the run class, because padding
legitimately appears inside a blob, and both legs keep byte-identical
boundary classes so the two normalisers cannot diverge.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

New head 2da9cea fixes the P1 raised on the previous head (= removed from the blob boundary class on both legs, with a test pinning <16+ alnum key>=<token> as plain). All 30 checks green, thread resolved.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

@devantler I will perform a full review of the new head, including the boundary classification change and its regression coverage.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 51 minutes.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@devantler, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9db9991b-f7e2-4105-93c9-842add7f010f

📥 Commits

Reviewing files that changed from the base of the PR and between bae4493 and af19ace.

📒 Files selected for processing (2)
  • .claude/scripts/agent-telemetry.sh
  • .claude/scripts/agent-telemetry.test.sh
📝 Walkthrough

Walkthrough

The credential scanner now detects credential-shaped values inside base64-like runs. It extracts blob and ordinary matches in one pass, normalizes them consistently, and stores blob evidence in a temporary file with cleanup traps. Classification adds [blob-embedded] while preserving existing labels. Regression tests cover positive matches, URL and assignment-key exceptions, deep paths, regex parity, and excluded image payloads.

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely states the main change: requiring base64-run evidence before applying the blob-embedded label.
Description check ✅ Passed The description directly explains the classification change, safety rationale, tests, and remaining scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Tried and evaluated as a user (head 2da9cea)

Ran the miner's --section safety over the real local corpus twice — once with the boundary class as it was ([+/=]) and once as it now is ([+/]) — and compared the credential table.

The mislabelling was not hypothetical: on live data, GitHub-token rows that the old class buried under [blob-embedded: … likely a chance substring] come back as plain high-signal rows with the fix, which is the row a reader is told to act on. One additional generic-assignment row also surfaces, consistent with the key no longer being stripped off the value before normalisation. Absolute counts stay in the private operator notes rather than a public artifact; the direction is the point, and it is the direction the design says must never be wrong.

The blob-embedded rows that should be labelled are unchanged, so the label still does its job — the change is a narrowing, not a removal.

Programmatic proof is in the resolved thread: RED/GREEN over the full suite (521/1 ablated vs 522/0 fixed), the failing assertion named in advance, and the six-case boundary table. All 30 checks green at this head, including both Test agent telemetry miner matrix legs.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Parked at 2da9cea on a named, live-verified blocker: no review lane will serve

Everything except the review is clear at this head — 30 checks with 0 failing and 0 pending (CI - Required Checks success, both Test agent telemetry miner matrix legs green), 0 unresolved threads, no conflict, and the user evaluation recorded above.

The three lanes, each checked directly on a PR today:

lane state at this head evidence
CodeRabbit rate limited "Next review available in: 51 minutes" — 01:15Z, so free from roughly 02:06Z
Codex usage limit "You have reached your Codex usage limits for code reviews" — 01:26Z
Cursor Bugbot never ran check-run neutral with output.title: Error; "Bugbot couldn't run - usage limit reached" — 01:29Z

CodeRabbit's window reopens shortly and it is the priority lane, so the next run requests it rather than substituting a local round for a lane that is about to serve. If the window has passed and CodeRabbit still will not deliver, the local-review-round conditions are already met and recorded here.

Nothing else is owed on this PR.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Re-requesting CodeRabbit at the same head. The earlier refusal at this head (01:15Z) stated a
concrete window — "Next review available in: 51 minutes", i.e. ~02:06Z — and that window has
now elapsed, so the lane is due to serve again rather than being unavailable.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Your plan includes PR reviews subject to rate limits. Reviews are available now.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CodeRabbit did not serve at this head. Its stated 51-minute window (from 01:15Z) had elapsed when I
re-requested at 02:18Z; 44 minutes later there is no reaction on the trigger, no review object, and
no summary update — against a normal acknowledgement latency of seconds. Recording it as no-gate
so a later run resumes at the next lane rather than spending CodeRabbit again.

Advancing to Codex (lane 2).

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Advancing to Cursor Bugbot at this head. CodeRabbit acknowledged at 02:14:47Z ("Reviews are available
now") but produced no review object in the ~65 minutes since, and Codex reported its usage limit at
02:23:43Z. Bugbot is the one lane not yet tried at 2da9cea. The bare trigger follows in the next
comment — Bugbot exact-matches the whole comment body, so it cannot carry this note.

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_49704d04-5549-47b4-88c3-48a8a0dcdc15)

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Generated by the Agentic Engineer

Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)

Reviewed commit: 2da9ceab56593c79ff4e29f184fb96386bedbc37

Per-lane evidence at this exact head, checked directly on this PR:

  • CodeRabbit — requested 02:14:24Z; acknowledged 02:14:47Z ("Reviews are available now"), then
    produced no review object at all in the ~55 minutes to 03:09Z. This PR has zero
    coderabbitai[bot] review objects at any head.
  • Codexchatgpt-codex-connector[bot] reported usage limits exhausted at 02:23:43Z.
  • Cursor Bugbot — requested 03:09:41Z; check-run started 03:09:49Z and failed 03:09:50Z with
    conclusion=neutral, output.title=Error, and the comment "Bugbot couldn't run - usage limit
    reached". No retry window is stated, so the lane is genuinely unavailable rather than slow.

Reviewed for correctness and, because the diff is credential-detection logic, for security.

Verdict: 1 finding (P0: 0, P1: 1).

The finding is a false-downgrade in the direction this PR's own comments name as the one it must never
fail in. It does not suppress a row — the shape and count survive the label — so a live credential
is still reported; what changes is that it is annotated as probable noise. Detail inline.

Comment thread .claude/scripts/agent-telemetry.sh Outdated
…t blob evidence

The blob-embedded label existed to stop a token shape occurring by chance inside
a base64 blob from being counted as a real credential. Its evidence is a
surrounding base64 run, deliberately not the boundary character, because `/` is
also how a URL path segment ends.

But `/` was itself in the RUN class, which made the run's LENGTH the only test —
and a URL path reaches any length simply by being nested. `test/session` is 12
characters and stayed plain; `test/api/v1/sessions` is 20, so the very same JWT
was downgraded to "probably encoding noise" purely for sitting one path segment
deeper. That is the one direction this detector must never fail in, and it
reached github-tokens too: a token pasted into an API URL is a live credential.

Length never distinguished a blob from a path; it only looked like it did,
because the single fixture chosen happened to sit under the bar.

What separates them is how the run is broken up. Base64 emits `/` about once per
64 characters, so a genuine blob carries long slash-free stretches while a URL
path is short segments between slashes. Requiring the run to be unbroken by `/`
keeps the blob evidence and drops the path. `/` stays a valid boundary char.

Residual error is in the safe direction: a blob whose final slash-free stretch
is short stays a plain high-signal row, costing one extra triage, where the
opposite error buries a live credential.

Measured on the real 7-day corpus (120 sessions): one match moved out of the
blob bucket back to plain high-signal, and its shape is a four-segment URL path,
not a blob. The five genuine blob matches kept their label.

The strip class is changed identically. That half is not observable by example —
verified by ablation, because the strip only sees matches the label already
accepted — so it is bound by a structural assertion that the two classes stay
byte-identical instead, which fires when either drifts.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

New head bae44933 fixes the P1 my own local review round raised at 2da9cea, so every earlier
lane result is stale and the ladder restarts here at CodeRabbit.

What changed: the blob-embedded run class no longer contains /, so an ordinary nested REST path
can no longer masquerade as base64-run evidence and downgrade a live credential to "encoding noise".
RED 524/2 → GREEN 528/0; one real match on the 7-day corpus moved back to plain high-signal.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

@devantler I will review the new head. I will check the revised base64-run evidence rule and its regression coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/scripts/agent-telemetry.sh:
- Around line 250-256: Update the credential-matching flow around CREDMATCH and
CREDBLOB so raw or normalized credential values are never persisted in temporary
files, including after SIGKILL. Keep blob membership and classification in
memory, or replace file contents with an opaque non-reversible identity while
preserving the existing matching behavior and cleanup handling.
- Around line 953-964: The blob classification regex around CRED_BLOB_TABLE_RE
and CRED_BLOB_STRIP_RE must not treat a 16-character slash-free URL path segment
as blob evidence. Update the classification logic to exclude URL context or
require stronger non-URL evidence, while keeping both normalizer regexes
aligned; add a regression covering a URL such as
https://example.test/abcdefghijklmnop/ghp_… and verify the token is not labeled
blob-embedded.

In @.claude/scripts/agent-telemetry.test.sh:
- Around line 892-903: The parity assertion around BLOB_TABLE_CLASS and
BLOB_STRIP_CLASS currently compares only the run classes; extend it to extract
and compare each regex’s trailing boundary class ([+/]) as well. Keep the
existing run-class checks, add deterministic fixture-driven coverage under the
executable script tests, and report mismatched boundary values through the
parity assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4aa37068-94fa-4c1a-9a0d-4f6ec34d499d

📥 Commits

Reviewing files that changed from the base of the PR and between d0a7a9e and bae4493.

📒 Files selected for processing (2)
  • .claude/scripts/agent-telemetry.sh
  • .claude/scripts/agent-telemetry.test.sh
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-07-20T18:30:45.905Z
Learnt from: devantler
Repo: devantler-tech/monorepo PR: 2309
File: .claude/scripts/portfolio-surveyor.test.sh:35-38
Timestamp: 2026-07-20T18:30:45.905Z
Learning: For prose “subagent definition” documents under `.claude/agents/**` (e.g., `portfolio-surveyor.md`) that are interpreted by an LLM rather than executed deterministically: in CI, use focused textual contract assertions (e.g., checking required sections, prompts, and structural constraints). Do not attempt behavioral validation by simulating it via non-deterministic model invocations; behavioral validation should require live Surveyor runs.

For executable components under `.claude/scripts/**` (e.g., `release-bot-exemption.sh`): use fixture-driven, deterministic tests that exercise the script logic with controlled inputs/outputs (rather than model calls).

Applied to files:

  • .claude/scripts/agent-telemetry.sh
  • .claude/scripts/agent-telemetry.test.sh
📚 Learning: 2026-07-29T22:21:01.108Z
Learnt from: devantler
Repo: devantler-tech/monorepo PR: 2566
File: .claude/scripts/merge-confirmation-read.test.sh:0-0
Timestamp: 2026-07-29T22:21:01.108Z
Learning: In shell test harness/ablation scripts, capture the exit status of the command being tested immediately into `rc=$?` on the very next line. Avoid using `$?` as an argument to another command (e.g., `printf ... "$?"` or similar) when the same line also includes `$(...)` command substitution, because shell evaluation/expansion order can replace the tested command’s exit status with the exit status of the command substitution, producing incorrect measurements.

Applied to files:

  • .claude/scripts/agent-telemetry.sh
  • .claude/scripts/agent-telemetry.test.sh
📚 Learning: 2026-07-29T16:38:50.481Z
Learnt from: devantler
Repo: devantler-tech/monorepo PR: 2554
File: .claude/scripts/worktree-cleanup.test.sh:36-55
Timestamp: 2026-07-29T16:38:50.481Z
Learning: In `.claude/scripts` test fixture scripts, when running Git setup commands (e.g., `git worktree add`, `git submodule add`), do not suppress failures (e.g., by ignoring exit codes or redirecting errors to `/dev/null`) without explicitly checking command status. A setup failure can otherwise cause contract tests to falsely pass (for example, due to empty/incorrect Git porcelain output). Preserve stderr for diagnostics and fail the fixture immediately if setup steps return a non-zero exit code (use `set -e`/`set -o pipefail` and/or explicit `if ! command; then ...; exit 1; fi`).

Applied to files:

  • .claude/scripts/agent-telemetry.test.sh
🪛 Shellcheck (0.11.0)
.claude/scripts/agent-telemetry.sh

[info] 2993-2993: Expressions don't expand in single quotes, use double quotes for that.

(SC2016)

Comment thread .claude/scripts/agent-telemetry.sh Outdated
Comment thread .claude/scripts/agent-telemetry.sh Outdated
Comment thread .claude/scripts/agent-telemetry.test.sh Outdated
Addresses CodeRabbit's review of the previous commit.

Excluding `/` from the run class stopped multi-segment URL paths from chaining
into one long run, but left the run as a single path SEGMENT — and 16 characters
is well under what ordinary segments reach. A bare 32-character hex id, the
commonest long segment in a REST URL, cleared the bar outright, so
`…/0f8e7d6c5b4a392817263544536271a1/ghp_…` was still buried as encoding noise.
16 had only ever been calibrated against multi-segment runs, which no longer
exist here.

The threshold is now 40: above every identifier shape that actually occurs
(32-hex ids, UUID fragments, slugs), while a genuine blob is hundreds of
characters with `/` arriving about once per 64, so it still presents a
40-character slash-free stretch roughly half the time. That miss is the safe
half — the label exists to suppress noise, so under-labelling costs one triage
where over-labelling buries a live credential.

The positive fixture was 24 characters, which is not a realistic blob run; it is
now a 48-character one, so it exercises the rule rather than sitting under it.

Also extends the structural parity assertion to the BOUNDARY class, not just the
run class: if the boundaries drift, the strip removes a different span than the
label matched and an accepted match keeps part of its run inside the credential's
value.

531 assertions green. Both new guards ablated and each fires exactly its own
named assertion: reverting the threshold to 16 fails the hex-segment case, and
diverging the strip's boundary class fails the boundary-parity case.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

All three findings from the bae44933 review are addressed in 473bc188, so the ladder restarts
here at CodeRabbit on the new head.

  • Reject long URL path segments (Major) — accepted; the threshold was only calibrated for
    multi-segment runs and a 32-char hex id cleared it. Now 40, with your regression case added.
  • Assert boundary-class parity (Minor) — accepted; both run and boundary classes are compared.
  • Credential values in temp files (Major) — real, but pre-existing and structural rather than
    introduced here; filed as fix(telemetry): stop writing credential values to temp files that survive SIGKILL #2712 with acceptance criteria rather than folded into this fix.

CI green, 531 assertions green, both new guards ablated and each fires exactly its own assertion.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR metadata and changed files ---'
gh pr view 2707 --repo devantler-tech/monorepo --json headRefOid,baseRefOid,files --jq '{headRefOid, baseRefOid, files: [.files[] | {path, additions, deletions}]}'

printf '%s\n' '--- Changed-file outlines ---'
ast-grep outline .claude/scripts/agent-telemetry.sh --items all --view expanded || true
ast-grep outline .claude/scripts/agent-telemetry.test.sh --items all --view expanded || true

printf '%s\n' '--- Pull-request diff ---'
gh pr diff 2707 --repo devantler-tech/monorepo -- .claude/scripts/agent-telemetry.sh .claude/scripts/agent-telemetry.test.sh

printf '%s\n' '--- Regex, evidence, and temp-file references ---'
rg -n -C 5 -e 'blob-embedded|base64|BASE64|boundary|run class|mktemp|trap|tmp' \
  .claude/scripts/agent-telemetry.sh .claude/scripts/agent-telemetry.test.sh

Length of output: 1693


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='d0a7a9e5ccd071f4e8478003ab1ba7870cdbb60e'
head='473bc1889be8e68bbb0699a132e6d6c31f356472'

printf '%s\n' '--- Complete changed-file diff ---'
git diff --no-ext-diff --unified=80 "$base" "$head" -- \
  .claude/scripts/agent-telemetry.sh \
  .claude/scripts/agent-telemetry.test.sh

printf '%s\n' '--- Source: telemetry implementation, relevant lines ---'
rg -n -C 12 -e 'blob-embedded|base64|BASE64|boundary|mktemp|trap|tmp|decode|extract|normaliz' \
  .claude/scripts/agent-telemetry.sh

printf '%s\n' '--- Source: telemetry regression tests, relevant lines ---'
rg -n -C 12 -e 'blob-embedded|base64|BASE64|boundary|URL|path|assignment|parity|exclude' \
  .claude/scripts/agent-telemetry.test.sh

printf '%s\n' '--- Full changed test file with line numbers ---'
cat -n .claude/scripts/agent-telemetry.test.sh

Length of output: 187881


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='d0a7a9e5ccd071f4e8478003ab1ba7870cdbb60e'
head='473bc1889be8e68bbb0699a132e6d6c31f356472'

context_for_matches() {
  local file="$1"
  local pattern="$2"
  local line start end
  while IFS=: read -r line _; do
    start=$(( line > 35 ? line - 35 : 1 ))
    end=$(( line + 55 ))
    printf '\n--- %s: lines %s-%s ---\n' "$file" "$start" "$end"
    sed -n "${start},${end}p" "$file" | nl -ba -v "$start"
  done < <(rg -n -i "$pattern" "$file")
}

printf '%s\n' '--- Changed hunks containing blob-related terms ---'
git diff --no-ext-diff --unified=18 "$base" "$head" -- .claude/scripts/agent-telemetry.sh \
  | awk '
      /^diff --git/ {show=0}
      /^@@/ {hunk=$0; buf=$0 "\n"; show=0; next}
      {buf=buf $0 "\n"}
      /blob-embedded|base64|BASE64|boundary|BLOB/ {
        if (!show) {printf "%s", buf; show=1}
      }
      show && /^@@/ {show=0}
    '

printf '%s\n' '--- Implementation contexts ---'
context_for_matches .claude/scripts/agent-telemetry.sh 'blob-embedded|base64|boundary|leak.table|leak_table'

printf '%s\n' '--- Regression-test contexts ---'
context_for_matches .claude/scripts/agent-telemetry.test.sh 'blob-embedded|base64|boundary|long URL|URL path|assignment'

printf '%s\n' '--- All references to the telemetry target and test invocation ---'
rg -n -C 3 'agent-telemetry(\.test)?\.sh|TARGET=' .claude/scripts/agent-telemetry.test.sh

Length of output: 7549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='473bc1889be8e68bbb0699a132e6d6c31f356472'
impl=$(mktemp)
testf=$(mktemp)
trap 'rm -f "$impl" "$testf"' EXIT

git show "$head:.claude/scripts/agent-telemetry.sh" > "$impl"
git show "$head:.claude/scripts/agent-telemetry.test.sh" > "$testf"

printf '%s\n' '--- Implementation: credential regex declarations ---'
sed -n '850,980p' "$impl" | nl -ba -v 850

printf '%s\n' '--- Implementation: complete changed safety block ---'
sed -n '2960,3150p' "$impl" | nl -ba -v 2960

printf '%s\n' '--- Test match locations ---'
rg -n -i 'blob-embedded|base64|boundary|long URL|URL path|assignment' "$testf" || true

printf '%s\n' '--- Bounded test contexts ---'
while IFS=: read -r line rest; do
  start=$(( line > 24 ? line - 24 : 1 ))
  end=$(( line + 42 ))
  printf '\n--- test lines %s-%s (%s) ---\n' "$start" "$end" "$rest"
  sed -n "${start},${end}p" "$testf" | nl -ba -v "$start"
done < <(rg -n -i 'blob-embedded|base64|boundary|long URL|URL path|assignment' "$testf" | head -n 40)

Length of output: 1412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='473bc1889be8e68bbb0699a132e6d6c31f356472'
impl=$(mktemp)
testf=$(mktemp)
trap 'rm -f "$impl" "$testf"' EXIT

git show "$head:.claude/scripts/agent-telemetry.sh" > "$impl"
git show "$head:.claude/scripts/agent-telemetry.test.sh" > "$testf"

numbered_range() {
  local file="$1" start="$2" end="$3"
  sed -n "${start},${end}p" "$file" | awk -v n="$start" '{printf "%6d  %s\n", n++, $0}'
}

printf '%s\n' '--- Implementation: credential regex declarations ---'
numbered_range "$impl" 850 980

printf '%s\n' '--- Implementation: complete changed safety block ---'
numbered_range "$impl" 2960 3150

printf '%s\n' '--- Test match locations ---'
rg -n -i 'blob-embedded|base64|boundary|long URL|URL path|assignment' "$testf" || true

printf '%s\n' '--- Bounded test contexts ---'
while IFS=: read -r line rest; do
  start=$(( line > 24 ? line - 24 : 1 ))
  end=$(( line + 42 ))
  printf '\n--- test lines %s-%s (%s) ---\n' "$start" "$end" "$rest"
  numbered_range "$testf" "$start" "$end"
done < <(rg -n -i 'blob-embedded|base64|boundary|long URL|URL path|assignment' "$testf" | head -n 32)

Length of output: 34897


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='473bc1889be8e68bbb0699a132e6d6c31f356472'

show_range() {
  local path="$1" start="$2" end="$3"
  git show "$head:$path" | sed -n "${start},${end}p" \
    | awk -v n="$start" '{printf "%6d  %s\n", n++, $0}'
}

printf '%s\n' '--- Implementation: combined scan and blob regex definitions ---'
show_range .claude/scripts/agent-telemetry.sh 969 1025

printf '%s\n' '--- Regression tests: blob-evidence fixtures and parity assertion ---'
show_range .claude/scripts/agent-telemetry.test.sh 769 955

printf '%s\n' '--- Regression tests: value normalization and duplicate handling ---'
show_range .claude/scripts/agent-telemetry.test.sh 956 1110

printf '%s\n' '--- Exact diff at the reviewed head for the blob-related test range ---'
git diff --no-ext-diff --unified=12 \
  d0a7a9e5ccd071f4e8478003ab1ba7870cdbb60e "$head" -- \
  .claude/scripts/agent-telemetry.test.sh \
  | sed -n '/blob-embedded/,+230p'

Length of output: 44574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='d0a7a9e5ccd071f4e8478003ab1ba7870cdbb60e'
head='473bc1889be8e68bbb0699a132e6d6c31f356472'

show_range() {
  local path="$1" start="$2" end="$3"
  git show "$head:$path" | sed -n "${start},${end}p" \
    | awk -v n="$start" '{printf "%6d  %s\n", n++, $0}'
}

printf '%s\n' '--- Diff whitespace check ---'
git diff --check "$base" "$head"

printf '%s\n' '--- Scratch-file creation and cleanup ---'
show_range .claude/scripts/agent-telemetry.sh 220 285

printf '%s\n' '--- Every reference to the new evidence variables ---'
git show "$head:.claude/scripts/agent-telemetry.sh" \
  | grep -nE 'CREDMATCH|CREDBLOB|CRED_BLOB_(TABLE|STRIP|ANCHORED|RUN_MIN)' \
  || true

printf '%s\n' '--- All changed implementation hunks ---'
git diff --no-ext-diff --unified=8 "$base" "$head" -- .claude/scripts/agent-telemetry.sh

printf '%s\n' '--- Pull-request checks reported by GitHub ---'
gh pr view 2707 --repo devantler-tech/monorepo --json statusCheckRollup \
  --jq '.statusCheckRollup[] | {name, status, conclusion, detailsUrl}'

Length of output: 32581


@devantler Full review complete for 473bc188.

I found no blocking issues.

The revised rule uses a 40-character, slash-free base64 run and [+/] as the boundary class. The implementation preserves rows and counts. It applies blob-embedded only after positive evidence and normalized-value matching.

The regression coverage includes URL paths, long URL segments, assignment keys, regex-class parity, and image-payload exclusion. The new scratch files use mktemp and are present in all cleanup traps.

git diff --check passed. The required checks and both telemetry-miner matrix jobs are successful.

You are interacting with an AI system.

@devantler

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CodeRabbit is rate limited at this head (CodeRabbit status: Review rate limited), so the ladder
advances to Codex. It reviewed the previous head and its three findings are all addressed in
473bc188.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Codex is at its usage limit at this head, so the ladder advances to Cursor Bugbot. The bare trigger
follows in the next comment (Bugbot exact-matches the whole comment body).

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8ba011e8-c5f1-4f2e-b8ad-488beedf0a35)

Found reviewing my own diff. Raising CRED_BLOB_RUN_MIN to 40 silently made an
existing safety test vacuous.

Test (2b) exists to prove that `=` stays OUT of the blob boundary class, because
`<long alnum key>=<token>` is the commonest real leak shape and labelling it
encoding noise would bury a live credential. Its fixture key
`myverylongsecrettoken` is 21 characters. Under the old threshold of 16 that
cleared the run and the test genuinely reached the boundary check; under 40 it
does not, so the test now passes on the run length alone and can never exercise
its own subject.

Proven by ablation: adding `=` back to the boundary class — precisely what (2b)
guards against — did not fail (2b) at all, only the new structural parity
assertion. With the key lengthened to 47 characters, that same ablation fails
(2b) as it should.

This is the same failure mode this PR was opened to fix, one test over: a fixture
whose parameter sits outside the region the rule applies to tests the value, not
the invariant. Changing a threshold means re-checking every fixture calibrated
against the old one.

531 assertions green.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

State at 338b9e54 — ready except for the review gate, ladder resumes here

CodeRabbit's three findings on bae44933 are all addressed, and reviewing my own diff afterwards
turned up one more, which is fixed in this head.

The self-review finding, because it is the interesting one. Raising the run threshold to 40
silently made an existing safety test vacuous. Test (2b) exists to prove = stays out of the blob
boundary class — <long alnum key>=<token> is the commonest real leak shape. Its fixture key was 21
characters, which cleared the old threshold of 16 but not 40, so the test began passing on run length
alone and could no longer reach the boundary check at all. Ablation confirmed it: adding = back to
the boundary class did not fail (2b). With the key lengthened to 47, that ablation fails it again.

That is the same defect this PR was opened to fix, one test over — a fixture parameterised outside
the region the rule applies to. Changing a threshold means re-checking every fixture calibrated
against the old one.

Review ladder — all three lanes refused at 473bc188, checked directly per-PR

lane evidence time
CodeRabbit CodeRabbit status → Review rate limited 04:5xZ
Codex You have reached your Codex usage limits for code reviews 04:57:17Z
Cursor Bugbot check-run neutral / output.title: Error + Bugbot couldn't run - usage limit reached 04:59:14Z

Bugbot and Codex are account-level usage limits with no stated retry window, so re-triggering
them is waste rather than diligence. CodeRabbit did serve at bae44933 (its review is the reason
this head exists), so it is the lane to retry once its window rolls.

Not claiming the local-review fallback on this evidence, because it was gathered at the
superseded head 473bc188 and the fallback requires per-lane evidence at the current head. The
next run re-checks the three lanes at 338b9e54 and either takes a real review or posts a qualifying
local round then.

Staying a draft. CI green apart from the macOS telemetry job still running at the time of writing;
531 assertions pass locally, and every guard added in this PR has been ablated and fires exactly its
own named assertion.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

@devantler I will review the current head and the updated regression coverage.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CodeRabbit is rate limited at this head and Codex did not respond, so the ladder advances to Cursor
Bugbot. The bare trigger follows in the next comment (Bugbot exact-matches the whole comment body).

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0be0f9b1-6a65-40d8-af91-a1c0c3661716)

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Generated by the Agentic Engineer

Cross-instance finding — P1, verified by probe at 338b9e54

Posted as a review rather than a push: this branch is checked out in a worktree owned by a live session, so I stood down from writing to it (two-writer discipline).

A credential that appears BOTH inside a base64 run AND plainly is labelled [blob-embedded: … likely a chance substring]. The plain occurrence is real exposure evidence, and it is buried — the exact direction this change's own comments say the detector must never fail in ("a wrong label buries a live credential", "every ambiguous value falls through to the plain high-signal row").

The value is not ambiguous here: there is positive plain evidence, and it loses to the blob label anyway.

Probe, run against the script at this head

Both controls behave as designed, so the probe discriminates in both directions (a negative control alone could not prove the matcher works):

Case Corpus contains Result
A (control) blob occurrence only 1 github-token … [blob-embedded: …] ✅ as designed
B (control) plain occurrence only 1 github-token (classic/app) ✅ as designed
C the same token, both ways 1 github-token … [blob-embedded: …] ❌ plain occurrence buried

Fixture for C was a single session record containing sig=<48 base64 chars>/<token> and, later in the same text, the identical token plainly. No trailing characters after either occurrence — with a trailing zz the greedy gh[pousr]_[A-Za-z0-9]{16,} absorbs it, the two occurrences become different values, two rows appear, and the probe is vacuous. That vacuous version is what I ran first.

Root cause

CREDBLOB is the set of values that have a blob occurrence, not the set of values whose occurrences are all blob-embedded. sort -u in cred_normalise then collapses the plain and blob occurrences of one value into a single row, and the label is applied on set membership alone.

Suggested fix

Label only when the value has no plain occurrence — subtract the plain set from the blob set, rather than taking blob membership as sufficient. That keeps the stated asymmetry intact: ambiguity, and now also any positive plain evidence, falls through to the plain high-signal row.

A regression test belongs alongside (2b)/(2c): same value both ways, assert the row is not labelled.

Lane state at this head, for the record

All three review lanes refused at 338b9e54, each within ~10s of its trigger: CodeRabbit Review rate limited (05:12:09Z, head status description confirms), Codex usage limit (05:18:00Z), Bugbot usage limit + check-run neutral/Error (05:19:44Z). I did not claim the local-review fallback, because this finding means the PR is not ready regardless.

Comment thread .claude/scripts/agent-telemetry.sh Outdated
The blob-embedded label keyed on membership in the set of values with *a*
base64-run occurrence, not the set whose occurrences are *all* blob-embedded.

cred_normalise ends in `sort -u`, so a credential occurring both inside an
encoded blob and plainly collapses to ONE row — and that row was labelled
"likely a chance substring", burying the plain occurrence. The plain
occurrence is the genuine exposure evidence, and a leaked token routinely
appears both in prose and inside an encoded payload of the same transcript,
so this downgraded exactly the shape a real leak takes.

The label's own contract already states that ambiguity falls through to the
plain high-signal row. Subtracting the plain set enforces that rather than
assuming it.

Subtraction uses awk set membership on a file rather than `comm`: both sides
are `sort -u` output, but `comm` re-compares them, so the two would have to
agree on collation as well as order. This also matches the file-reading idiom
the label pass already uses, whose getline-on-empty-file behaviour is safe
where the NR==FNR idiom would eat the first data line.

Proven in both directions by ablation: dropping the subtraction fires
"a token ALSO occurring plainly is NOT labelled blob-embedded"; emptying the
blob set fires the existing "a token inside a base64 run is labelled
blob-embedded". Each ablation fires exactly one named assertion.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

@devantler I will review the current head and the updated telemetry classification coverage.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Validation record for af19ace

Programmatically tested. agent-telemetry.test.sh 533/533 green. The new case carries a control asserting the row is reported at all, so its negative assertion cannot pass vacuously on an empty table.

Two-sided ablation, each firing exactly one named assertion:

Ablation Assertion that fires
Drop the plain-set subtraction (!($0 in plain)1) a token ALSO occurring plainly is NOT labelled blob-embedded
Empty the blob set (!($0 in plain)0) the pre-existing a token inside a base64 run is labelled blob-embedded

The second direction is the one that matters for scope: it proves the subtraction did not quietly disable the label, which a one-sided ablation would have missed.

Tried as a user, and the result is honestly negative. I ran the real script over the live 2-day corpus, then ran the pre-fix version over the same corpus and diffed the credential tables: identical. So on current data this changes nothing an operator sees — no regression, and no live occurrence of the defect in that window either. The defect is a latent fail-open proven by fixture, not one currently firing; I am not claiming a measured improvement it did not produce.

Both runs exit 0. Shellcheck reports no new findings — the four warnings on this file (lines 888, 1362, 1384, 1487) all pre-date this change and lie outside the edited region.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CodeRabbit rate-limited at this head (09:22:42Z, no window stated). Advancing to Codex.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Codex reports its usage limit reached. Advancing to Bugbot.

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4dfbc5b4-5795-43d0-ae44-2fe37801ac7a)

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Generated by the Agentic Engineer

Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)

Reviewed commit: af19ace5334409623af4afba6af5c0a26a2f1f25

Per-lane state at this exact head — all three tried, none will deliver:

Lane Outcome When
CodeRabbit Review rate limited, no retry window stated 2026-08-07T09:22:42Z
Codex You have reached your Codex usage limits for code reviews — no window; needs account credits 2026-08-07T09:35:28Z
Cursor Bugbot check-run neutral / Error, Bugbot couldn't run - usage limit reached — admin must raise the limit 2026-08-07T09:36:15Z

Two are billing limits and one is a rolling rate limit; none is a judgement about this change, and none states a window that would clear inside this run.

What I checked

The subtraction is correct at the boundaries, not just the happy path.

  • Empty plain set — getline on an empty file yields nothing, plain stays empty, every blob value keeps its label. This is why the plain set is read as a file rather than joined with NR==FNR, which would eat the first data line when the joined file is empty.
  • Ordering — $CREDPLAIN is written before the awk that reads it, so the subtraction can never run against a stale or absent file.
  • Comparability — the fix relies on the blob and plain legs normalising to the same string for the same credential. That property already underpinned the original $0 in blob test; it is not newly assumed, and the new fixture exercises it directly.

Shell-level safety. The script runs set -uo pipefail (no -e), so the added cred_plain_matches | cred_normalise pipeline exiting non-zero on an empty corpus cannot abort the run. Verified by running it against an empty corpus: exit 0, table renders as clean.

Value hygiene. $CREDPLAIN holds normalised credential values, so it is created with mktemp (mode 600) and added to both the EXIT and the HUP/INT/TERM traps, matching every other credential scratch file. It is never printed.

Decode-pass budget preserved. The change adds no second decode pass — the batching contract test, which asserts exactly one credential-table jq invocation, is among the 533 passing.

Observation (pre-existing, deliberately not fixed here)

If mktemp fails partway through the temp-file block, the files created before it leak, because the traps are installed after the whole block. My new line participates in that pattern but does not introduce it — every scratch file in that block has the same shape. Fixing it belongs in its own change, not smuggled into a correctness fix.

Verdict

Verdict: no P0/P1 findings.

I held this to the bar I would hold a bot finding to rather than waving it through as my own work; the diff is small, single-concern, and its central claim is proven in both directions by ablation. I also did not overclaim the user evaluation — the pre/post differential on the live corpus is identical, so this closes a latent fail-open rather than changing anything an operator currently sees, and the PR says exactly that.

@devantler
devantler marked this pull request as ready for review August 7, 2026 09:38
@devantler
devantler merged commit c979ea6 into main Aug 7, 2026
36 checks passed
@devantler
devantler deleted the claude/cred-table-blob-run-evidence-2522 branch August 7, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant