GitHub Apps skill for Codex

Least-privilege bots with short-lived tokens. Codex designs GitHub Apps from features to permissions, so installers see only what the bot needs. It authenticates per installation with Octokit-managed tokens, verifies X-Hub-Signature-256, answers webhooks within ten seconds and moves work to a queue, posts check runs in batches of 50 annotations, and adds a redelivery job because GitHub never retries failed deliveries on its own.

  • Integrations
  • $github-apps
  • 184 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- github-apps

Installs to ~/.agents/skills/github-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $github-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds GitHub Apps - minimal permission and event selection, app JWTs and installation access tokens, webhook servers with X-Hub-Signature-256 verification, Octokit App and Probot handlers, the Checks API, installation lifecycle events, user access tokens, manifests and local webhook forwarding. Use when creating a GitHub bot or integration installed on orgs or repos, replacing a PAT-based bot with an App, posting check runs or PR comments from a service, debugging Bad credentials or Resource not accessible by integration, or handling installation events.

Use it when

  • Building a bot or integration that orgs and users install
  • Replacing a personal access token bot with scoped app credentials
  • Posting check runs or PR comments from an external service
  • Webhook deliveries fail, time out or trigger loops

Use something else when

How to check the result

On a test repo the dev app handles each delivery ID once, rejects a request with a bad signature, updates records on installation_repositories events, and the redelivery job recovers a forced failure.

The mistake it prevents

Doing slow work inside the webhook handler, so deliveries exceed ten seconds, are marked failed and are never retried automatically.

~/.agents/skills/github-apps/SKILL.md

--- name: github-apps description: Builds GitHub Apps - minimal permission and event selection, app JWTs and installation access tokens, webhook servers with X-Hub-Signature-256… license: MIT ---

Done well means: the app asks for the fewest permissions and events that its features need, authenticates per installation with short-lived tokens, verifies every webhook before acting, answers within 10 seconds and does real work asynchronously, and handles installs, uninstalls and repository selection changes without manual cleanup.

Before writing code

  1. Read AGENTS.md; check for an existing app (app.yml, Probot config, octokit App usage) and how it is hosted (long-running server, serverless, Actions).
  2. Decide App versus alternatives:
NeedChoose
Bot for many repos or orgs, acts as itself, scoped permissionsGitHub App
Act as the signed-in user in your product (their repos, their name)GitHub App user access tokens (or an OAuth App if legacy)
A script you run for yourselfPAT and $github-api
Automation inside one repo on push or scheduleActions with GITHUB_TOKEN
  1. Secrets: GITHUB_APP_ID (or client ID), GITHUB_APP_PRIVATE_KEY (PEM), GITHUB_WEBHOOK_SECRET. Never commit the private key; if it leaks, the user must generate a new key in the app settings and delete the old one.
  2. Creating or changing the app registration, requesting new permissions (installers must re-approve), and any write to real repositories need explicit user confirmation. Develop against a test org or a personal test repository.

Webhook delivery and API calls need network access; local development needs a forwarding service (smee.io or a tunnel), which the user should approve.

Permissions and events

Start from features, not from a list of everything:

FeatureRepository permissionSubscribe to
Comment on PRsPull requests: write (or Issues: write for issue comments)pull_request
Post CI-style resultsChecks: writecheck_suite, pull_request
Read code to analyseContents: readpush
Label or triage issuesIssues: writeissues
Commit statuses onlyCommit statuses: writepush or pull_request

Metadata: read is always included. Avoid Administration and Contents: write unless the feature truly needs them; installers read the permission list before accepting.

Authentication model

private key --sign--> app JWT (RS256, iss = client ID or app ID, exp <= 10 min)
app JWT --POST /app/installations/{id}/access_tokens--> installation token (expires in 1 hour)
installation token --> REST/GraphQL calls as the app on that installation
  • Set iat about 60 seconds in the past to tolerate clock drift.
  • Narrow tokens when possible: the access token request accepts repositories and permissions subsets.
  • Let Octokit do this: it creates JWTs, caches installation tokens and refreshes them before expiry. Do not hand-roll the flow unless there is no maintained library for the language.

Webhook server with Octokit

import { App, createNodeMiddleware } from "octokit";
import http from "node:http";

const app = new App({
  appId: process.env.GITHUB_APP_ID!,
  privateKey: process.env.GITHUB_APP_PRIVATE_KEY!.replace(/\\n/g, "\n"), // env vars often hold escaped newlines
  webhooks: { secret: process.env.GITHUB_WEBHOOK_SECRET! },
});

app.webhooks.on("pull_request.opened", async ({ octokit, payload }) => {
  // octokit is already authenticated as this installation
  await enqueue({ installationId: payload.installation!.id, repo: payload.repository.full_name, pr: payload.pull_request.number });
});

app.webhooks.on("installation.deleted", async ({ payload }) => {
  await db.installations.markRemoved(payload.installation.id);
});

app.webhooks.on("installation_repositories", async ({ payload }) => {
  await db.repos.sync(payload.installation.id, payload.repositories_added, payload.repositories_removed);
});

app.webhooks.onError((err) => console.error("webhook failed", err));

// Verifies X-Hub-Signature-256 before dispatching; default path /api/github/webhooks
http.createServer(createNodeMiddleware(app)).listen(3000);

Worker that does the slow part:

async function reviewPullRequest(job: { installationId: number; repo: string; pr: number }) {
  const octokit = await app.getInstallationOctokit(job.installationId);
  const [owner, repo] = job.repo.split("/");
  const { data: pr } = await octokit.rest.pulls.get({ owner, repo, pull_number: job.pr });
  const run = await octokit.rest.checks.create({
    owner, repo, name: "acme-lint", head_sha: pr.head.sha, status: "in_progress",
  });
  const findings = await lint(octokit, owner, repo, pr);
  await octokit.rest.checks.update({
    owner, repo, check_run_id: run.data.id, status: "completed",
    conclusion: findings.length ? "failure" : "success",
    output: { title: `${findings.length} issues`, summary: summarize(findings), annotations: findings.slice(0, 50) },
  });
}

The Checks API only accepts up to 50 annotations per request; send more with additional checks.update calls. Check runs are only writable by GitHub Apps, not PATs.

Probot wraps the same model (app.on("pull_request.opened", handler), context.octokit) and is a reasonable choice when the project already uses it.

Webhook rules

  • Respond with 2xx within 10 seconds; GitHub marks slower deliveries as failed.
  • GitHub does not automatically redeliver failed deliveries. Build a recovery job that lists failed deliveries (GET /app/hook/deliveries) and redelivers them (POST /app/hook/deliveries/{delivery_id}/attempts), or reconcile from the API.
  • Dedupe on X-GitHub-Delivery. Route on X-GitHub-Event plus payload.action.
  • Ignore events from your own app (payload.sender.type === "Bot" and your app’s slug) to avoid loops when your comments trigger new events.

User access tokens

When the app must act as a user, use the web flow with the app’s client ID and secret. User access tokens expire after 8 hours and come with a refresh token valid for 6 months (when token expiration is enabled). The effective permissions are the intersection of the app’s permissions and what the user can do.

Local development

npx smee-client --url https://smee.io/<channel> --target http://localhost:3000/api/github/webhooks

Point a development app (not the production app) at the smee channel. Replay a delivery from the app’s Advanced settings page to retest a handler.

Rate limits

Installation tokens get at least 5,000 requests per hour, scaling with repositories and users up to 12,500 (higher on Enterprise Cloud). All work for one installation shares that budget, so cache reads and use conditional requests. Secondary limits apply as in $github-api.

Gotchas

  • “Resource not accessible by integration”: the app lacks a permission, the repo is not in the installation’s selection, or installers have not accepted an updated permission request.
  • “Bad credentials” or JWT errors: wrong key, private key newlines mangled, exp more than 10 minutes ahead, or server clock skew.
  • Using the app JWT for repository calls: JWTs only work on /app endpoints; get an installation token first.
  • Hard-coding an installation ID: each org or user that installs the app has its own.
  • Long work inside the webhook handler: deliveries time out and are not retried.
  • Pull requests from forks: the head repo may not be in the installation; read code by SHA from the base repository.

Verification

  • Install the dev app on a test repo, open a PR, and see the handler run once per delivery ID.
  • Send a request with a wrong signature to the endpoint: it returns 400 or 401 and does nothing.
  • Remove a repo from the installation’s selection and confirm installation_repositories updates your records.
  • Run the failed-delivery recovery job against a delivery you forced to fail.

Report template

**App:** acme-lint (dev app: acme-lint-dev), hosted on Fly.io, Octokit App 4.x
**Permissions:** Checks write, Pull requests read, Contents read, Metadata read
**Events:** pull_request, installation, installation_repositories
**Flow:** webhook -> queue -> worker posts check run with up to 50 annotations per call
**Reliability:** dedupe on X-GitHub-Delivery; hourly redelivery job for failed deliveries
**Needs user:** production app registration changes, permission upgrade notice to installers

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "GitHub Apps"
  short_description: "Least-privilege bots with short-lived tokens"
  default_prompt: "$github-apps Turn our PAT-based PR bot into a GitHub App with the minimum permissions"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem