fix(hooks): require approval before running hooks from a committed .wt.toml - #134
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #134 +/- ##
==========================================
+ Coverage 37.73% 39.68% +1.94%
==========================================
Files 33 34 +1
Lines 3506 3901 +395
==========================================
+ Hits 1323 1548 +225
- Misses 2089 2242 +153
- Partials 94 111 +17
🚀 New features to boost your workflow:
|
timvw
force-pushed
the
fix/hook-trust
branch
3 times, most recently
from
August 19, 2026 18:37
fd16cde to
5b2ce7c
Compare
…t.toml .wt.toml lives in the working tree, so it is committed and travels with the repository. wt merged its [hooks] table into the effective hook config and ran it through `sh -c` with no prompt and no opt-in, which made cloning an untrusted repo and running `wt create` arbitrary code execution as the invoking user (#129). git does not transfer its own hooks on clone for this reason, and direnv requires `direnv allow` before sourcing a committed .envrc; wt had neither protection. WT_HOOKS_DISABLED=1 does not count: it is opt-out, and only helps someone who already knows about the risk. Hooks now carry the config layer that supplied them, and anything that arrived from a repo-level .wt.toml needs approval before it runs. Approval is pinned to (repository, sha256 of .wt.toml): an edit — a pull that adds a post_create, a branch whose .wt.toml differs — asks again, and an identical .wt.toml in another repository is not covered, since `make setup` is only as safe as the Makefile next to it. Approvals live in ~/.config/wt/trust.toml, never in the repository and never in .git/config, which a repo handed over as a directory also owns. Declining skips the hooks and warns; it never aborts the operation, including for pre-hooks. Refusing to create a worktree because a repository asked to run something you declined would make the safe answer the expensive one. With no terminal to ask on — scripts, CI, --format json — the answer is skip, unless WT_HOOKS_APPROVE_ALL=1 says otherwise. hooks_policy (config file or WT_HOOKS_POLICY) chooses how much is gated: prompt-untrusted (default), prompt-all, trusted-only, off. prompt-all covers what trust alone cannot — your own post_checkout npm install runs whatever lifecycle scripts sit in the repo you are standing in. It is deliberately not read from .wt.toml: a repository picking how closely wt scrutinises that same repository's hooks would put the lock on the inside of the door. Adds `wt trust`, `wt trust --list` and `wt untrust`. Closes #129
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #129.
.wt.tomllives in the working tree, so it is committed and travels with the repository. wt merged its[hooks]table into the effective hook config and ran it throughsh -cwith no prompt and no opt-in — cloning an untrusted repo and runningwt createwas arbitrary code execution as the invoking user. git does not transfer its own hooks on clone for this reason, and direnv requiresdirenv allowbefore sourcing a committed.envrc.WT_HOOKS_DISABLED=1does not count: it is opt-out, and only helps someone who already knows about the risk.What changes
Hooks now carry the config layer that supplied them, and anything from a repo-level
.wt.tomlneeds approval before it runs:Approval is pinned to (repository, sha256 of
.wt.toml) and both must match. An edit — a pull that adds apost_create, a branch whose.wt.tomldiffers — asks again; an identical.wt.tomlin another repository is not covered, sincemake setupis only as safe as the Makefile next to it. Approvals live in~/.config/wt/trust.toml, never in the repository and never in.git/config, which a repo handed over as a directory also owns.The prompt shows every command the file contributes, not just the batch about to run, because approving persists trust for the whole file — a benign
post_createmust not be able to buy silent consent for an unseenpre_remove. Commands and paths are escaped for display so a.wt.tomlcannot redraw the prompt that is asking about it.Approving all hook execution
hooks_policy(config file orWT_HOOKS_POLICY) chooses how much is gated:prompt-untrustedprompt-alltrusted-onlyoffprompt-allcovers what trust alone cannot: your ownpost_checkout = ["npm install"]runs whatever lifecycle scripts sit in the repo you are standing in. It is deliberately not read from.wt.toml— a repository picking how closely wt scrutinises that same repository's hooks would put the lock on the inside of the door.Failure behaviour
Declining skips the hooks and warns; it never aborts the operation, including for pre-hooks. Refusing to create a worktree because a repository asked to run something you declined would make the safe answer the expensive one. With no terminal to ask on — scripts, CI,
--format json— the answer is skip, unlessWT_HOOKS_APPROVE_ALL=1says otherwise.Notes on the design
git worktree listactually registers this checkout against it — otherwise a directory shipping a.gitfile pointing at a repo you trust would inherit its approval. It falls back to the working tree's own path.wt removefirespost_removeafter git has deleted the directory wt was standing in.Testing
cmd/trust_test.go, including the security: committed .wt.toml executes arbitrary shell from untrusted repos #129 repro, trust invalidation on edit, non-transfer between repos, the spoofed-common-dir case (real git), and the deleted-cwd case.e2e/scenarios/hooks.yaml.go test ./...green, e2e 241 passed / 0 failed / 13 skipped.codexover 6 rounds until it returned no new findings.Docs
README.md,docs/configuration.md,llms.txtand the Claude Code skill all document trust andhooks_policy.