stacked is a minimal, login-free CLI for managing stacked diffs on top of
plain git. The CLI command is st.
A stack is a chain of small branches where each one is based on the branch below
it instead of all on main. This keeps each change small and reviewable while you
keep building on top. The hard part of stacking by hand is keeping every branch
rebased on its parent as the parent changes — stacked automates exactly that.
stacked is a thin, ergonomic layer over git:
- No host API. It never opens pull requests or calls a forge API. Remote Git
commands still contact your configured remotes:
st syncfetches andst submitpushes your branches; you open PRs yourself. - No third-party dependencies. It is written in pure Go using only the standard
library and shells out to your system
git. - Local metadata only. The stack topology (which branch's parent is which) is
stored in a single JSON file inside your repo's
.gitdirectory.
stacked stores all of its state in:
<your-repo>/.git/stacked/state.json
Because it lives inside .git, it is per-repo, never committed, and never pushed.
The file records the trunk branch and, for each tracked branch, its parent branch
and the parent commit SHA it was last rebased onto:
{
"trunk": "main",
"branches": {
"feat-a": {
"name": "feat-a",
"parent": "main",
"parentSHA": "a1b2c3d4..."
},
"feat-b": {
"name": "feat-b",
"parent": "feat-a",
"parentSHA": "e5f6a7b8..."
}
}
}parentSHA is the key to restacking. A branch B stacked on parent P remembers
the P commit it was based on. When P advances (you add or amend a commit),
B.parentSHA no longer matches P's tip, so B "needs restack". Restacking runs:
git rebase --onto <current tip of P> <B.parentSHA> <B>
and then updates B.parentSHA to P's new tip. Any operation that moves a branch
tip (modify, delete, sync) automatically restacks that branch's upstack
(all of its descendants), always going parents-before-children and reading live
tips at each step. Commands that move HEAD around restore your original branch
when they finish.
The file is written atomically (temp file + rename) so an interrupted command cannot corrupt your stack metadata.
Requires Go 1.26+ and a working git on your PATH. The full gate
(make ci) additionally needs golangci-lint v2 — an external binary, never a
module dependency: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2.
# install onto your PATH
go install ./cmd/st
# ...or build locally (Makefile stamps the version from git)
make build # -> ./st
go build -o st ./cmd/st
# sanity check
st version # version, commit, build time, go version
st --helpThe main package lives at ./cmd/st, so go install produces a binary named
st. The metadata lives under the repository's common git dir, so linked
worktrees of the same repo share one stack.
st is built to be driven programmatically: it never prompts, JSON-capable
subcommands accept --json, and failures report stable exit codes (2
conflict, 3 not initialized, 4 dirty tree). See
docs/AGENT.md for the full machine interface (JSON schemas,
exit codes, idempotency).
The repo closes its own loop — make ci is fmt + strict lint + race tests + e2e +
a coverage gate, and the engine you'll touch most tests in milliseconds. See
CONTRIBUTING.md for the add-a-command recipe and
CLAUDE.md for the architecture.
Run st help (or st <command> -h) at any time. Flags may be placed
before or after the positional branch name.
Every command below except completion (plus help/version) accepts --json; see docs/AGENT.md.
| Command | Aliases | Summary |
|---|---|---|
st init [--trunk <name>] |
Initialize stack tracking in this repo. | |
| `st create [-m | --message ] [-a | --all]` |
st log [--json] |
ls |
Show the stack as a tree (trunk at the bottom); --json for scripting. |
st status [--json] |
stat |
Show the current branch, its parent/children, and restack state. |
st checkout [name] |
co |
Check out a tracked branch, or list branches when no name is given. |
st up [n] |
u |
Move up the stack to a child branch. |
st down [n] |
d |
Move down the stack toward trunk. |
st top |
t |
Jump to the top (leaf) of the current stack. |
st bottom |
b |
Jump to the bottom branch (just above trunk). |
st track [--parent <branch>] |
Start tracking the current git branch. | |
st untrack [name] |
Stop tracking a branch (re-parents its children). | |
| `st modify [-m | --message ] [-a | --all] [--commit]` |
st restack [--dry-run] |
r |
Rebase the current branch and everything above it onto their parents (--dry-run previews). |
st continue |
Resume a restack interrupted by a merge conflict. | |
st abort |
Abort an in-progress restack/rebase. | |
st fold |
Fold the current branch into its parent (parent absorbs its commits). | |
| `st squash [-m | --message ]` | |
st onto <target> |
move |
Move the current branch (and its upstack) onto a new parent. |
st rename [old] <new> |
mv |
Rename a branch and update the stack metadata. |
| `st delete [-f | --force]` | rm |
st sync [--no-delete] [--remote <name>] [--dry-run] |
s |
Fetch trunk, fast-forward it, restack everything, prune merged branches (--dry-run previews). |
st submit [--remote <name>] [--dry-run] |
ss |
Push the stack to the remote and print the repo URL (no PRs). |
st undo |
Undo the last stack-mutating command. | |
st validate |
doctor |
Check the stack state for drift or inconsistencies. |
st repair |
Reconcile the metadata with the repository (fix drift). | |
st completion <bash|zsh|fish> |
Print a shell completion script. | |
st guide |
Print the recommended workflow (handy for agents). | |
st help / st version |
-h/-v |
Show help / print the version. |
Creates .git/stacked/state.json. If --trunk is omitted, the trunk is detected
from origin/HEAD, then the current branch, then defaults to main.
st init # auto-detect trunk
st init --trunk mainCreates <name> off the current branch, switches to it, and tracks it. -a
stages all changes first; -m commits the staged changes onto the new branch.
st create feat-a -m "add A"Renders the stack as a tree with the trunk at the bottom and each branch above its
parent. The current branch is marked ◉, others ○, drifted branches are tagged
(needs restack), and each branch shows its top commit subject.
Prints the current branch's role (trunk / tracked / untracked), its parent and children, whether it needs a restack, and whether the working tree is clean.
Checks out a tracked branch (or the trunk). With no argument, lists the trunk and
all tracked branches, marking the current one with *.
Walk n levels up (toward leaves) or down (toward trunk) and check out the result.
up stops at a branch point with multiple children and tells you to pick one.
Jump to the leaf of the current stack (top) or to the bottom branch just above
trunk (bottom).
track starts managing an existing git branch; the parent is inferred from the
commit graph or set with --parent. untrack stops managing a branch and
re-parents its children onto that branch's parent (the git branch is not deleted).
Amends the current branch's tip (or, with --commit, adds a new commit), then
restacks every descendant so the rest of the stack rebases onto the new tip. A
bare st modify stages all changes and amends without editing the message.
Rebases the current branch onto its parent's current tip, then restacks its entire
upstack in topological order. From the trunk, restacks every tracked branch. Your
original branch is restored when done. Requires a clean working tree (commit or
stash first). If a rebase hits a conflict, resolve it, stage the files with
git add, then run st continue.
Resumes a restack that stopped on a merge conflict. After you resolve the conflict
and git add the files, st continue completes the in-progress rebase, records
the branch's new base, and restacks the rest of the stack — picking up exactly
where it left off. If it hits another conflict, resolve and run st continue
again.
Deletes a tracked branch, re-parents its children onto the deleted branch's parent,
and restacks them. -f force-deletes an unmerged branch.
Fetches the remote, fast-forwards the trunk, deletes branches already merged into
the trunk (re-parenting their children), restacks every remaining stack onto the
updated trunk, and restores your original branch. --no-delete keeps merged
branches; --dry-run previews the prune/restack plan without fetching or changing
anything.
Pushes every branch on the current stack — from the bottom branch up to the
currently checked-out branch — using --force-with-lease, setting each branch's
upstream (-u). It is login-free and never opens PRs; it prints your repository's
URL so you can open pull requests on your host by hand.
--dry-run prints the plan without pushing. Most stack-mutating commands also accept
--json for machine-readable output.
Aborts an in-progress restack (git rebase --abort). Branches that already
restacked keep their new positions; the conflicted branch is rolled back and still
needs a restack.
Folds the current branch into its parent: the parent advances to include the
branch's commits, the branch is deleted, and its children are re-parented onto the
parent. The branch must be in sync first (st restack if needed).
Collapses every commit on the current branch (since its parent) into one, then
restacks its descendants. With no -m, the message is composed from the existing
commit subjects.
Re-parents the current branch onto target (the trunk or a tracked branch) and
rebases it and its descendants there. target may not be the branch itself or one
of its descendants. On conflict, resolve and run st continue.
Renames a branch (the current one by default) with git branch -m and updates the
stack metadata: the branch's record, the trunk name if applicable, and every
child's parent pointer.
Reverts the last stack-mutating command: the metadata is rolled back and each
recorded branch is reset to its prior tip. It does not touch your working
tree, so uncommitted changes are preserved (run git status to review). The
journal keeps the last several operations.
Fixes the drift st validate reports: untracks branches whose git branch was
deleted outside st, re-parents branches with an invalid parent onto the trunk, and
breaks parent cycles. Re-parented branches may then need st restack.
Prints a shell completion script for st's subcommands, e.g.
st completion zsh > "${fpath[1]}/_st".
Checks the recorded stack against the actual repository and reports problems — a
missing trunk, tracked branches whose git branch was deleted outside stacked,
parents that are no longer the trunk or a tracked branch, and parent cycles — plus
warnings for branches that have drifted and need a restack. Exits non-zero when any
problem is found.
# 0. one-time setup in your repo
st init
# 1. build the first change as its own small branch
echo 'A' > a.txt && git add -A
st create feat-a -m "add A"
# 2. stack a second change on top of the first
echo 'B' > b.txt && git add -A
st create feat-b -m "add B"
# 3. see the stack (trunk at the bottom, current branch marked ◉, colorized on a TTY)
st log
# ◉ feat-b add B
# ○ feat-a add A
# ○ main
# 4. drop down to revise the first branch
st down # now on feat-a
# 5. amend feat-a; feat-b is automatically restacked onto the new feat-a
echo 'A2' >> a.txt
st modify -m "add A (revised)"
# Amended feat-a with new message
# Restacked 1 branch(es):
# feat-b
# 6. if anything ever drifts, fix the whole stack in one shot
st restack # no-op here: everything up to date
# 7. pull in trunk updates, fast-forward main, restack, prune merged branches
st sync
# 8. push the whole stack to the remote (no PRs are opened)
st submit # or: st submit --dry-runstackedshells out to your systemgit; it is a convenience layer, not a reimplementation of git.- The stack metadata is local to the repo (under the common git dir, so linked
worktrees share it) and is not shared via the remote. A teammate cloning the
repo starts with an empty stack until they
trackbranches. - Mutating commands take an advisory lock so two
stinvocations don't clobber each other's metadata: flock on unix-like platforms, an exclusive lock file elsewhere (e.g. Windows). - On a rebase conflict during a restack, resolve it,
git addthe files, and runst continue(orst abortto back out).stackedrecords progress as each branch lands, then restacks the rest of the stack. st undoreverts the last mutating command's metadata and branch positions but does not modify your working tree.stackeddeliberately opens no pull requests. Afterst submit, open the PRs it prints (or create them on your host yourself).- Not implemented:
absorb(auto-distributing staged hunks into the right ancestor commits) is intentionally left out — it is a sizable blame/fixup subsystem and is noted as a future addition rather than shipped half-done.