# GitHub Stacked PRs: a practical guide to gh stack

> Learn GitHub Stacked PRs with gh stack: design focused layers, submit the chain, handle review feedback, rebase safely, and merge.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-07-25 | Topics: [Git](https://flaviocopes.com/tags/git/) | Canonical: https://flaviocopes.com/github-stacked-prs/

GitHub Stacked PRs let you split one large change into several small pull requests that depend on each other.

Each pull request stays focused. Reviewers see one layer at a time. You can still merge the whole feature as one operation.

This solves a problem I often see with large features: the code is ready, but the pull request is too large to review well.

Instead of opening one 2,000-line pull request, you might open 3 smaller ones:

- a data model
- an API built on that model
- a user interface built on that API

The pull requests form a chain. GitHub understands that chain, shows it in the UI, runs checks for every layer, and helps keep all the branches up to date.

In this tutorial we'll build one stack from start to finish. We'll also change a branch in the middle, resolve a rebase conflict, and see what happens when we merge only part of the stack.

> GitHub Stacked PRs is in private preview as I write this. Your repository must have the feature enabled. You can [join the waitlist](https://gh.io/stacksbeta), and you should expect some commands or details to change before the final release.

## The problem with one large pull request

Imagine we're adding user search to an application.

The feature needs 3 parts:

1. types and data access
2. an API endpoint
3. a search interface

We could put everything on one branch:

```text
main
  └── user-search
```

That gives us one large pull request.

The reviewer must understand the data layer, API, and interface at the same time. A small concern about the data model can invalidate comments already made on the UI.

We could also create 3 independent branches from `main`:

```text
main
  ├── user-model
  ├── search-api
  └── search-ui
```

But this does not model reality. The API needs the data model, and the UI needs the API.

The branches are not independent.

A stack represents that dependency:

```text
main
  └── user-model
        └── search-api
              └── search-ui
```

The first branch starts from `main`. The second starts from the first. The third starts from the second.

This is the central idea behind stacked pull requests.

## A stack is both cumulative and divided

There are two useful ways to look at a stack.

Git sees cumulative branches. The `search-ui` branch contains the model, API, and UI because it descends from both lower branches.

The pull request interface shows divided changes:

```text
PR #1: main       → user-model
PR #2: user-model → search-api
PR #3: search-api → search-ui
```

PR #2 shows only what changed between `user-model` and `search-api`. It does not show the model again.

PR #3 shows only the UI layer.

This gives the reviewer small diffs without pretending the changes are unrelated.

## What GitHub adds to the old stacked-branch workflow

Developers have stacked branches for years. You can do it with regular Git commands and ordinary pull requests.

GitHub's feature adds a first-class **Stack** object around those PRs.

That gives us a few important things:

- a stack map in the pull request header
- navigation between every PR in the stack
- branch protection and CODEOWNERS checks based on the final target branch
- GitHub Actions runs for every layer as if it targeted the final branch
- a cascading rebase for the entire chain
- partial or full stack merges from one merge action
- stack data in the REST API, GraphQL API, and webhooks

The branches and pull requests are still normal Git branches and PRs. The extra stack relationship tells GitHub how they belong together.

This difference matters. A chain of PR base branches is the Git structure. The Stack object is the GitHub feature built around it.

## Before you start

You need:

- Git 2.20 or later
- GitHub CLI 2.0 or later
- a GitHub repository you can push to
- Stacked PRs enabled for that repository

If you are new to the basics, read my [Git guide](https://flaviocopes.com/git/) and [first GitHub pull request tutorial](https://flaviocopes.com/how-to-make-first-github-pull-request/) first.

Check that GitHub CLI is authenticated:

```bash
gh auth status
```

Then install the official extension:

```bash
gh extension install github/gh-stack
```

You can use `gh stack` for every command. The extension can also create a shorter `gs` command:

```bash
gh stack alias
```

I'll use the full command in this tutorial. It makes each example easier to recognize.

## Create the first layer

Start from an updated `main` branch:

```bash
git switch main
git pull --ff-only
```

Now initialize a stack and name its first branch:

```bash
gh stack init user-model
```

This creates and checks out `user-model`. It also records the new stack in `.git/gh-stack`.

That file lives inside `.git`, so it is local metadata. It is not committed to your repository.

The command also enables Git's `rerere` feature. `rerere` means “reuse recorded resolution.” If the same conflict appears during several cascading rebases, Git can remember how you resolved it.

Let's pretend we now add the user model and its tests. Commit the first focused change:

```bash
git add src/users.ts tests/users.test.ts
git commit -m "Add user search model"
```

The bottom of our stack now looks like this:

```text
main
  └── user-model
```

This branch should make sense on its own. A reviewer can check the model without thinking about HTTP or interface code.

## Add the API layer

The API depends on the model, so it belongs above `user-model`.

Add a branch to the top of the stack:

```bash
gh stack add search-api
```

`gh stack add` creates the branch at the current `HEAD` and checks it out.

Add the API code and commit it:

```bash
git add src/api/search.ts tests/search-api.test.ts
git commit -m "Add user search endpoint"
```

The stack now has two layers:

```text
main
  └── user-model
        └── search-api
```

Notice that `search-api` contains the model commits too. It must contain them because the endpoint uses the model.

Its pull request will not repeat them. The PR base will be `user-model`, so the diff only contains the API changes.

## Add the interface layer

Let's add one more branch:

```bash
gh stack add search-ui
```

Then add and commit the interface:

```bash
git add src/components/UserSearch.ts tests/user-search.test.ts
git commit -m "Add user search interface"
```

We now have the full chain:

```text
main
  └── user-model
        └── search-api
              └── search-ui
```

This is a good stack because the dependency arrows all point down.

The UI depends on the API. The API depends on the model. Nothing in `user-model` depends on a branch above it.

## Inspect the stack before publishing it

Use `gh stack view` to see the current stack:

```bash
gh stack view
```

You can also ask for a compact list:

```bash
gh stack view --short
```

Or get JSON for a script:

```bash
gh stack view --json
```

I also like to inspect each layer with regular Git before submitting:

```bash
git diff main..user-model
git diff user-model..search-api
git diff search-api..search-ui
```

This is a useful test. If the last command shows changes to the data model, some work may live on the wrong branch.

## Create the pull requests

Submit the stack:

```bash
gh stack submit
```

This command pushes all branches, creates or updates the pull requests, and links them as a Stack on GitHub.

It also gives every PR the correct direct base:

```text
user-model → main
search-api → user-model
search-ui  → search-api
```

The command opens an editor for the PRs. You can accept the generated titles, write clearer ones, and toggle each new PR between ready and draft.

For a non-interactive submission, use generated titles:

```bash
gh stack submit --auto
```

New PRs created by `--auto` are drafts by default. Add `--open` when you want them ready for review:

```bash
gh stack submit --auto --open
```

If you only want to push the branches, without creating or updating PRs, run:

```bash
gh stack push
```

The distinction is useful:

- `push` updates branches
- `submit` updates branches, PRs, and the GitHub Stack
- `sync` fetches, rebases, pushes, and reconciles local and remote state

## How reviewers should read a stack

A stack tells a story from the bottom up.

For our example, review in this order:

1. `user-model`
2. `search-api`
3. `search-ui`

The lower layers establish concepts used above them. Starting from the UI would force the reviewer to guess how the API and model work.

GitHub shows a stack map at the top of every PR. The reviewer can jump between layers without returning to the repository's PR list.

Each review is still independent. A reviewer can approve the model, request changes on the API, and leave the UI untouched.

My advice is to write each PR description for its own layer. Mention the larger feature, but explain exactly what that PR adds and what it deliberately leaves for the next one.

## Change a branch in the middle of the stack

This is where stacked PRs usually become tedious without tooling.

Suppose a reviewer asks us to change the API while we're working on `search-ui`.

Move down one branch:

```bash
gh stack down
```

You can also select the branch by name:

```bash
gh stack checkout search-api
```

Make the requested change where it belongs:

```bash
git add src/api/search.ts
git commit -m "Validate empty search queries"
```

Now `search-ui` is still based on the old tip of `search-api`. The history is no longer one clean chain.

Rebase every branch above the current one:

```bash
gh stack rebase --upstack
```

Then push the rewritten branches:

```bash
gh stack push
```

`gh stack push` uses `--force-with-lease` and an atomic push. The lease stops you from overwriting unexpected remote work, and the atomic push means all branch references update or none do.

This is the daily rhythm of stacked development:

```text
change the correct layer
        ↓
rebase the layers above it
        ↓
push the updated stack
```

Do not put an API fix on the UI branch just to avoid a rebase. That makes the UI diff harder to understand and defeats the point of the stack.

## Rebase the entire stack on main

While we work, other changes keep landing on `main`.

Update the entire chain with:

```bash
gh stack rebase
```

This performs a cascading rebase. GitHub starts at the trunk and works upward:

1. rebase `user-model` onto the latest `main`
2. rebase `search-api` onto the new `user-model`
3. rebase `search-ui` onto the new `search-api`

Then push:

```bash
gh stack push
```

You can also run the all-in-one command:

```bash
gh stack sync
```

`sync` fetches remote changes, updates the trunk, rebases the stack, pushes it, updates PR state, and reconciles the local stack with GitHub.

Use `rebase` when you expect to resolve conflicts interactively. Use `sync` for the routine case where you want everything brought up to date.

## Resolve a cascading rebase conflict

A conflict in a lower layer can affect every layer above it.

Run the rebase as usual:

```bash
gh stack rebase
```

If Git stops, open the reported files and resolve the conflict markers.

Stage the resolved files:

```bash
git add src/users.ts
```

Then continue:

```bash
gh stack rebase --continue
```

The command continues through the remaining branches.

If the rebase is going in the wrong direction, abort it:

```bash
gh stack rebase --abort
```

The extension restores all branches to their pre-rebase state.

This is also where the automatically enabled `rerere` option helps. A resolution recorded on one branch can be reused when a similar conflict appears higher in the chain.

## Rebase locally or on GitHub

GitHub also provides a **Rebase Stack** button in the pull request UI.

The server rebases every unmerged branch and force-pushes the result. This is convenient when the rebase is clean.

There are two reasons to prefer the CLI:

- local rebases let you resolve conflicts in your editor
- local rebases follow your Git commit-signing configuration

The server-side rebase is unavailable when conflicts need manual resolution. It also keeps the original author but sets the person who clicked the button as committer, and those new commits are not signed.

If your rules require signed commits, rebase locally.

## Merge the whole stack

Once every layer is approved and passing its checks, merge the top PR.

GitHub merges that PR and every unmerged PR below it, from bottom to top.

For our stack, merging `search-ui` lands all 3 PRs:

```text
user-model → search-api → search-ui
```

GitHub supports all 3 merge methods:

- **squash merge** creates one squashed commit per PR
- **rebase merge** replays all commits onto the target branch
- **merge commit** creates one merge commit for the group and preserves the branch commits

The squash behavior is particularly useful. A stack of 3 PRs becomes 3 focused commits on `main`, not one giant squash commit.

GitHub describes a stack merge as one atomic operation. There is a small operational nuance: its FAQ says an unexpected failure partway through can leave the lower PRs already merged while the failed PR and higher PRs remain open. Pre-merge checks reduce this risk, but there is no rollback of PRs that already landed.

## Merge only part of the stack

You do not have to merge everything.

Suppose the model and API are ready, but the UI needs more work. Merge the `search-api` PR.

GitHub lands:

```text
user-model → search-api
```

The `search-ui` PR stays open.

GitHub then retargets the remaining PR to `main` and rebases its unique commits onto the updated trunk.

You cannot merge a middle PR while leaving a lower PR unmerged. A PR always brings every unmerged layer below it.

This rule follows the dependencies. The API cannot land without the model it uses.

After a partial merge, update your local stack:

```bash
gh stack sync --prune
```

This syncs the remaining branches and removes local branches for merged PRs.

Once the entire stack is merged, it is complete. You cannot extend that finished Stack object. If you add new branches locally and submit them, the CLI starts a new stack from the trunk.

## How squash merges avoid fake conflicts

Squash merging normally creates a problem for dependent branches.

Imagine the bottom PR contains commits `A` and `B`. The next branch contains `A`, `B`, `C`, and `D`.

After a squash merge, `main` contains a new commit `S1` instead of `A` and `B`:

```text
before: main ← A ← B ← C ← D
after:  main ← S1
```

A naive rebase might try to replay `A` and `B` even though their changes are already inside `S1`. This can produce conflicts that are not real code conflicts. They exist because commit identities changed.

GitHub's stack rebase finds the unique commits and uses `git rebase --onto` to replay only `C` and `D` on top of `S1`.

The remaining branch becomes:

```text
main ← S1 ← C' ← D'
```

This detail is one of the strongest reasons to use stack-aware tooling. The easy part is creating several branches. The hard part is maintaining their meaning after merges rewrite history.

## GitHub Actions and the CI multiplication problem

GitHub evaluates every PR as if it targeted the stack's final base, usually `main`.

A workflow configured for pull requests into `main` runs for every PR in the stack. Required checks and branch protection apply to every layer too.

This closes a common hole in manual stacks, where only the bottom PR triggers the right checks.

It can also multiply CI usage. A stack of 5 PRs might run the same expensive integration suite 5 times.

GitHub exposes stack metadata through `github.event.pull_request.stack`. We can run an expensive job only for standalone PRs or the top PR, which contains the complete feature:

```yaml
jobs:
  integration:
    if: >-
      github.event.pull_request.stack == null ||
      github.event.pull_request.stack.position == github.event.pull_request.stack.size
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration
```

Keep fast checks on every layer. They give reviewers confidence in each focused change.

Reserve the most expensive end-to-end work for the top PR when that tradeoff makes sense for your project.

One subtle detail: GitHub evaluates CODEOWNERS from the stack base. If PR #1 changes `CODEOWNERS`, that new file does not control reviews for PR #2 yet because the change has not landed on the final base.

## Bring existing branches into a stack

You do not have to start with `gh stack init`.

If you already have dependent branches, list them from bottom to top:

```bash
gh stack init user-model search-api search-ui
```

Then submit them:

```bash
gh stack submit
```

Existing PRs are detected and linked when possible.

If you prefer another local stacking tool, keep using it. GitHub supports standard branches and PRs, so tools such as Jujutsu, Sapling, and git-town can manage the local chain.

Use `gh stack link` when you only want to create the relationship on GitHub:

```bash
gh stack link user-model search-api search-ui
```

This does not create local `gh stack` tracking. It pushes branches, creates or finds their PRs, fixes the base branch chain, and links the PRs on GitHub.

## Restructure a stack

Sometimes the first plan is wrong.

Maybe the API PR is too large. Maybe two tiny branches should be one. Maybe a layer belongs lower in the dependency chain.

Open the interactive editor:

```bash
gh stack modify
```

The interface can:

- drop a branch from the stack
- fold one branch into an adjacent branch
- insert a branch
- reorder branches
- rename branches
- preview and undo changes before applying them

The working tree must be clean, and the stack must have a linear history.

After applying the change, update GitHub:

```bash
gh stack submit
```

Restructuring is powerful, but use it early. Reordering layers after detailed reviews have started creates unnecessary work for reviewers.

## Use stacks with AI coding agents

GitHub publishes an agent skill for `gh-stack`:

```bash
gh skill install github/gh-stack
```

You can also install it with the Skills CLI:

```bash
npx skills add github/gh-stack
```

The skill teaches a compatible coding agent how to plan layers, create branches, move through the stack, and rebase after mid-stack changes.

This is a good fit for agent work. An agent can produce a large diff quickly, but a human reviewer still needs understandable boundaries.

The hard part is not asking the agent to “split this into 5 PRs.” The hard part is choosing a dependency order where every layer tells a coherent story.

Review that plan before the agent starts moving commits.

## How to design a good stack

A good stack is not one PR per commit.

Create a new layer when the concern, dependency, or reviewer changes.

Good boundaries include:

- schema before code that reads the schema
- shared types before their consumers
- core logic before delivery code
- API before interface
- implementation before integration tests for the whole feature
- production code before documentation, when docs depend on the final behavior

Keep one effort in one stack. A search UI and an unrelated billing fix should not share a chain.

I aim for every layer to answer one sentence:

> This PR adds ___ so the next layer can ___.

If that sentence needs 3 “and” clauses, the layer is probably too broad.

Also avoid very deep stacks. Small PRs help review, but every layer adds CI runs, branches, rebases, and review state. Three clear PRs are usually better than 12 microscopic ones.

## When not to use a stack

Use a regular pull request when the change is already small and independent.

Do not create a stack when:

- every branch can target `main` independently
- the change takes less time to review than the stack takes to explain
- you need branches across different forks, which GitHub Stacked PRs does not support
- your team is not comfortable with rebasing rewritten branches
- the feature cannot be divided into coherent layers

Stacks are most useful for dependent work that is too large for one review but too connected for independent PRs.

## The commands I would remember

You do not need to memorize the entire CLI.

These commands cover the normal workflow:

```bash
gh stack init user-model   # start the first layer
gh stack add search-api    # add a layer on top
gh stack submit            # push and create/update PRs
gh stack view              # inspect the stack
gh stack up                # move away from trunk
gh stack down              # move toward trunk
gh stack rebase            # cascade changes through the stack
gh stack push              # push every branch
gh stack sync              # fetch, rebase, push, and sync state
gh stack modify            # restructure the stack
```

The mental model matters more than the commands:

```text
foundations at the bottom
dependencies above them
review from bottom to top
change code in the layer where it belongs
rebase everything above that layer
```

Once that feels natural, `gh stack` removes most of the branch bookkeeping.

You can find the latest commands and preview status in the [official GitHub Stacked PRs documentation](https://github.github.com/gh-stack/) and the [github/gh-stack repository](https://github.com/github/gh-stack).
