Run gh commands from inside a sandboxed environment that blocks
api.github.com — by routing them through GitHub Actions over the one
host that is allowed: github.com.
Built originally to work around the Cowork VM block documented in
claude-code#37970,
but it works anywhere you have git, curl, jq, a token, and a host
that permits plain git push to GitHub.
Sandboxed VM github.com Actions runner
(api.github.com BLOCKED) (ALLOWED) (full net access)
───────────────────── ───────── ─────────────────
gh-bridge api /user ──► git push req/<uuid> ──► on: push → run real gh
→ capture stdout/stderr/exit
→ push resp/<uuid>
poll git ls-remote ◄── ◄──
shallow-clone resp/<uuid>
print stdout, exit $code
- Transport: git-over-HTTPS to
github.com. - Compute: a private bridge repo running the
bridge.ymlworkflow on the free Actions runner. - Wire format: two JSON blobs,
schema/request.jsonandschema/response.json.
Typical round-trip: ~15–35 s per call (dominated by Actions queue + cold start). Not fast enough for interactive use; fine for CI/agent jobs that already think in seconds.
You need one private bridge repo per user/team.
1. Fork this repo (or gh repo create your-org/gh-bridge --private --clone and copy the files in).
2. On your VM, install the client:
git clone --depth 1 \
"https://x-access-token:${TOKEN}@github.com/your-org/gh-bridge.git" /tmp/ghb
install -m 755 /tmp/ghb/bin/gh-bridge ~/.local/bin/3. Set env vars and call it like gh:
export GH_BRIDGE_REPO=your-org/gh-bridge
export GH_BRIDGE_TOKEN=<fine-grained PAT: Contents read+write on gh-bridge>
export PATH="$HOME/.local/bin:$PATH"
gh-bridge api /repos/your-org/some-repo --jq .full_name
# → "your-org/some-repo" (exit 0)Exit code, stdout, and stderr match what gh would have produced.
v0 ships with a single-path client and a single bridge.yml that runs
whatever gh command you hand it, under the repo's auto-minted
GITHUB_TOKEN.
- ✅ Any
gh api …call against the bridge repo itself - ✅ Any
ghsubcommand that's happy withGITHUB_TOKEN's default scope (contents,issues,pull-requests: writeon the bridge repo) - ❌ Cross-repo / cross-org calls —
GITHUB_TOKENcan't see them - ❌ User-scope calls like
gh api /user
To widen the surface, add a user/org PAT as a repo secret and teach
bridge.yml to prefer it over the default GITHUB_TOKEN. Planned for
v0.1.
| Var | Required | Default | Notes |
|---|---|---|---|
GH_BRIDGE_REPO |
yes | — | owner/repo of the bridge repo |
GH_BRIDGE_TOKEN |
yes | — | PAT with Contents RW on the bridge repo |
GH_BRIDGE_HOST |
no | github.com |
For GHES deployments |
GH_BRIDGE_POLL |
no | 3 |
Seconds between ls-remote polls |
GH_BRIDGE_TIMEOUT |
no | 180 |
Max seconds to wait for a response |
GH_BRIDGE_KEEP |
no | 0 |
If 1, don't delete resp/<uuid> |
bin/gh-bridge POSIX bash client. Needs git, curl, jq, od, mktemp.
.github/workflows/
bridge.yml Actions runner. Triggered by req/** branch pushes.
schema/
request.json JSON Schema for the request wire format.
response.json JSON Schema for the response wire format.
Honest about the threat model:
- Token scope: use a fine-grained PAT limited to the single bridge repo with Contents read+write. Don't reuse your personal token.
- Runner isolation: every request runs on a fresh Actions runner.
Default
GITHUB_TOKENcan only touch the bridge repo. - Replay protection: each request has a random 128-bit UUID and the runner refuses to process a branch whose name doesn't match the file's UUID. No signatures — there's nothing to forge inside the closed repo, but don't point a public bridge repo at untrusted branch pushes.
- Response retention:
req/*branches are deleted by the workflow on success.resp/*branches are not auto-pruned yet; set a repo Actions schedule to sweep them if you care.
- Not a drop-in for every
ghsubcommand — some interactive flows (gh auth login,gh codespace ssh) don't round-trip through JSON. - Not low-latency.
- Not a multi-tenant service. One user, one private bridge repo.
v0 is deliberately minimal. PRs welcome for:
- Broader command coverage + test fixtures
- PAT-via-secret path for cross-repo / user-scope calls
- Pruner for stale
resp/*branches - GHES / Enterprise Cloud support hardening
MIT — see LICENSE.
Built at MSApps while wiring a Claude Code Cowork agent into GitHub and hitting the sandbox block. Shared publicly because someone else will hit the same wall.