A red team CLI for auditing what a GitHub token can do. Hand it any GitHub token (classic PAT, fine-grained PAT, OAuth user token, or a workflow GITHUB_TOKEN) and it reports the reach.
- Token identity: owner, account type, 2FA status, emails, orgs, OAuth scopes, rate-limit headroom
- Accessible repositories: visibility, archived state, secret + workflow-run counts (for workflow-based attacks), the token's collaborator role per repo
- Inferred permissions: GitHub exposes no introspection endpoint for fine-grained PAT or
GITHUB_TOKENperms. Patdown probes a curated set of endpoints per scope (account / org / repo) and infersR,W, orRWfrom response status. Writes are detected only with-W. - Workflow-specific intel: for
GITHUB_TOKENonly: Actions enablement, default workflow permissions, self-hosted runner count, environment list, OIDC subject format.
- Triaging a leaked token from a pentest or red-team find: figure out reach + impact in one command
- Auditing your own CI workflow's
permissions:block to confirm the runtime token can or can't do specific things - Pre-engagement recon when handed a token of unknown provenance
- Read probes are pure
GETs. No mutation, no audit-log noise. - Write probes (
-W) send realPOST/PUT/PATCHrequests with deliberately invalid bodies. They appear in org audit logs. See Write probe semantics below for the exact endpoints and status interpretation. - Concurrency is bounded (default 5) to stay under GitHub's rate-limit threshold.
ghr_refresh tokens andghu_user-to-server tokens are rejected at startup since neither is usable directly against the GitHub REST API.
| Prefix | Type | Auto-probes permissions? |
|---|---|---|
ghp_ |
Classic PAT | no (scopes already exposed via X-OAuth-Scopes) |
gho_ |
OAuth user token | no |
github_pat_ |
Fine-grained PAT | yes |
ghs_ |
GITHUB_TOKEN (Actions runtime) | yes |
Patdown is distributed via GitHub only. Pick one:
# pipx (isolated env)
pipx install git+https://github.com/nopcorn/patdown.git
# plain pip (current env)
pip install git+https://github.com/nopcorn/patdown.git
# uvx (ephemeral)
uvx --from git+https://github.com/nopcorn/patdown.git patdown --helpTo upgrade later: pipx upgrade patdown (or pip install --upgrade git+...).
# token from env
export GH_TOKEN=ghp_xxx
patdown # enumerate everything
patdown owner/name # focus on one repo
patdown owner/name -W # also probe writes
# token from clipboard / pipe
pbpaste | patdown --token-stdin owner/name
# structured output
patdown --json | jq .All 15 canonical permissions: keys are tracked. 13 are probed via REST/GraphQL/packages; id_token and models cannot be probed safely (OIDC issuance requires the runner; models.github.ai is a separate host).
24 repo perms, 14 account perms, 13 org perms (per unique org owning an accessible repo).
Gaps (intentionally not probed):
metadata: implicit, required for any/repos/...call to succeedworkflows: write-only, no read endpointissues:write: bothPOST /issues(422) andPATCH /issues/0(404) are indistinguishable from "granted"codespaces_lifecycle_admin: no distinct read endpointmerge_queues: branch-scoped only (could support this in the future)single_file: path-restricted; not distinguishable fromcontentscopilot_chat,knowledge_bases: no documented public REST endpoint
These tokens use OAuth scopes, not granular fine-grained perms. The scope list is returned by GitHub in the X-OAuth-Scopes response header on every call, so there is no probe pass.
Scope hierarchy: admin:* includes write:* which includes read:* for the same subject. repo is a superset of public_repo, repo:status, repo_deployment, repo:invite, security_events, and (private-repo aspects of) workflow.
No permission probe table is rendered for these tokens as the scopes alone are authoritative.
usage: patdown [-h] [--version] [-W] [-j CONCURRENCY] [--max-repos MAX_REPOS]
[--include-public] [--json] [--token-stdin] [-v] [-q]
[repo]
Enumerate what a GitHub token can reach.
positional arguments:
repo owner/name to focus on. Omit to enumerate every
accessible repo.
options:
-h, --help show this help message and exit
--version show program's version number and exit
-W, --check-writes Probe write perms (GITHUB_TOKEN or fine-grained PAT).
Real POSTs/PUTs with invalid bodies - audit-log
visible.
-j, --concurrency CONCURRENCY
Concurrent probe workers (default: 5). Higher = faster
but more secondary-rate-limit risk.
--max-repos MAX_REPOS
Cap repos enumerated/probed (default: no cap). No-op
when a repo is given.
--include-public Include pull-only public repos when enumerating. No-op
when a repo is given.
--json Emit JSON to stdout instead of human tables.
--token-stdin Read token from stdin (first line).
-v, --verbose Debug logging.
-q, --quiet Suppress info logs.
Token is read from $GH_TOKEN, $GITHUB_TOKEN, or --token-stdin.
Examples:
GH_TOKEN=ghp_xxx patdown # enumerate everything
GH_TOKEN=ghp_xxx patdown owner/name # focus on one repo
GH_TOKEN=ghp_xxx patdown owner/name -W # also probe writes
pbpaste | patdown --token-stdin owner/name # paste token from clipboard
GH_TOKEN=ghp_xxx patdown --json | jq . # structured output
Token types recognized: classic PAT (ghp_), fine-grained PAT (github_pat_),
OAuth user token (gho_), GITHUB_TOKEN / Actions install (ghs_).
Human mode prints three rich tables to stdout (token info, repo targets, permissions). JSON mode dumps a single object with all the same info.
- Token Information: type, owner/login, 2FA, emails, orgs, scopes, rate limit. Empty rows are omitted.
- Repository Targets: repo enumeration with secret/run counts, or a workflow-specific summary (Actions enablement, default workflow perms, runners, environments, OIDC subject).
- Permissions: one row per scope (
<account>,<org:foo>,owner/repo). Granted column usesR,W,RW. Writes are bolded red. Un-granted perms are omitted.
Each write probe sends a real authenticated POST/PUT with {} or an invalid body:
422→ granted (validation rejected before any mutation)403/404→ denied201/200→ accidental success (flagged as side-effect)410→ feature disabled- Other → inconclusive
Caveats: writes appear in org audit logs; archived repos skip writes (403 regardless); pull_requests:create uses a nonexistent head ref so no PR lands. A 403 "not permitted to create" indicates the PR-creation gate is set on the org/repo, separate from missing perm.
patdown/
cli.py argparse entry point
github_wrapper.py token classification + HTTP session
constants.py probe tables
probes.py REST / GraphQL / packages probe helpers
concurrency.py parallel() fan-out + get_json()
collectors.py perm gathering + write classification
repos.py repo listing / pagination
render.py rich-table emitters
auditor.py Auditor orchestrator
tests/ pytest classification fixtures
PRs welcome. pip install -e '.[test]' && pytest to run tests.