Public Export Sanitization
Sanitize internal content before it goes public (repo, blog, talk).
by laurigates · MIT
The skill
Sanitize internal content before it goes public (repo, blog, talk). Use when publishing, to catch leaked project ids, service-account emails, hostnames, names, and repo-escaping links.
Sanitizing Internal Content Before It Goes Public
When moving content from a private or internal source into a public destination (a public repo, a blog post, a conference talk, a shareable draft), the expensive failure is a silent context leak: an internal cloud project id, a service-account email, an internal hostname, a personal name, or a Markdown link that points back into a private repo. A human reviewer reliably misses one class of these every time, and the miss becomes visible only after it is public.
The judgment (what to keep, how to reword) is yours. The detection is a script: run it, don't re-derive it.
When to Use This Skill
| Use this skill when... | Use instead when... |
|---|---|
| Moving internal docs, code, or notes into a public repo, post, or talk | Scanning for tokens and keys: git-plugin:git-security-checks (gitleaks) |
| Checking that an exported doc set has no links back into private sources | Checking a doc is readable by a zero-context reader: agent-patterns-plugin:cold-read-gate |
| Genericizing infrastructure identifiers for a public write-up | Verifying a machine-read value is correct: documentation-plugin:docs-verify-machine-facts |
The tripwire: run it on every export
bash "${CLAUDE_SKILL_DIR}/scripts/check-public-export.sh" --patterns <org.patterns> <export-tree>
It scans for internal identifiers and for Markdown links that escape the
export or are broken. Exit 1 means findings to review; exit 2 is a usage
error. Patterns ignore <placeholder> tokens, so a genericized tree comes back
clean. Dot-directories such as .claude/ and .github/ are scanned too; only
.git/ is skipped.
Built-in patterns are org-neutral only: GCP service-account emails
(*.iam.gserviceaccount.com), 12-digit GCP project numbers, and absolute home
paths. Your organisation's identifier shapes go in a --patterns file, one
label::regex per line (extended regex, # comments allowed), kept wherever
your private config lives rather than in the public tree:
# org.patterns
GCP project id (acme-*)::\bacme-[a-z][a-z0-9-]{2,}\b
Internal hostname (corp.example)::\b[a-z0-9-]+\.corp\.example\b
Staff email::\b[a-z0-9._%+-]+@example\.com\b
Without a --patterns file the scan checks only the built-in shapes, so a
clean result then says nothing about org-specific identifiers.
Two modes:
- Pre-export gate (default, strict): scan the export set on its own; any link leaving it is flagged. This is the self-containment gate below.
- Post-placement verify: once the content sits inside a larger public
repo, pass
--repo-root <repo>so links to public siblings (../LICENSE,../other-doc) are allowed and only repo-escaping or broken links flag.
Other options: --names <file> (personal names, which can't be regex'd; seed
the list from the source's git authors and access grants; one literal entry per
line; only a bare # or # followed by whitespace is a comment line, and a
trailing whitespace-#-whitespace tail such as Alice Smith # author is a
trailing comment, so #1234 still flags that PR or issue number), --allow <regex>
(dismiss a known-benign hit, such as a CSS class that shares a project-id
prefix; check the regex does not also hide real ids), --no-links, -q.
It takes one tree per run. Given several paths it scans only the last, and the summary line then reads like a clean sweep of all of them. Loop over the trees instead, and check that each summary names the tree you meant.
No pattern catches private repository names, and they are often the leak that matters most: a repo name is not shaped like an identifier, so no regex can recognise it. Grep for them separately, with the list taken from GitHub rather than from memory:
gh repo list <owner> --visibility private --limit 1000 --json name --jq '.[].name' > /tmp/private-repos.txt
grep -rnoFf /tmp/private-repos.txt <export-tree>
Short repo names match as substrings (a repo named dot matches "two-dot
diff"), so read each hit rather than counting them.
It is not a secret scanner. Run gitleaks for tokens and keys; this catches
context leakage, a different axis.
Curation rubric (per candidate doc or file)
Keep a candidate only if it clears all five:
| Gate | Pass condition |
|---|---|
| Audience | The source already marks it shareable (an inventory tag, an explicit "OK to share"); internal-only means drop it. |
| Self-containment | No dependency on private docs or infra; links rewritten to public targets or removed. |
| Relevance | Teaches the public audience something genuinely useful. |
| Sensitivity | No secrets; internal identifiers genericized; safe in the open. |
| Durability | Durable knowledge, not a point-in-time snapshot that rots. |
Genericization checklist (identifier to placeholder)
Replace the real value and keep the architecture. Common classes:
- Cloud project id / number:
<gcp-project-id>/<project-number> - Service-account email:
<sa-name>(or<sa-name>@<gcp-project-id>.iam.gserviceaccount.com; the bare form keeps the scan clean and consistent) - Database, KMS, and secret-manager resource names:
<cloud-sql-instance>,<…-keyring>,<…-secret> - Internal hostnames:
<…-host> - Private repo names: describe the shape instead ("a 16-file PR", "a node-pack repo"). A public repo in the same org can be named; check its visibility rather than assuming it from the org
- Internal issue/PR refs (
#NNNN): drop, or "(tracked internally)" - Personal names and usernames: a role ("a team member", "an applicant")
- Internal cost or accounting codes: the project name, not the numeric code
Keep the non-sensitive facts that carry the value: region, machine types, CIDRs, component/chart/image versions, public DNS hostnames, public upstream URLs.
A claim about how the organisation is configured ("this org's main has no
required status checks") needs more than a placeholder: it is internal, and it
goes stale when the setting changes. Replace it with the check the reader can
run on their own repo. For that example: mergeStateStatus is UNSTABLE when
a failing check is present but not required, and BLOCKED when it is required
and the merge is refused.
Link rewriting
No link may leave the export set except to public upstream. Links into the
private source (../adr/…, ../../infra/…, .claude/rules/…) become
self-contained prose or are repointed within the export. The script's link
check is the backstop for the class humans miss most.
Delivery discipline
- Branch off a fresh
origin/main, stage only the new paths (git add <paths>, never-A), and check thatgit log origin/main..HEADshows only your commit before pushing. - If the export PR is squash-merged and you pushed more commits to its branch
afterward, those commits are in neither
mainnor the squash. Replay them onto a freshorigin/mainin a new PR. - Run the tripwire once more against the content in its destination
(
--repo-root <repo>) before opening the PR.
Agentic Optimizations
| Context | Command |
|---|---|
| Summary line only | bash "${CLAUDE_SKILL_DIR}/scripts/check-public-export.sh" -q --patterns <org.patterns> <tree> |
| Inside a public repo | bash "${CLAUDE_SKILL_DIR}/scripts/check-public-export.sh" --repo-root <repo> --patterns <org.patterns> <tree> |
| Identifiers only | bash "${CLAUDE_SKILL_DIR}/scripts/check-public-export.sh" --no-links --patterns <org.patterns> <tree> |
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: laurigates/public-export-sanitization-58642074053.
Details
- Publisher
- laurigates
- Category
- Security › Application security
- License
- MIT
- Updated
- 2026-10-19
References
More from laurigates
- Docs Refresh Refresh plugin catalog docs (README, PLUGIN-MAP, d2 diagram) so per-plugin skill/agent counts mat…
- Plugin Authoring Add, modify, or delete a skill or plugin in this repo — frontmatter shape, the seven metadata fil…
- Accessibility Implementation WCAG 2.1/2.2 compliance, ARIA patterns, keyboard nav, focus management, a11y testing.
- Design Tokens Design tokens and CSS custom properties: theme systems, dark mode, component libraries.