Skip to content

Keep internal tracking ids out of the published package - #151

Merged
bogatyrjov1 merged 3 commits into
mainfrom
scrub-internal-refs-from-published-package
Aug 4, 2026
Merged

Keep internal tracking ids out of the published package#151
bogatyrjov1 merged 3 commits into
mainfrom
scrub-internal-refs-from-published-package

Conversation

@bogatyrjov1

Copy link
Copy Markdown
Contributor

What

The published backthread package shipped internal tracking ids. Three of them sat in strings a user reads without ever opening the bundle:

Where Before After
backthread doctor PROJECT-scoped only — blind in git worktrees + other repos (ARP-680). Re-run ... PROJECT-scoped only — blind in git worktrees + other repos. Re-run ...
backthread sweep ... — left for GitHub-derived recovery (ARP-538). ... — left for GitHub-derived recovery.
~/.cursor/hooks/backthread-*.sh header # Backthread wrapper for Cursor — generated by \backthread install --agent cursor` (ARP-692).` # Backthread wrapper for Cursor — generated by \backthread install --agent cursor`.`

The rest were comments that esbuild inlines into the committed dist-bundle/backthread.js, plus five in the shipped plugin hook manifest (cli/hooks/hooks.json). All rewritten to keep the rationale and drop the id — e.g. "a per-project hook is exactly what froze the dogfood log" became "a per-project hook is exactly what silently stops capturing", which is the fact a reader can act on.

The check

scripts/check-no-internal-refs.mjs resolves the exact file set npm pack ships — the files list in cli/package.json plus package.json — and fails on an internal id, a tracker URL, a private repository name or a company email domain. Source files that never reach the tarball are deliberately out of scope; the bundle is in scope, which is the part that matters, because a comment can travel into it silently.

It runs after the bundle rebuild in CI, so it inspects the same bytes the tarball will carry, and again in the release job — a publish cannot be taken back. --dir <path> points it at an extracted package/ so the published artifact can be verified directly.

Before:

FAIL — 14 internal reference(s) in files we publish:
  dist-bundle/backthread.js:7681  ARP-680  (issue tracker id)
  ...
  hooks/hooks.json:4  ARP-763  (issue tracker id)

After: OK — no internal references in the shipped surface.

Notes

  • Patch release 0.18.1 via npm run bump (four version files + bundle in lockstep).
  • Full suite green: 2164 tests, 0 failures. No behaviour change — only text.

…0.18.1)

Three strings a user actually reads ended in an internal tracking id — the
`doctor` hook warning, a `sweep` line about unattributable transcripts, and the
header of the Cursor wrapper script the installer writes into the home
directory. They carry no meaning outside the team, so they are gone and the
sentences say the same thing without them. The shipped plugin hook manifest and
the comments esbuild inlines into the committed bundle are scrubbed the same way.

Add scripts/check-no-internal-refs.mjs: it resolves the exact file set npm pack
ships (the `files` list plus package.json) and fails on an internal id, a tracker
URL, a private repository name or a company email domain. Because it runs after
the bundle rebuild it sees the same bytes the tarball will carry, so a comment
that reaches dist-bundle is caught in CI rather than by a reader on npm. It also
takes `--dir` so the same check can be run against an extracted tarball.

Wired into CI and into the release job — a publish cannot be taken back.
@bogatyrjov1

Copy link
Copy Markdown
Contributor Author

REVIEWER: [high] The check misses a surface we actually distribute. cli/package.json's files list is the npm tarball only, but the Claude Code plugin marketplace and the Gemini/Codex extensions install straight from git.claude-plugin/marketplace.json and extensions/** reach users without ever going through npm. Scanning only cli/ declares victory while a leak sits in a file a Codex user reads on install. And there is one: extensions/codex/README.md carries three ids (ARP-505 / ARP-507 in a heading, ARP-503 in a sentence) — exactly the class this PR exists to remove, missed because the scan was scoped to the npm package.

Fixed: the scan now also covers .claude-plugin/marketplace.json and extensions/, and the README lines are rewritten (## Verify-live status (spike-flavored — ARP-505 / ARP-507)## Verify-live status (not yet confirmed on a real install); the install fallback sentence drops its trailing id). The --dir mode deliberately stays npm-package-only, since an extracted tarball contains nothing else. Re-proved RED against origin/main with the final script: 17 findings, up from the 14 the narrower version saw.

@bogatyrjov1

Copy link
Copy Markdown
Contributor Author

REVIEWER: [low] The extension test used the whole path, not the filename: abs.slice(abs.lastIndexOf('.')) on .claude-plugin/plugin.json picks up the dot in the directory name. It could only ever fail to skip a file (never wrongly skip a text one), so nothing leaked past it — but it would silently stop working the day a directory is named like an image. Now takes the extension off the basename, and only when the dot isn't the leading character.

REVIEWER: [low] findings carried a col field nothing printed, and re.lastIndex = 0 guarded against state matchAll does not share (it clones the regex). Both removed. The finding's name also shadowed the concept "filename" in the output loop — renamed to kind.

The npm tarball is not the only thing a stranger receives. The Claude Code
plugin marketplace manifest and the Gemini / Codex extension directories are
installed from git, so a scan scoped to the package's `files` list declares
victory while a leak sits in a file a Codex user reads on install — and one did:
extensions/codex/README.md carried three tracking ids, one of them in a heading.

Widen the scan to .claude-plugin/marketplace.json and extensions/, and rewrite
those lines. `--dir` stays package-only: an extracted tarball contains nothing
else.

Also take the extension off the basename rather than the whole path, so a dot in
a directory name can't decide whether a file is scanned.
CI has failed at the vulnerability gate on every push to main since 2026-07-24
(dev-only advisories behind pinned versions), and every later step is skipped —
so the new guard would have been merged in a state where it could never fire on
a pull request.

It is a plain node script with no dependencies, so it now runs first. It reads
the committed bundle; the bundle-sync step still ties those bytes to a fresh
build, and the release job keeps its copy after the rebuild.
@bogatyrjov1

Copy link
Copy Markdown
Contributor Author

REVIEWER: [high] The guard I added never ran. CI on this repo has failed at the vulnerability gate on every push to main since 2026-07-24 (dev-only advisories behind pinned versions), and every step after it — including mine — is skipped. So the check was added, reviewed, and merged in a state where it could not have caught anything on a pull request. A guard that only runs when an unrelated gate is happy is not a guard.

Moved it to the first step, before npm ci: it is a plain node script with no dependencies, so it has no reason to sit behind an install or an audit. It reads the committed bundle, and the bundle-sync step further down is what ties those bytes to a fresh build — the two together are equivalent to scanning the tarball. The release job keeps its copy after the rebuild, where there is no audit gate and it scans freshly built bytes directly.

Verified on the pushed run rather than assumed.

@bogatyrjov1
bogatyrjov1 merged commit 32ba51e into main Aug 4, 2026
1 check failed
@bogatyrjov1
bogatyrjov1 deleted the scrub-internal-refs-from-published-package branch August 4, 2026 20:25
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.

2 participants