Role-scoped GitHub App installation tokens for automated agents. Part of the clagentic suite.
Gatekeeper stands at the GitHub gate. When an automated agent needs to act on a repository, Gatekeeper mints a short-lived, role-scoped GitHub App installation token narrowed to exactly what that role is allowed to do — and nothing more.
It ships four generic roles out of the box:
| Role | Can do | Cannot do |
|---|---|---|
builder |
Push feature branches, open/update PRs | Merge, push to the default branch |
reviewer |
Submit PR reviews (approve / request changes), comment | Push code, merge |
merger |
Merge PRs, push to the default branch | Open PRs, author feature work |
security |
Post security review comments, request changes | Push code, merge |
The roles are generic. Gatekeeper does not know or care what agents you run. You map your own agents to roles in your own configuration. Gatekeeper's only job is: given a role, return a token scoped to that role's permissions.
You can also define custom roles in config.yaml without forking code — for example a releaser scoped only to tagging, or a deployer with deployment write access. See docs/ROLES.md under "Adding a custom role" for the config schema.
GitHub forbids an actor from approving its own pull request. A workflow where one identity builds, reviews, and merges therefore cannot produce a credible, auditable "built → reviewed → merged by separate actors" trail. Gatekeeper solves this by minting distinct, role-narrowed tokens from distinct GitHub Apps, so every PR visibly flows through separate build, review, and merge actors. An optional security role can add an independent security review gate on top of that core trail.
The App private keys never touch the agent. Gatekeeper reads them from a pluggable secret broker (OpenBao by default), signs the App JWT server-side, and hands the agent only a ≤1-hour installation token narrowed to its role.
- It is not a dispatcher, a queue, or an agent framework. It mints tokens. That is the whole surface.
- It is not coupled to any specific set of agents. Agent→role mapping lives in the consumer, not here.
- It does not store long-lived secrets. The broker does.
Gatekeeper serves any consumer that can call its mint path — nothing here
imports or assumes a particular caller. clagentic: loadout
is the reference consumer: its TokenProvider seam treats Gatekeeper as
one interchangeable minting provider among several (a static token, a
bring-your-own minting command, or no provider at all each work equally well
there). Neither project imports the other.
Concretely, the seam works like this: Gatekeeper mints a scoped, short-lived
GitHub App installation token for a role (see "Usage" below); a consumer
calls gatekeeper mint --role <role> and uses the returned token for the
git/API operations that role permits. --json mode additionally returns the
broker-verified App slug alongside the token (see "Structured output" below
and docs/ROLES.md)
so a consumer that needs to know which App/bot identity a role maps to can
adopt that value instead of separately declaring it.
This is optional in both directions. A Gatekeeper user who never touches
loadout loses nothing — Gatekeeper's entire contract is the CLI/JSON output
documented in this README, usable by any caller that can shell out or read
JSON. A loadout user who never touches Gatekeeper loses nothing either — the
Forgejo path works fully standalone with a static token, and GitHub's
App-token path accepts any TokenProvider implementation, not only
Gatekeeper's.
Gatekeeper's attestation layer (internal/attestation) resolves who is
asking before anything is minted (see docs/SETUP.md).
Two additions extend that substrate for a remote-facing, agent-to-agent
caller — a caller whose minted credential crosses a trust boundary to a
peer, rather than being used purely locally:
- Structured sidecar records (
attestation.sidecars[].identity_field): a sidecar entry can opt into parsing its file as a structured (JSON or YAML) record and reading a named field as the attested identity, instead of treating the whole file as the identity string. The rest of the record — a parent-session id, a spawn id, a generic caller-type classification, a spawn timestamp — is captured for cross-attribution and audit whenever present. A structured record that is present but malformed (unparseable, or missing/empty/non-string in the named field) is a hard, fail-closed error naming the field — never treated as "no identity." - Domain-aware fail-closed MISS: for a remote-facing (A2A) mint request, a per-spawn attestation miss now refuses outright rather than falling through to a session-scoped identity — closing a confused-deputy path where a spawn with no attestation of its own would otherwise mint a peer-facing credential under its parent's (higher-trust) identity. Local GitHub/reader mints are unaffected: a per-spawn miss still falls through to the session sidecar exactly as before, since a long-lived lead session legitimately has no per-spawn sidecar of its own.
What this repository ships today: the attestation substrate above —
structured-record parsing, the attribution fields it carries, and the
domain-aware resolution policy (internal/attestation.DomainResolver).
What it does NOT yet ship: an actual A2A token-minting command in
gatekeeper itself. That mint path is a separate, gated epic; this
substrate is what it will consume once it lands. See
docs/SETUP.md
for the published required-fields contract a sidecar producer implements,
and docs/SIDECAR-READ-CONTRACT.md for
the generalized, tool-agnostic read-contract sections this substrate
follows.
# Mint a token for the builder role, scoped to one repo.
gatekeeper mint --role builder --repo owner/name
# Returns a short-lived installation token on stdout.A consumer (e.g. an agent dispatcher) calls gatekeeper mint --role <role> with the role mapped to its agent, then uses the returned token for the git/API operations that role permits.
When a role has the App-slug verification gate configured (app_slug +
app_slug_path, see docs/ROLES.md),
Gatekeeper already reads the App's actual slug from the broker and checks
it against the configured expectation before minting. --json surfaces
that verified value instead of discarding it:
gatekeeper mint --role builder --json
# {"token":"ghs_...","expires_at":"2026-01-01T00:00:00Z","app_slug":"your-builder-app-slug"}app_slug is the broker-verified identity, not a copy of the configured
expectation — a consumer that needs to know which App/bot identity a role
maps to (for example, to attribute a commit or a review to the right bot
login) can inherit this value instead of separately declaring it in its own
configuration. Optional, additive, backward compatible: the default
gatekeeper mint invocation (no --json) is unchanged, and a role with no
App-slug binding configured simply omits app_slug from the JSON object.
Gatekeeper does not require any particular consumer to exist — this is a
plain, documented CLI output shape any tool can read, or ignore.
Copy config.example.yaml to config.yaml and fill in your values. All deployment-specific values — org name, broker endpoint, broker secret paths, role→app bindings — live there. No hardcoded org names, hostnames, paths, or identities exist in the code.
github:
owner: your-org-name
api_base: https://api.github.com
broker:
type: openbao # openbao | vault | env | file
endpoint: https://broker.example.com
auth: approle # approle | token
roles:
builder:
app_id_path: secret/gatekeeper/builder/app-id
installation_id_path: secret/gatekeeper/builder/installation-id
private_key_path: secret/gatekeeper/builder/private-key
# ... reviewer, mergerSee config.example.yaml for the full reference.
Registering a GitHub App requires a one-time manual step — Gatekeeper cannot script first-time App creation.
- Register four GitHub Apps on your org: one each for
builder,reviewer,merger, andsecurity, with the per-role permissions indocs/ROLES.md. - Install each App on the target repos.
- Store each App's
app-id,installation-id, andprivate-keyin your broker at the paths yourconfig.yamlpoints to. - Apply a branch ruleset (see
docs/GOVERNANCE.md) that requires PR + review and restricts who may push the default branch.
After that, everything is code.
Gatekeeper's broker is pluggable:
| Type | Use case | Credentials from |
|---|---|---|
openbao |
Production | BROKER_ROLE_ID + BROKER_SECRET_ID (AppRole) or BROKER_TOKEN |
vault |
Production (Vault) | Same env vars |
env |
Local dev / CI | Env var name is the secret path |
file |
Local dev / CI | File path is the secret path |
The private key is read server-side only, used to sign the App JWT, and never returned, logged, or persisted.
docs/SETUP.md— attested identity, the fail-closed trust model, and configuring your own attestation sourcedocs/ROLES.md— per-role GitHub App permission tablesdocs/GOVERNANCE.md— branch ruleset and CODEOWNERS referencedocs/DESIGN.md— module architecture and security invariants
go build ./cmd/gatekeeperRequires Go 1.22+. No external dependencies beyond the standard library.
make installBuilds the gatekeeper binary and installs it to /usr/local/bin by default. Override the destination with PREFIX and/or DESTDIR — no paths are hardcoded to any specific environment:
# Install under your home directory instead of /usr/local
PREFIX=$HOME/.local make install
# Stage into a packaging root without touching the live prefix
make install DESTDIR=/tmp/staging PREFIX=/usrIf clagentic:gatekeeper is useful to you: ko-fi.com/clagentic
Not affiliated with Anthropic or OpenAI. Claude is a trademark of Anthropic. Codex is a trademark of OpenAI. Provided "as is" without warranty. Users are responsible for complying with their AI provider's terms of service.
FSL-1.1-MIT — Functional Source License 1.1, with MIT as the Change License.
Free for personal, internal-business, evaluation, research, and non-commercial use. Not free for offering this tool (or a substantial fork) as a competing commercial product. Each release auto-converts to MIT on its second anniversary.
