Auto-accept GitHub repository invitations from senders you explicitly trust. Leave every other invitation exactly where it is — pending, untouched, for you to review by hand.
If you're on a team (or a popular open-source project), you can end up with a steady stream of repository invitations: teammates adding you to new repos, bots inviting you to CI/automation repos, org-wide onboarding flows, and occasionally strangers or compromised accounts trying their luck.
The tempting fix is a cron job that blanket-accepts everything. Don't do that. Accepting an invitation from someone you don't know:
- Adds a repository you didn't choose into your GitHub account, which then shows up in your repo list, your notifications, and your contribution graph.
- Exposes you to code and CI/CD workflows you have not reviewed. GitHub Actions in a repo you've joined can run with your account's context in some flows, and a malicious repo is a real attack vector.
- Implies an association. Being a collaborator on a repo is visible, and it can look like an endorsement or affiliation you never intended.
invite-gate solves the actual problem: auto-accept invitations from a
short, explicit list of senders you already trust (teammates, your org, your
own bots), and leave literally everyone else pending so a human looks at it.
There is no "accept all" mode. There never will be — see
Security notes.
# From PyPI
uv tool install invite-gate
# Or straight from GitHub
uv tool install git+https://github.com/bkrabach/invite-gateYou'll also need the GitHub CLI (gh) installed
and authenticated:
gh auth logininvite-gate never asks for or stores a token itself — it shells out to
gh, which manages your credential. See
Security notes for why this matters.
# 1. Check gh is installed, authenticated, and correctly scoped
invite-gate doctor
# 2. Trust one or more senders (creates the allowlist if it doesn't exist yet)
invite-gate allow some-teammate some-org some-app[bot]
# 3. See what's pending right now
invite-gate status
# 4. Run the gate: auto-accept anything from an allowlisted sender,
# leave the rest pending
invite-gate run
# 5. Wire it up to run periodically
invite-gate install-timerinvite-gate status # human-readable: pending invites split into "would auto-accept" and "needs your review"
invite-gate run [--dry-run] # poll; accept allowlisted; leave rest pending; refresh cache; this is the timer entrypoint
invite-gate motd # ultra-concise cached summary for shell startup; silent+exit 0 when nothing pending
invite-gate accept <ID>... # accept specific invitation id(s)
invite-gate decline <ID>... # decline (DELETE) specific invitation id(s)
invite-gate allow <LOGIN>... # append login(s) to allowlist (idempotent); then accept any pending from them; report what it did
invite-gate list # print current allowlist
invite-gate doctor # check gh presence/auth/scope, config, state dir, cache age -- reports every check
invite-gate install-timer # print (does NOT execute) ready-to-paste systemd/launchd units for scheduling `run`
invite-gate version # print the installed version
invite-gate update [--check] # check PyPI for a newer version and upgrade in place (--check only reports)
Global flags: --json (machine-readable output for status/run),
--version, -h/--help.
status and run both make a live call to GitHub (via gh) and refresh the
local pending cache as a side effect. motd never does — see below.
motd is meant to be wired into your shell's startup (.bashrc, .zshrc,
etc.) so you get a one-glance reminder of pending invitations every time you
open a terminal, without adding any latency to shell startup. To keep that
promise, motd never makes a network call — it only reads the cache
file that run and status last wrote. If that cache doesn't exist yet, it
prints nothing and exits 0. If it exists but is more than 24 hours old, it
still prints its summary, with a short note that the cache is stale.
Add a line like this to your shell profile:
invite-gate motdExample output when there's something to review:
2 GitHub invites need review:
octocat/some-repo from octocat invite-gate accept/decline 12345
acme/widgets from acme-bot invite-gate accept/decline 12346
invite-gate allow <sender> to auto-accept future invites from a sender
Output is capped at 10 rows (… and N more (invite-gate status) beyond
that), uses no color by default, and only uses color when stdout is a TTY
and NO_COLOR is unset.
The allowlist is a plain text file, one GitHub login per line:
# invite-gate allowlist
some-teammate
some-org
some-app[bot] # bot logins keep their brackets -- they're part of the login
Rules:
#starts a comment; trailing comments (login # note) are fine.- Blank lines are ignored.
- Leading/trailing whitespace is stripped.
- Matching is case-insensitive (GitHub logins are case-insensitive).
- No wildcards. A literal
*in the file is treated as a literal, unmatchable login andinvite-gateprints a warning explaining why — see Security notes.
Running invite-gate allow <login> creates the file (with a helpful
commented template) if it doesn't exist yet, and is idempotent — allowing an
already-allowed login is a no-op for the file, though it still checks for
and accepts any pending invitation from that sender.
A real-name-free example is shipped as
allowlist.example.txt.
invite-gate never hardcodes a home directory; it always resolves paths
through the XDG base directory variables (falling back to their documented
defaults):
| What | Path |
|---|---|
| Allowlist | ${XDG_CONFIG_HOME:-~/.config}/invite-gate/allowlist.txt |
Pending cache (used by motd) |
${XDG_STATE_HOME:-~/.local/state}/invite-gate/pending.json |
| Activity log | ${XDG_STATE_HOME:-~/.local/state}/invite-gate/activity.log |
The activity log records only: timestamp, action, repo full name, sender login, and invitation id. Nothing else, ever — see Security notes.
invite-gate run is the entrypoint you want on a timer. Run:
invite-gate install-timerThis prints (it does not execute) ready-to-paste unit files: a systemd
user service + timer for Linux, and a launchd plist for macOS. It detects
your platform and shows the relevant one first, followed by the exact enable
commands. Review the printed units, adjust the path to the invite-gate
binary if needed, and install them yourself.
By default the timer polls every 15 minutes. Override it with --interval:
invite-gate install-timer --interval 5min
invite-gate install-timer --interval 1h--interval takes a number plus a required unit -- s/sec/secs/second/
seconds, m/min/mins/minute/minutes, h/hr/hrs/hour/hours,
or d/day/days (case-insensitive, decimals allowed, e.g. 1.5h). A bare
number with no unit is rejected rather than guessed. The minimum is 60
seconds: invite-gate run makes a GitHub API call every time it fires, and
polling faster than once a minute risks tripping GitHub's secondary rate
limits for no benefit. There's no maximum, but GitHub collaborator
invitations expire after 7 days, so an interval measured in days risks
missing them -- keep it well under a day.
This tool exists specifically to be conservative about what it auto-actions. The following are load-bearing, not incidental:
-
invite-gatenever handles a GitHub token. Every GitHub call goes through theghCLI (gh api ...). The tool never readsGITHUB_TOKEN/GH_TOKENfrom the environment, never accepts a token as a flag, and never writes one anywhere. Before invokinggh, it explicitly stripsGITHUB_TOKENandGH_TOKENfrom the subprocess environment, soghalways uses its own stored credential (~/.config/gh/hosts.yml) rather than an ambient token that might belong to a different identity. Ifghis missing or unauthenticated,invite-gatefails loudly with an actionable message — it never degrades quietly. -
No wildcard / accept-all, ever. There is no
*support, no--allflag, and no config option that means "accept from everyone." If you put a literal*in your allowlist file,invite-gatetreats it as a literal login string — which no real GitHub account can have — and prints a warning telling you why wildcards are deliberately unsupported. This is intentional and will not change: the entire point of the tool is precise, reviewed trust, not convenience at the cost of blanket acceptance. -
A missing or empty allowlist is not permission to act.
invite-gate runrefuses to accept anything and exits non-zero with a clear message if the allowlist doesn't exist or has no entries. Read-only commands (status,doctor,list) still work fine with no config at all. -
Non-allowlisted invitations are never auto-actioned — not accepted, not declined. They're recorded and surfaced for you to decide on, via
statusormotd. -
motdnever makes a network call. It only reads the cache file written byrun/status, specifically so it's safe and fast to put in your shell startup. Missing cache -> silent, exit 0. Stale cache (>24h) -> still prints, with a short staleness note. -
Nothing sensitive is ever logged or printed. The activity log records only timestamp, action, repo full name, sender login, and invitation id.
-
All writes go to XDG paths. No home directory is ever hardcoded; see File locations.
-
Any API failure exits non-zero. There is no degraded mode, no silent fallback, and no retry-forever loop. If
ghfails, you find out.
invite-gate needs to read and act on /user/repository_invitations, which
requires the OAuth repo scope on your gh credential. repo is a broad
scope — it grants full read/write access to your repositories, not just
invitations — because GitHub doesn't expose a narrower scope for the
invitations endpoints. There isn't a way around this without GitHub adding a
finer-grained scope; invite-gate doesn't request anything beyond what gh
itself already asks for when you run gh auth login. invite-gate doctor
checks for this scope explicitly and tells you how to fix it if it's
missing.
git clone https://github.com/bkrabach/invite-gate
cd invite-gate
uv pip install -e ".[dev]"
python -m pytestAll tests mock subprocess calls to gh — the test suite never touches the
network.
MIT © 2026 Brian Krabach. See LICENSE.