invite-gate
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.
Why this exists
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.
Install
# From PyPI
uv tool install invite-gate
# Or straight from GitHub
uv tool install git+https://github.com/bkrabach/invite-gate
You'll also need the GitHub CLI (gh) installed
and authenticated:
gh auth login
invite-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.
Quickstart
# 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-timer
Commands
invite-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`
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: shell-startup summary
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 motd
Example 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.
Config format
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.
File locations (XDG)
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.
Scheduling
invite-gate run is the entrypoint you want on a timer. Run:
invite-gate install-timer
This 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.
Security notes
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.
Why repo scope?
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.
Development
git clone https://github.com/bkrabach/invite-gate
cd invite-gate
uv pip install -e ".[dev]"
python -m pytest
All tests mock subprocess calls to gh — the test suite never touches the
network.
License
MIT © 2026 Brian Krabach. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file invite_gate-0.2.0.tar.gz.
File metadata
- Download URL: invite_gate-0.2.0.tar.gz
- Upload date:
- Size: 27.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
379131d656f559e62d1790a9229d6d6fe1e851c5cf6480f19a94aebf654ddac8
|
|
| MD5 |
a52230c125383473d1a160bf4c11126c
|
|
| BLAKE2b-256 |
3bb185c0184651faa512b9a9d9c70fbd20c6d0c81d8a7a7543de50c77d3d4577
|
Provenance
The following attestation bundles were made for invite_gate-0.2.0.tar.gz:
Publisher:
publish.yml on bkrabach/invite-gate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
invite_gate-0.2.0.tar.gz -
Subject digest:
379131d656f559e62d1790a9229d6d6fe1e851c5cf6480f19a94aebf654ddac8 - Sigstore transparency entry: 2515794955
- Sigstore integration time:
-
Permalink:
bkrabach/invite-gate@118f02bacdb1fbf09eb7af7761786ac7cb32c1a4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bkrabach
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@118f02bacdb1fbf09eb7af7761786ac7cb32c1a4 -
Trigger Event:
release
-
Statement type:
File details
Details for the file invite_gate-0.2.0-py3-none-any.whl.
File metadata
- Download URL: invite_gate-0.2.0-py3-none-any.whl
- Upload date:
- Size: 22.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e548d1c37c570b97066a0166093b5ef69be16cdf367d996968c971b982ba25b
|
|
| MD5 |
3b1b4ebe18b31a957edeac912cc1bb59
|
|
| BLAKE2b-256 |
7b3c0dceab4fe57e7b11593acc6876788104486a4f0de4fc455f5d4964afa5b6
|
Provenance
The following attestation bundles were made for invite_gate-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on bkrabach/invite-gate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
invite_gate-0.2.0-py3-none-any.whl -
Subject digest:
3e548d1c37c570b97066a0166093b5ef69be16cdf367d996968c971b982ba25b - Sigstore transparency entry: 2515795020
- Sigstore integration time:
-
Permalink:
bkrabach/invite-gate@118f02bacdb1fbf09eb7af7761786ac7cb32c1a4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bkrabach
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@118f02bacdb1fbf09eb7af7761786ac7cb32c1a4 -
Trigger Event:
release
-
Statement type: