Publish to GitHub
A focused workflow skill that takes your local ~/.claude/skills/ and ~/.claude/agents/
trees and produces a clean, public-safe staging directory ready to push to a public GitHub
repository.
Why this skill exists
Most Claude Code users accumulate private context inside their skills over time — business names, internal paths, project-specific examples, customer details. Publishing the skill tree to public github without cleanup is a privacy hazard. This skill is the opposite of that: it produces a vetted, scrubbed copy in a staging directory while never modifying your live ~/.claude/ tree.
What this skill does NOT do
- Does not modify your live
~/.claude/skills/or~/.claude/agents/ - Does not push to git (you do that yourself)
- Does not create the GitHub repository (you do that on github.com)
- Does not manage credentials, tokens, or SSH keys
- Does not auto-replace your local files
- Does not invent scrub rules — you control them via
~/.claude/publish-config.json - Does not try to detect implicit business fingerprints (manual review still required)
Architecture
The skill is split into two parts:
- Generic engine —
scripts/publish_prep.py(publishable, contains zero project-specific strings) - Private config —
~/.claude/publish-config.json(user-owned, NEVER published)
The engine reads its rules from the config: which directories to exclude, which strings to find/replace, which patterns to grep for, and which README/docs files to bundle into the staging dir.
This split is intentional: the engine can be safely published to a public github repo, while every user maintains their own private rules in their own home directory.
Operations
publish — full workflow (most common)
Trigger phrases: "publish to github", "deploy public skills", "prepare publish", "create publish staging", "publish skills".
Steps Claude follows:
Locate the config
- Check
$PUBLISH_CONFIGenv var - Check
~/.claude/publish-config.json - Check
./publish-config.json - If none found, tell the user to copy
templates/publish-config.example.jsonto~/.claude/publish-config.jsonand customise it.
- Check
Pre-flight summary
- Read the config and show the user what will happen:
- Which directories are excluded
- How many scrub rules are configured
- How many forbidden patterns will be checked
- Where the staging dir will be created
- Ask for confirmation before running.
- Read the config and show the user what will happen:
Run
publish_prep.py(pass--extended-scanto include the extended checks)python3 ~/.claude/skills/publish-to-github/scripts/publish_prep.py --extended-scan- The script handles all deterministic operations (file copy, scrubs, grep verification).
- With
--extended-scanit also runs the extended security patterns fromchecks/security-patterns.mdagainst the staging directory. - Capture and present the output.
Review extended security warnings
- The
--extended-scanphase reports findings at critical / high / medium / low severity: API key shapes, private key blocks, DB connection strings, real-looking emails, public IPs, internal TLDs, E.164 phones, large files, dangling symlinks. - Findings are warnings, not errors. Review each and either add to config
(
forbidden_patterns/scrubs/exclusions) or accept as false positive. - Suppressing false positives: Add entries to
extended_scan_ignorein yourpublish-config.jsonto suppress known false positives (e.g. networking skills that legitimately document public DNS IPs, mongodb skill that documentsmongodb://user:pass@example strings). See the example template attemplates/publish-config.example.jsonfor the schema.
- The
Sync metadata — README counts/version + GitHub About (mechanized; see
sync-metadata)- Run
scripts/sync_metadata.pyto drive ALL THREE drift-prone surfaces from ONE live source (thesource.root+subdirs+exclusionstree). This replaces the old hand-editing of counts, which silently drifted (the catalog README regressed to159 skills / 4 agentsand the public About to141+ … Geminibefore it was caught — see therepo-metadata-maintenancememory).
# README: rewrite **N skills**/**M agents**/**K workflows** bullets + the # `**Version:** · **Last published:** · **N skills · …**` header (+ optional --bump). python3 scripts/sync_metadata.py readme --readme <staging>/README.md [--bump patch|minor|major] # GitHub About: substitutes {skills}/{agents}/… into the publish-config `about` block. # Dry-run prints the gh command; add --apply to set it (mirrors "print, don't auto-mutate"). python3 scripts/sync_metadata.py about --repo <owner/repo> [--apply]- Also hand-add a
## Changelogentry for the release (the one surface that needs prose). - Show the user the diff before committing.
- Run
Publish gate — 3-tree
_metaidentity (S043 / #119 C2)- When staging to the agent-foundry public repo via
scripts/stage-to-public.sh, the script now reconciles the safety-critical_metasubset (gates.py,claims.py,identity_check.py,classify*.py, the arbiter spawners, the HARD-RULE scan/apply machinery, …) from prod (~/.claude) into the public repo BEFOREgit add, then runs:python3 ~/.claude/skills/_meta/gates.py G_IDENTITY <public_repo> \ --pair prod-foundry --strict --foundry-root <public_repo> - Exit 2 BLOCKS the publish — you cannot push a state that leaves the
published
_metadrifted from prod. This is the authoritative check (it lives in the script, not a hook, because--no-verifysilently bypasses hooks with no log — C7). The agent-foundry pre-push hook, if installed, is verify-only defense-in-depth. - This makes the standing prod-vs-agent-foundry lag self-healing: the next publish reconciles it automatically.
- When staging to the agent-foundry public repo via
Print git commands
- First publish:
git init→add→commit→remote add→push - Re-publish: detect existing remote in config, print equivalent commands
- Never auto-execute git commands without explicit user confirmation.
- First publish:
Print rollback / re-run instructions
check — security scan only
Trigger phrases: "scan skills for leaks", "security check on skills", "check for secrets".
Run only the verification phase against either:
- The current live tree (greps
~/.claude/skills/and~/.claude/agents/) - An existing staging directory (
--verify <path>)
No staging dir is created, no files are modified.
bump-version — README version bump only
Trigger phrases: "bump skill version", "increment version", "release new version".
Read the current README, increment the version, update counts and date, write back. Does not run scrubs, does not create a staging dir.
sync-metadata — reconcile README counts/version + GitHub About from the live tree
Trigger phrases: "sync repo metadata", "update the about section", "fix the skill counts", "the README counts are stale", "update repo description/topics".
The single-source reconciler for the three surfaces that drift because no one tool
owned them (the repo-metadata-maintenance memory). scripts/sync_metadata.py:
counts— print live{skills,agents,workflows,commands}fromsource.rootapplyingexclusions(the same set the publisher uses, so it matches the published count).readme --readme P— rewrite the**N skills**/**M agents**/**K workflows**bullets, the**N skills · M agents · …**header tuple, and**Last published:**;--bump {patch|minor|major}also bumps**Version:**. Idempotent; reports changes.about --repo R— read the publish-configaboutblock (a{owner/repo: {description, topics, homepage}}map whose description may use{skills}/{agents}/{workflows}/{commands}placeholders), substitute live counts, and print thegh repo editcommand — or run it with--apply(the only mutating path; topics are additive via--add-topic).sync --readme P --repo R [--apply]— both at once.
Config: add an about block to ~/.claude/publish-config.json. Read-only on the
source tree; --apply is the sole outward mutation and is opt-in.
stage-public — gated scrub-then-publish to your public repo
Trigger phrases: "publish to my public repo", "scrub and push to github", "stage to public".
The generic, gated publish driver (scripts/stage_to_public.sh) — the publishable
companion to publish_prep.py. It takes a scrubbed staging dir, HARD-GATES on the
forbidden-pattern verify (a leak can NEVER reach the public repo — the gate re-runs the
verify and aborts with exit 2 if anything remains), mirrors it into your public repo, shows
the diff, and pushes only after you confirm.
# 1. build your scrub list once (see below) at ~/.claude/publish-config.json
# 2. produce a scrubbed staging dir
python3 scripts/publish_prep.py --extended-scan
# 3. gated publish to YOUR public repo
PUBLIC_REPO_ROOT=/path/to/your/public-clone scripts/stage_to_public.sh # or --repo-root PATH ; --yes for CI
Contains zero project-specific logic and zero private strings (HARD-RULE) — safe to ship
publicly so anyone can reuse the framework. It does NOT do any ecosystem-specific
reconciliation (e.g. a safety-critical _meta identity gate) — that belongs in your own
private wrapper that calls this driver and adds steps on top.
Bring your own scrub list
The framework is shared; the scrub list is yours. publish_prep.py +
stage_to_public.sh carry no private data — every rule about what to redact lives in
your own ~/.claude/publish-config.json (user-owned, gitignored, NEVER published).
Start from templates/publish-config.example.json and add:
forbidden_patterns— strings that must NEVER appear publicly (your home path e.g./home/<you>, machine/dev paths, employer/customer/business names). The scrub gate blocks the publish on any match — so list yours up front.scrubs— per-filefind→replaceto rewrite specific private strings to placeholders.exclusions— paths to drop entirely (runtime caches, private skills, local state).about— your repo's GitHub description/topics (forsync-metadata).
This is why the framework is publishable and the config is not: another person installs the same engine + gate, writes their OWN forbidden-pattern/scrub list, and gets the same leak-proof publish for their own tree.
validate-config — sanity check the config
Trigger phrases: "validate publish config", "check publish config".
Parse ~/.claude/publish-config.json, verify schema, report what's configured.
init-config — create a starter config
Trigger phrases: "set up publish config", "create publish config", "init publish".
If ~/.claude/publish-config.json does not exist, copy
templates/publish-config.example.json to it and walk the user through customising it.
Config file format
See templates/publish-config.example.json for the full schema. Top-level structure:
{
"version": 1,
"source": {
"root": "~/.claude",
"subdirs": ["skills", "agents"]
},
"exclusions": [
"skills/private-skill-name"
],
"scrubs": [
{
"file": "skills/some-skill/SKILL.md",
"replacements": [
{ "find": "private-string", "replace": "generic-placeholder" }
]
}
],
"forbidden_patterns": [
"private-string",
"/absolute/private/path"
],
"bundle_files": [
{ "source": "~/path/to/REPO_README.md", "dest": "README.md" },
{ "source": "~/path/to/docs/dependencies/", "dest": "docs/dependencies" }
]
}
The source.root is the base directory the script walks. Defaults to ~/.claude.
The source.subdirs are the subdirectories to copy. Defaults to skills and agents.
S055 — workflows subdir (workflow-adoption keystone): the saved-workflow
library at ~/.claude/workflows/ publishes through the generic subdir walk
(zero engine change — it is just another entry in source.subdirs). No scrub
rule may target workflows/ (G-W3): workflow files carry only
ecosystem-relative paths (progress/, .alf/, .ledger/), so prod↔foundry is
byte-identical. Scrubbing workflows/ is exactly the self-watch divergence
that tripped identity_check in the P0c false positive — do not add one.
Note on bundling directories: When bundle_files points source at a directory, the recursive copy SKIPS hidden files (.foo) and underscore-prefixed files (_bar). To include such a file, add a per-file bundle_files entry pointing directly at it instead of its parent directory.
Extended security checks
The base publish_prep.py greps for the forbidden_patterns you list in your config.
The skill workflow adds additional heuristic checks (see checks/security-patterns.md):
| Category | Patterns | Severity |
|---|---|---|
| API keys | sk-, ghp_, xoxb-, xoxp-, AIza, eyJ (JWT), pk_live_, sk_live_ |
critical |
| Private keys | BEGIN RSA PRIVATE, BEGIN OPENSSH PRIVATE, BEGIN PGP |
critical |
| AWS / cloud | AKIA, aws_access_key, arn:aws:, gcp-project- |
high |
| Passwords | password=, passwd=, pwd= followed by non-placeholder |
high |
| Tokens | token=, bearer followed by non-placeholder |
high |
| Real emails | non-RFC-2606 domains (anything not example.com/test/contoso/etc) | medium |
| Public IPs | non-RFC1918, non-TEST-NET | medium |
| Private hostnames | *.internal, *.corp, *.local, *.lan |
medium |
| Phone numbers | E.164 format +\d{10,15} |
low |
| Large files | >1MB | warning |
| Binary files | non-text content | warning |
| Dangling symlinks | symlinks pointing outside the staging dir | warning |
These checks produce warnings, not errors. The user reviews each finding and decides whether to add an exclusion, add a scrub rule, or accept the finding.
README versioning
If a bundled README has a version header like:
# My Claude Skills
Version: 1.2.3
Last published: 2026-04-08
The skill can auto-bump the version on each publish:
- Patch bump (default):
1.2.3→1.2.4 - Minor bump:
1.2.3→1.3.0(user opts in: "bump minor") - Major bump:
1.2.3→2.0.0(user opts in: "bump major")
The skill also updates:
Last published: <today>line- Skill count in any "Skills: N skills, M agents" badge
- Category counts in collapsible sections (if recognisable as
<details>blocks)
If the README has no version header, the skill prompts to add one on first publish.
File layout
~/.claude/skills/publish-to-github/
├── SKILL.md (this file — workflow Claude follows)
├── scripts/
│ └── publish_prep.py (generic engine — no private strings)
├── templates/
│ ├── publish-config.example.json (config template for new users)
│ ├── README.example.md (starter README template)
│ └── CHANGELOG.example.md (changelog template)
├── checks/
│ └── security-patterns.md (extended scan patterns + how to extend)
└── docs/
└── workflow.md (detailed end-to-end walkthrough)
The user's private config lives at ~/.claude/publish-config.json — outside the skill
directory, never published, gitignored at the user level.
Hard rules
Anti-patterns
| Anti-pattern | Why it fails | Correct approach |
|---|---|---|
Hard-coding scrub rules in publish_prep.py |
Couples the engine to one user's data; can't be published without leaking | Keep rules in ~/.claude/publish-config.json (private) |
| Auto-running git push | One bad commit becomes a permanent public mistake | Print commands, let the user run them |
| Modifying the live ~/.claude/ tree | Loses local examples, breaks ongoing sessions | Operate only on the staging copy |
| Trying to detect implicit fingerprints automatically | False sense of security; LLMs can't reliably spot semantic leaks | Surface a manual review checklist |
| Skipping the forbidden-patterns grep | Defeats the purpose | The grep is the final safety net — never skip |
| Treating the example config as the user's config | Example contains placeholders, not real rules | First-run setup must copy → customise |
When NOT to use this skill
- For one-off scrubbing of a single file → use
Editdirectly - For managing private context at runtime (overlaying business names, brand voice on generic skills) → that needs a different design (an MCP server or Codex CLI integration). This skill is publish-time only.
- For managing secrets (API keys, tokens, credentials) → use a real secret manager (
gpg,pass, OS keychain). This skill is for documentation and example data, not credentials.
Quick reference
Skill's job: prepare a clean, scrubbed staging dir of ~/.claude/skills/ + ~/.claude/agents/
ready for public github publish.
Engine: scripts/publish_prep.py (generic, reads config)
Config: ~/.claude/publish-config.json (private, user-owned, never published)
Output: /tmp/claude-skills-public-<timestamp>/ (cleaned, scrubbed, verified)
Workflow: config → exclude → scrub → bundle docs → verify → print git commands
What it doesn't do: push, commit, manage credentials, modify live tree, detect implicit fingerprints