Rebounder Tech Blog

Written by the people who actually run these systems in production.

The Leak Check Only Grepped Half the Package

Published About 4 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

Build a leak check for a distribution package from a fixed grep word list and no amount of vocabulary helps if the paths it inspects do not cover the whole package.

The short version

We had built the leak check for a paid distribution package out of a fixed grep word list, and it had a hole no amount of vocabulary could close: the paths the check grepped were only part of the package.

  • The installer script shipped to buyers with an internal note left in it
  • The file bundled as the terms of use was a draft, from before legal review
  • The grep check passed every time. Both files were outside the directory being inspected

Adding words would not prevent a recurrence, so we revisited the inspected paths themselves.

What it looks like

We run a build script distributing an internal skill collection as a paid package. It gathers artefacts into an output directory with rsync, greps for internal-specific terms (company names, personal names, fragments of local paths) to check none remain, and then zips. The check was green every time; as far as CI went, all you saw was “no internal-specific text”.

Then a buyer’s review pointed out two things.

  • The bundled installer script had shipped with an internal comment to the effect that “this repository is the internal source of the port, not something a purchaser is expected to use day to day”
  • The file bundled as the “terms of use” was a draft, from before internal legal review

Both exist as text and both read sensibly. The problem is that grep had never inspected these files at all.

Why

Revisiting the check command, grep’s target path was not the whole output directory but only the subdirectory holding the skills.

The installer script and the “terms of use” file, meanwhile, were copied outside that subdirectory — directly into the output root. They were never in grep’s target paths to begin with.

However many internal terms you pile into the word list, a file outside the paths grep reads has nothing to match against. This finding did also surface genuine gaps in the vocabulary (terms indicating a pre-legal-review stage, and a name referring to another internal project, were unregistered), but fixing only that would have left the same hole. As long as the inspected paths cover half the package, no enrichment of the vocabulary ever reaches the files outside them.

Fixing it

We made it two-stage.

1. Widen grep’s target paths to the whole package, root included.

# before: inspected only the skills subdirectory
grep -rInE "$WORDS" "$OUT/skills/"

# after: covers the whole package, including the installer and docs copied to the root
grep -rInE "$WORDS" "$OUT" --include="*.md" --include="*.sh"

2. Add a sanitising step before grep.

grep is a mechanism for finding only the words on the list, so it is powerless against a new leak pattern not on it. Instead of chasing individual words forever, we put a step first that mechanically handles the categories that must always be dropped at distribution time. Provenance comments (statements of which repository something came from) and internal-only config values are rewritten with sed just before distribution.

for f in "$OUT/install.sh" "$OUT/uninstall.sh"; do
  sed -i '' '/<pattern naming the internal repository>/d' "$f"
  sed -i '' 's/<internal-only config value>/<default for distribution>/' "$f"
done
bash -n "$OUT/install.sh" && bash -n "$OUT/uninstall.sh" \
  || { echo "sanitising broke the script"; exit 1; }

sed rewriting can break a script’s syntax, so a bash -n syntax check runs immediately after sanitising. The order is now drop it mechanically in sanitising, and let grep inspect the whole thing as the last line of defence.

Making it less likely to recur

A denylist check readily produces a sense of safety with every word added — “that one is covered now”. But the reality here was that the paths, not the vocabulary, were covering half. A check returning green every time is not proof that what it inspects is right.

The order to review should be reversed. Doubt what the check command actually reads first, confirm it covers the whole package, and only then work on enriching the vocabulary. In a case like this one, where the output directory’s structure changes partway (more files copied to the root), revisiting the inspection scope falls behind updating the vocabulary easily.

The same shape of “the assumed scope of a check and its real scope diverge” appears in A staging Default in env Deleted Production Images too. In both, the check is running and what it looks at is narrower than anyone thought.

Frequently asked questions

Q1Would a longer word list have prevented it?

Not on its own. The pre-fix command grepped only the subdirectory holding the skills, not the installer or the terms file copied to the package root. A perfect vocabulary gives the same result if the paths are not in scope.

Q2Why was it only caught in the buyer's review?

The grep check exited 0 every time, so all CI ever showed was "checked, nothing found". That the inspected paths cover only half the package is not visible from an exit code until a human reads what actually shipped.

Q3What changed besides adding words to the list?

We widened grep to the whole package including the root, and added a sanitising step before it. sed rules mechanically strip provenance comments and internal-only config values at build time, and grep runs afterwards as the last line of defence. bash -n then checks the scripts still parse.

Q4Does this apply beyond our own build script?

Yes, to denylist checks generally. Settle on a set of paths once and stop worrying, and nobody revisits the scope when files move or new outputs appear. Doubt what the check actually reads before enriching its vocabulary.

Environment verified

  • bash 3.2 / macOS BSD sed (sed -i '')
  • 2026-08-14, build script fixed in response to 12 buyer review findings and 15 operational audit items

What this article is based on

  • Shell file lines 31-37commit dd5f794
  • Shell file lines 24-28commit dd5f794
  • Shell file lines 21-27commit cc42262
  • Shell file lines 31-35commit cc42262

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.