* gh-cli: intercept GitHub fetches from MCP fetch tools The fetch hook only matched WebFetch, so a GitHub URL fetched through an MCP fetch tool bypassed it entirely. Match `mcp__.*[Ff]etch` as well, which covers Exa's `web_fetch_exa` and equivalents from other MCP servers. A matcher alone is not enough: WebFetch passes a single `url`, while MCP fetch tools pass a `urls` array and batch several pages into one call. Read both shapes. A tool call is atomic, so one GitHub URL anywhere in a batch denies the whole call, and each offending URL is labeled with its own suggestion. Single-URL calls keep the message they had. Split the URL classification into suggest_api, suggest_raw, and suggest_github_com behind a suggest_for_url dispatcher so it can run per URL in a loop, and collapse eight verbatim copies of the clone hint into one helper. Behavior is unchanged; blob and tree merge into one branch because they emitted identical text. The plugin README's interception table now also lists the pull, issues, releases, and gist patterns the hook already handled but never documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * gh-cli: address review on the MCP fetch interceptor Test the matcher. Nothing exercised the regex that decides whether the hook runs at all, so a matcher firing on nothing would still pass every other suite here. matcher.bats reads it out of hooks.json and checks it against real tool names, and refuses to run against an empty matcher, which would make every match assertion pass vacuously. Widen the matcher to fetch, scrape, crawl, and extract. Firecrawl's scrape and Tavily's extract retrieve a URL like any fetch tool but carry no "fetch" in the name, so they bypassed the hook while the README promised "any MCP fetch tool". The README now names what matches. Check a string-valued `urls`. A server declaring `urls: string | string[]` sent a bare string, `arrays` dropped it, and the fetch went out unauthenticated. Listing the field twice keeps it under both shapes. `prompt` is still not scanned, so a prompt mentioning a GitHub URL does not false-deny. Give the closing note its own line when several URLs are denied; it previously trailed only the last entry. Guard assert_suggestion_starts_with against an empty reason and an empty prefix, either of which let it pass while inspecting nothing. Document the api.github.com contents, releases, and actions rows. The generic `gh api` row was actively wrong for /contents/, pointing readers at the anti-pattern the shim exists to block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.5 KiB
gh-cli
A Claude Code plugin that intercepts GitHub URL fetches and redirects Claude to use the authenticated gh CLI instead.
Problem
Claude Code's WebFetch tool, MCP fetch tools (Exa's web_fetch_exa and similar), and Bash curl/wget commands don't use the user's GitHub authentication. This means:
- Private repos: Fetches fail with 404 errors
- Rate limits: Unauthenticated requests are limited to 60/hour (vs 5,000/hour authenticated)
- Missing data: Some API responses are incomplete without authentication
Solution
This plugin provides:
- PreToolUse hooks that intercept GitHub URL access via
WebFetch, MCP fetch/scrape/crawl/extract tools, orcurl/wget, and suggest the correctghCLI command - A
ghPATH shim that blocks anti-patterns: API/contents/fetching and non-session-scoped temp directory clones - A SessionEnd hook that automatically cleans up cloned repositories when the session ends
What Gets Intercepted
The fetch hook matches WebFetch plus any MCP tool whose name contains fetch,
scrape, crawl, or extract — mcp__exa__web_fetch_exa,
mcp__firecrawl__firecrawl_scrape, mcp__tavily__tavily_extract, and
equivalents from other MCP servers. Web tools named something else entirely
(browser navigation, for instance) are not covered; hooks/matcher.bats records
exactly which names match.
It reads both fetch payload shapes — WebFetch's single url, and the urls
field MCP fetch tools use to batch several pages into one call, whether that
field arrives as an array or a bare string. Because a tool call is atomic, one
GitHub URL anywhere in a batch denies the whole call; the denial names each
offending URL alongside its suggestion. WebFetch's prompt field is
deliberately not scanned, so a prompt that merely mentions a GitHub URL is not
denied.
| Tool | Pattern | Suggestion |
|---|---|---|
WebFetch, MCP fetch |
github.com/{owner}/{repo} |
gh repo view owner/repo |
WebFetch, MCP fetch |
github.com/.../blob/... |
gh repo clone + Read |
WebFetch, MCP fetch |
github.com/.../tree/... |
gh repo clone + Read/Glob/Grep |
WebFetch, MCP fetch |
github.com/.../pull/{n} |
gh pr view |
WebFetch, MCP fetch |
github.com/.../issues/{n} |
gh issue view |
WebFetch, MCP fetch |
github.com/.../releases/download/... |
gh release download |
WebFetch, MCP fetch |
api.github.com/repos/.../pulls |
gh pr list / gh pr view |
WebFetch, MCP fetch |
api.github.com/repos/.../issues |
gh issue list / gh issue view |
WebFetch, MCP fetch |
api.github.com/repos/.../contents/... |
gh repo clone + Read |
WebFetch, MCP fetch |
api.github.com/repos/.../releases |
gh release list |
WebFetch, MCP fetch |
api.github.com/repos/.../actions |
gh run list |
WebFetch, MCP fetch |
api.github.com/... (anything else) |
gh api <endpoint> |
WebFetch, MCP fetch |
raw.githubusercontent.com/... |
gh repo clone + Read |
WebFetch, MCP fetch |
gist.github.com/... |
gh gist view |
Bash |
curl https://api.github.com/... |
gh api <endpoint> |
Bash |
curl https://raw.githubusercontent.com/... |
gh repo clone + Read |
Bash |
wget https://github.com/... |
gh release download |
Bash (shim) |
gh api repos/.../contents/... |
gh repo clone + Read |
Bash (shim) |
gh repo clone ... /tmp/... (non-session-scoped) |
Session-scoped clone path |
What Passes Through
- Non-GitHub URLs (any domain that isn't
github.com,api.github.com,raw.githubusercontent.com, orgist.github.com) - GitHub Pages sites (
*.github.io) - Commands already using
gh(except anti-patterns blocked by the shim; see table above) - Git commands (
git clone,git push, etc.) - Search commands that mention GitHub URLs (
grep,rg, etc.)
Note: When hooks deny blob/tree/raw URLs, the denial message explicitly warns against using gh api to fetch and base64-decode file contents as a fallback — clone the repo instead.
Automatic Cleanup
Cloned repositories are stored in session-scoped temp directories ($TMPDIR/gh-clones-<session-id>/). A SessionEnd hook automatically removes them when the session ends, so there's no manual cleanup needed and concurrent sessions don't interfere with each other.
Prerequisites
- GitHub CLI (
gh) must be installed and authenticated (gh auth login) - If
ghis not installed, the hooks pass through without disruption
Installation
/plugin marketplace add trailofbits/skills
/plugin install gh-cli