/repo-topic-tagger — GitHub Topic Curation
Apply a curated, discoverability-optimized set of GitHub topics to one or more repositories.
Arguments
$ARGUMENTS
<owner/repo>— one or more target repos (e.g.agentic-research/mache). If omitted, defaults to the repo containing the current working directory.--dry-run(default) — propose changes but do not apply. Always the default. The user must explicitly pass--applyto write.--apply— after presenting the proposal, prompt for approval and callgh apito write the new topic set.--max=N— cap proposed tag count at N (default 12; GitHub allows 20 max).
What this solves
Many of the user's public repos ship with zero topics. This kills discoverability:
- They never surface in GitHub's topic browser (
github.com/topics/<x>) - They never appear in "related repositories" cards
- LLM-based repo discovery tools weight topic metadata heavily
- Human searchers filtering by topic on GitHub never see them
Topics are cheap to set, never break anything, and compound over time as GitHub's index picks them up.
Reference: a good topic set
agentic-research/cloister is the canonical good example:
capnproto, cloudflare-workers, durable-objects, gateway,
hypervisor, mcp, model-context-protocol, rust, typescript, workerd
Why it works:
- Language(s):
rust,typescript - Framework / runtime:
cloudflare-workers,workerd,durable-objects - Substrate / protocol:
capnproto,mcp,model-context-protocol - Problem-space label:
hypervisor,gateway
Ten tags, every one of them is something a real searcher would type.
GitHub topic rules (hard constraints)
The GitHub API will reject violations. Validate before submitting:
- Lowercase only
- Alphanumerics, hyphens,
+,.allowed; no underscores, no spaces - Each topic ≤ 50 chars
- ≤ 20 topics per repo
- Convention: prefer hyphens (
code-intelligence, notcodeintelligence)
Workflow
Phase 1: Inventory
For each target repo:
gh repo view <owner/repo> --json name,description,repositoryTopics,visibility,isFork,isArchived,homepageUrl,primaryLanguage,languages
Skip with a printed reason if:
visibility != PUBLIC(unless user explicitly overrides)isFork == trueisArchived == true
Phase 2: Signal gathering
For each remaining repo, collect signals from the codebase (use gh for remote; if a local clone is available, prefer reading files directly):
- Language metadata —
primaryLanguageand thelanguagesmap. The top 1–3 languages are almost always topics. - Package manifests — detect framework signals:
Cargo.toml→rust, plus notable deps (tokio,axum,serde,tree-sitter,wasmtime, …)package.json→typescriptornodejs, plus framework deps (next,react,hono,workerd,vite, …)go.mod→go, plus deps (cobra,bubbletea,cgo,tree-sitter, …)pyproject.toml/requirements.txt→python, plus deps (fastapi,pytorch,transformers, …)flake.nix/shell.nix→nixDockerfile/docker-compose.yml→dockerapko.yaml/melange.yaml→apko,chainguard.github/workflows/*→ CI signals (usually not topic-worthy alone)
- README + description — extract domain nouns. Look specifically for:
- Protocol names (
mcp,lsp,oauth,webauthn,cms,pkcs7) - System category (
cli,tui,library,framework,daemon,mcp-server) - Problem-space (
observability,vulnerability-scanning,code-intelligence,identity,signing,knowledge-graph)
- Protocol names (
- Project family / org context — the user's ecosystems. If repo is in
agentic-research, proposeagentic-researchandart-ecosystem. If injamestexasand references rosary/mache/ley-line, proposeart-ecosystem. - Existing topics — keep, don't fight. Only remove if actively misleading.
Phase 3: Proposal
Produce a structured proposal per repo:
## <owner/repo>
Description: <current description>
Current topics: <list or "(none)">
Proposed topics (N total):
Language: rust, typescript
Framework: cloudflare-workers, workerd, durable-objects
Protocol: capnproto, mcp, model-context-protocol
Domain: hypervisor, gateway
Ecosystem: agentic-research, art-ecosystem
Add: [list]
Remove: [list, or "(none)"]
Keep: [list]
Rationale: <1–2 sentences>
Rationale rules:
- Add ≤
--max(default 12). Quality beats quantity. - Prefer canonical names —
model-context-protocolovermcp-protocol,cloudflare-workersovercf-workers. Checkhttps://github.com/topics/<x>exists if uncertain. - Avoid singletons that only describe this one repo. Topics are for finding kin.
- Avoid promotional adjectives — no
fast,modern,lightweightunless they're an established topic (high-performanceis borderline; usually skip).
Phase 4: Approval
Print the full proposal across all in-scope repos. Then:
- If
--dry-run(default): stop. Output a hint that the user can re-run with--applyto write. - If
--apply: print "Apply these N topic changes? (yes/no)" and wait. Only proceed on explicit affirmative.
Phase 5: Apply
For each approved repo:
gh api -X PUT "repos/<owner>/<repo>/topics" \
-H "Accept: application/vnd.github.mercy-preview+json" \
--input - <<EOF
{"names": ["tag1", "tag2", "..."]}
EOF
Or equivalently with field args:
gh api -X PUT "repos/<owner>/<repo>/topics" \
-f "names[]=tag1" -f "names[]=tag2" ...
After each call:
- Re-read with
gh repo view <owner/repo> --json repositoryTopicsto confirm - Report success or failure with the exact diff that landed
Phase 6: Report
End with a compact summary:
Applied:
agentic-research/mache — +8 topics, -0
agentic-research/rosary — +9 topics, -0
jamestexas/agents — +5 topics, -0
Skipped:
agentic-research/x-ray — empty repo, no signals to tag
agentic-research/.github — meta-repo, not user-facing
Deferred (no signals / needs human input):
jamestexas/papers — unclear scope
Important rules
- Default to
--dry-run. The user must explicitly opt in to writes. Topics are public and indexed by third parties. - One PR-equivalent per repo per pass. Don't bundle topic changes with description or README rewrites — those are separate skills.
- Do not touch private repos unless the user explicitly names them. They get no discoverability benefit.
- Do not touch forks or archived repos unless explicitly named.
- If a repo has a working topic set (≥ 5 tags spanning ≥ 3 categories), leave it alone unless the user asked to revise it.
- No emoji in topics. GitHub strips them silently and they're not searchable.
Examples
/repo-topic-tagger agentic-research/mache
→ dry-run proposal for mache only
/repo-topic-tagger agentic-research/mache agentic-research/rosary --apply
→ proposal for both, then prompts for approval, then writes
/repo-topic-tagger
→ uses current working directory's GitHub remote (derived via `gh repo view`)
Bulk mode (orgs)
For a whole-org sweep:
gh repo list <owner> --limit 100 --json name,visibility,isFork,isArchived,repositoryTopics \
| jq -r '.[] | select(.visibility=="PUBLIC" and .isFork==false and .isArchived==false) | .name'
Iterate, but produce all proposals first, present them as a single document, and only apply after a single batched approval. Do not stream-apply.