Skip to content
Back to skills

Public Export Sanitization

ASecurity

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.

  • 58 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 25, 2026
developmentgobashsqlnodegcpgitapidatabasesecuritydocumentation

Works with

  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 3, 2026

npx -y skills add laurigates/claude-plugins --skill public-export-sanitization --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Public Export Sanitization?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Public Export Sanitization
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/laurigates-public-export-sanitization/badge)](https://www.skillsdirectory.com/skills/laurigates-public-export-sanitization)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: public-export-sanitization
description: "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."
allowed-tools: Bash(bash *), Read, Grep, Glob, Edit, TodoWrite
created: 2026-09-24
modified: 2026-09-29
reviewed: 2026-09-24
---

# 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
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.

**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:

```text
# 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), `--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:

```bash
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 that `git log origin/main..HEAD`
  shows 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 `main` nor the squash. Replay them onto
  a fresh `origin/main` in 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>` |

Files in this skill

  • SKILL.md6.1 KB
  • scripts/check-public-export.sh8.4 KB
  • scripts/tests/test-check-public-export.sh3.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…