
Worktrunk 扩展机制实战指南Hooks、Aliases 与自定义子命令【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunkWorktrunk 是面向并行 AI Agent 工作流的 Git worktree 管理 CLI参见 README.md它提供了三种互补的扩展机制生命周期Hooks、可复用命令Aliases与 Git 风格的自定义子命令。本文以 docs/public/extending.md 为骨架结合仓库源码与测试系统讲解这三种机制的触发时机、配置语法、模板变量、参数路由与执行细节并给出可直接落地的实战配方。读完本文你将能够为 switch / start / commit / merge / remove 等生命周期事件编写自动化钩子用wt name封装自己的高频命令与多步骤流水线甚至通过PATH上的可执行文件扩展出任意语言的子命令。三种扩展机制总览Worktrunk 的扩展能力由三条彼此独立的路径构成设计上与 git 的惯例一脉相承HooksAliasesCustom subcommands触发方式自动生命周期事件手动wt name手动wt name定义位置TOML 配置TOML 配置PATH上任意可执行文件模板变量支持支持不支持通过仓库共享.config/wt.toml.config/wt.toml分发二进制语言Shell 命令Shell 命令任意语言三者共享同一个命名空间wt name的解析顺序是内置命令优先其次是别名最后才是 PATH 上的自定义子命令详见 src/commands/custom.rs 的模块说明。Hooks 与 Aliases 定义在同一个 TOML 配置中共享同一套模板引擎用户配置受信任而项目配置首次运行时需要批准。当用户与项目配置定义了同名条目时两者都会运行用户配置优先。Hooks生命周期自动化Hooks 是挂在 worktree 生命周期事件上的 shell 命令。十个 Hook 覆盖五个生命周期事件——switch、start、commit、merge、remove每个事件都有一个阻塞式pre-变体失败即中止操作和一个后台post-变体。完整类型与用途参考见 docs/public/hook.md。[pre-start] deps npm ci [post-start] server npm run dev -- --port {{ branch | hash_port }} [pre-merge] test npm test其中{{ branch | hash_port }}是模板过滤器hash_port将分支名哈希为 10000–19999 区间内稳定的端口保证每个 worktree 的 dev server 互不冲突——这是并行 worktree 场景下最常用的模式之一。从源码看 Hook 的执行模型src/commands/hooks.rs 的模块文档揭示了两个执行模型理解它们有助于预判 hook 的行为Plan-backed有 TOCTOU 防护pre-merge、post-merge、pre-remove、post-remove、post-switch、pre-start、post-start属于这一类。这些 hook 的批准与执行之间可能隔着 merge、rebase 或git worktree add等状态变更rebase 甚至可能改写调用方 worktree 的.config/wt.toml因此命令批准时会把已批准的命令冻结进ApprovedHookPlan执行器只渲染运行这份冻结值绝不再读一遍配置。Invocation-resolved无 gate→exec 变更pre-commit、post-commit、pre-switch、手动wt hook type和别名走这条路径配置在调用时解析通过同一个Repository实例的缓存保证批准与执行读到的字节一致。另外两个值得注意的源码事实每个 hook 都从发起调用的 worktree的.config/wt.toml选择命令与wt config show读的是同一份文件post-*钩子按来源user / project各自构成独立的后台流水线因此两者同时启动、互不等待写入同一文件或在同一 worktree 里跑 git 的并发 hook 可能产生竞争最好把相互依赖的命令放进同一个来源。用户与项目 Hook 的取舍维度项目 Hooks用户 Hooks位置.config/wt.toml~/.config/worktrunk/config.toml作用域单个仓库所有仓库批准需要不需要执行顺序pre-*在用户之后post-*与用户并行pre-*最先post-*与项目并行项目命令首次运行会弹出批准提示如▲ repo needs approval to execute 3 commands:批准记录保存在~/.config/worktrunk/approvals.toml命令一旦变更就需要重新批准。CI 场景可用--yes跳过提示用--no-hooks整体跳过钩子该参数被wt switch、wt merge、wt remove、wt step commit、wt step squash接受但不被wt hook接受。当用户与项目都定义了同名 hook 时可以用user:name或project:name语法精确定位运行某一个源码中ParsedFilter见 src/commands/hooks.rs 的测试实现了这一过滤器解析。Hook 的三种 TOML 形态Hook 的取值有三种形态由 TOML 形状决定详见 docs/public/hook.md 的 Hook forms 一节字符串单条命令如pre-start npm install表多条命令并发执行如[post-start]下同时启动server与watch流水线[[hook]]块序列按序执行块内多个键并发任一失败即中止后续步骤[[post-start]] install npm ci [[post-start]] build npm run build server npm run dev这里install先完成然后build与server一起跑。模板在流水线开始前做语法检查、执行到每一步时才渲染因此前一步可以通过wt config state vars写入{{ vars.key }}供后一步读取——wt hook type --dry-run与wt hook show --expanded会把这类变量引用原样保留而不是解析预览无法预知前一步的写入。Hook 模板变量与过滤器Hooks 可用的模板变量分为四类完整表格见 docs/public/hook.md 的 Template variables 一节active{{ branch }}、{{ worktree_path }}、{{ worktree_name }}、{{ commit }}、{{ short_commit }}、{{ upstream }}——随 worktree 变化operation{{ base }}、{{ base_worktree_path }}、{{ target }}、{{ target_worktree_path }}、{{ pr_number }}、{{ pr_url }}——与具体操作相关repo{{ repo }}、{{ repo_path }}、{{ owner }}、{{ remote_repo }}、{{ primary_worktree_path }}、{{ default_branch }}、{{ remote }}、{{ remote_url }}——整个仓库恒定default_branch在每个 worktree 中相同exec/user{{ cwd }}、{{ hook_type }}、{{ hook_name }}、{{ args }}以及来自wt config state vars的{{ vars.key }}。常用过滤器包括sanitize/与\替换为-、sanitize_db数据库安全标识符[a-z0-9_]、最长 48 字符并带哈希后缀、sanitize_hash文件系统安全名 3 字符哈希后缀保证不同原名不冲突、hash3 字符 base36 摘要、hash_port映射到 10000–19999 端口、dirname/basename、codename(n)确定性友好单词codename(2)约有 126 万种组合。这些过滤器在 src/cli/mod.rs 的 CLI 文档中有对应说明并被wt list的 URL 模板见 src/commands/list/collect/mod.rs与wt step eval见 src/cli/step.rs实际使用。未定义变量会直接报错可选行为请用条件或默认值{% if upstream %}git fetch git rebase {{ upstream }}{% endif %}。变量会自动做 shell 转义因此不要在{{ ... }}外加引号。此外hooks 会以 JSON 形式把全部模板变量写入 stdin模板表达不了复杂逻辑时可以用 Python 等脚本读取ctx json.load(sys.stdin)。Aliases可复用的wt name命令Aliases 配置在[aliases]表下把一段可复用的 shell 命令挂到wt name[aliases] deploy fly deploy --configfly.{{ env }}.toml --appmyproject-{{ branch }} open open http://localhost:{{ branch | hash_port }} since-main git log --oneline {{ default_branch }}..HEADwt deploy --envstaging wt openwt name的解析顺序是内置命令 → 别名 → 自定义子命令。内置命令永远优先——clap 只有在没有内置匹配时才会走到Commands::Custom因此别名不可能遮蔽wt switch这类内置命令但注意别名与wt step的子命令如commit、for-each同名时wt commit仍可运行该别名只是wt step commit路径被内置遮蔽参见 src/commands/alias.rs 中TOP_LEVEL_BUILTINS与BUILTIN_STEP_COMMANDS的区分。同名的用户别名与项目别名都会运行用户优先项目别名同样需要首次批准。模板与--KEYVALUE智能路由Aliases 使用与 Hooks 相同的模板引擎变量、过滤器、函数以及--KEYVALUE智能路由——模板引用了KEY就绑定为变量否则原样转发给{{ args }}。例如wt deploy --envstaging会设置{{ env }}如果某个别名模板没有引用env则该 token 原样进入{{ args }}。src/commands/alias.rs 的AliasOptions::parse是这条路由的源码实现几个值得掌握的细节--是字面转发转义符之后的每个 token 一律进入{{ args }}不做任何绑定。wt deploy -- --branchfoo会把字面量--branchfoo转发给{{ args }}即使模板引用了{{ branch }}键中的连字符规范化为下划线--my-varx绑定{{ my_var }}空白分隔形式--KEY VALUE会无条件消费下一个 token 作为值即使它以--开头因此推荐使用形式与 hooks 不同别名模板不自动填充操作上下文变量target、base、pr_number等但同样可用--KEYVALUE绑定。位置参数{{ args }}{{ args }}渲染为空格连接、经 shell 转义的字符串可直接拼进命令[aliases] s wt switch {{ args }}wt s some-branch wt s feature/api wt s has a space需要更精细的操作时可以索引{{ args[0] }}、循环{% for a in args %}…{% endfor %}、计数{{ args | length }}。一个特别实用的特性把{{ args }}转发给某个wt命令的别名会自动继承该命令的参数与标志补全——co wt switch {{ args }}使得wt co Tab与wt switch Tab一样补全分支名。检查与预览wt config alias show name打印模板原文wt config alias dry-run name [-- args...]打印渲染后的命令不执行。wt config alias show deploy wt config alias dry-run deploy wt config alias dry-run deploy -- --envstaging实现位于 src/commands/config/alias.rsdry-run复用与wt alias完全相同的参数解析器AliasOptions::parse与模板上下文因此预览结果与真实运行一致show不带名字则按名字顺序列出所有别名同名用户/项目条目都会展示用户在前。wt alias --help不会展示 clap 风格帮助页——模板本身就是文档——而是被拦截并提示转向wt config alias show/dry-run源码见 src/commands/alias.rs。多步骤流水线[[aliases.NAME]]定义多步骤流水线使用与 hooks 相同的[[block]]语义块按序执行块内键并发任一步失败即中止后续[[aliases.release]] test cargo test [[aliases.release]] build cargo build --release package cargo package --no-verify [[aliases.release]] publish cargo publish {{ args }}每一步都能看到相同的{{ args }}与绑定变量。wt release -- --dry-run会把--dry-run转发给publish不影响前面的步骤。改变父 shell 目录wt switch、离开被移除 source 的wt merge、以及移除当前 worktree 的wt remove即使从别名内部调用也会改变父 shell 的目录——Worktrunk 的 shell 集成负责把目录变更传播回去源码侧对应DirectivePassthrough对 CD 指令的透传见 src/commands/alias.rs。除此之外的 shell 状态不会持久化别名运行在子 shell 中cd、export等只影响该子 shell。延迟展开到嵌套的wt命令别名体在发起调用的 worktree 中只渲染一次所以一个wt step for-each别名如果直接写{{ branch }}这个值会在 for-each 迭代之前就被烤进当前 worktree 的分支名。{% raw %}…{% endraw %}可以延迟变量它作为字面量{{ branch }}存活过派发渲染再由 for-each 在每个 worktree 中展开。一个值得注意的坑延迟的{{ branch }}含有空格别名体的sh -c会把它拆成{{、branch、}}三个 token导致 for-each 报Failed to expand for-each argument: syntax error。解决办法是给 for-each 自己的sh -c …把值保持为单个 token[aliases] show-branches wt step for-each -- sh -c echo {% raw %}{{ branch }}{% endraw %}wt show-branches会打印每个 worktree 各自的分支名。wt switch --execute同样支持延迟-x之后的程序与参数保持分离因此要把延迟模板作为别名体的单个 token 加引号。这里{{ worktree_path }}针对的是正在创建的 worktree而不是别名发起时所在的 worktree[aliases] echo-target wt switch {{ args }} --no-cd --execute echo -- {% raw %}{{ worktree_path }}{% endraw %}仓库级变量如{{ default_branch }}则无需延迟——它在每个 worktree 中都相同裸写即正确。配方一把每个 worktree rebase 到其 upstream[aliases] up git fetch --all --prune; wt step for-each -- sh -c git rev-parse --verify -q {u} /dev/null || exit 0 g$(git rev-parse --git-dir) rebasing() { test -d $g/rebase-merge || test -d $g/rebase-apply; } rebasing exit 0 git diff --quiet HEAD || { git merge --ff-only --no-autostash {u}; exit 0; } git rebase {u} --no-autostash || { rebasing || exit 0; git rebase --abort; } wt up先抓取所有远程再把每个 worktree 对齐到自己的 upstream没有 upstream 或已有 rebase 进行中则跳过有已跟踪文件被修改或暂存则 fast-forward否则 rebase冲突则中止。它 rebase 到 git 原生的{u}而不是{{ … }}模板因此 git 自行解析每个 worktree 的 upstream无需延迟展开。由于清扫会碰到你离开每个 worktree 时的任意状态脚本的主体是各种守卫git fetch --all只要有一个远程失败就以非零退出。用连接时一个凭据过期的远程就足以跳过整个清扫用;则清扫仍会在已抓取的 refs 上继续且抓取错误照常打印git rebase拒绝在含已修改/暂存跟踪文件无论是否有东西可 rebase的 worktree 中运行——git merge --ff-only是 git 在那里仍然愿意做的部分把单纯落后的分支前进否则不做任何改动分叉的分支或与你的编辑冲突的传入文件都会原样留下 worktree 并打印原因。未跟踪文件既不触发拒绝也不触发git diff守卫所以只携带新文件的 worktree 仍会 rebase——除非某个新文件与传入提交新增的文件同名git 会拒绝覆盖rebasing函数区分git rebase失败的两种方式中途冲突会留下进行中的 rebase由 abort 回卷拒绝启动跟踪文件在你眼皮下被修改、未跟踪文件挡路、pre-rebasehook 拒绝则没有东西可 abort、没有东西要清理清扫继续——无条件git rebase --abort在那里会以fatal: no rebase in progress替代 git 自己的消息并以 128 退出两个分支都传--no-autostash因为全局的rebase.autostash/merge.autostash会破坏各自依赖的前提带冲突弹出的 autostash 会把冲突标记留在 worktree 中且仍以 0 退出清扫会对刚留下冲突的 worktree 误报成功。因此该清扫仅在留下需要关注的 worktree即 abort 本身失败时才非零退出。git 拒绝做的事都是原子拒绝清扫带着 git 的原因继续处理下一个 worktree。这一点在别名被用作 hook 步骤时尤其重要因为失败的步骤会中止流水线其余部分。配方二把进行中的改动搬到新 worktreewt switch --create会把你带进一个干净的 worktree。要带走已暂存、未暂存和未跟踪的改动可以配合git stash# .config/wt.toml [aliases] move-changes if git diff --quiet HEAD test -z $(git ls-files --others --exclude-standard); then wt switch --create {{ to }} --execute sh -- -c \ if [ $# -gt 0 ]; then exec $; fi worktrunk-move {{ args }} else git stash push --include-untracked --quiet wt switch --create {{ to }} --execute sh -- -c \ git stash pop --index; if [ $# -gt 0 ]; then exec $; fi worktrunk-move {{ args }} fi 运行wt move-changes --tofeature-xyz。守卫在无在途改动时跳过 stash否则git stash push捕获一切显式的sh -c步骤在新 worktree 中弹出保留暂存/未暂存的分层。--之后的内容作为 argv 转发在 pop 之后于新 worktree 中运行例如wt move-changes --tofeature-xyz -- claude会在那里打开 Claude。想复制而非移动就在 push 之后加一条git stash apply --index --quiet。配方三追踪指定 hook 的日志wt config state logs --formatjson输出结构化条目branch、source、hook_type、name、path用jq解析出一条记录后包进别名即可快速访问[aliases] hook-log tail -f $(wt config state logs --formatjson | jq -r --arg name {{ name | sanitize_hash }} --arg kind {{ kind }} .hook_output[] | select(.branch {{ branch | sanitize_hash }} and .hook_type $kind and .name $name) | .path | head -1) 运行wt hook-log --kindpost-start --nameserver即可跟踪当前分支上serverhook 的日志。--kind选择 hook 类型分支通过{{ branch }}取自当前 worktree。sanitize_hash把branch和name改写成文件系统安全形式并附加哈希后缀以保持不同原名唯一与 Worktrunk 落盘时应用的变换一致因此即使名称含/之类的字符别名也能解析到正确的日志。Custom subcommands把任意可执行文件变成wt name任何名为wt-name且位于PATH上的可执行文件都会变成wt name这正是 git 对git-foo使用的同一模式该能力当前标记为实验性。内置命令与别名优先于它。wt sync origin # runs: wt-sync origin wt -C /tmp/repo sync # -C is forwarded as the childs working directory参数原样透传、stdio 继承、子进程退出码原样传播。从源码看src/commands/custom.rs分发逻辑依次尝试别名 →PATH上的wt-name→ clap 原生InvalidSubcommand错误错误提示会把已配置的别名混入 did you mean 候选池。子进程被信号杀死时按 shell 惯例以128 signal呈现Interrupted普通非零退出则直接转发退出码而不额外打印错误行src/commands/custom.rs集成测试见 tests/integration_tests/custom.rs。别名在仓库外无法运行此时用户配置中定义的别名名会以明确的 aliases only run inside a git repository 错误结束而不会落到 PATH 查找。社区中已有按此模式实现的例子均可通过cargo install安装后以wt name使用仓库内不收录其源码worktrunk-sync按 git 历史推断的依赖顺序 rebase 堆叠的 worktree 分支cargo install worktrunk-sync后以wt sync运行workz为当前 worktree 提供无冲突的端口区间以及独立的数据库与 Docker Compose 项目合并进.env.local使并行 worktree 互不干扰安装后把其wt-workz适配器放入PATH以wt workz运行。参考Hooks 与 Aliases 的接口差异除了下表列出的差异Hooks 与 Aliases 行为一致。完整行为参考见 docs/public/hook.md。接口差异对照表维度HooksAliases调用方式wt hook type [args...]嵌套在hook内置命令下wt name [args...]顶层裸位置参数作为名字过滤器wt hook pre-merge test build只运行test和build转发给{{ args }}从位置参数到达{{ args }}必须用--wt hook pre-merge -- extra任意裸位置参数都会进入跳过批准标志支持子命令后--yes/-ywt hook pre-merge --yes仅全局形式wt -y alias别名后的--yes落入{{ args }}来源区分user:/project:/user:name/project:name过滤器语法用户先运行再项目无过滤器语法强制绑定转义--var KEYVALUE已弃用但仍强制绑定无智能路由是唯一途径--helpwt hook --help列出 hook 类型wt hook type --help展示该类型的标志与参数模板体即文档wt alias --help重定向到wt config alias show/dry-runwt --help与wt step --help会把已配置别名与内置命令一并列出检查wt hook show [type] [--expanded]wt config alias show name/wt config alias dry-run namestdin全部模板变量以 JSON 提供用json.load(sys.stdin)解析继承父进程 stdin管道透传wt switch等交互式 TUI 保留 tty模板上下文附加项hook_type、hook_name、各类型操作变量base、target、pr_number等在共享基础变量之上增加args小结Worktrunk 的三层扩展机制覆盖了从事件自动触发到手动快捷命令再到任意语言子命令的完整谱系Hooks 让 switch / start / commit / merge / remove 的生命周期自动化pre-*阻塞把关、post-*后台执行Aliases 让高频操作沉淀为带模板与智能参数路由的wt name命令支持{{ args }}、多步骤流水线与延迟展开Custom subcommands 则把扩展边界开放给PATH上的任意可执行文件。三者共享配置与模板引擎配合 docs/public/hook.md 的过滤器/函数/JSON 上下文参考、docs/public/tips-patterns.md 的更多配方以及 src/commands/alias.rs、src/commands/hooks.rs、src/commands/custom.rs 的源码实现你可以把这些机制组合成适合自己与并行 Agent 团队的工作流基础设施。【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考