Skip to main content

Workforest

Git worktree forest management: one main checkout plus any number of disposable, per-branch worktrees in a predictable location — created, set up, opened, and cleaned up with one command.

~/dev/
├── api/                  # main checkout
└── worktrees/
    └── api/
        ├── feature-x/    # wf create feature-x
        └── fix-y/
  • Create a worktree for any branch (local, remote, or brand new) and have it set up automatically: symlinks for untracked assets (node_modules, .env, …) and project-defined setup scripts.
  • Open it in your editor — in the current shell, or in a new terminal window via a configurable command template.
  • Run named project scripts with well-known WF_* environment variables.
  • Delete worktrees safely, or checkout: collapse one back into the main checkout.
  • Drive everything from an interactive fzf TUI (wf with no arguments).

Install

# Arch Linux
yay -S workforest        # AUR

# macOS (or Linux with Homebrew)
brew install arkadyburyakov/tap/workforest

# anywhere else
uv tool install workforest   # or: pipx install workforest

This installs two commands: workforest and its alias wf. Then add one line to your ~/.bashrc / ~/.zshrc:

eval "$(workforest shell-init)"

This upgrades wf to a shell function (needed so wf open can change your shell's directory — a plain binary cannot) and registers completions. Without it everything still works, but "open in current shell" prints the cd command instead of performing it.

Requirements: Linux or macOS, git ≥ 2.36, Python ≥ 3.14 (the AUR and Homebrew packages bring their own). Optional: fzf for the TUI.

Quick start

wf create feature/login     # create worktree + run hooks + open in $EDITOR
wf list                     # what's in the forest
wf open login -o 'lazygit'  # open with any command instead
wf run test                 # run a named script from config
wf run make check -j2       # extra args are appended to the script command
wf checkout login           # fold the branch back into the main checkout
wf delete fix-y             # remove a worktree (asks about dirty changes)
wf                          # interactive TUI (fzf)

Any unknown first word is an opener shortcut: wf edit apiwf open api -o edit.

Configuration

Layered, YAML or JSON; later layers override earlier ones:

Layer Location Typical content
system /etc/workforest/config.yaml org-wide defaults
user ~/.config/workforest/config.yaml your terminal/editor setup
project (shared) .workforest.yaml in the repo root repo policy, committed
project (local) .vscode/ or .idea/ .workforest.yaml personal overrides, untracked

Scalars and lists replace; the scripts/openers mappings merge per key (null removes an entry). workforest config shows the merged result and where each layer came from; workforest init scaffolds a project file (--local for a personal one).

All keys, with defaults:

worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME"  # where the forest lives
opener: ""              # default opener; "" → $VISUAL → $EDITOR
openers: {}             # name -> command template, e.g. edit: "$EDITOR {target}"
window_command: ""      # "" → current shell; or e.g.
                        # "kitty --title {title} --directory {worktree} $WF_COMMAND"
symlinks: []            # untracked assets linked from main into new worktrees
setup_scripts: []       # shell snippets run in a fresh worktree
scripts: {}             # name -> snippet for `wf run NAME`

Openers and window_command are command templates sharing one variable family, which the launched process (and every script) also receives as environment variables:

Variable Value
WF_MAIN main worktree path, /home/user/Projects/project_name
WF_NAME repo name, project_name
WF_WORKTREES_DIR resolved worktrees directory
WF_WORKTREE this worktree's path
WF_BRANCH its branch (empty if detached)
WF_TARGET the -p argument, default . (launch-only)
WF_TITLE window label, project_name: feat-x (launch-only)

In templates, $WF_X (like any $ENV variable) inserts raw text that word-splits into multiple arguments, while {x}{worktree}, {target}, {title}, … — inserts the shell-quoted value as exactly one argument. Openers run with the worktree root as working directory; in window_command the resolved opener command is additionally available as $WF_COMMAND (spliced into argv words) or {command} (one argument, for $SHELL -c wrappers).

Fully commented reference configs: config.yaml (user/system) and .workforest.yaml (project) — installed to /usr/share/doc/workforest/examples/ by the Arch package.

Script environment

setup_scripts, scripts, and hooks run via $SHELL -c with:

Variable Value
WF_MAIN main worktree path
WF_NAME repo name (main checkout directory name)
WF_WORKTREE current/new worktree path
WF_WORKTREES_DIR resolved worktrees directory
WF_BRANCH branch of the current/new worktree

worktrees_dir is a template using the same naming pattern: $WF_MAIN and $WF_NAME (plus regular environment variables like $HOME) expand there — the per-worktree variables don't, since no worktree exists yet when the base directory is resolved.

Example project config

# .workforest.yaml — committed to the repo
symlinks: [node_modules, .env]
setup_scripts:
  - npm install --prefer-offline
scripts:
  test: npm test
  migrate: npm run db:migrate

Commands

workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
workforest open   [NAME]   [-o OPENER] [-p PATH]
workforest list   [--porcelain]
workforest delete NAME...  [--force] [--delete-branch | --keep-branch]
workforest checkout NAME   [--force]
workforest run    SCRIPT [ARGS...]
workforest tui    [MODE]
workforest init   [--local]
workforest config [--json]
workforest shell-init [bash|zsh]

Exit codes: 0 ok · 1 error · 2 usage · 3 cancelled · 4 config error. Human messages go to stderr; stdout carries only machine output (cd directives for the wf wrapper, --porcelain listings, dumps).

Development

uv sync           # venv + dev dependencies (uv.lock)
make check        # ruff + mypy --strict + pytest (coverage gate ≥ 90%)
make install      # install this checkout as a uv tool (~/.local/bin/workforest)
make uninstall    # remove it again

Packaging recipes live under packaging/ (one directory per package manager: packaging/AUR/, packaging/homebrew/). Release: bump __version__ and push to main — CI tags the release and publishes to PyPI, the AUR, and the Homebrew tap, committing the regenerated recipes back to the repo.

License

MIT

Release files for workforest 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for workforest 0.2.2
File Size Uploaded
workforest-0.2.2.tar.gz 74.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for workforest 0.2.2
File Interpreter ABI Platform
workforest-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 108.1 kB

Release files / workforest-0.2.2.tar.gz

Download URL workforest-0.2.2.tar.gz
Size 74.8 kB
Tags Source
SHA-256 checksum
How to use checksums
56a136c23ee2fd884af5ead398e5011f5bdcd34d135e1bd10edce3ea6b611067
BLAKE2b-256 checksum
How to use checksums
26add90b1a0e2931c89e1ebd9dc069981f7b85b16661f0847c8fa6c2f4e8c8af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / workforest-0.2.2-py3-none-any.whl

Download URL workforest-0.2.2-py3-none-any.whl
Size 33.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
179cfdbb459389e87b58971f586621cb768bee0bf43bac54e4e6c3c27d48b289
BLAKE2b-256 checksum
How to use checksums
5b450fe85e74b53139eebf18b6d674254f97042c5d71c50806ad1fba62ccb55d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page