Skip to main content

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`
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: 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 and invite-gate prints 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:

  1. invite-gate never handles a GitHub token. Every GitHub call goes through the gh CLI (gh api ...). The tool never reads GITHUB_TOKEN/GH_TOKEN from the environment, never accepts a token as a flag, and never writes one anywhere. Before invoking gh, it explicitly strips GITHUB_TOKEN and GH_TOKEN from the subprocess environment, so gh always uses its own stored credential (~/.config/gh/hosts.yml) rather than an ambient token that might belong to a different identity. If gh is missing or unauthenticated, invite-gate fails loudly with an actionable message — it never degrades quietly.

  2. No wildcard / accept-all, ever. There is no * support, no --all flag, and no config option that means "accept from everyone." If you put a literal * in your allowlist file, invite-gate treats 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.

  3. A missing or empty allowlist is not permission to act. invite-gate run refuses 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.

  4. Non-allowlisted invitations are never auto-actioned — not accepted, not declined. They're recorded and surfaced for you to decide on, via status or motd.

  5. motd never makes a network call. It only reads the cache file written by run/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.

  6. Nothing sensitive is ever logged or printed. The activity log records only timestamp, action, repo full name, sender login, and invitation id.

  7. All writes go to XDG paths. No home directory is ever hardcoded; see File locations.

  8. Any API failure exits non-zero. There is no degraded mode, no silent fallback, and no retry-forever loop. If gh fails, 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

invite_gate-0.3.0.tar.gz (33.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

invite_gate-0.3.0-py3-none-any.whl (25.8 kB view details)

Uploaded Python 3

File details

Details for the file invite_gate-0.3.0.tar.gz.

File metadata

  • Download URL: invite_gate-0.3.0.tar.gz
  • Upload date:
  • Size: 33.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for invite_gate-0.3.0.tar.gz
Algorithm Hash digest
SHA256 c2a6a7e7173d547f757c493fcc9a21b029f55ba6f463e749e947a8a1fd171118
MD5 8d989ef595fc22c23d74648ac95505ca
BLAKE2b-256 972a5068c35a70680706767ae2a263e9ea357f65cd75753674676543315d3102

See more details on using hashes here.

Provenance

The following attestation bundles were made for invite_gate-0.3.0.tar.gz:

Publisher: publish.yml on bkrabach/invite-gate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file invite_gate-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: invite_gate-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 25.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for invite_gate-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 658d629dbb46c97bf105bb0db47ea1e2f4f92cf6f6d492556281ec52e87cf553
MD5 19103774f9f662a495ae28e00c28ba5f
BLAKE2b-256 963ad7a35fc8b4610336c9ff10a2df9a6918bff002bcc1710fb81a9c3055e38b

See more details on using hashes here.

Provenance

The following attestation bundles were made for invite_gate-0.3.0-py3-none-any.whl:

Publisher: publish.yml on bkrabach/invite-gate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page