GitHub Org Chart
Produces two artifacts in ${TMPDIR:-/tmp}/gh-org-chart/:
<org>-org.json— canonical data: teams, members (with role), repos (witharchivedflag and permission), CODEOWNERS paths. Cache + hand-editable source of truth.<org>-org.html— single self-contained interactive explorer rendered from the JSON. Opens viafile://, works offline.
Set OUT_DIR once at the start of every phase and reuse it:
OUT_DIR="${TMPDIR:-/tmp}/gh-org-chart"
mkdir -p "$OUT_DIR"
Arguments
/gh-org-chart— prompt for org./gh-org-chart <org>— render-if-fresh: if<org>-org.jsonexists andmtimeis within 24h, re-render from it. Otherwise collect, then render./gh-org-chart <org> --refresh— force re-collect./gh-org-chart <org> --no-codeowners— skip CODEOWNERS scan (faster on big orgs)./gh-org-chart <org> --no-members— drop members from collection and output.
Phase 1: Intake
Resolve org: if provided as argument, use it. Otherwise ask:
Which GitHub org should I chart?
Verify auth: run
gh auth status. Confirmread:orgis in the scopes line. If not, instruct:gh auth refresh -s read:orgthen re-run the skill.
Phase 2: Decide collect vs. reuse
Locate
$OUT_DIR/<org>-org.json.If
--refreshwas passed, or the file does not exist, or its mtime is older than 24h, run collect (Phase 3). Otherwise skip to Phase 4.Freshness check:
JSON="$OUT_DIR/$ORG-org.json" if [[ "$REFRESH" == "1" ]] || [[ ! -f "$JSON" ]] || \ [[ $(($(date +%s) - $(stat -f %m "$JSON" 2>/dev/null || stat -c %Y "$JSON"))) -gt 86400 ]]; then NEEDS_COLLECT=1 fi
Phase 3: Collect
Run the bundled collect script (handles zsh
noclobberviarm -f):rm -f "$OUT_DIR/$ORG-org.json" "${CLAUDE_SKILL_DIR:-$HOME/.claude/skills/gh-org-chart}/scripts/collect.sh" \ "$ORG" $FLAGS > "$OUT_DIR/$ORG-org.json"Where
$FLAGSis built from--no-codeownersand--no-membersif set. CODEOWNERS scanning is the long pole — expect ~1 API call per owned repo. Big orgs (200+ owned repos with rate limiting): consider--no-codeowners.
Phase 4: Render
Run the renderer:
python3 "${CLAUDE_SKILL_DIR:-$HOME/.claude/skills/gh-org-chart}/scripts/render.py" \ "$OUT_DIR/$ORG-org.json"This writes
<org>-org.htmlnext to the JSON (i.e. in$OUT_DIR).Open it (macOS):
open "$OUT_DIR/$ORG-org.html"Other platforms: report the path so the user can open it themselves.
Phase 5: Report
- Summarize (set
JSON="$OUT_DIR/$ORG-org.json"):- Teams:
jq '.teams | length' "$JSON" - Owned repos (admin or maintain):
jq '[.teams[].repos[] | select(.permission == "admin" or .permission == "maintain") | .name] | unique | length' "$JSON" - CODEOWNERS path attributions:
jq '[.teams[].repos[].codeowner_paths // [] | length] | add // 0' "$JSON" - Member entries:
jq '[.teams[].members[]] | length' "$JSON"(omit if--no-members). - Whether the JSON was freshly collected or reused from cache.
- Teams:
Notes
- Hand-editing the JSON: edits survive across
/gh-org-chart <org>runs because the freshness check reuses the file. Use--refreshwhen you want collection to overwrite your edits. - CODEOWNERS attribution: only
@<org>/<team-slug>owners produce attributions. Individual user owners (@alice) and external orgs (@other-org/team-x) are intentionally ignored — this is a team-ownership view. - No reporting lines: GitHub teams reflect permission grouping, not management hierarchy. Manager → report relationships need a different data source (HRIS).
- Performance: CODEOWNERS scan is restricted to owned repos (permission ≥ maintain) to keep API calls bounded. Collecting roles adds one
?role=maintainercall per team. - Data shape (for hand-editing / jq):
membersis[{login, role}]whereroleis"maintainer"or"member"; each repo carriespermissionand anarchivedflag.--no-membersleavesmembersempty.