@jakeginnivan/grove
v0.3.2
Published
Git worktree manager with profiles, shell integration, and agent-friendly non-interactive commands
Maintainers
Readme
grove
A git worktree manager. Clone once, then work on several branches at the same time in separate directories — no stashing, no branch switching.
The binary is grove; the shell integration gives you wt as the everyday
shortcut, with tab completion and directory-changing built in.
Cross-platform (macOS, Linux, Windows), with first-class non-interactive commands so coding agents can drive it.
Why worktrees
grove gives each repo this layout:
~/_code/my-service/
main/ # primary checkout — shared reference, read-only
260810-fix-login/ # a task worktree
260811-add-metrics-ABC-1/ # another, tagged with a Jira keyEvery directory is a real checkout backed by one clone, so switching tasks is
cd, not git stash.
Install
npm install -g @jakeginnivan/groveThen add the shell integration:
# ~/.zshrc — after your compinit
eval "$(grove shell-init zsh)"That defines a wt function which changes directory and completes repo names,
worktrees, branches, and profiles on Tab. Bash, fish, and PowerShell
are also supported — pass the shell name, or omit it to auto-detect.
A child process cannot change its parent shell's working directory. grove
prints a sentinel line and the generated wt function performs the cd. This
is the same approach zoxide and direnv use.
Completions are registered for both wt and grove. Place the eval after
compinit in your .zshrc; if compinit has not run, the function is still
defined and completion registration is skipped silently.
Everything works without the wrapper too — commands print the path instead of
jumping, so cd "$(grove pick myrepo)" remains available.
Configure paths and your branch prefix:
grove setupUsage
Examples use wt (the shortcut); grove works identically everywhere.
Clone and register a repo
wt clone [email protected]:owner/my-service.gitClones into <code-dir>/my-service/main and registers it. Existing clones can
be adopted with wt repos add <path>.
Start work on something
wt new my-service "fix flaky login test"Creates the branch you/fix-flaky-login-test from the latest origin/HEAD,
puts it in 260810-fix-flaky-login-test/, and cd's you into it.
With a ticket:
wt new my-service "fix login" --jira ABC-123
# branch: you/ABC-123-fix-login
# worktree: 260810-fix-login-ABC-123A key already present in the title is detected automatically.
Check out an existing branch
wt checkout my-service colleague/their-branchResolves a local branch first, then origin/<branch> (creating a local
tracking branch). Omit the branch and you get a searchable list of every local
and origin/ branch, most recently committed first, annotated with its age and
whether it is already checked out — type to filter by branch name or commit
subject. Pass --create for a branch that does not exist yet, which prompts for
the new name instead.
Stack a branch on another
By default a new worktree branches off the latest main. To build on work that is not merged yet:
wt new my-service "address feedback" --on 260810-fix-flaky-login-test--on accepts a worktree directory, a worktree path, or a branch name. The
parent is recorded in git config (branch.<name>.wt-parent), so wt list
shows the stack:
260810-fix-flaky-login-test you/fix-flaky-login-test
260810-address-feedback you/address-feedback on you/fix-flaky-login-testStacked branches have no upstream until you push; use git push -u origin HEAD.
Move around
wt pick my-service # interactive picker
wt pick my-service fix-login # jump by substring
wt pick my-service --main # jump to the main checkoutKeep main current
wt sync my-service # fetch + fast-forward the main checkout
wt sync # every registered reposync only fast-forwards when the main checkout is clean and on the default
branch. Otherwise it reports why it skipped and changes nothing.
Clean up
wt cleanup my-service # interactive multi-select
wt cleanup my-service --merged --yes # everything already merged
wt cleanup my-service 260810-old --yes # a specific worktree
wt cleanup self # the worktree you are standing inWorktrees with uncommitted changes or unpushed commits are skipped unless
you pass --force, and --yes alone will not override that. Removed
directories go to the system trash where available. --dry-run shows what
would happen.
Profiles
A profile is a base directory that repositories are grouped under. Typical setup: work code in one directory, alongside open-source dependencies you read but do not modify.
grove profile add work ~/_code/work \
--description "Internal work code" \
--rule "Never copy code from other profiles into this one (licensing)."
grove profile add oss ~/_code/oss \
--description "Open-source dependencies, for reference" \
--rule "Reference only. Do not copy source into work repositories."
grove profile default workA profile decides where grove clone puts a repo, and carries --rule
statements that the agent skills surface. Adding one configures it
immediately — there is no second step. Each write goes inside a marked block
grove owns, so your own settings survive:
| File | What it gets |
| --- | --- |
| <profile>/.gitconfig | A managed header; add your own per-profile git settings below it |
| ~/.gitconfig | includeIf gitdir: stanzas pointing at each profile config |
| ~/.claude/settings.json | additionalDirectories + Read(<dir>/**) so agents can read your repos |
The per-profile .gitconfig is wired up but intentionally empty. Anything you
add below the managed block applies to every repo in that directory — a work
email, a signing key, or pushInsteadOf rules if you want to block a host:
# ~/_code/work/.gitconfig, below the grove-managed block
[url "blocked://"]
pushInsteadOf = https://github.com/Because these files live outside the project, an interactive run lists them
and asks once before writing. Pass -y to skip the prompt, or --no-apply
to record the profile without touching anything.
Removing a profile cleans up after itself: its includeIf stanza and grove's
managed block both go, and hand-written config is left alone.
grove profile apply re-syncs everything. You need it only after editing
~/.config/grove/config.json by hand, or when setting up a new machine from
an existing config. It is idempotent; --dry-run previews the changes.
Once profiles exist, wt clone picks one: --profile wins, then the default
profile, then a prompt when several exist and none is the default.
Non-interactively without a default, pass --profile <name>.
wt clone [email protected]:facebook/react.git --profile ossSet or inspect the default at any time:
grove profile default # show the current default
grove profile default work # set it
grove profile default --clearCloning a URL into a profile that blocks its host is refused outright.
Rules are surfaced to agents through grove profile list --json and the
installed skills, so an assistant working in your work tree knows it may
read oss code but not copy it across.
Agent support
Install the skills
grove skills installThis detects the agent tools installed on your machine and asks which of them to install for, with all pre-selected:
Which tools should grove install skills for?
◉ Claude Code ~/.claude/skills
◉ Codex CLI ~/.codex/skills
◉ GitHub Copilot CLI ~/.copilot/skills
◉ Gemini CLI ~/.gemini/skillsTwo skills are installed:
- wt-repos — discovering cloned repos and their paths, which profile each
belongs to and what rules apply, and running
grove syncbefore reading code so the agent is not reasoning about a stale checkout. - wt-worktree — creating worktrees for tasks, checking out branches, stacking work, and cleaning up safely.
Each tool reads skills from its own directory, so grove writes a copy per
tool. Supported: claude, codex, copilot, cursor, gemini, and
opencode. Skip the prompt with --target:
grove skills install --target claude codex # named tools
grove skills install --target all # every known tool
grove skills install --target ~/somewhere # an explicit directoryWithout a TTY (--json, CI, or an agent), install goes to every detected
tool without prompting. grove skills list shows where the skills are
installed and which tools were detected; grove skills uninstall removes
them.
Non-interactive commands
Every command runs unattended. --json prints a machine-readable result and
implies --no-interactive:
grove repos --json
grove list my-service --json
grove new my-service --title "fix login" --json
grove checkout my-service some-branch --json
grove sync my-service --json
grove profile list --json
grove cleanup my-service 260810-old --yes --jsonHuman-readable output goes to stderr, so stdout stays a clean JSON stream.
When a required value is missing, the command exits 2 rather than hanging on a prompt:
{
"ok": false,
"error": {
"code": "needs_input",
"message": "A title is required in non-interactive mode.",
"hint": "Pass --title, or run in an interactive terminal."
}
}error.code is stable and safe to branch on. Notable codes: needs_input,
unknown_repo, unknown_profile, branch_exists, branch_in_use,
branch_not_found, worktree_exists, unknown_stack_parent,
no_matching_worktree, ambiguous_worktree, alias_conflicts_with_repo.
Prompts are also skipped automatically when stdin is not a TTY.
Per-repo setup commands
A repo can declare commands to run in each new worktree, via worktree.json
or .cursor/worktrees.json committed at the root of the repo itself (the
main/ checkout, not the parent directory holding the worktrees):
{
"setup-worktree": ["pnpm install", "cp $ROOT_WORKTREE_PATH/.env .env"]
}Commands run inside the new worktree with ROOT_WORKTREE_PATH pointing at the
main checkout. Because this is arbitrary repository-controlled shell code,
Grove does not run it by default. Review the file first, then opt in with
--setup. A failing command warns and continues rather than aborting the
worktree. The older --no-setup flag remains accepted for compatibility.
Configuration
grove setup writes ~/.config/grove/config.json:
| Key | Meaning |
| --- | --- |
| branchPrefix | Prefix for generated branches (default <user>/) |
| defaultCodeDir | Clone directory used when no profile matches |
| profiles | Named base directories, each with an optional description and rules |
| defaultProfile | Profile used when --profile is not given |
| reposFile | Repo registry location (default ~/.wt_repos) |
| useTrash | Trash removed worktrees instead of deleting |
| managedClaudePermissions | Internal ownership record used to revoke only Claude grants Grove added |
GROVE_BRANCH_PREFIX, GROVE_DEFAULT_CODE_DIR, GROVE_REPOS_FILE, and
GROVE_TRASH_DIR override the file. The older WT_* names are still honoured.
The registry format is shared with the original zsh helper, so an existing
~/.wt_repos keeps working:
my-service /Users/you/_code/my-service
ms my-serviceThe second line makes ms an alias.
Development
Node 24+ and pnpm 11+. With mise, mise install picks
both up from mise.toml; otherwise see pnpm's install
guide — pnpm ships a native binary, so Corepack
is not involved (Node removed it in v25).
pnpm install
pnpm build # bundle to dist/
pnpm test # unit + integration tests against real git repos
pnpm typecheckBuilt with TypeScript 7. Integration tests run the built bundle against
throwaway git repos with a redirected HOME, so they exercise what ships
without touching your real configuration.
CI runs the suite on Node 24 across Linux, macOS, and Windows.
Releasing
Releases are driven by changesets, which need no GitHub token and work offline. When you change something users can observe, describe it:
pnpm changesetPick the bump and write a sentence for the release notes. Commit the generated file with your change.
CI fails a branch that touches the package without one. That is wider than just
src/: changesets counts any tracked file outside a dot-directory, so docs/,
test/ and this README need one too. Changes confined to .buildkite/,
.github/ or .vscode/ do not.
For a change that ships nothing users can observe — a refactor, a test, a docs fix — add a changeset with no bump:
pnpm changeset add --emptyThat satisfies the check and adds nothing to the changelog.
On merge to main, CI opens a Version Packages PR that applies the pending
changesets — bumping the version and folding them into CHANGELOG.md. The
version PR is the release gate: nothing ships until you merge it.
Merging it stages the release on npm rather than publishing it. Staging needs no 2FA and so can happen in CI; approving requires 2FA and so happens from your machine:
npm stage list @jakeginnivan/grove
npm stage view <stage-id> # inspect before approving
npm stage approve <stage-id> # publishes itThe build annotates the staged version with these commands when it finishes.
See npm help stage for the
full flow.
Releases are published with a token rather than npm trusted
publishing over OIDC: npm does not
support Buildkite as a trusted publisher. Packages published this way carry no
provenance attestation, so npm audit signatures has nothing to verify.
License
MIT
