Last active
July 15, 2026 11:40
-
-
Save 1stvamp/fad10a1db96989cdfda9d629898864fe to your computer and use it in GitHub Desktop.
gh wrapper to use a different scoped PAT for your work repos (specific github org) when the org policy doesn't allow classic PATs for multi-repo/org access
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/python3 | |
| """gh wrapper: use the scoped triggerdotdev PAT for gh commands that target the | |
| triggerdotdev org, and pass everything else straight through to the real gh. | |
| Lives in ~/bin and shadows the real gh on PATH. It finds the next gh on PATH | |
| (skipping itself), and for invocations that target the triggerdotdev org it sets | |
| GH_TOKEN to $GITHUB_TRIGGERDOTDEV_TOKEN before exec'ing it. The org now rejects | |
| classic PATs org-wide, so gh's normal token 403s against triggerdotdev; this | |
| routes those calls through the fine-grained token instead, while leaving every | |
| other host/org on the user's default auth. | |
| Org is detected from, in order: -R/--repo, $GH_REPO, the current repo's git | |
| remotes, and finally any argument that names the org (e.g. `gh api | |
| repos/triggerdotdev/...`). An explicit -R/--repo or $GH_REPO for a different org | |
| wins and stops the fallback, so you can't accidentally leak the token elsewhere. | |
| """ | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| ORG = "triggerdotdev" | |
| TOKEN_VAR = "GITHUB_TRIGGERDOTDEV_TOKEN" | |
| def find_real_gh(): | |
| """First gh on PATH that isn't this script.""" | |
| self_real = os.path.realpath(sys.argv[0]) | |
| seen = set() | |
| for d in os.environ.get("PATH", "").split(os.pathsep): | |
| if not d or d in seen: | |
| continue | |
| seen.add(d) | |
| cand = os.path.join(d, "gh") | |
| if ( | |
| os.path.isfile(cand) | |
| and os.access(cand, os.X_OK) | |
| and os.path.realpath(cand) != self_real | |
| ): | |
| return cand | |
| return None | |
| def owner_from_spec(spec): | |
| """Owner from owner/repo, [host/]owner/repo, an https URL, or a git remote.""" | |
| if not spec: | |
| return None | |
| spec = spec.strip() | |
| m = re.match(r"^[^/@]+@[^/:]+:(.+)$", spec) # scp-like: git@github.com:owner/repo | |
| if m: | |
| spec = m.group(1) | |
| elif "://" in spec: | |
| spec = spec.split("://", 1)[1] # https://host/owner/repo -> host/owner/repo | |
| if spec.endswith(".git"): | |
| spec = spec[:-4] | |
| parts = [p for p in spec.split("/") if p] | |
| if len(parts) >= 2: | |
| return parts[-2] # repo is last, owner is second-to-last | |
| return None | |
| def repo_arg_owner(argv): | |
| """Owner named by an explicit -R/--repo flag, or None if not given.""" | |
| i = 0 | |
| while i < len(argv): | |
| a = argv[i] | |
| val = None | |
| if a in ("-R", "--repo"): | |
| val = argv[i + 1] if i + 1 < len(argv) else None | |
| elif a.startswith("--repo="): | |
| val = a[len("--repo="):] | |
| elif a.startswith("-R="): | |
| val = a[len("-R="):] | |
| elif a.startswith("-R") and len(a) > 2: | |
| val = a[2:] | |
| if val is not None: | |
| return owner_from_spec(val) | |
| i += 1 | |
| return None | |
| def git_remotes_target_org(): | |
| try: | |
| out = subprocess.run( | |
| ["git", "remote", "-v"], | |
| capture_output=True, | |
| text=True, | |
| timeout=5, | |
| ) | |
| except Exception: | |
| return False | |
| if out.returncode != 0: | |
| return False | |
| for line in out.stdout.splitlines(): | |
| cols = line.split() # name\turl (fetch|push) | |
| if len(cols) >= 2: | |
| owner = owner_from_spec(cols[1]) | |
| if owner and owner.lower() == ORG: | |
| return True | |
| return False | |
| def targets_org(argv): | |
| # 1. explicit -R/--repo wins outright (stops any fallback) | |
| owner = repo_arg_owner(argv) | |
| if owner is not None: | |
| return owner.lower() == ORG | |
| # 2. $GH_REPO also wins outright | |
| if os.environ.get("GH_REPO"): | |
| owner = owner_from_spec(os.environ["GH_REPO"]) | |
| if owner is not None: | |
| return owner.lower() == ORG | |
| # 3. current repo's remotes | |
| if git_remotes_target_org(): | |
| return True | |
| # 4. org named in any arg, e.g. `gh api repos/triggerdotdev/...` | |
| needle = ORG.lower() + "/" | |
| return any(needle in a.lower() for a in argv) | |
| def main(): | |
| argv = sys.argv[1:] | |
| real_gh = find_real_gh() | |
| if not real_gh: | |
| sys.stderr.write("gh-wrapper: could not find the real gh on PATH\n") | |
| return 127 | |
| env = dict(os.environ) | |
| if targets_org(argv): | |
| token = os.environ.get(TOKEN_VAR) | |
| if token: | |
| env["GH_TOKEN"] = token | |
| env.pop("GITHUB_TOKEN", None) # so the scoped GH_TOKEN wins unambiguously | |
| else: | |
| sys.stderr.write( | |
| "gh-wrapper: targeting %s but %s is unset; falling back to default auth\n" | |
| % (ORG, TOKEN_VAR) | |
| ) | |
| try: | |
| os.execve(real_gh, [real_gh] + argv, env) | |
| except OSError as e: | |
| sys.stderr.write("gh-wrapper: failed to exec %s: %s\n" % (real_gh, e)) | |
| return 126 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment