github-api-patterns
**Scope**: Efficient API usage for code analysis — repos, trees, content, PR reviews, rate limiting. Not Actions/webhooks. **Version range**: GitHub REST API v3; GraphQL not covered
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
**Scope**: Efficient API usage for code analysis — repos, trees, content, PR reviews, rate limiting. Not Actions/webhooks. **Version range**: GitHub REST API v3; GraphQL not covered
Agent definition
github-api-patterns.mdGitHub REST API Patterns Reference
> **Scope**: Efficient API usage for code analysis — repos, trees, content, PR reviews, rate limiting. Not Actions/webhooks. > **Version range**: GitHub REST API v3; GraphQL not covered
Rate limits: 60 req/hr unauth, 5000 req/hr auth. Patterns below minimize request count.
Efficient Analysis Sequence
1. GET /users/{username} → 1 req — public_repos count
2. GET /users/{username}/repos?sort=stars → 1-2 req — top repos by activity
3. GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1 → 1 req per repo — full file tree
4. GET /repos/{owner}/{repo}/contents/{path} → 1 req per file — file content (base64)
5. GET /repos/{owner}/{repo}/pulls?state=all → 1-2 req per repo — PR list
6. GET /repos/{owner}/{repo}/pulls/{pr}/reviews → 1 req per PR — review commentsTotal for 5 repos × 10 files × 3 PRs each: ~5 + 5 + 50 + 15 = **75 requests**. Well within authenticated limits.
---
Pattern Table
| Endpoint | Optimal Use | Request Count | Notes | |----------|-------------|---------------|-------| | `/users/{username}/repos?sort=stars&per_page=5` | Get top 5 repos by stars | 1 | Add `&type=owner` to exclude forked repos | | `/repos/{o}/{r}/git/trees/{sha}?recursive=1` | Full file tree in one call | 1 per repo | Truncated above ~100k files — check `truncated` field | | `/repos/{o}/{r}/contents/{path}` | Single file content (base64) | 1 per file | `content` field is base64 — decode before parsing | | `/repos/{o}/{r}/pulls?state=closed&per_page=10` | Most recent merged PRs | 1-2 per repo | Use `closed` for historical preference signals | | `/repos/{o}/{r}/pulls/{n}/reviews` | Review comments per PR | 1 per PR | `body` field has the comment text | | `/rate_limit` | Check remaining before large batches | 1 | Check before each analysis phase |
---
Correct Patterns
Rate Limit Check Before Batch Operations
Always check rate limits before starting a new analysis phase:
def check_rate_limit(headers: dict) -> None:
"""Raise if fewer than 10 requests remaining."""
remaining = int(headers.get("X-RateLimit-Remaining", 0))
reset_time = int(headers.get("X-RateLimit-Reset", 0))
if remaining < 10:
wait_seconds = reset_time - time.time()
raise RateLimitError(
f"Rate limit nearly exhausted ({remaining} remaining). "
f"Resets in {wait_seconds:.0f}s. "
"Provide a GitHub token for 5000 req/hr: --token <your_token>"
)---
Fetch File Tree (Recursive, One Request)
def get_file_tree(owner: str, repo: str, branch: str = "HEAD") -> list[str]:
"""Return all file paths in a repo using one API call."""
# First get the commit SHA for branch
resp = github_get(f"/repos/{owner}/{repo}/commits/{branch}")
tree_sha = resp["commit"]["tree"]["sha"]
# Fetch entire tree recursively
resp = github_get(f"/repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=1")
if resp.get("truncated"):
# Repo has >100k files — fall back to directory-by-directory
return get_file_tree_paged(owner, repo, tree_sha)
return [item["path"] for item in resp["tree"] if item["type"] == "blob"]One request vs N+1 recursive directory calls. 500-file repo saves 30+ requests.
---
Decode File Content
import base64
def get_file_content(owner: str, repo: str, path: str) -> str:
"""Fetch and decode file content from GitHub API."""
resp = github_get(f"/repos/{owner}/{repo}/contents/{path}")
if resp.get("encoding") != "base64":
raise ValueError(f"Unexpected encoding: {resp.get('encoding')}")
# Content has newlines inserted by GitHub — strip before decoding
return base64.b64decode(resp["content"].replace("\n", "")).decode("utf-8", errors="replace")GitHub `content` includes `\n` that must be stripped before base64 decode.
---
Prioritize Top Repos
def get_analysis_repos(username: str, max_repos: int = 5) -> list[dict]:
"""Return repos sorted by relevance: stars + recent activity, excluding forks."""
repos = github_get(f"/users/{username}/repos?sort=pushed&per_page=20&type=owner")
# Sort by stars (strongest signal), filter forks (project-specific style)
owned = [r for r in repos if not r["fork"]]
owned.sort(key=lambda r: r["stargazers_count"], reverse=True)
return owned[:max_repos]Forks contain upstream style, not user's. Star-sorted owned repos = most polished work.
---
Pattern Catalog
Use Recursive Tree Endpoint for File Listing
**Detection** (in your own code pattern):
grep -rn 'GET /repos.*contents' scripts/ | grep -v 'tree'
**Signal**:
# Expensive: 1 request per directory level, recursive
def list_files(owner, repo, path=""):
items = github_get(f"/repos/{owner}/{repo}/contents/{path}")
for item in items:
if item["type"] == "dir":
yield from list_files(owner, repo, item["path"]) # N+1 requests!
else:
yield item["path"]**Why**: 200-file repo = 21 requests. Recursive tree = 1.
**Fix**: `GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1`
---
Handle Pagination for All List Endpoints
**Detection**:
grep -rn 'per_page' scripts/ | grep -v 'pagination\|next\|link'
**Signal**:
repos = github_get(f"/users/{username}/repos?per_page=100")
# Assumes all repos fit in one page — fails for users with 100+ repos**Why**: Max 100 items/page. 100+ repos = silently incomplete.
**Fix**:
def paginate(url: str) -> list:
results = []
while url:
resp = requests.get(url, headers=auth_headers)
results.extend(resp.json())
# GitHub uses Link header for pagination
url = resp.links.get("next", {}).get("url")
return results---
Authenticate All API Requests
**Detection**:
grep -rn 'github_get\|requests.get' scripts/ | grep -v 'Authorization\|token'
**Signal**:
Read more
GitHub REST API Patterns Reference
> **Scope**: Efficient API usage for code analysis — repos, trees, content, PR reviews, rate limiting. Not Actions/webhooks. > **Version range**: GitHub REST API v3; GraphQL not covered
Rate limits: 60 req/hr unauth, 5000 req/hr auth. Patterns below minimize request count.
Efficient Analysis Sequence
1. GET /users/{username} → 1 req — public_repos count
2. GET /users/{username}/repos?sort=stars → 1-2 req — top repos by activity
3. GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1 → 1 req per repo — full file tree
4. GET /repos/{owner}/{repo}/contents/{path} → 1 req per file — file content (base64)
5. GET /repos/{owner}/{repo}/pulls?state=all → 1-2 req per repo — PR list
6. GET /repos/{owner}/{repo}/pulls/{pr}/reviews → 1 req per PR — review commentsTotal for 5 repos × 10 files × 3 PRs each: ~5 + 5 + 50 + 15 = **75 requests**. Well within authenticated limits.
---
Pattern Table
| Endpoint | Optimal Use | Request Count | Notes | |----------|-------------|---------------|-------| | `/users/{username}/repos?sort=stars&per_page=5` | Get top 5 repos by stars | 1 | Add `&type=owner` to exclude forked repos | | `/repos/{o}/{r}/git/trees/{sha}?recursive=1` | Full file tree in one call | 1 per repo | Truncated above ~100k files — check `truncated` field | | `/repos/{o}/{r}/contents/{path}` | Single file content (base64) | 1 per file | `content` field is base64 — decode before parsing | | `/repos/{o}/{r}/pulls?state=closed&per_page=10` | Most recent merged PRs | 1-2 per repo | Use `closed` for historical preference signals | | `/repos/{o}/{r}/pulls/{n}/reviews` | Review comments per PR | 1 per PR | `body` field has the comment text | | `/rate_limit` | Check remaining before large batches | 1 | Check before each analysis phase |
---
Correct Patterns
Rate Limit Check Before Batch Operations
Always check rate limits before starting a new analysis phase:
def check_rate_limit(headers: dict) -> None:
"""Raise if fewer than 10 requests remaining."""
remaining = int(headers.get("X-RateLimit-Remaining", 0))
reset_time = int(headers.get("X-RateLimit-Reset", 0))
if remaining < 10:
wait_seconds = reset_time - time.time()
raise RateLimitError(
f"Rate limit nearly exhausted ({remaining} remaining). "
f"Resets in {wait_seconds:.0f}s. "
"Provide a GitHub token for 5000 req/hr: --token <your_token>"
)---
Fetch File Tree (Recursive, One Request)
def get_file_tree(owner: str, repo: str, branch: str = "HEAD") -> list[str]:
"""Return all file paths in a repo using one API call."""
# First get the commit SHA for branch
resp = github_get(f"/repos/{owner}/{repo}/commits/{branch}")
tree_sha = resp["commit"]["tree"]["sha"]
# Fetch entire tree recursively
resp = github_get(f"/repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=1")
if resp.get("truncated"):
# Repo has >100k files — fall back to directory-by-directory
return get_file_tree_paged(owner, repo, tree_sha)
return [item["path"] for item in resp["tree"] if item["type"] == "blob"]One request vs N+1 recursive directory calls. 500-file repo saves 30+ requests.
---
Decode File Content
import base64
def get_file_content(owner: str, repo: str, path: str) -> str:
"""Fetch and decode file content from GitHub API."""
resp = github_get(f"/repos/{owner}/{repo}/contents/{path}")
if resp.get("encoding") != "base64":
raise ValueError(f"Unexpected encoding: {resp.get('encoding')}")
# Content has newlines inserted by GitHub — strip before decoding
return base64.b64decode(resp["content"].replace("\n", "")).decode("utf-8", errors="replace")GitHub `content` includes `\n` that must be stripped before base64 decode.
---
Prioritize Top Repos
def get_analysis_repos(username: str, max_repos: int = 5) -> list[dict]:
"""Return repos sorted by relevance: stars + recent activity, excluding forks."""
repos = github_get(f"/users/{username}/repos?sort=pushed&per_page=20&type=owner")
# Sort by stars (strongest signal), filter forks (project-specific style)
owned = [r for r in repos if not r["fork"]]
owned.sort(key=lambda r: r["stargazers_count"], reverse=True)
return owned[:max_repos]Forks contain upstream style, not user's. Star-sorted owned repos = most polished work.
---
Pattern Catalog
Use Recursive Tree Endpoint for File Listing
**Detection** (in your own code pattern):
grep -rn 'GET /repos.*contents' scripts/ | grep -v 'tree'
**Signal**:
# Expensive: 1 request per directory level, recursive
def list_files(owner, repo, path=""):
items = github_get(f"/repos/{owner}/{repo}/contents/{path}")
for item in items:
if item["type"] == "dir":
yield from list_files(owner, repo, item["path"]) # N+1 requests!
else:
yield item["path"]**Why**: 200-file repo = 21 requests. Recursive tree = 1.
**Fix**: `GET /repos/{owner}/{repo}/git/trees/{sha}?recursive=1`
---
Handle Pagination for All List Endpoints
**Detection**:
grep -rn 'per_page' scripts/ | grep -v 'pagination\|next\|link'
**Signal**:
repos = github_get(f"/users/{username}/repos?per_page=100")
# Assumes all repos fit in one page — fails for users with 100+ repos**Why**: Max 100 items/page. 100+ repos = silently incomplete.
**Fix**:
def paginate(url: str) -> list:
results = []
while url:
resp = requests.get(url, headers=auth_headers)
results.extend(resp.json())
# GitHub uses Link header for pagination
url = resp.links.get("next", {}).get("url")
return results---
Authenticate All API Requests
**Detection**:
grep -rn 'github_get\|requests.get' scripts/ | grep -v 'Authorization\|token'
**Signal**:
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

