timber manages Git worktrees from registered bare repositories.
There is no required “main” worktree. Repositories are stored as bare Git directories, and worktrees are created on demand under a shared root:
<worktree-root>/<repo-name>/<worktree-name>/<repo-short-name>
Repository names may be structured with / (e.g. nnutter/timber).
The worktree leaf keeps only the short name (the last segment).
Defaults:
- bare repos:
$TIMBER_WORKTREE_ROOT/<repo-name>/.git(fallback:~/worktrees/<repo-name>/.git) - worktrees:
$TIMBER_WORKTREE_ROOT/<repo-name>/<worktree-name>/<repo-short-name>(fallback:~/worktrees/<repo-name>/<worktree-name>/<repo-short-name>)
Existing installs keep bare repositories under $XDG_DATA_HOME/timber/repos (fallback: ~/.local/share/timber/repos). timber repo fix moves them into the worktree root and repairs linked worktrees.
For Timber-created worktrees, the worktree name and branch name are identical (including /).
Timber-created branches track the repository's default branch, so the upstream names the base branch of the worktree's pull request.
A plain git push would then push to that tracked branch, so each registered repository records push.default=current: git push sends the checked-out branch to the branch of the same name on the remote, and leaves tracking alone.
timber repo add, repo init, and repo import record the setting; timber repo fix records it in repositories registered before it existed.
Pass an optional second argument to create a worktree at an exact checkout path instead of using the shared root:
timber create feature/login@timber /path/to/checkout
timber create feature/login@timber ../checkout
timber create @timber ../checkout # generated nameBranch-attached worktrees created directly with Git are also visible when linked to a registered repository.
Other manual worktrees use their checkout directory name; conflicts within a repository are resolved by adding parent directory components until the name is unique, reserving Timber-created names.
For example, two checkout directories can become one/checkout and two/checkout.
These derived names can change when conflicting worktrees are added or removed.
Detached worktrees are not listed or managed.
Example:
- repo name:
timber - bare repo:
~/worktrees/timber/.git - branch:
nn/my-feature - worktree path:
~/worktrees/timber/nn/my-feature/timber
Structured example (t repo add nnutter/timber defaults to the structured name; GitHub URLs keep owner/repo, other hosted paths keep their full grouping):
- repo name:
nnutter/timber - bare repo:
~/worktrees/nnutter/timber/.git - branch:
structured-names - worktree path:
~/worktrees/nnutter/timber/structured-names/timber
Rename with timber repo rename <old> <new> to move between simple and structured names; standard-layout worktrees move with the repo.
Sorting timber list --sort repo orders by the full structured name, so the grouping prefix controls grouping.
Use timber repo import <path> to register an existing clean clone and recreate its worktrees (including the former main checkout) in this layout.
When invoked through the shell wrapper (t repo import <path>), the shell also cds to $HOME after success.
Use timber repo init <name> to create a local repository without a remote.
It creates the bare repository with one empty commit on main, or on -b/--initial-branch <branch>, so worktrees can be created before the repository has any history.
Because there is no origin, created worktree branches have no upstream: timber remove needs --force for them until you add a remote.
Register a repository, create a worktree, inspect it with list, step inside it, and remove it again.
The still below shows list reporting a merged, clean worktree:
Prefer video? docs/demo/demo.mp4 shows the same run.
All three are generated from docs/demo/demo.tape; re-render them with mise run demo after changing the tape.
Install using Go,
go install github.com/nnutter/timber@latesttimber requires Git on PATH.
Install the Pi review extension and timber-review skill:
pi install https://github.com/nnutter/timberSelect and install the Timber skills for your agents with the skills CLI:
npx skills add https://github.com/nnutter/timberRun timber config to show settings and create the config file if absent.
The file is $XDG_CONFIG_HOME/timber/config.toml (fallback: ~/.config/timber/config.toml).
Automatic mise trust and automatic hk hook installation are disabled by default.
On the first eligible worktree creation in an interactive terminal, Timber offers to enable each feature.
The prompts default to No and save your answers, so Timber does not ask again.
After No, a prompt shows the matching timber config flag so you can enable the feature later.
An explicit --no-auto-mise-trust or --no-auto-hk-install also suppresses its prompt.
Non-interactive creation does not prompt or record a choice.
timber config --auto-mise-trust
timber config --no-auto-mise-trust
timber config --auto-hk-install
timber config --no-auto-hk-installEnabling automatic trust permits mise configuration in eligible worktrees to execute code.
Eligibility requires a root-level, regular mise.toml and a GitHub upstream owned by you or one of your organizations.
Timber uses authenticated gh lookups and caches the identity for 24 hours in $XDG_DATA_HOME/timber/github-identity.json (fallback: ~/.local/share/timber/github-identity.json).
Remove this cache after switching GitHub accounts or changing organization membership to force a fresh lookup.
If Timber cannot refresh an invalid cache, it warns and skips trust without failing worktree creation.
A failed mise trust also produces a warning and keeps the worktree.
Enabling automatic hk hook installation runs hk install in eligible worktrees.
Installed hooks run repository-defined commands on Git events.
Eligibility requires a root-level, regular hk.pkl and a GitHub upstream owned by you or one of your organizations.
Timber adds --mise when the worktree also has a root-level, regular mise.toml, so hooks can use mise-managed tools.
A failed hk install also produces a warning and keeps the worktree.
Linked worktrees share one hooks directory by default.
create and switch give each worktree its own directory at <git-dir>/hooks so that hooks can differ per worktree.
Timber copies the hooks that the worktree had before the split, and switch adds the setting to a worktree that an older Timber version created.
Timber sets core.hooksPath only when no scope sets it already.
A hooks path from your Git configuration, from a hook manager such as hk, or from an earlier Timber run stays in control.
A failure to configure hooks in switch exits non-zero, so the shell does not change directory.
Per-worktree configuration needs the extensions.worktreeConfig Git extension.
Timber turns it on when it first configures hooks, and moves the bare repository marker into the main worktree configuration first, because Git stops special-casing core.bare once the extension is on.
Git versions older than 2.20 reject this extension.
Entry points:
- internal/timber/config.go: settings and config command
- internal/timber/github_identity.go: identity cache
- internal/timber/mise_trust.go: eligibility and trust
- internal/timber/hk_install.go: hk hook installation
- internal/timber/worktree_hooks.go: per-worktree hook configuration
timber review embeds the Go implementation from nnutter/git-review.
It stores review state separately from the standalone tool.
History from the standalone tool and earlier Timber builds is not migrated.
Interactive review still requires tuicr on PATH.
timber review status --json
timber review log --oneline
timber review commits
timber review feedback --jsonA review covers the stack patch and the commit message that tuicr shows.
A rebase keeps a review because neither changes.
An amend! or reword! fixup, or applying one, changes the message and asks for a new review.
timber review auto delegates review of Refactor: stacks to an AI model.
An approval is recorded as model evidence and shows as Auto-Approved.
An abstention is recorded and shows as Auto-Reviewed until the stack content or message changes.
Each result is recorded as its stack completes, so a failed or interrupted run keeps finished work and the next run resumes with the stacks that have no result.
Configure the endpoint in $XDG_CONFIG_HOME/timber/model.json, a file that follows Pi's models schema:
{
"$schema": "https://pi.dev/schemas/models.schema.json",
"providers": {
"openai": {
"baseUrl": "https://api.openai.com/v1",
"api": "openai-responses",
"apiKey": "$OPENAI_API_KEY",
"models": [{ "id": "gpt-5.2", "maxTokens": 16384 }]
}
}
}A model must use api: "openai-responses".
--model or TIMBER_REVIEW_MODEL selects provider/model or an exact model ID; a file with one model needs no selection.
apiKey and headers values can be a literal, $NAME/${NAME} environment interpolation, or !security ... output, which keeps a secret out of the file.
Timber runs only /usr/bin/security with the remaining words as literal arguments and never uses a shell, so !security find-generic-password -s my-service -w reads one keychain item.
The flag beats the environment and the environment beats the file, so a missing file falls back to OPENAI_API_KEY and OPENAI_BASE_URL.
--dry-run assesses without recording results.
The generated shell wrapper also supports t review …, including nested command and flag completion.
Review commands operate on the current checkout, not a Timber worktree selector.
Run timber review --help for all commands and flags.
Install the review feedback skill with timber review --install-skill.
Use --force to replace an existing installation.
For the Pi extension and package installation, see pi/README.md.
timber bump creates the next semantic version tag for a worktree's repository and pushes it to origin.
timber bump --patch feature/login@timber
timber bump --patch @timber
timber bump --minor --pattern 'v1.*' feature/login@timber
timber bump --major --no-push feature/login@timber
timber bump --patch --release --no-open feature/login@timberExactly one of --major, --minor, or --patch is required.
The command reads the highest strict vMAJOR.MINOR.PATCH tag in the repository, creates the next tag as an annotated tag on the selected commit, and pushes it to origin.
Tags belong to the repository rather than to one checkout, so every worktree of a repository sees the same tags; the selector names the commit that the new tag points at.
<worktree>@<repo>tags that worktree's branch tip.@<repo>tags the repository's default branch, so a release names the commit thatoriginserves and needs no checkout; the command works outside a Git directory.- No selector tags the worktree that contains the current directory.
Before it reads tags or creates one, the command fetches the remote of the tracked branch, and origin, so a tag created on another machine is in the candidate list.
It refuses a worktree that is behind its upstream, because the new tag would name an outdated commit; update the worktree and run the command again.
The default branch is fetched, so the check does not apply to @<repo>.
A repository without a remote still bumps locally.
--patternlimits candidates the waygit tag -l <pattern>does. Tags that are not strict semver are skipped. A pattern that matches no tag is an error, so a typo cannot invent a fresh version; without a pattern, a repository without a usable tag starts atv0.0.0.--no-pushleaves the new tag local.--releasecreates a draft GitHub release for the new tag with authenticatedghand prints its URL after the tag. The URL is opened withopenorxdg-open;--no-openskips that. A missing or failing browser warns and still reports the bump as successful.
The new tag is on stdout, so TAG=$(timber bump --patch feature/login@timber) captures it.
Entry points:
- internal/timber/bump/version.go: parsing, ordering, and bumping of tags
- internal/timber/bump_tag.go: tag creation, push, draft release, and browser steps
- internal/cli/bump/bump.go: command flags and output
Install the bundled general-purpose Timber skill for agents with:
timber --install-skill
# To update an existing installation explicitly:
timber --install-skill --forceThis writes skills/timber/SKILL.md to ~/.agents/skills/timber/SKILL.md, using the same installation layout as timber todo --install-skill for the separate timber-todo checklist skill.
Existing skill files are preserved unless --force is given.
No repository registration or GitHub authentication is needed to install a skill.
Use a build containing this feature if an older binary does not recognize the flag.
The general skill covers repository discovery, explicitly qualified worktree creation, JSON status inspection, TODO lookup, and approval-gated cleanup.
It requires Timber and Git on the agent runner's PATH; GitHub PR status also requires authenticated gh.
It does not require the interactive zsh wrapper or Herdr.
See the skill for configuration, side effects, and safety limitations.
Agents that discover ~/.agents/skills can load the installed timber skill.
For agents with another skill directory, copy skills/timber into that agent's documented discovery location; the installer does not configure individual agents.
Restart or refresh the agent's skill catalog if it caches discovery.
Install the bundled Herdr plugin with:
timber herdr installThat command writes the plugin to ~/.config/herdr/plugins/timber ($XDG_CONFIG_HOME/herdr/plugins/timber when set) and runs herdr plugin link on the copy.
Copying the files is not enough on its own: Herdr only registers actions after plugin link or plugin install.
The popup runs timber tui --herdr --no-title after it adds common tool paths.
The TUI can create a new worktree or open a Herdr space for an existing one.
Worktree spaces open grouped under a parent space for the repository; the parent runs a Status tab that refreshes timber list @<repo> (with --pr when gh is authenticated).
The command prints a keybinding snippet to add to ~/.config/herdr/config.toml:
[[keys.command]]
key = "prefix+shift+s"
type = "plugin_action"
command = "nnutter.timber.open"
description = "open or create Timber Space"
[[keys.command]]
key = "prefix+shift+d"
type = "plugin_action"
command = "nnutter.timber.delete"
description = "delete Timber worktree"This repository uses mise for tools and tasks.
mise install
mise run setup
mise run allmise run all runs fixes, tests, and checks.
mise run fix applies every configured fixer, including go fix and go mod tidy.
mise run test runs the Go and plugin shell tests with coverage.
Zsh completion tests also need zsh, python3, and PTY support; mise run ci-test requires these dependencies rather than silently skipping those tests.
mise run check runs the configured checks, including gitleaks, and validates skills.
mise run coverage runs the tests and prints coverage.
mise run demo re-renders the demo GIF, MP4, and still from docs/demo/demo.tape (needs a working Chrome for VHS).
mise run setup installs the hk Git hooks from hk.pkl without removing other hook managers.
The commit-message hook requires timber on PATH.
Generate a zsh function that wraps the CLI (default name t):
timber generate zsh
# or: timber generate zsh --name t --out $XDG_DATA_HOME/zsh/site-functions --forceThe default command generates the wrapper t, the completion _t, and the autoload helper _t_autoload.
The helper starts with #autoload t, which lets compinit make t available without source or an explicit autoload command.
With --name foo, the command generates foo, _foo, and _foo_autoload, and the helper starts with #autoload foo.
Ensure the output directory is on fpath before zsh runs compinit, then restart zsh or run compinit.
The generated function:
- routes most commands to
timber(t create,t list,t prune, …) - after a successful
t create,cds into the new worktree unless--no-cd,--herdr, or automatic Herdr workspace creation applies - provides a shell-only
switchthatcds into a worktree t switch -cort switch --createcreates the worktree first, thencds, unless--no-cdor a Herdr space is openedt switch <name>uses that worktree if the name exists in exactly one registered repositoryt switch <Tab>completes worktree names from every registered repository- Unique names complete without
@ - If a name exists in more than one repository, completion offers
<name>@<repo>for each one (artisinal@liaison,artisinal@persona) - after a successful
t removeof the worktree the shell is in, or after a successfult repo import,cds to$HOME
t repo add nnutter/timber
t create feature/login@timber # then cd into it
t create feature/custom@timber ../checkout # custom location, then cd
t create @timber # random name, then cd into it
t switch feature/login@timber
t switch feature/login # unique name across repositories
t switch -c feature/new@timber # create then cd
t switch feature/new@timber -c # same; -c can follow the name
t tui # open an existing worktree or create a new one
t herdr install # install the Herdr plugin
t herdr space # current worktree
t herdr space feature/login@timber
t create --no-cd other@timber # create only
t remove feature/login # cd $HOME only if you are inside it
t list
t list @timberThe following aliases are available for the commands below:
| Command | Alias |
|---|---|
create |
c |
hooks |
hk |
list |
ls |
prune |
clean |
remove |
rm |
repo |
rp |
review |
r |
todo |
td |
The nested command groups have these aliases as well:
hooks commit-msg→hooks cmpr create→pr crepo list→repo lsrepo remove→repo rmrepo rename→repo mvreview approve→review a(review confirmalso works)review commits→review c(review runandreview challengealso work)review files→review freview log→review lreview status→review s
Zsh completion expands an alias to the command it stands for: t rp<tab> becomes t repo with a trailing space.
The expansion waits while a command name shares the typed prefix, so t r<tab> still lists review, repo, and remove.
Worktree commands take an optional <worktree>@<repo> qualifier on the name argument.
feature/login@timberselects that worktree in thetimberrepositoryfeature/loginis enough when the name exists in exactly one registered repository@timberselects the repository with no worktree name (creategenerates a random name;listandprunepin that repository)
list and prune use every registered repository unless @<repo> pins one.
create (and switch -c) use the current repository when the cwd is a managed worktree of a registered repo, otherwise an interactive picker.
remove, move, herdr space, and pr create with no name target the managed worktree that contains the cwd.
bump with no name targets the same worktree, and bump @<repo> targets the repository's default branch.
In non-interactive environments commands that need a single repository fail unless @<repo> is set or the cwd auto-detects a managed repo.
Worktree names must not contain @.
For full flags and examples, see timber --help and timber <command> --help.
| Command | What it does |
|---|---|
repo add |
Register a bare repository (GitHub owner/repo shorthand supported) |
repo init |
Create a local repository with an empty initial commit (-b/--initial-branch, default main) |
repo list |
List registered repositories with aliases and origin URLs |
repo import |
Convert an existing clone into a managed repository |
repo fix |
Move bare repositories from the old data directory into the worktree root, repair worktree links, and set push.default=current |
repo remove |
Unregister a bare repository once its worktrees are gone |
repo rename |
Rename a repository and move its worktree directories |
config |
Show settings or change them with --auto-mise-trust / --no-auto-mise-trust / --auto-hk-install / --no-auto-hk-install |
create |
Create a worktree for a branch (--no-herdr skips the Herdr space) |
switch |
Shell-only: cd into a worktree, creating it first with -c |
tui |
Interactively open an existing worktree or create a new one |
herdr install |
Install the bundled Herdr plugin |
herdr space |
Open a Herdr Agent + Shell space for a worktree |
list |
List worktrees with merge/dirtiness status, TODO.md progress, and commit review progress (--json, --pr, --sort, -L/--location for checkout paths in the last column) |
prune |
Remove clean, merged worktrees (--dry-run, --prompt) |
remove |
Remove one worktree and delete its branch (--force overrides safety) |
move |
Move a worktree checkout to a new path (name[@repo] <path>) |
pr create |
Open a GitHub pull request for a pushed worktree branch (--title, --body, --base, --draft) |
bump |
Create and push the next semantic version tag (--major/--minor/--patch, --pattern, --no-push, --release, --no-open) |
hooks commit-msg |
Add a Gerrit-compatible Change-Id to a commit message (timber review commit-msg stays as a hidden alias) |
todo |
Open the current worktree's TODO.md in $EDITOR |
generate zsh |
Generate the t wrapper, completion, and autoload helper |
# once: install wrapper
timber generate zsh
# register a repo
t repo add nnutter/timber
# day to day
t create feature/login@timber
t switch feature/login@timber
# ... work ...
t switch main@timber # if you created a main worktree
t prune @timber
# or:
t remove feature/login
