gh-stack gh stack is a GitHub CLI extension for managing stacked branches and pull requests . A stack is an ordered list of branches where each branch builds on the one below it, rooted on a trunk branch (typically the repo's default branch). Each branch maps to one PR whose base is the branch below it, so reviewers see only the diff for that layer. main (trunk) └── feat/auth-layer → PR #1 (base: main) - bottom (closest to trunk) └── feat/api-endpoints → PR #2 (base: feat/auth-layer) └── feat/frontend → PR #3 (base: feat/api-endpoints) - top (furthest from trunk) The bottom of the stack is the branch closest to the trunk, and the top is the branch furthest from the trunk. Each branch inherits from the one below it. Navigation commands ( up , down , top , bottom ) follow this model: up moves away from trunk, down moves toward it. When to use this skill Use this skill when the user wants to: Break a large change into a chain of small, reviewable PRs Create, rebase, push, or sync a stack of dependent branches Navigate between layers of a branch stack View the status of stacked PRs Tear down and rebuild a stack to remove, reorder, or rename branches Prerequisites The GitHub CLI ( gh ) v2.0+ must be installed and authenticated. Install the extension with: gh extension install github/gh-stack Before using gh stack , configure git to prevent interactive prompts: git config rerere.enabled true
remember conflict resolutions (skips prompt on init)
git config remote.pushDefault origin
if multiple remotes exist (skips remote picker)
Agent rules
All
gh stack
commands must be run non-interactively.
Every command invocation must include the flags and positional arguments needed to avoid prompts, TUIs, and interactive menus. If a command would prompt for input, it will hang indefinitely.
Always supply branch names as positional arguments
to
init
,
add
, and
checkout
. Running these commands without arguments triggers interactive prompts.
When a prefix is set, pass only the suffix to
add
.
gh stack add auth
with prefix
feat
→
feat/auth
. Passing
feat/auth
creates
feat/feat/auth
.
Always use
--auto
with
gh stack submit
to auto-generate PR titles. Without
--auto
,
submit
prompts for a title for each new PR.
Always use
--json
with
gh stack view
.
Without
--json
, the command launches an interactive TUI that cannot be operated by agents. There is no other appropriate flag — always pass
--json
.
Use
--remote
You're on feat/data-models with several new files in your working tree.
Stage only the model files for this branch:
git add internal/models/user.go internal/models/session.go git commit -m "Add user and session models" git add db/migrations/001_create_users.sql git commit -m "Add user table migration"
Now create a new branch for the API layer and stage the API files there:
gh stack add api-routes
created & switched to feat/api-routes branch
git add internal/api/routes.go internal/api/handlers.go git commit -m "Add user API routes" This keeps each branch focused on one concern. Multiple commits per branch are fine — the key is that all commits in a branch relate to the same logical concern, and changes that belong to a different concern go in a different branch. When to create a new branch Create a new branch ( gh stack add ) when you're starting a different concern that depends on what you've built so far. Signs it's time for a new branch: You're switching from backend to frontend work You're moving from core logic to tests or documentation The next set of changes has a different reviewer audience The current branch's PR is already large enough to review One stack, one story Think of a stack from the reviewer's perspective: the stack of PRs should tell a cohesive story about a feature or project. A reviewer should be able to read the PRs in sequence and understand the progression of changes, with each PR being a small, logical piece of the whole. When to use a single stack: All the branches are part of the same feature, project, or closely related effort. Even if the work spans multiple concerns (models, API, frontend), they're all building toward the same goal. When to create a separate stack: The work is unrelated to your current stack — a different feature, a bug fix in an unrelated area, or an independent refactor. Don't mix unrelated work into a single stack just because you happen to be working on both. Start a new stack with gh stack init or switch to an existing stack with gh stack checkout for each distinct effort. Small, incidental fixes (e.g., fixing a typo you noticed) can go in the current stack if they're trivial. But if a change grows into its own project, it deserves its own stack. Quick reference Task Command Create a stack (recommended) gh stack init -p feat auth Create a stack without prefix gh stack init auth Adopt existing branches gh stack init --adopt branch-a branch-b Set custom trunk gh stack init --base develop branch-a Add a branch to stack (suffix only if prefix set) gh stack add api-routes Add branch + stage all + commit gh stack add -Am "message" api-routes Push branches to remote gh stack push Push to specific remote gh stack push --remote origin Push branches + create PRs gh stack submit --auto Create PRs as drafts gh stack submit --auto --draft Sync (fetch, rebase, push) gh stack sync Sync with specific remote gh stack sync --remote origin Rebase entire stack gh stack rebase Rebase upstack only gh stack rebase --upstack Continue after conflict gh stack rebase --continue Abort rebase gh stack rebase --abort View stack details (JSON) gh stack view --json Switch branches up/down in stack gh stack up [n] / gh stack down [n] Switch to top/bottom branch gh stack top / gh stack bottom Check out by PR gh stack checkout 42 Check out by branch (local only) gh stack checkout feature-auth Tear down a stack to restructure it gh stack unstack Workflows End-to-end: create a stack from scratch
1. Initialize a stack with the first branch
gh stack init -p feat auth
→ creates feat/auth and checks it out
2. Write code for the first layer (auth)
cat
auth.go << 'EOF' package auth func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // verify token next.ServeHTTP(w, r) }) } EOF
3. Stage and commit using standard git commands
git add auth.go git commit -m "Add auth middleware"
You can make multiple commits on the same branch
cat
auth_test.go << 'EOF' package auth func TestMiddleware(t *testing.T) { // test auth middleware } EOF git add auth_test.go git commit -m "Add auth middleware tests"
4. When you're ready for a new concern, add the next branch
gh stack add api-routes
→ creates feat/api-routes (prefix applied automatically — just pass the suffix)
5. Write code for the API layer
cat
api.go << 'EOF' package api func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/users", handleUsers) } EOF git add api.go git commit -m "Add API routes"
6. Add a third layer for frontend
gh stack add frontend
→ creates feat/frontend (just the suffix — prefix is automatic)
cat
frontend.go << 'EOF' package frontend func RenderDashboard(w http.ResponseWriter) { // calls the API endpoints from the layer below } EOF git add frontend.go git commit -m "Add frontend dashboard"
── Stack complete: feat/auth → feat/api-routes → feat/frontend ──
7. Push everything and create draft PRs
gh stack submit --auto --draft
8. Verify the stack
gh stack view --json Shortcut: If you prefer a faster flow, gh stack add -Am "message" branch-name combines staging, committing, and branch creation into one command. This is useful for single-commit layers but bypasses deliberate staging. Making mid-stack changes This is a critical workflow for agents. When you're working on a higher layer and realize you need to change something in a lower layer (e.g., you're building frontend components but need to add an API endpoint), navigate down to the correct branch, make the change there, and rebase .
You're on feat/frontend but need to add an API endpoint
1. Navigate to the API branch
gh stack down
or: gh stack checkout feat/api-routes
2. Make the change where it belongs
cat
users_api.go << 'EOF' package api func handleGetUser(w http.ResponseWriter, r *http.Request) { // new endpoint the frontend needs } EOF git add users_api.go git commit -m "Add get-user endpoint"
3. Rebase everything above to pick up the change
gh stack rebase --upstack
4. Navigate back to where you were working
gh stack top
or: gh stack checkout feat/frontend
5. Continue working — the API changes are now available
Why this matters: If you make API changes on the frontend branch, those changes will end up in the wrong PR. The API PR won't include them, and the frontend PR will have unrelated API diffs mixed in. Always put changes in the branch where they logically belong. Modify a mid-stack branch and sync When you need to revisit a branch after the initial creation (e.g., responding to review feedback):
1. Navigate to the branch that needs changes
gh stack bottom
or: gh stack checkout feat/auth
or: gh stack checkout 42 (by PR number)
2. Make changes and commit
cat
auth.go << 'EOF' package auth // updated implementation EOF git add auth.go git commit -m "Fix auth token validation"
3. Rebase everything above this branch
gh stack rebase --upstack
4. Push the updated stack
gh stack push Routine sync after merges
Single command: fetch, rebase, push, sync PR state
gh stack sync Squash-merge recovery When a PR is squash-merged on GitHub, the original branch's commits no longer exist in the trunk history. gh stack detects this automatically and uses git rebase --onto to correctly replay remaining commits.
After PR #1 (feat/auth) is squash-merged on GitHub:
gh stack sync
→ fetches latest, detects the merge, fast-forwards trunk
→ rebases feat/api-routes onto updated trunk (skips merged branch)
→ rebases feat/frontend onto feat/api-routes
→ pushes updated branches
→ reports: "Merged: #1"
Verify the result
gh stack view --json
→ feat/auth shows "isMerged": true, "state": "MERGED"
→ feat/api-routes and feat/frontend show updated heads
If sync hits a conflict during this process, it restores all branches to their pre-rebase state and exits with code 3. See Handle rebase conflicts for the resolution workflow. Handle rebase conflicts (agent workflow)
1. Start the rebase
gh stack rebase
2. If exit code 3 (conflict):
- Parse stderr for conflicted file paths
- Read those files to find <<<<<<< / ======= / >>>>>>> markers
- Edit files to resolve conflicts
- Stage resolved files:
git add path/to/resolved-file.go
3. Continue the rebase
gh stack rebase --continue
4. If another conflict occurs, repeat steps 2-3
5. If unable to resolve, abort to restore everything
gh stack rebase --abort Parsing --json output
Get stack state as JSON
output
$( gh stack view --json )
Check if any branch needs a rebase, and rebase if so
needs_rebase
$( echo " $output " | jq '[.branches[] | select(.needsRebase == true)] | length' ) if [ " $needs_rebase " -gt 0 ] ; then echo "Branches need rebase, rebasing stack..." gh stack rebase fi
Get all open PR URLs
echo " $output " | jq -r '.branches[] | select(.pr.state == "OPEN") | .pr.url'
Find merged branches
echo " $output " | jq -r '.branches[] | select(.isMerged == true) | .name'
Get the current branch
echo " $output " | jq -r '.currentBranch'
Check if the stack is fully merged (all branches merged)
echo " $output " | jq '[.branches[] | .isMerged] | all' Restructure a stack (remove a branch, reorder, or rename) Use unstack to tear down the stack, make structural changes, then re-init:
1. Remove the stack (locally and on GitHub)
gh stack unstack
2. Make structural changes — e.g. delete a branch, reorder, rename
git branch -m old-branch-1 new-branch-1
3. Re-create the stack with the new structure
gh stack init
--base
main
--adopt
new-branch-1 new-branch-2 new-branch-3
Commands
Initialize a stack —
gh stack init
Creates a new stack.
Always provide at least one branch name as a positional argument
— running without branch arguments triggers interactive prompts that agents cannot use.
gh stack init [flags]
Set a branch prefix (recommended — subsequent add calls only need the suffix)
gh stack init -p feat auth
→ creates feat/auth
Multi-part prefix (slashes are fine — suffix-only rule still applies)
gh stack init -p monalisa/billing auth
→ creates monalisa/billing/auth
Create a stack with new branches (no prefix — use full branch names)
gh stack init branch-a branch-b branch-c
Use a different trunk branch
gh stack init --base develop branch-a branch-b
Adopt existing branches into a stack
gh stack init
--adopt
branch-a branch-b branch-c
Flag
Description
-b, --base
Create a new branch and switch to it (just the suffix — prefix is applied automatically)
gh stack add api-routes
Write code, stage deliberately, and commit
git add internal/api/routes.go internal/api/handlers.go git commit -m "Add user API routes"
Make more commits on the same branch as needed
git add internal/api/middleware.go git commit -m "Add rate limiting middleware" Shortcut — stage, commit, and branch in one command:
Create a new branch, stage all changes, and commit
gh stack add -Am "Add API routes" api-routes
Create a new branch, stage tracked files only, and commit
gh stack
add
-um
"Fix auth bug"
auth-fix
Flag
Description
-m, --message
Push all branches
gh stack push
Push to specific remote
gh stack push
--remote
upstream
Flag
Description
--remote
Submit and auto-title new PRs (required for non-interactive use)
gh stack submit --auto
Submit and create PRs as drafts
gh stack submit
--auto
--draft
Flag
Description
--auto
Auto-generate PR titles without prompting (
required
for non-interactive use)
--draft
Create new PRs as drafts
--remote
Rebase the entire stack
gh stack rebase
Rebase only branches from trunk to current branch
gh stack rebase --downstack
Rebase only branches from current branch to top
gh stack rebase --upstack
After resolving a conflict: stage files with git add, then:
gh stack rebase --continue
Abort and restore all branches to pre-rebase state
gh stack rebase
--abort
Flag
Description
--downstack
Only rebase branches from trunk to the current branch
--upstack
Only rebase branches from the current branch to the top
--continue
Continue after resolving conflicts
--abort
Abort and restore all branches
--remote
Always use --json
gh stack view --json Flag Description --json Output stack data as JSON to stdout ( required for non-interactive use) --json output format: { "trunk" : "main" , "prefix" : "feat" , "currentBranch" : "feat/api-routes" , "branches" : [ { "name" : "feat/auth" , "head" : "abc1234..." , "base" : "def5678..." , "isCurrent" : false , "isMerged" : true , "needsRebase" : false , "pr" : { "number" : 42 , "url" : "https://github.com/owner/repo/pull/42" , "state" : "MERGED" } } , { "name" : "feat/api-routes" , "head" : "789abcd..." , "base" : "abc1234..." , "isCurrent" : true , "isMerged" : false , "needsRebase" : false , "pr" : { "number" : 43 , "url" : "https://github.com/owner/repo/pull/43" , "state" : "OPEN" } } ] } Fields per branch: name — branch name head — current HEAD SHA base — parent branch's HEAD SHA at last sync isCurrent — whether this is the checked-out branch isMerged — whether the PR has been merged needsRebase — whether the base branch is not an ancestor (non-linear history) pr — PR metadata (omitted if no PR exists). state is "OPEN" or "MERGED" . Navigate the stack Move between branches without remembering branch names. These commands are fully non-interactive. gh stack up
Move up one branch (further from trunk)
gh stack up 3
Move up three branches
gh stack down
Move down one branch (closer to trunk)
gh stack down 2
Move down two branches
gh stack top
Jump to the top of the stack (furthest from trunk)
gh stack bottom
Jump to the bottom (first non-merged branch above trunk)
Navigation clamps to stack bounds. Merged branches are skipped when navigating from active branches.
Check out a stack —
gh stack checkout
Check out a stack from a pull request number or branch name.
Always provide an argument
— running
gh stack checkout
without arguments triggers an interactive selection menu.
gh stack checkout
By PR number (pulls from GitHub)
gh stack checkout 42
By branch name (local only)
gh stack checkout feature-auth
When a PR number is provided (e.g.
123
), the command fetches the stack on GitHub, pulls the branches, and sets up the stack locally. If the stack already exists locally and matches, it switches to the branch.
⚠️ Agent warning:
If the local and remote stacks have different branch compositions, this command triggers an interactive conflict-resolution prompt that cannot be bypassed with a flag. To avoid this: run
gh stack unstack
first to remove the conflicting local stack, then retry
gh stack checkout
Tear down the stack (locally and on GitHub), then rebuild
gh stack unstack gh stack init --base main --adopt branch-2 branch-1 branch-3
reordered
Only remove local tracking (keep the stack on GitHub)
gh stack unstack --local
Specify a branch to identify which stack to tear down
gh stack unstack feature-auth
Flag
Description
--local
Only delete the stack locally (keep it on GitHub)
Argument
Description
[branch]
A branch in the stack (defaults to the current branch)
Output conventions
Status messages
go to
stderr
with emoji prefixes:
✓
(success),
✗
(error),
⚠
(warning),
ℹ
(info).
Data output
(e.g.,
view --json
) goes to
stdout
.
When piping output, use
2>/dev/null
to suppress status messages if only data output is needed.
Exit codes and error recovery
Code
Meaning
Agent action
0
Success
Proceed normally
1
Generic error
Read stderr for details; may indicate commit/push failure
2
Not in a stack
Run
gh stack init
to create a stack first
3
Rebase conflict
Parse stderr for conflicted file paths, resolve conflicts, run
gh stack rebase --continue
4
GitHub API failure
Check
gh auth status
, retry the command
5
Invalid arguments
Fix the command invocation (check flags and arguments)
6
Disambiguation required
A branch belongs to multiple stacks. Run
gh stack checkout