yas is a CLI tool for managing stacked PRs on GitHub. Each branch in a stack builds on its parent, so you can ship a large change as a chain of small, reviewable PRs while yas keeps the branches, rebases and PR bases in sync.
- ⽙ Stacks as a tree —
yas listshows every branch, its parent, its PR link and whether it needs a restack or resubmit - 🗂️ Worktree aware — branches can live in separate worktrees, and the shell hook changes directory when you switch
- ↩️ Painless rebases —
yas restackautomatically rebases all branches in the stack - ✨ Auto-resolve conflicts – use Claude to automatically resolve conflicts during rebase
- 🚀 One-command PR submit —
yas submitpushes and opens or updates the PR - 📝 PR annotations — every submitted PR gets a summary of the stack it belongs to, so reviewers can see where it fits
- 🧹 Sync after merges —
yas syncpulls latest trunk, refreshes PR status and deletes local branches whose PRs merged - ✅ Review and CI at a glance —
yas list --statusshows review decision and CI state next to each PR - 🔀 Fast branch switching —
yas branchchecks out by name or opens an interactive picker, and hops between worktrees for you - 🧭 Restructure freely —
yas movereparents a branch and its descendants;yas addadopts existing branches into a stack
Requires Go 1.24+, Git 2.38+ and the GitHub CLI (gh), logged in.
go install github.com/dansimau/yas/cmd/yas@latestInstall the shell hook so yas branch can change directory when
switching between worktrees. Add to ~/.zshrc or ~/.bashrc:
eval "$(yas hook zsh)" # or: eval "$(yas hook bash)"$ yas ls --status
main
├── auth-refactor [https://github.com/acme/app/pull/101] (review: ✅, CI: ✅)
│ └── auth-tests (needs restack) [https://github.com/acme/app/pull/102] (review: ❌, CI: ⏳) *
└── fix-typo (not submitted)Each branch shows its PR link, whether it needs restack or needs submit, and with --status the PR review and CI state. The * marks the branch you are on.
yas branch auth-refactor # or: yas br auth-refactor
yas branch # no argument opens an interactive pickerIf the target branch lives in another worktree, yas changes directory for you automatically.
yas branch add-rate-limitingThis creates add-rate-limiting on top of the current branch and records the parent relationship. Use --parent <branch> to stack on something else, or --worktree to check it out in a new worktree.
git commit -am "Add rate limiting"
yas submit --draftyas pushes the branch, opens a draft PR with its base set to the parent
branch, and annotates the PR description with the stack. Run yas submit
again after more commits to update it.
From the PR branch:
$ yas mergeOnce a PR in the stack is merged on GitHub:
$ yas sync
🧹 Checking for merged PRs...
Deleted branch 'auth-refactor'
🔄 Pulling main...Merged branches are removed locally, their children are reparented onto trunk, and trunk is fast-forwarded to the remote.
After trunk moves (a sync, or a merge elsewhere), rebase the stack and push the rebased branches back to their PRs:
yas restack --all # or `yas restack` for the current stack only
yas submit --outdated # resubmit only branches whose PRs are behindyas submit --stack resubmits every branch in the current stack. If a rebase hits conflicts, resolve them and run yas continue, or back out
with yas abort.
Use yas sync --restack to do the sync and restack everything in one step.
yas can hand rebase conflicts to an external tool instead of stopping. Currently
Claude Code is supported (it
runs claude -p ... in the conflicted worktree, restricted to editing files):
yas config set --conflict-resolver=claude # default: none
yas config set --after-resolve=continue # default: stopBy default, yas stops so you can verify the resolution. Use --after-resolve=continue to automatically continue 🤞
If the tool fails or leaves markers behind, yas stops exactly as it would for a manual conflict, and yas continue / yas abort work as usual.
| Command | Description |
|---|---|
yas init |
Set up yas in the current repository |
yas list (ls) |
List stacks; --status, --all, --current-stack |
yas branch (br) |
Switch to a branch |
yas branch <name> (br) |
Switch to the named branch, creating it if it doesn't exist |
yas add [branch] |
Track an existing branch and set its parent |
yas move |
Move the current branch and its descendants to a new parent |
yas submit |
Push and open/update PRs |
yas restack [branch] |
Rebase all branches in the current stack or --all for all branches; --conflict-resolver, --after-resolve |
yas continue / yas abort |
Resume or abort a restack after conflicts |
yas sync |
Pull trunk, refresh PR status, delete merged branches |
yas merge |
Merge the PR for the current branch |
yas delete |
Delete a branch and its worktree |
yas config |
Show or set repository configuration |
yas hook <bash|zsh> |
Print the shell integration hook |
The repository also ships workyard, a separate tool for working on a branch across a whole
directory tree of repositories. A workyard is a fast copy of a directory (say, your multi-repo
workspace) in which every git repository is replaced by a git worktree of the original, checked
out at the branch you choose. On macOS (APFS) everything that is not a repository is cloned
copy-on-write, so a workspace of many repos and gigabytes of dependencies is ready in seconds.
go install github.com/dansimau/yas/cmd/workyard@latestInstall the shell hook so workyard switch can change directory (it is separate from the yas
hook, which only wraps yas):
eval "$(workyard hook zsh)" # or: eval "$(workyard hook bash)"$ cd ~/code/workspace
$ workyard create feature-x # in .workyard/yards/feature-x, on branch feature-x
Created workyard in 0.42s
$ cd .workyard/yards/feature-x/web
$ workyard git status # git with your arguments, in every repo
$ workyard git diff --stat
$ workyard exec make test # any command, in every repo (add --parallel to run them at once)
$ workyard create feature-y # from inside a yard: another yard of ~/code/workspace
$ workyard ls # workyards created from ~/code/workspace
$ workyard switch # pick a workyard of ~/code/workspace and cd to it
$ workyard remove feature-x # by name (or path), from anywhere in the source or its yardsLike a git worktree, a workyard only holds a pointer back to where it came from (the .workyard
file at its root); the source keeps the bookkeeping under .workyard/. The source itself must not
be a git repository (for that, use git worktree) or another workyard. By default a yard lives in
the source's .workyard/yards/<name> (which is never copied), much like yas keeps worktrees in
.yas/worktrees/; set yards-dir in the config to put them elsewhere, or give --dest. A yard may
not otherwise overlap its source. The name may contain slashes, like a branch name.
Without --source, the source is that of the workyard you are in, else the nearest ancestor with a
.workyard/ directory, else the current directory, so workyard create works from anywhere inside
the source or one of its yards.
When the branch does not exist in a repository it is created from that repository's trunk (main
or master, or whatever .workyard/config.yaml says). A branch that exists only on a remote is
created to track it; a tag or commit is checked out detached; a branch that is already checked out
in the source is an error.
| Command | Description |
|---|---|
workyard create [--source DIR] [--dest DIR] [--branch BRANCH] NAME |
Create a workyard, on branch NAME unless --branch says otherwise; --dry-run shows the plan |
workyard git [--parallel] [git args] |
git with the given arguments in every repo; shorthand for workyard exec git ... |
workyard exec [--parallel] <command> [args] |
Any command in every repo, with the repo as working directory; exit code 1 if it failed anywhere |
workyard list (ls) |
Workyards created from the current source (or the source of the current workyard) with branch, repository count and creation time |
workyard switch (sw) |
Interactively choose one of the workyards list shows and change directory to it (needs the shell hook) |
workyard hook <bash|zsh> |
Print the shell integration hook |
workyard remove [NAME|PATH] [-f] [--yes] |
Remove a workyard by name or path (an existing path wins), or the one you are in after confirmation: the worktrees (dirty ones only with -f, locked with -f -f), the directory, and the branches workyard created (unmerged ones only with -f) |
git and exec run one repository at a time by default, with the command connected to the terminal
so it can be interactive (and colored, as if you had run it there), and a ==> path (branch) header
before each. --parallel (or exec.parallel in the config) makes them run the repositories at once
with output captured, printing the header only for repositories that produced output. For the
parallel runs --unordered prints results as they finish
and -p/--parallelism controls concurrency. Commands see the environment unchanged. Run them from
anywhere inside the workyard, or set WORKYARD_ROOT.
Optional source configuration in <source>/.workyard/config.yaml:
| Key | Description |
|---|---|
trunk |
Branch to create new branches from when the requested branch does not exist (default: autodetect main, then master) |
repos.<path>.trunk |
Per-repository override, keyed by path relative to the source |
exec.parallel |
Make workyard exec and workyard git run the repositories at once by default (--no-parallel overrides) |
yards-dir |
Directory new workyards are created in, as <yards-dir>/<name>; relative to the source, ~ is your home (default: .workyard/yards) |
Things to know: files ignored by git (node_modules, build output) are not copied into worktrees;
submodules are not initialised (-v reports both); if you move a workyard with mv, run
git worktree repair in each repository; the copy-on-write fast path needs source and destination on
the same APFS volume, otherwise files are copied.
Configuration lives in .yas/yas.yaml and is managed with yas config set:
| Option | Description |
|---|---|
--trunk-branch=<name> |
The trunk branch, e.g. main |
--trunk-branch-aliases=<a,b> |
Comma-separated branch names that are treated as the trunk branch when passed as arguments, e.g. master,trunk. Useful when switching between repos with different trunk names. When an alias is replaced, yas prints a note (once per invocation). Pass an empty value to clear. |
--auto-prefix-branch / --no-auto-prefix-branch |
Prefix new branch names with your username |
--worktree-branch / --no-worktree-branch |
Create new branches in worktrees by default |