Stacked PRs on GitHub with Jujutsu
n July 30, 2026, GitHub put stacked pull requests into public preview. Stacked PRs are an ordered chain of PRs where each targets the one below it, reviewable in parallel and mergeable in a single operation. The stacked-diff community (Graphite, Sapling, git-branchless) has built tooling around this for years; now it's available on GitHub repos. If you already primarily use jj, the experience of PRs on GitHub works better with the concept of stacking changes in jj. GitHub's "stack" is just the chain of PR base branches.
This post is for people who are fluent in jj and git already and want an example of using GitHub’s new feature in their existing workflow. If you are unfamiliar with jj I'd recommend starting with Steve Klabnik's tutorial and then coming back to this article. I'm going to focus on integrating these two tools and pitfalls to avoid along the way. I’ve been lucky enough to test this feature out over the past month or so in private preview, so hopefully my time figuring out a workflow can give you a head start. Setup assumed throughout: colocated repo, jj and gh available, origin on GitHub.
The operating model
GitHub's stacked PRs have no separate metadata layer for the graph. A stack is the chain of base branches: the bottom PR targets main, and each PR above targets the branch of the PR below it. GitHub adds a stack map to the top of each PR (so reviewers see where a layer sits) and a merge operation that lands a PR plus every unmerged layer beneath it at once — but the structural relationship is nothing more than "PR B is based on branch A." That's the whole reason this is painless from jj: all that’s really happened is GitHub has gained the ability to visualize what jj already does.
Installation
GitHub's Stacked PRs feature ships as an official gh extension:
gh extension install github/gh-stackIt backs the web, mobile, and Copilot experiences too; the CLI is just one front end.
The division of labor
jj and gh stack both know how to rebase. In a colocated repo, that's the one thing that can trip you up: if gh stack rewrites branches behind jj's back, jj re-imports them as new commits and you get divergent change IDs and a confused graph from two rebase engines fighting over the same commits.
The jj-safe surface of gh stack is small and deliberate: link, view, and merge. Everything else (init, add, sync, rebase, modify, submit, the navigation verbs) either runs a git rebase or creates local tracking branches — both of which fight jj. More on that below
Building and publishing a stack
Build the chain in jj exactly as you always would — a bookmark per logical slice — then hand the whole list to gh stack link. This is GitHub's own documented jj workflow:
jj new main -m "feat(config): parse and validate config"
jj bookmark create change1 --revision @
jj new -m "feat(auth): JWKS validator + auth layer"
jj bookmark create change2 --revision @
jj new -m "feat(dataplane): live data source"
jj bookmark create change3 --revision @Then link them bottom to top:
gh stack link change1 change2 change3That one command pushes the branches, opens a PR for each one with the correct base, and links them into a stack on GitHub. Crucially for us: it creates no local tracking state: it only talks to the GitHub API. That's what keeps jj the single source of truth. (It's the command GitHub explicitly recommends for jj, Sapling, and git-town users precisely because of this.)
Useful flags:
gh stack link --base develop --open change1 change2 change3
# └ alternate trunk └ open as ready, not draftRead-only, safe any time:
gh stack view # the stack + PR links; --json / -s for scriptingIf you prefer
jjto own the push explicitly,jj git push --allfirst;gh stack linkwill then just create/link the PRs over the already-pushed
refs. Either order is fine — the point is thatlinknever creates local
branches.
Working in the stack
This is the part that's nicer than any git-branch stack. A reviewer asks for a change to the bottom PR:
jj edit change1 # working copy moves into the bottom commit
# ...fix it; auto-amends...jj does its thing and automatically rebases the stack as needed, along with its conflict handling so the workflow you're used to stays the same. Re-push the changes to keep GitHub in sync:
jj git push --bookmark 'glob:change*'The PRs update in place. The stack linkage survives untouched, because the branch names and bases never changed, only their tips moved. You never ran a gh stack rebase; jj did all the history work and GitHub just saw new tips.
Adding a layer later
Re-run link with the full list, referring to the existing PRs by number and the new bookmarks by name:
gh stack link 123 124 125 change4 change5Merging
Two behaviors worth knowing, both new and both server-side:
- Merge the top (or any intermediate layer) and GitHub lands that PR and every unmerged layer below it in one operation.
- Merge a lower layer on its own and the PRs above it automatically rebase and retarget — GitHub re-points the next PR's base to
main(or to the new bottom) for you. No local rebase, no manual retargeting.
You can drive the merge from the web UI or gh stack merge. After a merge, sync your local jj view the normal way — jj git fetch then jj rebase -d main@origin for anything still in flight. (Let jj do that rebase. Not gh stack rebase.)
I personally drive merges from GitHub's UI and then jj git fetch and jj rebase -d main@origin.
Pitfalls to avoid in a colocated jj repo
The extension is built for people managing the stack with native git. Those commands assume they own your local branches and history — which is jj's job. Given this, you should avoid the following commands:
gh stack init/gh stack add— create and locally track branches
(andinitenablesgit rerere), duplicating yourjjbookmarks.gh stack submit— pushes via that local tracking; overlapsjj git push.
It's the counterpart toinit, not tolink.gh stack sync/gh stack rebase/gh stack modify— these run
cascading git rebases. This is the one that actually corrupts yourjj
graph. Always reshape withjj rebase -d main@origininstead.gh stack unstackwithout--local— deletes the stack on GitHub.
Know which you mean.
Rule of thumb: if a gh stack subcommand would change your local branches or history, jj should be doing it. link, view, and merge are the ones that only ever touch GitHub.
Does it hold up under real work?
Short answer, yes. It's been refreshing to see GitHub add new capabilities to its UI adopting some modern VCS paradigms. My team has been using it in private preview for about a month to manage significant work with little unexpected friction. I will caveat that since this is a new feature, agents don't know about it. If you're one to let them manage your VCS interactions, you'll need to do some extra prompting to make them aware.
That isn't to say the whole experience is sunshine and roses. It's still a preview feature. Public preview, but still preview. It could use some polish. My biggest complaint is conversations can get hard to follow with how much force-pushing happens in this workflow. There's also still little in the way of interdiff reviewing, but if you really care about that or it is integral to your workflow you'd probably be better off using Gerrit or Phorge.
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.