Skip to content

Repository files navigation

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.

About

Auto-accept GitHub repository invitations from senders you trust — and only those.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages