Landing: an approval lands the PR¶
An agent's PR used to need three things from a person: read it, approve it, then come back after CI and click merge. With landing on, a person reads the PR and approves it. hangar takes it from there, and GitHub's branch protection stays the gate.
- Agent PRs come from a GitHub App (
hangar-agents[bot]), not from a person's token. A PR's author can't approve it, so with the App as author GitHub lets the person who asked for the work approve it. Whether that approval alone lands the PR is the self-approval rule: by default only for a low-risk PR (docs and tests) with an independent AI review of the approved head, never for a tainted PR; otherwise a second reviewer must approve. - An independent AI review comes first (section 6a): a hangar job on another harness than the author's (or an org review bot's check) reviews each agent PR and posts one advisory COMMENT review.
- The control plane, never the agent, arms auto-merge. It does so after a qualifying approval of the PR's current head and green required checks, and only for that commit.
- A push after the approval needs a new review. hangar disarms before any agent turn starts on the job, and GitHub dismisses the stale approval.
- hangar knows when work lands. Each job records its PR's state (
open,approved,merged,closed), when it merged, and how long it took from green checks to merged. A merged or closed PR ends the job's session. - Each agent PR gets a review packet: a comment with the verification, cost, harness and model, files touched, risk and links.
- One inbox lists what needs you:
hangar://inbox,hangar inbox,GET /api/inbox, and reaches you in Slack: a DM for each question or approval, and a digest twice a day.
1. The GitHub App¶
Create it¶
- GitHub → Settings → Developer settings → GitHub Apps → New GitHub App. Create it under the organization if the repos are an org's.
- Name it (the PRs show
<name>[bot]), and set any homepage URL. Turn Webhook off: hangar's repo webhook already delivers what it needs (see the webhook for using the App's instead). - Repository permissions, exactly these:
| Permission | Access | What hangar does with it |
|---|---|---|
| Contents | Read and write | push agent branches (through the git gateway); merge a PR that is already mergeable (PUT /pulls/{n}/merge); read CODEOWNERS and the base branch's required checks (GET /contents/…, GET /branches/{b}) |
| Pull requests | Read and write | open PRs, write and edit their comments (the review packet), read their files and reviews, arm and disarm auto-merge |
| Metadata | Read | added by GitHub to every App; also reads rulesets (GET /rules/branches/{b}) |
Leave everything else at No access, in particular Workflows (without it GitHub refuses pushes that change .github/workflows/), Administration, Issues and Checks.
4. Create it, note the App ID, and Generate a private key.
5. Install App on only the repositories agents work on.
6. Store it, and tell hangar to use it:
jq -n --arg id 123456 --rawfile key ~/Downloads/hangar-agents.*.private-key.pem \
'{app_id: $id, private_key: $key}' | hangar cred add github-app --harness git --kind github_app
rm ~/Downloads/hangar-agents.*.private-key.pem
hangar config set HANGAR_GITHUB_APP_CREDENTIAL github-app
hangar config set HANGAR_GITHUB_APP_SLUG hangar-agents # optional: the digest's review link lists only its PRs
Don't put the App on a bypass list. Leave it out of "Allow specified actors to bypass required pull requests" and out of every ruleset's bypass list. The App can merge (that is what Contents: write allows), and branch protection is what makes a merge wait for a person.
What each identity does¶
| Who | Does what | Token |
|---|---|---|
| The App, for the control plane | opens agent PRs; comments on them (outcome, auto-fix rounds, the packet); arms and disarms auto-merge; merges an approved PR that is already mergeable | an installation token minted per repo with {"contents": "write", "pull_requests": "write"} only, cached in memory until shortly before it expires, never written anywhere |
| The App, for the git gateway | pushes agent branches for sandboxes in credential_mode: gateway with no git credential of their own |
an installation token with {"contents": "write"} only |
The control token (HANGAR_GITHUB_TOKEN_CREDENTIAL) |
comments on issues, reads issue comments, checks team membership (trust and CODEOWNERS teams), reads Actions logs for auto-fix, reads classic branch protection for hangar doctor |
as before |
The App's key is a github_app credential: control-plane only, refused in any sandbox (security/credentials.md). If the App can't act on a repo (not installed there), hangar uses the control token and says so in the job's events.
The permissions above were checked against GitHub's "Permissions required for GitHub Apps" page (October 2026): POST /repos/{o}/{r}/pulls and POST/PATCH issue comments need Pull requests: write; PUT /pulls/{n}/merge needs Contents: write; GET /branches/{b} and GET /contents/{path} need Contents: read; GET /rules/branches/{b} needs Metadata: read; GET /branches/{b}/protection needs Administration: read, which the App deliberately lacks.
2. Turn it on for a repo¶
Landing is per repo, in the repo policy, and off by default:
hangar repo set acme/app --land auto_after_approval --approver alice --approver bob --merge-method squash \
--risk-path 'infra/**' --risk-path '**/migrations/**'
| Field | |
|---|---|
land |
auto_after_approval lands on approval; manual (or unset) never arms anything, and the inbox tells the approver to merge |
approvers |
GitHub logins whose approval lands the PR |
merge_method |
squash (default), merge or rebase |
risk_paths |
globs that make a PR high risk in its packet, on top of .github/** and .hangar/** |
require_stale_dismissal |
unset or true: arm only when the base branch dismisses stale approvals (section 3); false (--no-require-stale-dismissal) opts the repo out |
self_approval |
whether the job's own person may be its PR's only approver: never, low_risk_with_ai_review (the default) or any (the old behaviour). See section 4 |
ai_review, ai_review_harness, ai_review_model, ai_review_budget_usd, ai_review_inline_max |
the AI first-pass review: hangar, check:<name> or off (unset). Then the reviewing harness, its model, what one review may spend, and its most inline comments |
max_open_agent_prs |
review capacity: open agent PRs awaiting a person before their new jobs here wait |
Who may approve. A login in approvers, or a code owner of every path the PR touches (a rename's old path included). A file no line owns needs an approver from the policy, so owning one docs file doesn't land changes elsewhere; a PR with more than 300 files needs a policy approver too. CODEOWNERS is read from the base branch (.github/CODEOWNERS, then CODEOWNERS, then docs/CODEOWNERS, as GitHub does). The last matching line wins, and a line with no owners leaves a file unowned. A team entry (@org/team) counts when the control token can see the person's active membership, which needs the org's Members: read. Anyone else's approval is recorded in the job's events and lands nothing. GitHub's own rules still apply on top: with "require review from code owners" on, GitHub waits for a code owner even after an approver's approval.
3. The branch protection to enable¶
hangar arms auto-merge; GitHub decides when it merges. These settings on the default branch make that decision safe. Set them under Settings → Branches (classic protection) or Settings → Rules → Rulesets:
| Setting | Classic protection | Ruleset (pull_request rule) |
Why |
|---|---|---|---|
| Require a pull request before merging, with at least 1 approval | required_approving_review_count ≥ 1 |
required_approving_review_count ≥ 1 |
no merge without a person |
| Dismiss stale pull request approvals when new commits are pushed | dismiss_stale_reviews |
dismiss_stale_reviews_on_push |
an approval covers the commit it was given on |
| Require approval of the most recent reviewable push | require_last_push_approval |
require_last_push_approval |
whoever pushed last can't be the one approving it |
| Require status checks to pass | required_status_checks |
required_status_checks rule |
CI decides "green", not hangar's view of it |
| Allow auto-merge (Settings → General) | otherwise GitHub refuses to arm |
hangar won't arm auto-merge on a base branch that doesn't dismiss stale approvals. It disarms on every push it hears of, but a person with write access can push while hangar misses the webhook, and GitHub keeps auto-merge on after such a push: without dismissal it could merge that push on the earlier approval. So before arming, hangar reads the base branch's protection and rulesets (cached for 5 minutes); when neither dismisses stale approvals, or neither can be read, it doesn't arm, and the job's events and hangar doctor (a failure) say why and how to fix it. A repo can opt out with hangar repo set owner/repo --no-require-stale-dismissal, which hangar doctor then reports as a warning.
hangar doctor checks these for every repo with land: auto_after_approval and warns about what else is missing. It reads classic protection with the control token when that token has Administration: read, and rulesets through GET /rules/branches/{b}, which the App can read. It also warns when no App is configured, and when jobs don't use the git gateway. In direct mode a job's sandbox holds a GitHub token that could arm auto-merge itself, so branch protection would be the only gate.
5. The webhook¶
hangar setup github (and hangar doctor --fix) registers these events on each mapped repo: issues, issue_comment, pull_request_review_comment, pull_request_review, check_run, check_suite, status and pull_request, which is new for landing. A hook set up before needs pull_request added; hangar doctor names it.
To use the App's webhook instead of per-repo hooks, point it at https://<hangar>/api/hooks/github with the same secret as HANGAR_GITHUB_WEBHOOK_SECRET_CREDENTIAL, and subscribe to: Pull request, Pull request review, Pull request review comment, Check run, Check suite, Status, Issues and Issue comment. GitHub only offers an event to an App holding read access to it, so this needs three more read-only permissions on top of section 1: Checks: Read (check run, check suite), Commit statuses: Read (status) and Issues: Read (issues, issue comment). Tokens hangar mints keep asking for contents and pull requests only. Use one webhook or the other, not both.
If the pull_request webhook never arrives, a merged PR stays open in hangar's records. When GitHub's webhook doesn't arrive covers the fallback.
What it doesn't cover¶
- Direct-mode sandboxes hold a GitHub token. If that token can write to the repo, the agent can arm auto-merge on its own PR, or approve it as the token's owner. So hangar lands a PR only when its delivery recorded that the job's sandbox pushed through the gateway:
credential_mode: direct, or a mode hangar couldn't read, means merging by hand, and once a job's sandbox was direct it stays that way. The inbox says so. "Only the control plane arms" holds only with the git gateway; branch protection ("approval of the most recent push", since the token's owner pushed it) is what stops the agent's own approval from merging. - Merge queues. With a merge queue on the branch, GitHub queues instead of merging, and ignores
merge_method. - Check results come from webhooks. The App has no Checks permission, so hangar can't list a commit's checks. It learns them from
check_run,check_suiteandstatusdeliveries, and falls back to GitHub'smergeable_statewhen one was missed. - The approval is checked against the PR's head when the review arrives. An approval GitHub accepts on an older commit doesn't count for hangar.
References (checked October 2026)¶
- GitHub REST: Pulls (merge with
sha: 409 when the head moved;auto_merge,mergeable_state), Branches, Branch protection (dismiss_stale_reviews,require_last_push_approval), Rules for a branch (dismiss_stale_reviews_on_push,require_last_push_approval,required_status_checks), Permissions required for GitHub Apps. - GitHub GraphQL:
enablePullRequestAutoMerge(pullRequestId,expectedHeadOid,mergeMethod,commitBody) anddisablePullRequestAutoMerge(mutations). There is no REST route for enabling auto-merge on a PR. - Automatically merging a pull request: people with write access enable it; it is disabled when someone without write access pushes, which is why hangar disarms on every push itself.
- Webhook events and payloads: the permission each event needs for an App, and the
pull_requestactions (closedwithmerged,synchronize,auto_merge_enabled,auto_merge_disabled,ready_for_review, …).
Moved from this page¶
| Section | Now on |
|---|---|
| 4. How a PR lands | How a PR lands |
| Tainted PRs | How a PR lands |
| Agent-drafted ADRs | How a PR lands |
| Self-approval: who may be the only approver | How a PR lands |
| What hangar records | How a PR lands |
| 6. The review packet | The review packet and the inbox |
| 6a. AI first-pass review | The review packet and the inbox |
| 7. The inbox: what needs you | The review packet and the inbox |
| Delivery: it comes to you | The review packet and the inbox |