ghx β GitHub CLI Cache Proxy

A caching proxy for the GitHub CLI (gh) that eliminates redundant API calls, prevents rate limiting, and dramatically speeds up repeated commands.
Built for AI agent workflows where multiple agents (Copilot CLI, coding agents, MCP servers) hammer the same gh commands simultaneously.
Highlights
- π 10x faster cached responses (~0.1s vs ~1s)
- π Singleflight coalescing β 5 agents asking the same thing = 1 API call
- π― Allowlist-based β only caches known-safe read-only commands
- π§Ή Auto-invalidation β mutations flush related cache entries
- π Web dashboard β real-time hit rates, per-command stats, request log
- π Drop-in replacement β just use
ghx instead of gh
- π¦ No
gh required β auto-downloads GitHub CLI on first use if not installed
Install
Homebrew (macOS β recommended)
brew tap brunoborges/tap
brew install ghx
Quick install script (macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/brunoborges/ghx/main/install.sh | bash
This detects your OS and architecture, downloads the latest release, and installs ghx and ghxd to /usr/local/bin. If no real gh binary is found on the system, a lightweight gh shim is also installed that routes all gh commands through ghx for automatic caching. To install elsewhere:
curl -fsSL https://raw.githubusercontent.com/brunoborges/ghx/main/install.sh | INSTALL_DIR=~/.local/bin bash
PowerShell (Windows)
irm https://raw.githubusercontent.com/brunoborges/ghx/main/install.ps1 | iex
This installs ghx.exe and ghxd.exe to %LOCALAPPDATA%\ghx\bin and adds it to your user PATH. If no real gh.exe is found, a gh.cmd shim is also installed.
Manual download
Download the latest release for your platform from GitHub Releases:
# macOS (Apple Silicon)
curl -fsSL https://github.com/brunoborges/ghx/releases/latest/download/ghx-darwin-arm64.tar.gz | tar xz
sudo cp ghx ghxd /usr/local/bin/
# Linux (x64)
curl -fsSL https://github.com/brunoborges/ghx/releases/latest/download/ghx-linux-amd64.tar.gz | tar xz
sudo cp ghx ghxd /usr/local/bin/
# Linux (arm64)
curl -fsSL https://github.com/brunoborges/ghx/releases/latest/download/ghx-linux-arm64.tar.gz | tar xz
sudo cp ghx ghxd /usr/local/bin/
On Windows, download the .zip from GitHub Releases and extract ghx.exe and ghxd.exe to a directory on your PATH.
Build from source
git clone https://github.com/brunoborges/ghx.git
cd ghxd
make build
# Binaries are in bin/ghx and bin/ghxd
sudo cp bin/ghx bin/ghxd /usr/local/bin/
Agents Plugin (Claude Code & Copilot CLI)
If you use Claude Code or GitHub Copilot CLI, install the plugin and your agent will automatically prefer ghx over gh:
# Add the marketplace (one-time)
/plugin marketplace add brunoborges/agent-plugins
# Install the plugin
/plugin install ghx@agent-plugins
Local development / testing: claude --plugin-dir ./agent-plugin
The plugin:
- Lazy-installs
ghx and ghxd binaries on first use
- Adds
ghx to PATH so agents use it automatically
- Includes a skill that teaches agents to prefer
ghx for all GitHub CLI calls
See the plugin README for details. Plugin releases are available on the Releases page with the plugin-v* tag.
Using ghx Without Installing GitHub CLI
You don't need to install the GitHub CLI (gh) separately. When ghx needs the real gh binary and can't find one, it automatically downloads the latest release from cli/cli to ~/.ghx/bin/gh.
How it works
When ghx or ghxd needs to execute a gh command, it resolves the real binary using this order:
- User override β if
gh_path is set in ~/.ghx/config.yaml or via GHX_GH_PATH env var, use that path directly
- PATH scan β search
PATH for a real gh binary (skipping ghx shims)
- Managed location β check
~/.ghx/bin/gh (previously auto-downloaded)
- Auto-download β download the latest GitHub CLI from cli/cli releases to
~/.ghx/bin/gh
The download includes SHA-256 checksum verification against the official release checksums.
The gh shim
When no real GitHub CLI (gh) binary is found on the system, every installation method places a lightweight gh shim script alongside the ghx binary. This shim redirects all gh commands through ghx:
#!/bin/sh
# ghx-shim: this script redirects gh commands through ghx for caching
exec ghx "$@"
This means existing tools, scripts, and CI workflows that call gh will automatically benefit from caching β no changes needed. If a real gh binary is already available, the shim is skipped and you can use ghx directly.
| Install method |
Shim location |
Notes |
| install.sh |
$INSTALL_DIR/gh (default /usr/local/bin/gh) |
Skipped if a real gh binary exists anywhere on the system |
| install.ps1 |
%LOCALAPPDATA%\ghx\bin\gh.cmd |
Skipped if a real gh.exe exists on the system |
| Homebrew |
$(brew --prefix)/bin/gh |
Skipped if gh is already installed (via Homebrew or otherwise) |
| Agent plugin |
Plugin bin/ directory (on PATH) |
Skipped if a real gh binary exists on the system |
Example first-run experience
$ ghx pr list
ghx: GitHub CLI (gh) not found, downloading...
ghx: downloading GitHub CLI v2.74.0...
ghx: GitHub CLI v2.74.0 installed to /Users/you/.ghx/bin/gh
#1 My first PR (main <- feature-branch)
Subsequent runs use the cached gh binary at ~/.ghx/bin/gh β no re-download.
Overriding the gh binary path
If you have gh installed in a non-standard location, point ghx at it:
# ~/.ghx/config.yaml
gh_path: /opt/homebrew/bin/gh
Or via environment variable:
export GHX_GH_PATH=/opt/homebrew/bin/gh
Updating the managed gh binary
When ghx auto-downloads gh, the binary is stored at ~/.ghx/bin/gh. To upgrade it to the latest release:
ghx ghcli upgrade
If the managed binary is more than 30 days old, ghx prints a one-line reminder:
ghx: managed gh binary is 45 days old β run 'ghx ghcli upgrade' to update
To check which gh binary is in use:
$ ghx ghcli status
gh binary: /Users/you/.ghx/bin/gh
source: managed (~/.ghx/bin/gh)
version: 2.74.0
installed: 3 days ago
Usage
Use ghx exactly like gh β the daemon starts automatically on first use:
# These are cached (read-only commands)
ghx pr list --repo owner/repo --json number,title
ghx issue view 42 --json title,state
ghx api /repos/owner/repo --jq '.stargazers_count'
ghx run list --repo owner/repo
# These pass through directly to gh (mutations)
ghx pr create --title "My PR" --body "Description"
ghx issue close 42
Cache behavior
First call: ghx pr list ... β 1.1s (cache miss, calls gh)
Second call: ghx pr list ... β 0.1s (cache hit, instant)
After 30s: ghx pr list ... β 1.0s (TTL expired, fresh call)
Per-command options
ghx --no-cache pr list ... # Bypass cache for this call
ghx --ttl 120 pr list ... # Override TTL to 120 seconds
GHX_NO_CACHE=1 ghx pr list ... # Same via env var
GHX_TTL=60 ghx pr list ... # Same via env var
Daemon Management
ghx xdaemon start # Start in foreground
ghx xdaemon start -d # Start detached (background)
ghx xdaemon stop # Graceful shutdown
ghx xdaemon status # Show uptime and cache stats
ghx xdaemon restart # Stop + start
The daemon auto-starts on first ghx call. If the daemon can't start, ghx falls back to running gh directly β it never blocks you.
Cache Management
ghx xcache stats # Show hit rates and per-command breakdown
ghx xcache flush # Flush all entries
ghx xcache keys # List cached keys (debugging)
Example stats output
Uptime: 2h 34m
Total Requests: 1,247
Cache Hits: 891 (71.4%)
Cache Misses: 203 (16.3%)
Passthrough: 153 (12.3%)
Coalesced: 87
Cache Size: 142 / 1000 entries
Top Commands:
pr list 412 hits / 48 misses (89.6%)
issue view 198 hits / 32 misses (86.1%)
pr view 143 hits / 67 misses (68.1%)
api get 88 hits / 31 misses (73.9%)
Web Dashboard
When the daemon is running, a live dashboard is available at:
http://localhost:9847/
It shows:
- Real-time hit rate and request counters
- Per-command breakdown with hit/miss rates and average latency
- Request log β live tail of recent requests with cache result and timing
The dashboard auto-refreshes every 2 seconds. No external dependencies β it's a single HTML page embedded in the binary.
JSON API
The dashboard data is also available as JSON for scripting:
curl http://localhost:9847/api/stats | jq .
curl http://localhost:9847/api/log?limit=50 | jq .
curl http://localhost:9847/api/ttl-analysis | jq .
What Gets Cached
Only explicitly allowlisted read-only commands are cached:
| Command |
Cached |
gh pr list/view/status/checks/diff |
β
|
gh issue list/view/status |
β
|
gh repo view/list |
β
|
gh run list/view |
β
|
gh workflow list/view |
β
|
gh release list/view |
β
|
gh search repos/issues/prs/commits/code |
β
|
gh api (GET only) |
β
|
gh label list |
β
|
gh gist list/view |
β
|
gh project list/view |
β
|
gh cache list |
β
|
gh ruleset list/view/check |
β
|
gh org list |
β
|
gh pr create/merge/close/edit |
β (mutation β invalidates PR cache) |
gh issue create/edit/delete/close |
β (mutation β invalidates issue cache) |
gh auth/config/codespace/secret |
β (always passthrough) |
Mutations automatically invalidate related cache entries. For example, gh pr merge 42 flushes all cached PR entries for that repo.
Custom Commands
You can add your own commands to the allowlist via ~/.ghx/config.yaml:
additional_cacheable:
- "gh status"
- "gh variable list"
- "gh secret list"
Each entry should be the full command prefix (e.g., "gh status" for a single-word subcommand, or "gh variable list" for two-word). Custom commands are classified with ResourceUnknown β they participate in caching but won't be invalidated by mutation detection. To apply changes, restart the daemon: ghxd --restart.
Configuration
Configuration file: ~/.ghx/config.yaml
# Default TTL for all cached commands (default: 30s)
ttl: 30s
# Per-command TTL overrides
ttl_overrides:
pr_list: 60s
pr_view: 30s
issue_list: 60s
run_list: 15s
# Max cached entries before LRU eviction (default: 1000)
max_cache_entries: 1000
# Dashboard HTTP port (default: 9847)
dashboard_port: 9847
# Auto-start daemon on first ghx call (default: true)
auto_start: true
# Additional commands to cache
additional_cacheable:
- "gh status"
# Path to gh binary (default: auto-resolved)
# Resolution order: this setting β PATH β ~/.ghx/bin/gh β auto-download
# gh_path: /usr/local/bin/gh
Architecture
βββββββββββ βββββββββββ βββββββββββ
β Agent 1 β β Agent 2 β β Agent 3 β
β (ghx) β β (ghx) β β (ghx) β
ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ
β β β
ββββββββββββββΌβββββββββββββ
β Unix Domain Socket
ββββββββ΄βββββββ
β ghxd β
β (daemon) β
βββββββββββββββ€
β Cache (LRU) β
β Singleflightβ
β Metrics β
β Dashboard β
ββββββββ¬βββββββ
β exec
βββββ΄ββββ
β gh β
βββββββββ
Key design decisions:
- Allowlist, not denylist β only known-safe commands are cached
- Context-aware cache keys β includes repo, branch, host, and auth token hash to prevent cross-context collisions
- Singleflight β concurrent identical requests share a single
gh execution
- Coarse invalidation β mutations flush the entire resource namespace (all PR cache for that repo)
- Graceful fallback β if daemon is down or fails,
ghx runs gh directly
- Self-contained β auto-downloads
gh if not installed; no external dependencies beyond a network connection
Security
- Unix socket with
0600 permissions (owner-only access)
- Auth tokens are never stored β only a SHA256 fingerprint is used in cache keys
- Dashboard binds to
127.0.0.1 only (not accessible from network)
- In-memory cache only (lost on daemon restart)
- Each user runs their own isolated daemon
Development
# Build
make build
# Run tests
make test
# Clean
make clean
License
MIT