An interactive TUI for managing git worktrees. Hop between branches instantly without stashing or committing.
| Term | What it is |
|---|---|
| Trunk | Your original repo directory. Can checkout any branch normally. |
| Leaf 🍃 | A worktree directory. Locked to one specific branch. |
myproject/ ← trunk (original repo, currently on 'main')
myproject--feature/ ← 🍃 leaf (branch 'feature', forked from main)
myproject--bugfix/ ← 🍃 leaf (branch 'bugfix', forked from develop)
You're working on a feature branch. A bug report comes in. You need to:
- Stash your changes
- Switch branches
- Fix the bug
- Switch back
- Pop your stash
- Remember what you were doing...
With hop, each branch lives in its own directory (a leaf). Switch instantly:
hop # Open selector, pick a leaf
hop bugfix # Jump to bugfix leafYour feature work is exactly where you left it—no stashing, no context switching.
# Download hop
curl -sL https://raw.githubusercontent.com/QiTianDaSh3ng/hop/main/hop.rb -o ~/.local/bin/hop
chmod +x ~/.local/bin/hop
# Set up your shell (interactive)
hop setupThat's it! Now run hop from any git repository.
hop add featureWhat happens:
- Creates a new branch called
feature - The branch forks from your current HEAD (whatever commit you're on)
- Creates a leaf directory
myproject--feature/ cds into the new leaf
# Before (trunk on 'main' @ abc123):
myproject/ ← you are here
# After:
myproject/ ← trunk (still on main @ abc123)
myproject--feature/ ← 🍃 you are here (leaf 'feature', forked from abc123)
hop add lm-fix --stayCreates the leaf but keeps you in your current directory. Perfect for spawning sandboxes:
🍃 Leaf created:
Leaf: lm-fix
Path: /home/you/myproject--lm-fix
Branch: ← main @ abc123
Trunk: /home/you/myproject
hop add experiment --cuttingA "cutting" in gardening is snipping off a piece of a plant including its current growth. The --cutting flag copies all files from wherever you currently are:
| What gets copied | Without --cutting |
With --cutting |
|---|---|---|
| Committed files | ✅ | ✅ |
| Modified files (not staged) | ❌ | ✅ |
| Staged files | ❌ | ✅ |
| Untracked files | ❌ | ✅ |
| Works with no commits (orphan) | ❌ Empty leaf | ✅ Full copy |
Key behavior: --cutting copies from your current directory, whether that's trunk or another leaf:
# In trunk
hop add first --cutting # Copies from trunk → "cutting from trunk"
# In 'first' leaf, make more changes...
hop add second --cutting # Copies from first → "cutting from first"This enables iterative workflows where each leaf can spawn new leaves with its latest state.
Use case: You're mid-work and want a language model to continue from your exact state:
hop add lm-fix --stay --cutting
# Language model gets your exact working directory to build on| Command | Description |
|---|---|
hop |
Interactive leaf selector |
hop [query] |
Selector with initial filter |
hop add <leaf> |
Create leaf, cd into it |
hop add <leaf> --stay |
Create leaf, stay in current dir |
hop add <leaf> --cutting |
Create leaf with all current files |
hop rm |
Remove current leaf (from inside) |
hop rm <leaf> |
Remove named leaf (from anywhere) |
hop list |
List all leaves |
hop boom |
Remove ALL leaves 💥 (with confirmation) |
hop boom --YES |
Remove ALL leaves (skip confirmation) |
hop setup |
Interactive first-time shell setup |
hop init |
Print shell wrapper (for manual setup) |
hop --help |
Show help |
| Flag | Works with | Description |
|---|---|---|
--json |
add --stay, rm, list, boom |
Machine-readable JSON output |
--quiet, -q |
all commands | Suppress emojis and decorative text |
For language models and automation, hop provides 100% headless operation with machine-readable output.
| Flag | Description |
|---|---|
--json |
Output machine-readable JSON |
--quiet, -q |
Suppress decorative output (emojis, colors) |
All these work without any TTY or user interaction:
# Create a leaf (returns path info)
ruby hop.rb add task-123 --stay --cutting --json
# Output: {"leaf":"task-123","path":"/path/to/repo--task-123",...}
# List all leaves as JSON
ruby hop.rb list --json
# Output: {"trunk":{...},"leaves":[...]}
# Remove a leaf
ruby hop.rb rm task-123 --json
# Output: {"removed":"task-123","path":"/path/to/repo--task-123"}
# Remove all leaves (no confirmation needed)
ruby hop.rb boom --YES --json
# Output: {"removed":[...]}# Create sandbox with current files
output=$(ruby hop.rb add lm-fix --stay --cutting --json)
leaf_path=$(echo "$output" | jq -r '.path')
# Work in the sandbox
cd "$leaf_path"
# ... make changes ...
# Clean up
ruby hop.rb rm lm-fix --quietFor human users with the shell wrapper installed:
# Create a sandbox for the language model with your current state
hop add lm-fix --stay --cutting
# Output:
# 🍃 Leaf created:
# Leaf: lm-fix
# Path: /home/you/myproject--lm-fix
# Branch: ← main @ abc123
# Trunk: /home/you/myproject
# Files: 🌱 main @ abc123
# Language model works in the leaf...
# You keep working in trunk...
# When done:
hop rm lm-fix # Remove from anywhere
# or
hop boom --YES # Nuke all leavesThe interactive selector shows a color-coded table:
🐇 Hop /home/you/myproject • 3 leaves
──────────────────────────────────────────────────────────────────────────
Search: █
──────────────────────────────────────────────────────────────────────────
NAME TIME FROM
──────────────────────────────────────────────────────────────────────────
→ 🏠 main Tue Dec 31 10:15 [2m] —
🌿 feature Tue Dec 31 10:10 [7m] main abc123 fresh
🌿 lm-fix Tue Dec 31 10:14 [3m] main abc123 cutting
🌿 attempt2 Tue Dec 31 10:16 [1m] main abc123 cutting [lm-fix] def456
──────────────────────────────────────────────────────────────────────────
fresh = clean branch cutting = includes uncommitted [leaf] = from leaf
↑↓ Navigate Enter Select Ctrl-D Delete Esc Cancel
| Element | Meaning |
|---|---|
| 🏠 | Trunk (main repository) |
| 🌿 | Leaf (worktree) |
| — | No origin info (trunk) |
branch commit |
Where leaf was forked from |
fresh |
Clean fork from HEAD |
cutting |
Includes uncommitted changes |
[leaf-name] |
Cutting was taken from another leaf |
When you run hop add feature:
| You're on | HEAD points to | New leaf feature starts at |
|---|---|---|
main |
commit abc123 | abc123 |
develop |
commit def456 | def456 |
| detached HEAD | commit 789xyz | 789xyz |
| no commits | (orphan) | empty (use --cutting to copy files) |
The new branch always forks from wherever HEAD is pointing. The trunk's branch doesn't change.
| Key | Action |
|---|---|
↑ / Ctrl-P |
Move up |
↓ / Ctrl-N |
Move down |
Enter |
Select / Create |
Ctrl-D |
Mark for deletion |
Esc |
Cancel / Exit delete mode |
Ctrl-A |
Beginning of line |
Ctrl-E |
End of line |
Ctrl-W |
Delete word backward |
Ctrl-K |
Delete to end of line |
Leaves are created as sibling directories:
~/code/myproject/ ← trunk
~/code/myproject--feature/ ← 🍃 leaf (branch: feature)
~/code/myproject--bugfix/ ← 🍃 leaf (branch: bugfix)
~/code/myproject--experiment/ ← 🍃 leaf (branch: experiment)
Hop stores leaf metadata in ~/.local/share/hop/:
~/.local/share/hop/
a1b2c3d4/ ← repo ID (hash of path)
feature.meta ← metadata for 'feature' leaf
lm-fix.meta ← metadata for 'lm-fix' leaf
Each .meta file contains:
parent_branch- Branch the leaf was forked fromparent_commit- Commit hash at fork timemode-freshorcuttingcopy_source- If cutting:trunkor leaf namecopy_source_commit- If cutting from leaf: that leaf's commitcreated- Timestamprepo_path- Full path to trunk
This keeps your repo and leaves clean—no hidden files added.
curl -sL https://raw.githubusercontent.com/QiTianDaSh3ng/hop/main/hop.rb -o ~/.local/bin/hop
chmod +x ~/.local/bin/hop
~/.local/bin/hop setupgit clone https://github.com/QiTianDaSh3ng/hop.git
cd hop
make install # Installs to ~/.local/bin/
hop setupUse hop init when you need manual control:
- Custom shell config location
- Non-standard shell setup
- Scripted/automated installation
- Inspecting or customizing the wrapper
hop init prints the shell wrapper function to stdout. Add to your shell config:
Bash/Zsh:
eval "$(hop init)"Fish:
eval (hop init | string collect){
inputs.hop.url = "github:QiTianDaSh3ng/hop";
imports = [ inputs.hop.homeManagerModules.default ];
programs.hop.enable = true;
}make test # Run test suite (70 tests)
make lint # Check Ruby syntax
make help # Show all available targets- Ruby (standard library only, no gems)
- Git
macOS comes with Ruby pre-installed. Most Linux distributions have it available.
hop setupadds a shell wrapper function- When you select a leaf, hop outputs a shell script (
cd /path/to/leaf) - The wrapper
evals this script, changing your directory
This is the same pattern used by z, zoxide, fzf, and other directory-jumping tools.
| Task | Git | hop |
|---|---|---|
| List worktrees | git worktree list |
hop (interactive!) |
| Create worktree | git worktree add ../path -b branch |
hop add leaf |
| Switch to worktree | cd ../project--branch |
hop → select |
| Remove worktree | git worktree remove path && git branch -D branch |
hop rm |
hop is inspired by try, but focused on git worktrees:
| Feature | try | hop |
|---|---|---|
| Purpose | Ephemeral experiment directories | Git worktree management |
| Naming | YYYY-MM-DD-name |
{trunk}--{leaf} |
| Location | Central directory (~/src/tries) |
Sibling to repo |
| Git integration | Optional detached worktrees | Core feature (named branches) |
Do Whatever The Fuck You Want
Hop between leaves like a rabbit. 🐇🍃