name: publish-to-github
description: >
Use when preparing to publish ~/.claude/skills/, ~/.claude/agents/, and ~/.claude/commands/
to a public github repository. Wraps publish_prep.py to scrub embedded private content, run extended security
checks, bundle README and documentation, and produce a clean staging directory ready for
git init / commit / push. Also trigger on "publish skills to github", "deploy public skills",
"prepare github release", "scan skills for secrets", "bump skill version", "create publish
staging directory".
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_CONFIG env var
- Check
~/.claude/publish-config.json
- Check
./publish-config.json
- If none found, tell the user to copy
templates/publish-config.example.json
to ~/.claude/publish-config.json and customise it.
-
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.
-
Run publish_prep.py (pass --extended-scan to 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-scan it also runs the extended security patterns from
checks/security-patterns.md against the staging directory.
- Capture and present the output.
-
Review extended security warnings
- The
--extended-scan phase 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_ignore in your
publish-config.json to suppress known false positives (e.g. networking skills
that legitimately document public DNS IPs, mongodb skill that documents
mongodb://user:pass@ example strings). See the example template at
templates/publish-config.example.json for the schema.
-
Sync metadata — README counts/version + GitHub About (mechanized; see sync-metadata)
- Run
scripts/sync_metadata.py to drive ALL THREE drift-prone surfaces from ONE
live source (the source.root+subdirs+exclusions tree). This replaces the old
hand-editing of counts, which silently drifted (the catalog README regressed to
159 skills / 4 agents and the public About to 141+ … Gemini before it was caught
— see the repo-metadata-maintenance memory).
# 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
## Changelog entry for the release (the one surface that needs prose).
- Show the user the diff before committing.
-
Publish gate — 3-tree _meta identity (S043 / #119 C2)
-
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.
-
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} from source.root
applying exclusions (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-config about block (a
{owner/repo: {description, topics, homepage}} map whose description may use
{skills}/{agents}/{workflows}/{commands} placeholders), substitute live counts,
and print the gh repo edit command — 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-file find→replace to rewrite specific private strings to placeholders.
exclusions — paths to drop entirely (runtime caches, private skills, local state).
about — your repo's GitHub description/topics (for sync-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
Edit directly
- 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