Skip to content

Four-eyes

Independent review on every change to a protected branch.

Updated View as MarkdownView on Github

Every pull request to a protected branch needs at least one independent approval from someone other than the author. Branch protection enforces this, including for administrators.

Three narrow exceptions apply only to non-production repositories that are approved for them through a four-eyes exemption: trivial changes, incident emergencies, and pair programming. Note each exception in the pull request.

Is my repository production?

Classify a repository by the path it can take, not by whether it has launched.

Your repository Four-eyes What to do
Has or will have a production environment, a production deploy pipeline, or access to production data Required, no exception Follow the review pace below.
Never used in production (sandbox, example, experiment) Exemption possible Request a four-eyes exemption.
Not sure Treat as production Ask your Engineering Navigator.

Request a four-eyes exemption

A repository that is not used in production, and never will be, can request a four-eyes exemption. A repository that might reach production is not eligible.

File the exemption ticket. It is the only route for this request. An exception ADR is not.

The ticket asks for:

  • The requestor’s full name and Slack handle.
  • The team, direct manager, and director, for the approval chain.
  • The repository path, or the group path when the exemption covers an entire group that will never be used in production.
  • The business justification for why four-eyes is not required for the project.
  • Approval from the direct manager and the director, with the observers the ticket names added.
  • The attestation.

Review pace

Reviews should follow this pace:

Pull request Starts Completes Escalate
Standard feature 24 hours 48-72 hours 24 hours, no response
Hotfix (P0/P1) 2 hours 4 hours 2 hours, no response
Security-sensitive 24 hours 72 hours, with Security 48 hours, no progress
Refactoring 48 hours 72 hours 72 hours, no response

The table sets three expectations. Authors are responsible for getting the change merged, finding a qualified reviewer within the hour, following up at 24 hours, and escalating to their EM at 48 hours. Reviewers should treat requests as interrupts, not background work. They should decline immediately if they cannot meet the time window. Engineering Managers are responsible for throughput, must plan review as real capacity, and must act within four hours of an escalation.

Pull request size

Keep pull requests as small as the change allows. Reviews are better when the reviewer can understand the whole diff.

If the authored diff is about 1500 lines or more, split the work into stacked pull requests instead of landing one large change. Do not count generated or machine-written files toward that total: package.json, lockfiles, vendored trees, and generated code. Coding agents follow the same bar. See AI change workflow.

How enforcement is configured

Four-eyes is a control, not a convention. A protected branch must use the settings below. This lets a team verify that a repository complies instead of trusting people to follow the rule. Configure the settings as a GitHub ruleset (preferred) or classic branch protection.

Setting Value What it prevents
Require a pull request before merging on Direct pushes to the protected branch
Required approving reviews at least 1 Merge without independent sign-off
Require review from Code Owners on Approval by an unqualified reviewer
Dismiss stale approvals on push on An approval that predates the merged diff
Require status checks to pass on Merge over a red build
Block force-pushes and deletions on Rewriting or removing protected history
Include administrators on A bypass path for privileged accounts
Require signed commits, linear history on An unattributable or tangled audit trail

NT-NinjaTrader/cloud is the reference implementation. It has three active rulesets (master, the release branch, and sre/apphub-host-develop), each with enforcement set to active. When protecting a new repository, use the same required approving-review count and Code Owner requirement.

The equivalent ruleset excerpt:

{
  "rules": [
    {
      "type": "pull_request",
      "parameters": {
        "required_approving_review_count": 1,
        "require_code_owner_review": true,
        "dismiss_stale_reviews_on_push": true
      }
    },
    { "type": "required_linear_history" },
    { "type": "required_signatures" },
    { "type": "non_fast_forward" }
  ]
}

See GitHub’s protected branches, rulesets, and code owners references. CODEOWNERS identifies the qualified reviewer. See code ownership. Independent approval separates the author from the approver. Audit responsibilities records this as evidence.

What a review must verify

Approval means that a reviewer checked the change, not just that they saw it. A review confirms:

  • Correctness. The change does what the pull request says, and edge cases are handled.
  • Tests. They exist, are meaningful, and cover the new behaviour rather than restating it.
  • Security and authz. The change does not widen access, leak secrets, or trust unvalidated input. See security.
  • Backward compatibility. API, schema, and message contracts stay compatible, or the pull request notes the break.
  • Observability. New paths emit the logs, metrics, and traces needed to operate them. See observability.

Self-approval and stale approvals

An author must never approve or merge their own pull request. The approver must be a qualified reviewer listed by CODEOWNERS, not just any available teammate.

Dismiss-stale-approvals ties each approval to the exact diff that will merge. If new commits are added after an approval, the approval is cleared and the change must be reviewed again. This prevents the approve-then-push-more pattern.

Automated and revert pull requests

Dependabot, Renovate, and automated reverts still need one independent human approval and all required status checks. Automation makes the merge process shorter; it does not remove the second pair of eyes.

Auto-merge is safe because it waits for these gates. It enables the merge only after approvals and checks are satisfied:

# .github/workflows/auto-merge.yml
on: pull_request
permissions:
  contents: write
  pull-requests: write
jobs:
  enable:
    if: github.actor == 'dependabot[bot]'
    runs-on: ubuntu-latest
    steps:
      - name: Enable auto-merge
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_URL: ${{ github.event.pull_request.html_url }}

This uses the pull_request trigger, not pull_request_target. pull_request_target is privileged, runs against the base repository, and could let an untrusted fork branch escalate. Enabling auto-merge only starts the merge process behind the branch-protection gates. The required human approval and checks still apply.

Every P0/P1 hotfix pull request must link its incident ticket. The CODEOWNERS approver requirement stays the same during the expedited window. See release and incidents. The window makes the review faster, but never removes the approval.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close