Hooks now require approval
This is a breaking change. A hook no longer runs because it is there.
Until now, a command in a repository's .wt.toml ran the moment wt create reached it. Clone a repository, make a worktree, and whatever its post_create said had already happened. That is the wrong default for a file anyone can commit, so v0.4.0 inverts it: wt shows you the command and asks, once, and remembers your answer.
$ wt create feature/api
⚠ These commands come from ~/src/acme/api/.wt.toml (not trusted):
→ [post_create] npm ci && ./scripts/setup-env.sh
[post_checkout] direnv allow
→ runs now; approving covers the rest too.
? Run these hooks?:
▸ Skip these commands
Run once
Run, and remember this until the commands changeThe safe answer is first, so an accidental Enter skips. It is the direnv model — nothing runs until you say so, and saying so is cheap.
The commands and the path they came from are printed with anything non-printable escaped. An approval prompt that the thing being approved can redraw — with an ESC[2J, a carriage return, or a right-to-left override — is not an approval prompt.
An approval is keyed by the commands the source declared, so editing them asks again and reverting them does not. Approving a repository does not approve the one next to it.
wt trust # approve this repository's hooks ahead of time
wt trust --list # what you have approved, and where
wt untrust # revokeFor trees you own outright, a whitelist in your config file skips the question entirely:
[trust]
prefix = ["~/src/mine", "~/work"]Three policies, set as hooks_policy in your config file or WT_HOOKS_POLICY in the environment:
| Policy | Behaviour |
|---|---|
prompt-untrusted (default) |
Anything not yet approved is shown and confirmed; approved hooks run |
prompt-all |
Every batch is confirmed every time; nothing is remembered, and [trust] rules do not apply either — "ask me every time" has to mean every time |
trusted-only |
Never prompts — approved hooks run, anything else is skipped. The one for CI |
off |
No hooks run at all |
WT_HOOKS_APPROVE_ALL=1 is the escape hatch for automation you control. It skips the check outright, so do not export it in a shell you browse repositories from.
There is no migration
Hooks you were already running will be asked about once. That is deliberate — a migration that silently blessed whatever was on disk would defeat the point of the change, and the whole value of an approval is that a person made it.
hooks_policy and [trust] are read from your config file and nowhere else
Not from .wt.toml, not from git config. The thing being gated does not get to supply the gate. A repository that could set its own hooks_policy to trusted-only and ship a [trust] rule covering itself would need no bypass at all.
Placements that would supply the gate are refused
A repository may choose the worktree pattern — that is project policy, and the point of the setting. But a pattern rendering to an absolute path is not anchored anywhere, so wt refuses to place a worktree where the checked-out files would become the gate:
wt's own config directory, config file, and trust store — the record of what you have approved- git's global configuration, and the files it pulls in via
include/includeIf, however deep —core.hooksPathis a hook command under another name - any repository's git directory, bare ones included. A bare repository has no
.gitin its path, so git itself is asked (git rev-parse --resolve-git-dir) rather than the name being read
Paths are compared with case folded and symlinks resolved, and a path that names a different directory depending on which process asks — /proc/self/cwd and friends — is not accepted as naming one at all.
This is a mitigation with stated boundaries, not a proof. The known limits are documented in docs/configuration.md and tracked as #154 and #155.
Docs: Requiring approval for every hook · #152 · #153
Copy files into new worktrees from git config
The [files] lists are now readable from git config at both scopes, as multi-valued wt.copy, wt.link and wt.exclude:
git config --local --add wt.copy .env
git config --local --add wt.copy .envrc
git config --local --add wt.link node_modules
git config --global --add wt.exclude '*.pem'--local lives in .git/config, so there is nothing committed and nothing to gitignore — and linked worktrees share the main repository's .git/config, so it applies from every worktree of the repo. --global sets defaults everywhere.
Lists accumulate across layers rather than overriding one another: a pattern set globally and another set locally both apply. wt config show prints the effective list and credits each pattern to the layer it came from, and wt copy --dry-run shows what would happen without changing anything.
Copies use reflinks where the filesystem supports them (APFS, Btrfs, XFS), so a large node_modules costs approximately nothing.
Together with the built-in file copy, this closes the case that previously needed a hook: copying files into a new worktree needs neither a hook nor a config file now.
Docs: Files · Closes #125 · #151
wt.repoRoot and wt.repoPattern are settable
Two of the advertised git config keys used an underscore, and git config variable names allow only alphanumerics and -:
$ git config --local wt.repo_root ~/dev/repos
error: invalid key: wt.repo_rootSo wt.repo_root and wt.repo_pattern were documented, read, and completely unsettable. The secondary failure was worse than the missing setting: writing the key by hand — or generating it, e.g. via home-manager's programs.git.extraConfig, which does not validate key names — makes git refuse to parse the whole file, at which point every wt.* setting in that scope silently stops applying.
Both now use the camelCase spelling wt.copyIgnored already used. The convention, stated once: a git config key is the TOML key with any section flattened and multi-word names camelCased.
Upgrading
brew upgrade timvw/tap/wt # or: go install github.com/timvw/wt@v0.4.0Expect to be asked about hooks you were already running. Approve them once with wt trust, or whitelist the trees they live in.
Full diff: v0.3.0...v0.4.0