Runtime-minted, minimally scoped GitHub tokens from a GitHub App: every
token is bound to a single repository with an explicit permission set,
expires within an hour, and is never written to disk. Ships as a Hermes agent
plugin and as a standalone CLI (ghapp) usable in any dev environment.
Two operating modes, selected by one environment variable:
- Local mode (default): this process holds the App private key (file,
inline env, or a command like
op read ...executed at mint time) and mints directly. For interactive/dev environments. - Broker mode (
GHAPP_BROKER_SOCKET=/run/ghbroker/ghbroker.sock): tokens are requested from a host-side broker daemon over a unix socket. The client process holds no key material — for containerized agent execution, where the key must not be readable from the agent's shell. The broker enforces a repository allowlist + permission ceiling and audits every decision.
agent container trusted host side
┌────────────────────────┐ ┌─────────────────────────────┐
│ git push (cred helper) │──UDS────▶│ ghapp serve │──▶ GitHub
│ gh-app / ghapp token │ socket │ policy: repos ∩ perms │ App API
│ github_app_* tools │ │ audit → journald │
│ (no key material) │ │ key: 0600, own user │
└────────────────────────┘ └─────────────────────────────┘
github_app:
client_id: "Iv23exampleclientid"
app_slug: "igou-dev"
installations: # one entry per account the app is installed on
david-igou: "143866260"
igou-io: "143866153"
private_key_cmd: op read op://lab_external_api_keys/igou-dev-github-app/private_key
# or: private_key_path: ~/.config/ghapp/key.pem
default_permissions: # used when a mint doesn't specify permissions
contents: write
pull_requests: write
issues: writeConfig lives in ~/.hermes/config.yaml (or set GHAPP_CONFIG=/path/to/file).
Environment variables override: GITHUB_APP_CLIENT_ID,
GITHUB_APP_INSTALLATIONS (owner=id[,owner=id]), GITHUB_APP_PRIVATE_KEY,
GITHUB_APP_PRIVATE_KEY_PATH, GITHUB_APP_PRIVATE_KEY_CMD,
GITHUB_APP_SLUG, GITHUB_API_URL.
The owner half of OWNER/REPO selects the installation, so one app installed
on several accounts needs no per-call installation plumbing. The legacy
single installation_id key still works.
pip install "hermes-github-app-plugin @ git+https://github.com/david-igou/hermes_github_app_plugin@main"
ghapp setup # interactive; --non-interactive with flags for scripts
ghapp doctor --repo OWNER/REPO# gitconfig
[credential "https://github.com"]
helper = ghapp
useHttpPath = trueWith that in place, git clone/fetch/push on HTTPS GitHub remotes just works:
the helper receives the repo path, mints a contents: write token for exactly
that repository, and hands it to git. Nothing is stored (store/erase are
no-ops). SSH remotes bypass the App identity — don't use them for bot work.
gh-app --repo OWNER/REPO -- pr list # GH_TOKEN scoped to that repo
gh-app --repo OWNER/REPO --permission contents=read -- run list
git-app --repo OWNER/REPO -- push origin branch # askpass variant, no helper needed
ghapp token --repo OWNER/REPO --permission contents=read [--json]
ghapp api /repos/OWNER/REPO/issues --method POST --data '{"title": "..."}'
ghapp status --repo OWNER/REPO--repo is required everywhere a token is minted: tokens are per-repository
by design. Request only the permissions the operation needs; unspecified
mints use default_permissions (or the broker's defaults in broker mode).
On the trusted host, as a dedicated user that owns the key:
ghapp serve --socket /run/ghbroker/ghbroker.sock --policy /etc/ghbroker/policy.yaml# /etc/ghbroker/policy.yaml — root-owned, immutable to the agent
policy:
allowed_repos: # OWNER/REPO, fnmatch globs allowed
- igou-io/igou-ansible
- igou-io/igou-inventory
max_permissions:
contents: write
pull_requests: write
issues: write
default_permissions:
contents: readMount the socket into the agent container and set
GHAPP_BROKER_SOCKET=/run/ghbroker/ghbroker.sock; every client above (and the
Hermes tools) switches to broker mode automatically. Requests exceeding policy
are denied, not clamped, and every decision is a structured JSON line on
stdout (journald under systemd): decision, repo, requested vs granted
permissions, peer pid/uid/gid, expiry.
API: POST /token {"repo": "OWNER/REPO", "permissions": {...}},
GET /status, GET /healthz.
Layered enforcement, outermost first: GitHub installation repo allowlist → app permission ceiling → broker policy → per-token scope (single repo, minimal perms, ≤1 h TTL).
The plugin registers github_app_status, github_app_verify_identity,
github_app_api, github_app_graphql, github_app_create_issue,
github_app_comment_issue, github_app_create_pr, github_app_comment_pr.
All of them mint per-repo tokens through the active backend and return auth
metadata (app slug, installation, scoped repositories/permissions, redacted
token, expiry). Mutating tools use the minimal permission set for the
operation (issues: write for issues, pull_requests: write for PRs, ...).
hermes plugins enable github-app- Prefer
github_app_*tools for API operations. gh ...→gh-app --repo OWNER/REPO -- ....git push→ plain push over HTTPS with the credential helper, orgit-app --repo OWNER/REPO -- push ....- No SSH remotes for bot-managed worktrees; no
gh auth statusas identity proof; no@me(the actor is<app-slug>[bot]). - Write summaries should include
auth_mode,app_slug,installation_id, repository, operation, and the grantedscoped_permissions.
pip install -e '.[dev]'
ruff format --check . && ruff check . && mypy && pytestDesign docs: docs/design-minimal-runtime-tokens.md, docs/github-app-setup.md.
Started as a fork of PickNikRobotics/hermes_github_app_plugin and has since
diverged permanently (down-scoped multi-installation minting, the broker, the
credential helper). MIT licensed; original attribution retained in LICENSE.
This package is not published to PyPI (that name belongs to upstream) —
install from a git ref.