Skip to content

Repo policy: the defaults every front door applies

Work reaches hangar through many doors: MCP, REST, the CLI, GitHub issues, Slack, inbound webhooks and schedules. A repo policy is one record per repository with the defaults its jobs get, whichever door they came through. A new kind of intake only has to name the repo.

hangar repo set acme/app --verify "uv run pytest -q" --env python
hangar repo set acme/app --model frontier            # every harness: its provider's strongest model
hangar repo set acme/app --model claude-code=balanced --model codex=gpt-6.1-sol
hangar repo list
hangar repo get acme/app            # the policy, and the environment it falls back to
hangar repo unset acme/app model    # clear one field
hangar repo unset acme/app          # remove the policy
hangar repo set acme/app --job-max-cost 5 --job-max-turns 12 --max-usd-per-day 60
hangar repo set acme/app --land auto_after_approval --approver alice --risk-path 'infra/**'   # docs/landing.md
hangar repo set acme/app --ai-review hangar --self-approval low_risk_with_ai_review       # AI review, self-approval
hangar repo set acme/app --branch-template 'feature/{ticket_key}-{job}' --require-ticket \
  --title-template '{ticket_key}: {title}'                                               # company naming
hangar repo check acme/app          # is it ready for agent work? (below)
Field What it sets
verify The repo's check (see jobs §7). "" means no check, even if the environment has one.
environment The environment jobs run in when they don't name one. For a repo in HANGAR_GITHUB_REPOS, it must be one of the environments listed there.
harness The default harness. It must be enabled in the environment; when a job runs in an environment without it, the job uses the environment's default and its events say why.
model One model id, or one per harness ({"claude-code": "opus", "codex": "gpt-5.5"}), as in an environment's model:. A single id applies only to the policy's harness, or, without one, to jobs that didn't pick a harness: a model id means nothing to another harness. A policy (frontier, balanced) applies to every harness and is resolved to a concrete id when the job starts (harnesses.md).
budget The default per-turn budget (max_minutes, max_tokens, max_cost_usd), field by field under the job's own.
job_budget The default job budget (max_cost_usd, max_tokens, max_turns).
max_usd_per_day The repo's daily spend cap.
credentials Whose login the repo's jobs run with: person (the person the job runs for; a job for no one fails), team, or person_then_team (the default: theirs when they have one, else the team's). See team.md.
egress {mode, presets, allow} for the repo's sandboxes: adds presets and hosts to the environment's allowlist, and can tighten its mode but never widen it (hangar repo set acme/app --egress-mode none --egress-allow docs.example.com). See egress allowlists.
build_secrets {SECRET_ID: credential} for the repo's environment builds, winning over the environment's (admins; PUT /api/repos/policy). See build secrets.
land auto_after_approval: an approval of the PR's head by an approver or code owner, with green required checks, lands it (landing.md). manual or unset: a person merges.
approvers GitHub logins whose approval lands the repo's agent PRs (--approver, repeated).
merge_method squash (default), merge or rebase, for PRs hangar lands.
risk_paths Globs (infra/**) that make a PR high risk in its review packet, on top of .github/** and .hangar/**.
diff_budget_lines Changed lines (insertions plus deletions) above which the review packet warns that the PR is big (--diff-budget-lines). Unset: no warning.
require_stale_dismissal false lets hangar arm auto-merge on a base branch that doesn't dismiss stale approvals on a push. Unset (the default) it refuses (landing.md).
verify_reports The JUnit, Cobertura or LCOV files verify writes, relative to the repo, **/ for any depth (--verify-report 'build/**/TEST-*.xml', repeated). The repo's .hangar/environment.yaml verify_reports comes first. Most runners need none: see jobs §8.
rerun_flaky true: a check whose every failure is on the repo's flaky list runs once more, and the rerun decides (--rerun-flaky). Unset: a failure stands.
heavy Docker on the heavy tier, deny by default. on: the repo's sandboxes run on it, with Docker inside (--heavy on). allow: the repo's .hangar/environment.yaml may ask for it (heavy: true or tier: heavy), and the request is honored (--heavy allow). deny, or unset: a request in the repo's file isn't honored; the sandbox runs on the default tier, and the agent's instructions and hangar-agent env doctor say Docker isn't enabled for the repo and give this command. A heavy environment (an admin's heavy: true) has Docker whatever this says. true and false, sent by an older client, mean on and deny.
profile standard, large or xlarge: the size of the repo's sandboxes (profiles). Large and xlarge run on the heavy tier.
max_profile The largest profile a job (label hangar.profile, hangar job submit --profile) or the repo's file may ask for. Unset: no more than profile or the environment's own.
self_approval Whether the job's own person may be its PR's only approver (landing.md): low_risk_with_ai_review (unset: the default; a low-risk PR with an independent AI review of the approved head), never, or any (--self-approval). It never loosens the taint gate: a tainted PR always needs someone other than its person.
ai_review The AI first-pass review of the repo's agent PRs: hangar (a review job on a different harness from the author's), check:<name> (an org review bot's check run or status on the head) or off (unset) (--ai-review).
ai_review_harness The reviewing harness. Unset: codex for a claude-code PR, else claude-code (--ai-review-harness).
ai_review_model The reviewer's model or policy. Unset: frontier, resolved to a concrete id when the review is submitted (--ai-review-model).
ai_review_budget_usd What one review may spend, its confirmation pass included. Unset: $1.50 (--ai-review-budget).
ai_review_inline_max The most inline comments one review posts, most severe confirmed findings first. Unset: 5; at most 20 (--ai-review-inline-max).
self_review With ai_review: hangar, the PR opens as a draft and waits for its review (ai-review.md). A blocking one starts one auto-fix round first, inside the job budget. Unset: false (--self-review).
max_open_agent_prs Review capacity: open agent PRs awaiting a person before their new jobs on this repo wait as awaiting_capacity; 0 turns it off. Unset: HANGAR_PERSON_MAX_OPEN_AGENT_PRS (5) (--max-open-agent-prs).
verify_allowed Globs (make test*, pytest tests/*) a ticket's own Verify command (agent tickets) must match to become that job's verify (--verify-allowed, repeated). Unset: a ticket's command is only ever a suggestion to the agent, and the job's check stays the policy's or the environment's. A matched command is the job's own check (verify_source: job), so the base check is skipped for it, as for any acceptance test a job brings. * alone is refused.
overnight true: the repo's intake jobs (Jira, GitHub, Slack) run at night in the overnight lane instead of waiting for 10:00 (--overnight; gke.md, #186). A job from any repo can ask for it with the label lane: overnight.
branch_template The repo's job branches, e.g. feature/{ticket_key}-{job}. It must contain {job}, or the set is refused and nothing changes. Unset: hangar/<job> (--branch-template; '' clears it). See change sets.
require_ticket true refuses a job for the repo without a ticket (jira:PAY-412) (--require-ticket, --no-require-ticket).
title_template The title of the repo's agent PRs, e.g. {ticket_key}: {title} (--title-template; '' clears it). Placeholders: {job}, {ticket}, {ticket_key}, {title}, {role}.
best_of_n {sequential, escalate_at, max_n} for best-of-N jobs. Attempt 1 runs alone first, may use escalate_at of the job budget, and max_n caps attempts per risk tier. See sequential best-of-N.

The repo is owner/repo for GitHub (any URL form, any case), or the git URL for another host.

Precedence

For each field: the job, then the repo's own .hangar/environment.yaml, then the repo's policy, then the environment, then hangar's settings.

  • A job's own verify, environment, harness or model always wins.
  • A repo's .hangar/environment.yaml can set its verify and, with extends, its environment (within the allowlist). hangar uses the file as the default branch had it at the last clone. See environments.md.
  • An environment can carry a verify: too, the fallback for every repo that runs in it:
name: python-app
extends: python
verify: uv run pytest -q
  • verify: "" at any level means "no check" and stops the fallback.
  • The job's events say what came from the policy (from the repo policy for acme/app: verify …), and verify_source on the job says where its check came from: job, repo or environment.

HANGAR_GITHUB_REPOS stays the allowlist: which repos untrusted and service callers (GitHub, Slack, webhooks, jobs tokens) may start work on, and in which environments. A policy never widens it.

REST

Admin tokens only. The repo is a query or body field, because git URLs carry slashes and colons.

Route
GET /api/repos Every policy, each with allowed_environments (from HANGAR_GITHUB_REPOS), the environment's own environment_verify, and the effective_verify.
GET /api/repos/policy?repo=acme/app One policy.
PUT /api/repos/policy {"repo": "acme/app", "verify": "pytest -q"}: sets the fields sent and keeps the rest; a field sent as null is cleared. A policy no job could run with is refused: an unknown environment, an environment the allowlist doesn't have for the repo, or a harness the environment doesn't enable.
DELETE /api/repos/policy?repo=acme/app[&field=verify] Clears the fields named, or removes the policy.
GET /api/repos/{owner}/{repo}/readiness The readiness report for a GitHub repo, the same object hangar repo check --json prints.

Policies are stored in hangar's database, written durably like hangar config settings, and apply to the next job without a restart. Every change is audited (repo.policy.set, .unset, .delete).

Agent scratch files

Delivery commits what the agent left, and agents leave notes. hangar keeps these out of the PR, with a narrow rule so it never drops work someone asked for. A file is scratch when all of these hold:

  • it is new on the job's branch (a file the base commit has is never touched);
  • it is HANDOFF.md, RESULT.md or NOTES.md at the repo root (any case), or a *.orig or *.rej patch leftover anywhere: one next to the file it was made from (a.py.orig beside a.py), so a golden file that only shares the suffix is delivered;
  • the task doesn't name it: neither the instructions, any follow-up, nor the context attached to the job mention the file name. Issue and ticket intake put the request in context files, so a file the issue asks for is delivered.

Scratch the agent left uncommitted isn't staged. Scratch it committed itself is removed in a separate commit, hangar: drop agent scratch files, so the change is visible and easy to revert. Either way the file stays in the sandbox, the job's events and result.dropped_files list it, and the PR body says what was left out.

Moved from this page

Section Now on
Is this repo ready? Is this repo ready?
The check on the base commit Is this repo ready?
Budgets Budgets and shadows
The eval budget Budgets and shadows
Shadows Budgets and shadows
Offline multi-repository release observations Offline multi-repository release observations