but
GitButler CLI Skill
Help users work with GitButler CLI (but command) in workspace mode.
New Session Workflow
EVERY new agent session that involves code changes MUST follow this flow:
- Sync first →
but pullto get latest upstream changes (prevents conflicts and stale base) - Check state →
but statusto see existing branches and unstaged changes - Decide branch →
- If an existing branch matches the task → reuse it (it's already applied)
- If this is new work →
but branch new <task-name>(e.g.feat/add-auth,fix/login-bug) - If you need to resume unapplied work →
but apply <branch>
- Make changes → Edit files as needed
- Stage & commit →
but commit <branch> -m "message" --changes <id>,<id> - Refine → Use
but absorborbut squashto clean up history - Push when ready →
but push <branch> - Create PR →
but pr new <branch> -t(uses default target branch)
Branch naming: Use conventional prefixes: feat/, fix/, chore/, refactor/
Commit early, commit often. Don't hesitate to create commits - GitButler makes editing history trivial. You can always squash, reword, or absorb changes into existing commits later. Small atomic commits are better than large uncommitted changes.
After Using Write/Edit Tools
When ready to commit:
- Run
but status --jsonto see uncommitted changes and get their CLI IDs - Commit the relevant files directly:
but commit <branch> -m "message" --changes <id>,<id>
You can batch multiple file edits before committing - no need to commit after every single change.
Critical Concept: Workspace Model
GitButler ≠ Traditional Git
- Traditional Git: One branch at a time, switch with
git checkout - GitButler: Multiple stacks simultaneously in one workspace, changes assigned to stacks
This means:
- ❌ Don't use
git status,git commit,git checkout - ✅ Use
but status,but commit,butcommands - ✅ Read-only git commands are fine (
git log,git diff)
Hard Safety Rules (Non-Negotiable)
- Never discard changes you did not create.
zz(unassigned) often contains work from other sessions/agents/users.- If unrelated changes exist, leave them untouched and ask before any discard action.
- Never leave your own changes in
zzat the end of work.- After edits, run
but status --jsonand move your file/hunk IDs to the correct branch viabut stageorbut commit --changes.
- After edits, run
- Validate branch ownership before commit.
- Confirm each changed file/hunk belongs to the intended branch/task, then commit only those IDs.
- Respect branch ownership across sessions.
- In multi-agent environments, branches may belong to other agent sessions. Never reword, rename, or push branches you didn't create in this session.
- If you need to modify another session's branch, ask the user first.
Quick Start
Installation:
curl -sSL https://gitbutler.com/install.sh | sh
but setup # Initialize in your repo
but skill install --path <path> # Install/update skill (agents use --path with known location)
Note for AI agents:
- When installing or updating this skill programmatically, always use
--pathto specify the exact installation directory. The--detectflag requires user interaction if multiple installations exist. - Use
--jsonflag for all commands to get structured, parseable output. This is especially important forbut status --jsonto reliably parse workspace state.
Core workflow:
but status --json # Always start here - shows workspace state (JSON for agents)
but branch new feature # Create new stack for work
# Make changes...
but commit <branch> -m "…" --changes <id>,<id> # Commit specific files by CLI ID
but push <branch> # Push to remote
Essential Commands
For detailed command syntax and all available options, see references/reference.md. For a hands-on learning guide, see references/tutorial.md. For a one-page quick lookup, see references/cheatsheet.md.
IMPORTANT for AI agents: Add --json flag to all commands for structured, parseable output.
Understanding state:
but status --json- Overview (START HERE, always use --json for agents)but status --json -f- Overview with full file lists (use when you need to see all changed files)but show <id> --json- Details about commit/branchbut diff <id>- Show diff
Flags explanation:
--json- Output structured JSON instead of human-readable text (always use for agents)-f- Include detailed file lists in status output (combines with --json:but status --json -f)
Organizing work:
but branch new <name>- Independent branchbut branch new <name> -a <anchor>- Stacked branch (dependent)but stage <file> <branch>- Pre-assign file to branch (optional, for organizing before commit)
Making changes:
but commit <branch> -m "msg" --changes <id>,<id>- Commit specific files or hunks (recommended)but commit <branch> -m "msg" -p <id>,<id>- Same as above, using short flagbut commit <branch> -m "msg"- Commit ALL uncommitted changes to branchbut commit <branch> --only -m "msg"- Commit only pre-staged changes (cannot combine with --changes)but amend <file-id> <commit-id>- Amend file into specific commit (explicit control)but absorb <file-id>- Absorb file into auto-detected commit (smart matching)but absorb <branch-id>- Absorb all changes staged to a branchbut absorb- Absorb ALL uncommitted changes (use with caution)
Getting IDs for --changes:
- File IDs:
but status --json- commit entire files - Hunk IDs:
but diff --json- commit individual hunks (for fine-grained control when a file has multiple changes)
Editing history:
but rub <source> <dest>- Universal edit (stage/amend/squash/move)but squash <commits>- Combine commitsbut reword <id>- Change commit message/branch name
Remote operations:
but pull- Update with upstreambut push [branch]- Push to remotebut pr new <branch>- Push and create pull request (auto-pushes, no need to push first)but pr new <branch> -m "Title..."- Inline PR message (first line is title, rest is description)but pr new <branch> -F pr_message.txt- PR message from file (first line is title, rest is description)- For stacked branches, the custom message (
-mor-F) only applies to the selected branch; dependent branches use defaults
Key Concepts
For deeper understanding of the workspace model, dependency tracking, and philosophy, see references/concepts.md.
CLI IDs: Every object gets a short ID (e.g., c5 for commit, bu for branch). Use these as arguments.
Parallel vs Stacked branches:
- Parallel: Independent work that doesn't depend on each other
- Stacked: Dependent work where one feature builds on another
The but rub primitive: Core operation that does different things based on what you combine:
- File + Branch → Stage
- File + Commit → Amend
- Commit + Commit → Squash
- Commit + Branch → Move
Workflow Examples
For complete step-by-step workflows and real-world scenarios, see references/examples.md.
Starting independent work:
but status --json
but branch new api-endpoint
but branch new ui-update
# Make changes, then commit specific files to appropriate branches
but status --json # Get file CLI IDs
but commit api-endpoint -m "Add endpoint" --changes <api-file-id>
but commit ui-update -m "Update UI" --changes <ui-file-id>
Committing specific hunks (fine-grained control):
but diff --json # See hunk IDs when a file has multiple changes
but commit <branch> -m "Fix first issue" --changes <hunk-id-1>
but commit <branch> -m "Fix second issue" --changes <hunk-id-2>
Cleaning up commits:
but absorb # Auto-amend changes
but status --json # Verify absorb result
but squash <branch> # Squash all commits in branch
Resolving conflicts:
but resolve <commit> # Enter resolution mode
# Fix conflicts in editor
but resolve finish # Complete resolution
Managing workspace:
but config target origin/test # Set default PR target (requires unapply all branches first)
but unapply <branch> # Remove branch from workspace (keeps commits)
but apply <branch> # Bring branch back into workspace
but teardown # Exit GitButler mode → normal git
but setup # Re-enter GitButler mode
but discard <ids> # Discard unstaged changes
Post-Merge PR Flow
After a PR is squash-merged on GitHub, follow this exact sequence:
but unapply <merged-branch> # MUST do BEFORE pull - prevents orphan branch errors
but pull # Pull merged changes from remote
Critical: If you but pull before unapplying the merged branch, GitButler will error with orphan branch conflicts. Always unapply first.
If but unapply fails (branch already gone from workspace after remote deletion with --delete-branch), but pull may also fail with "resolution mismatch" errors because the ghost stack still exists internally. In this case, the GitButler desktop app can handle it — tell the user to run but pull from the GUI. Alternatively, use but teardown → but setup → but config target origin/<branch> to reset.
After but teardown → but setup: Target config resets. Run but config target origin/<branch> again.
Using --no-hooks Safely
When pre-commit hooks fail on pre-existing errors unrelated to your changes, use --no-hooks. But this skips the formatter too:
bun run format # Format FIRST
but commit <branch> -m "msg" --changes <ids> --no-hooks # Then commit without hooks
Alternatively, commit normally and absorb formatter fixes:
but commit <branch> -m "msg" --changes <ids> # Commit (hooks may fix formatting)
but absorb # Absorb any auto-formatted changes
Known Issues & Workarounds
| Issue | What happens | Workaround |
|---|---|---|
but resolve loses target config |
After entering resolve mode, but config target resets to "not set" |
Run but config target origin/<branch> again after but resolve finish. If finish fails, do git checkout gitbutler/workspace → but teardown → but setup |
but absorb hunk lock |
Absorb assigns hunk to wrong commit when it's locked by another commit on a different branch | Use but amend <file> <commit> for explicit control instead of absorb |
but pr new has no --base flag |
Always creates PR against default target | Set target first: but config target origin/<branch> |
but config target requires unapply |
Cannot change target with applied branches | but unapply all → change target → but apply |
but config forge auth is interactive |
Cannot run in non-interactive agent mode | User must run in terminal + grant org access on GitHub |
but commit pre-commit hook fails |
Hook fails on pre-existing errors unrelated to your changes | but commit --no-hooks if errors are not from your changes. Always bun run format first since --no-hooks skips the formatter |
but branch delete last segment |
Cannot delete if it would leave anonymous segment | Use but unapply instead of delete |
but stage prefix matching |
Branch name can be abbreviated | but stage <id> ch works for chore/gitbutler-setup |
but discard hunk range error |
Discarding file-level changes sometimes fails with hunk range errors | Use git checkout -- <file> instead of but discard for file-level discards |
but teardown + but setup resets target |
After teardown/setup cycle, target config is lost | Run but config target origin/<branch> again after setup |
Lefthook pre-commit.old accumulates |
Lefthook creates pre-commit.old backup that conflicts on next install |
Add rm -f .git/hooks/pre-commit.old to prepare script in package.json |
but pull before unapply |
Pulling with merged branches still applied causes orphan errors | Always but unapply <merged-branch> before but pull |
but unapply after remote branch deletion |
but unapply fails with "branch not found" when remote deleted the branch (e.g. --delete-branch on merge), and subsequent but pull fails with "resolution mismatch" |
Use GitButler desktop app to pull, or but teardown → but setup → but config target origin/<branch> |
Split-hunk files stuck in zz |
File has hunks locked to commits on different branches — GitButler sets stack_id=None, plugin considers file "handled" via lock reference |
Manually commit each hunk: but diff --json to get hunk IDs, then but commit <branch> -m "msg" --changes <hunk-id> for each |
| Plugin auto-cleanup misses empty branches | ge-branch-* cleanup has ~12% failure rate; user-named empty branches are never auto-cleaned |
Run /b-branch-gc command or manually but unapply <branch-id> |
| Notifications not reaching agent | Plugin notification delivery is ~55% (259 queued vs 142 delivered in observed sessions) | Always verify state with but status --json — don't rely on <system-reminder> notifications alone |
| Workspace projection mismatch | but status shows a branch/stack, but but unapply <branch> or GUI stack_details says the stack is not found in the workspace | Treat as GitButler metadata corruption, not a git-history problem. Stop mutating in place, capture logs/project/commit graph for support, then use but teardown → back up .git/gitbutler → but setup. If it persists, use a fresh clone/worktree. |
| Many (no changes) commits after repeated resolve/rebase | A stack accumulates synthetic commits that are patch-equivalent to upstream or replayed duplicates | Do not keep resolving in place. Freeze refs, identify patch-equivalent commits with git cherry -v and rebuild clean branches from the logical base instead of preserving the broken history. |
| Applying one stack auto-unapplies another | GitButler says it had to unapply another stack to apply the requested one | The stacks overlap in content or ancestry. Unapply the currently active conflicting stack first, or rebuild the intended stack boundaries so only one logical chain remains applied at a time. |
| but push blocked by projection errors | Workspace/stack metadata is broken but the raw git refs are healthy | If the goal is to repair remote ancestry, verify branch refs with raw git and use git push --force-with-lease as a last resort. Afterwards return to gitbutler/workspace and reinitialize metadata with but setup. |
Diagnosing zz Stuck Files
When files are stuck in zz (unassigned) and don't auto-recover:
-
Identify the cause:
but status --json -f # Look for files in zz with [LOCKED] markers but diff --json # Get hunk-level IDs and see lock targets -
If hunks are locked to different branches (split-hunk scenario):
- Each hunk must be committed to its locked branch individually
but commit <branch> -m "msg" --changes <hunk-id>for each hunk- Or
but rub <hunk-id> <commit-id>to amend into the locked commit
-
If files have no locks but are still in
zz:- Plugin's
after-editmay have failed silently — stage manually but stage <file-id> <branch>or commit with--changes
- Plugin's
-
If many files are stuck after
but cursor stop:- Run
but rub <file-id> <branch-id>for each file to force assignment - This is the most reliable recovery method
- Run
Key insight: Auto-recovery won't fix multi-branch locked files. If you see [LOCKED] in zz, manual intervention is required.
Surgical Repair for Broken Stacks
When a stack is too damaged for normal but resolve to converge quickly — for example:
- repeated
conflictedcommits keep reappearing, git cherry -vshows lots of patch-equivalent duplicates,- GitButler shows many commits marked
(no changes), - or a single commit drags
.auto-resolution/**or other obvious snapshot junk,
use surgical repair instead of trying to preserve the broken history.
When surgical repair is the right move
- The raw git graph is understandable, but GitButler metadata/history replay is not.
- You can clearly identify the intended logical layers of the work, even if the current branch history no longer reflects them cleanly.
- A mixed commit contains the real feature files plus obvious accidental junk.
Surgical repair workflow
-
Freeze everything first
- Create backup refs for the current broken branches.
- Stash any workspace dirt/hook refresh noise.
-
Find the clean logical base
- Use
git cherry -v <base> <branch>to separate patch-equivalent duplicates from truly unique commits. - Use
git log --reverse <base>..<branch>to see the branch's commit layers in order.
- Use
-
Rebuild from clean bases, not from broken branch history
- Create scratch branches from the intended base.
- Cherry-pick only clean commits.
- If a commit is contaminated (for example adds
.auto-resolution/**), do file-level checkout of only the intended paths from that commit or from the existing branch tip, then create a new clean commit.
-
Validate each rebuilt layer independently
- Compare file deltas between layers with
git diff --name-only <base>..<layer>. - Run the project verification (
bun run check, build/tests/analyzers, etc.) on the rebuilt stack before replacing refs.
- Compare file deltas between layers with
-
Replace refs only after the clean stack is proven
- Move the real branch refs to the rebuilt scratch refs.
- Force-push with
--force-with-leaseonly after ancestry and checks are confirmed.
Why this is fast in practice
Surgical repair sounds heavier than repeated conflict resolution, but once the stack crosses into duplicated replay / (no changes) / metadata-corruption territory, rebuilding the logical deltas is usually faster and safer than preserving every historical artifact.
Critical Safety Rules
- NEVER discard changes you didn't create. Unassigned changes in
zzmay belong to other agents, sessions, or the user. Always ask the user before runningbut discardorgit checkout --on any change you don't recognize. In GitButler workspace, multiple actors work in parallel — discarding "stale" or "already merged" changes is a destructive assumption. - Always assign your changes to a branch immediately. Don't leave edits sitting in
zz(unassigned). After editing files, stage them to your working branch withbut stage <file-id> <branch>or commit directly with--changes.
Guidelines
- Always start with
but status --jsonto understand current state (agents should always use--json) - Create a new stack for each independent work theme
- Use
--changesto commit specific files directly - no need to stage first - Commit early and often - don't wait for perfection. Unlike traditional git, GitButler makes editing history trivial with
absorb,squash, andreword. It's better to have small, atomic commits that you refine later than to accumulate large uncommitted changes. - Use
--jsonflag for ALL commands when running as an agent - this provides structured, parseable output instead of human-readable text - Use
--dry-runflags (push, absorb) when unsure - Run
but pullfrequently — at session start, before creating branches, and before pushing. Stale workspace = merge conflicts - When updating this skill, use
but skill install --path <known-path>to avoid prompts - Check for
zzfiles with locks before finishing work. Runbut status --json -fand look for files inzzwith[LOCKED]markers. These won't auto-recover — you must manually commit each hunk to its correct branch using--changes <hunk-id>. See references/concepts.md — Hunk Locking for details. - Don't trust notifications alone — plugin notification delivery is ~55%. Always verify workspace state with
but status --jsonbefore making assumptions about branch assignments or commit status.
Plugin Auto-Behaviors (What Happens Behind the Scenes)
The GitButler plugin performs several actions automatically. You do NOT need to do these yourself — but you should know they happen so you don't duplicate work or get confused by unexpected state changes.
| Behavior | Trigger | What It Does |
|---|---|---|
| File auto-assign | After each file edit | Finds which branch owns the file and runs but cursor after-edit or but rub to assign it |
| Auto-commit | Session idle (agent stops editing) | Runs but cursor stop which commits all uncommitted changes to their assigned branches |
| LLM commit reword | After auto-commit | Rewrites generic commit messages using Claude Haiku based on the actual diff |
| Branch rename | After auto-commit | Renames ge-branch-* branches to descriptive names based on user's prompt |
| Empty branch cleanup | After auto-commit | Removes ge-branch-* branches with 0 commits (~88% success rate) |
| Session title sync | After auto-commit | Updates session title from branch name |
| Context injection | Before each agent message | Injects <system-reminder> with workspace notifications (branch created, commits made, etc.) |
What This Means for You
- Don't manually rename
ge-branch-*branches — the plugin will do it after idle - Don't reword auto-generated commit messages — the plugin rewrites them with LLM
- If you see unexpected commits — check if the plugin auto-committed during an idle event
- Notification delivery is ~55% — not all injected notifications reach you. Always verify state with
but status --jsonrather than relying on notifications - Empty branch cleanup can fail — if you see stale
ge-branch-*branches, run/b-branch-gc