How GitHub Fork Routing Works for Pull Requests with Different Upstream and Fork URLs

Fork routing in no-mistakes separates the push destination (your fork) from the PR target (upstream), allowing you to push branches to a personal fork while opening pull requests against the parent repository using --head <fork-owner>:<branch>.

When contributing to open-source projects, developers typically work with two distinct repository URLs: the upstream original and their personal fork. The no-mistakes CLI tool implements sophisticated GitHub fork routing that keeps these URLs distinct during the contribution lifecycle, ensuring pushes go to your fork while pull requests target the upstream repository.

Understanding Fork Routing Architecture

The Two-URL Model

When you initialize a repository with no-mistakes init --fork-url <url>, the system stores two critical values in its internal database (internal/db/repo.go):

  • upstream_url: The parent repository URL (where your origin remote points)
  • fork_url: Your personal fork URL where you have write access

This separation enables a workflow where the upstream repository remains read-only for direct pushes, while your fork serves as the writable staging area for branches.

Database Storage Structure

The repository metadata is persisted with distinct fields for each URL type. According to the source code analysis, the initialization process explicitly records both endpoints to enable later routing decisions in the pipeline steps.

How the Push Step Selects the Target Remote

The routing logic lives in internal/pipeline/steps/host.go within the buildHost function. When constructing the GitHub host configuration, the code checks for the presence of a fork URL:

// internal/pipeline/steps/host.go – buildHost for GitHub
if sctx.Repo.ForkURL != "" {
    // forkRepo is only used to extract the fork owner for --head owner:branch;
    // the plain slug (without host prefix) is correct here.
    forkRepo = github.RepoSlug(sctx.Repo.ForkURL)
}
return github.NewWithFork(..., host, repo, forkRepo), ""

When fork_url is configured:

  • Push target: Becomes repos.fork_url (your fork)
  • PR base repository: Remains repos.upstream_url (the parent)

Without fork routing, the push target defaults to repos.upstream_url, which requires direct write access to the parent repository.

How the PR Step Maintains Upstream as the Base

The PR creation workflow handles the split URL architecture through specific gh CLI flag combinations. As documented in docs/src/content/docs/reference/pipeline-steps.md, the PR step:

  1. Keeps --repo pointed at the parent repository (derived from upstream_url)
  2. Checks for existing PRs using the bare branch name
  3. Filters matching PRs by head owner to avoid collisions
  4. Creates new PRs with --head <fork-owner>:<branch>

This approach ensures the pull request's base is the upstream repository while the head points to the specific branch in your fork. The gate-model.md documentation clarifies that origin remains the parent reference throughout the process, while fork_url is used exclusively for branch pushes.

Practical Implementation Walkthrough

Initialize a repository with fork routing enabled:


# 1️⃣ Initialise a repo with a fork URL

no-mistakes init --fork-url https://github.com/alice/no-mistakes-fork.git

# → stores upstream_url = https://github.com/kunchenguid/no-mistakes.git

#   and fork_url    = https://github.com/alice/no-mistakes-fork.git

Push your feature branch to the fork:


# 2️⃣ Push a branch (the push target is the fork)

git push no-mistakes my-feature

# → the Push step uses repos.fork_url as the remote target

#   (see internal/pipeline/steps/host.go lines 41‑46)

Create or update the pull request against upstream:


# 3️⃣ Create or update a PR (base stays upstream, head is the fork)

no-mistakes axi pr create

# Internally the PR step runs:

#   gh --repo kunchenguid/no-mistakes pr create \

#       --head alice:my-feature   # fork owner + branch

#   (see docs/reference/pipeline-steps.md, PR section)

Verify your configuration:


# 4️⃣ Verify the stored URLs (optional)

no-mistakes repo list

# Output includes:

#   upstream_url: https://github.com/kunchenguid/no-mistakes.git

#   fork_url:     https://github.com/alice/no-mistakes-fork.git

Summary

  • Fork routing requires configuring both upstream_url and fork_url during repository initialization.
  • The Push step in internal/pipeline/steps/host.go routes branches to fork_url when present, falling back to upstream_url otherwise.
  • The PR step maintains the upstream repository as the base while formatting the head reference as <fork-owner>:<branch>.
  • This architecture prevents accidental writes to upstream while preserving the standard GitHub fork contribution workflow.

Frequently Asked Questions

What happens if I don't specify a fork URL during initialization?

Without a configured fork_url, no-mistakes defaults to using upstream_url for both pushes and pull requests. This requires direct write access to the upstream repository and follows the standard single-remote workflow.

How does the PR step differentiate between my fork and other contributors' forks?

The PR step filters existing pull requests by matching the head owner against your configured fork_url. This ensures you only interact with PRs originating from your specific fork when checking for existing branches, preventing collision with similarly named branches from other contributors.

Can I change the fork URL after initial setup?

Yes, you can update the fork_url stored in the database by re-running the initialization command with the --fork-url flag. The internal/db/repo.go implementation supports updating these configuration values to accommodate repository transfers or fork recreation scenarios.

Why does the push target change but the PR base stays the same?

This separation enforces the principle of least privilege. The upstream repository (origin) often requires maintainer-level permissions to push directly, while your fork grants you full write access. By pushing to the fork but opening PRs against upstream, you preserve the code review workflow while ensuring the upstream repository remains protected from accidental direct pushes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →