
Worktrunk 扩展机制完全指南Hooks 生命周期钩子、Aliases 别名与自定义子命令实战【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunk本文系统讲解 WorktrunkGit worktree 管理 CLI专为并行 AI Agent 工作流设计提供的三种扩展机制Hooks生命周期钩子、Aliases别名命令与Custom subcommands自定义子命令。通过本文你将掌握如何在.config/wt.toml中配置自动化钩子与可复用别名、如何编写多步骤流水线、如何利用模板引擎做智能参数路由与延迟展开以及如何通过PATH上的wt-name可执行文件扩展任意语言的子命令——让 Worktrunk 完全贴合你的团队与工作流。三种扩展机制总览Worktrunk 的扩展能力由三套机制构成它们触发方式、定义位置与能力边界各不相同HooksAliasesCustom subcommands触发方式自动生命周期事件手动wt name手动wt name定义位置TOML 配置TOML 配置PATH上的任意可执行文件模板变量支持支持不支持通过仓库共享.config/wt.toml.config/wt.toml分发二进制语言Shell 命令Shell 命令任意语言Hooks 与 Aliases 存放在同一份 TOML 配置中共享同一套模板引擎。用户配置~/.config/worktrunk/config.toml被信任项目配置.config/wt.toml首次运行需要审批当用户与项目配置定义了同名条目时两者都会执行用户优先。命令解析优先级从源码看顶层命令解析遵循严格的优先级链见 src/commands/custom.rs内置命令 → 别名Alias→PATH上的wt-name可执行文件。内置命令永远优先clap 只有在没有匹配到内置子命令时才会进入Commands::Custom分发分支因此不存在用别名或PATH上的wt-switch覆盖wt switch的可能别名优先于 PATH 二进制解析到别名后即走try_alias路径执行用户配置优先于项目配置同一名字的 PATH 二进制不会被调用且别名执行需要处于 git 仓库内在仓库外会直接报别名只能在 git 仓库内运行并终止不会回退到 PATH 二进制两者皆无合成 clap 原生的InvalidSubcommand错误错误提示的 did you mean 候选会混入已配置的别名名并附带嵌套子命令提示如wt squash→perhaps wt step squash?。这一解析逻辑在 tests/integration_tests/custom.rs 中有完整测试覆盖包括wt wt-test-extcmd-ok能找到PATH上的wt-wt-test-extcmd-ok并透传参数。Hooks五个生命周期事件 × 十个钩子Hooks 覆盖五个生命周期事件——switch切换、start创建、commit提交、merge合并、remove移除——每个事件各有一个阻塞式pre-变体失败即中止操作与一个后台post-变体事件pre-阻塞post-后台switchpre-switchpost-switchcreatepre-startpost-startcommitpre-commitpost-commitmergepre-mergepost-mergeremovepre-removepost-remove各钩子的定位与典型用途详细参考见 hook.md钩子用途pre-switch切换前在源 worktree 中运行——无论创建、切换到已有分支还是停留在当前分支post-switch所有切换结果后触发创建、切换到已有、停留在当前pre-start新 worktree 创建时运行一次阻塞post-start/--execute直至完成依赖安装、env 文件生成post-start新 worktree 创建时后台运行一次dev server、长构建、文件监听、缓存复制pre-commit格式化、lint、类型检查——在任何 Worktrunk 提交wt step commit、wt step squash及wt merge产生的提交之前运行post-commitCI 触发、通知、后台 lintpre-merge测试、安全扫描、构建验证——在 rebase 之后、合并到目标之前运行post-merge部署、通知、安装更新后的二进制。在目标分支的 worktree 中运行不存在则在主 worktreepre-removeworktree 删除前的清理保存测试产物、备份状态。在待删除的 worktree 中运行post-remove停止 dev server、移除容器、通知外部系统。模板变量指向已移除的 worktree最常用的创建钩子是post-start——它以后台方式运行任务dev server、文件复制、构建不阻塞 worktree 创建。除非后续步骤必须等该任务完成否则优先post-start而非pre-start。最简单的 TOML 配置示例[pre-start] deps npm ci [post-start] server npm run dev -- --port {{ branch | hash_port }} [pre-merge] test npm test合并流水线中的钩子顺序在wt merge期间阻塞式钩子按pre-commit → pre-merge → pre-remove顺序运行合并完成后所有post-*钩子一起启动各自锚定在对应 worktree 上——post-merge、post-switch 与 post-remove 在目标 worktreepost-commit 在提交产生的那个 worktree。若合并移除了该 worktreepost-commit会报告为跳过worktree 已不存在此时应使用pre-remove完成必须在移除前做的工作或用--no-remove保留 worktree。完整流水线见 merge.md。钩子的三种 TOML 形态钩子由 TOML 形状决定三种形态详见 hook.md字符串——单条命令pre-start npm install表——多条命令并发运行[post-start] server npm run dev watch npm run watch流水线——[[hook]]块按顺序执行块内多 key 并发任一步骤失败即中止后续[[post-start]] install npm ci [[post-start]] build npm run build server npm run dev这里install先运行随后build与server并发。模板会在流水线启动前做语法检查、随步骤执行时渲染——因此一个步骤可以写入按分支存储的变量后续步骤通过{{ vars.key }}读取。大多数钩子并不需要[[hook]]块只有存在依赖链典型如先装依赖、再并发跑构建与 dev server时才需要。项目钩子与用户钩子维度项目钩子用户钩子位置.config/wt.toml~/.config/worktrunk/config.toml作用域单一仓库所有仓库或按项目覆盖审批必需不需要执行顺序pre-*在用户钩子之后post-*与用户钩子并行pre-*最先post-*与项目钩子并行当用户与项目配置定义了同名钩子时可用user:name或project:name语法指定来源。pre-*钩子阻塞命令两个来源合为一条流水线用户命令先跑其失败会跳过项目的命令post-*钩子后台运行每个来源是独立的分离流水线——它们同时启动、互不等待一方的失败不影响另一方。两个post-*钩子若写同一文件或在同一 worktree 中跑 git 会竞争相互依赖的命令应放在同一来源中。安全与审批项目命令首次运行需要审批hook.md▲ repo needs approval to execute 3 commands: ○ pre-start install: npm ci ○ pre-start build: cargo build --release ○ pre-start env: echo PORT{{ branch | hash_port }} .env.local ❯ Allow and remember? [y/N]审批保存在~/.config/worktrunk/approvals.toml命令一旦变更需重新审批拒绝会跳过该操作的全部项目命令含已审批的并继续已保存的审批不受影响--yes绕过提示——适合 CI 与自动化--no-hooks跳过钩子——由执行钩子的命令接受wt switch、wt merge、wt remove、wt step commit、wt step squashwt hook不接受。审批管理用wt config approvals add与wt config approvals clear。底层实现上src/commands/hooks.rs凡是审批门禁与执行之间夹着状态变更merge、rebase、remove、git worktree add的钩子都采用plan-backed 模型门禁时一次性加载项目配置、选定命令并冻结成ApprovedHookPlan执行器只渲染运行这份冻结值杜绝 TOCTOU检查与使用之间的竞态导致执行了未经审批的命令。模板变量与过滤器钩子可使用运行时展开的模板变量完整清单见 hook.md按类别分为active随 worktree 变化{{ branch }}分离头下未定义、{{ worktree_path }}、{{ worktree_name }}、{{ commit }}、{{ short_commit }}、{{ upstream }}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 }}exec{{ cwd }}、{{ hook_type }}、{{ hook_name }}、{{ args }}user{{ vars.key }}wt config state vars的按分支变量。裸变量branch、worktree_path、commit指操作作用的分支switch/create 指目的地merge/remove 指来源base与target给出另一侧方向对照表见 hook.md。变量自动做 shell 转义——{{ ... }}外无需引号加引号反而可能因特殊字符出问题。未定义变量会报错可用条件或默认值处理可选行为[pre-start] # 若跟踪远程分支则 rebase 到上游如 wt switch --create feature --base origin/feature sync {% if upstream %}git fetch git rebase {{ upstream }}{% endif %}模板还支持 Jinja2 过滤器完整清单见 hook.mdsanitize/与\替换为-、sanitize_db数据库安全标识符[a-z0-9_]最多 48 字符带哈希后缀、sanitize_hash文件系统安全名变更时追加 3 字符哈希后缀保证唯一、hash3 字符 base36 摘要、hash_port哈希到 10000–19999 端口、dirname/basename、codename(n)确定性友好词汇等。经典的每 worktree 独立 dev server 端口就是hash_port[post-start] dev npm run dev -- --host {{ branch }}.localhost --port {{ branch | hash_port }}任何字符串都可哈希包括拼接# 每个 repobranch 组合唯一端口 dev npm run dev --port {{ (repo ~ - ~ branch) | hash_port }}worktree_path_of_branch(branch)函数可按分支名查另一 worktree 的路径用于跨 worktree 引用文件[pre-start] # 从主 worktree 复制配置 setup cp {{ worktree_path_of_branch(main) }}/config.local {{ worktree_path }}此外钩子执行时所有模板变量会以JSON 形式通过 stdin传给命令未设置的变量在 JSON 中同样缺席需用默认值读取适合模板难以表达的复杂逻辑[pre-start] setup python3 scripts/pre-start-setup.pyimport json, sys, subprocess ctx json.load(sys.stdin) if ctx.get(branch, ).startswith(feature/) and backend in ctx[repo]: subprocess.run([make, seed-db])值得一提的专用命令是wt step copy-ignored见 step.mdgit worktree 共享仓库但不共享未跟踪文件它负责在 worktree 间复制 gitignore 文件典型用法是放进post-start[post-start] copy wt step copy-ignoredAliases可复用的wt name命令别名在[aliases]下配置[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的解析顺序是内置命令 → 别名 → 自定义子命令见前文命令解析优先级。模板与智能参数路由别名与钩子共用同一套模板引擎变量、过滤器、函数以及--KEYVALUE智能路由模板引用了KEY则绑定否则转发给{{ args }}。例如wt deploy --envstaging会设置{{ env }}。从 src/commands/alias.rs 的AliasOptions::parse可见完整路由文法--KEYVALUE若KEY被模板引用则绑定KEYVALUE否则两个部分都作为位置参数转发--KEY VALUE空格形式无条件消费下一个 token 作为值——即使它以--开头。要避免歧义请用形式或把其他 flag 放在被绑定的 key 之前--KEY在末尾作为位置参数转发--字面转发转义——之后的所有 token 直接进入{{ args }}不做任何绑定连字符规范化key 中的-会转为_minijinja 将{{ my-var }}解析为减法所以--my-varvalue在模板引用{{ my_var }}时绑定它。别名的模板上下文还额外提供{{ args }}位置参数。操作上下文变量target、base、pr_number不会自动填充但依然可以通过--KEYVALUE绑定。变量渲染时机值得注意别名主体在分发时只渲染一次在调用所在的 worktree 中因此一个在wt step for-each中打印相同分支的别名{{ branch }}早已被烘焙成调用 worktree 的分支——这引出了下面的延迟展开技巧。位置参数{{ 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 }}时参见 hook.md 的 Passing values 一节。--之后的 token无条件转发绕过任何绑定。写wt deploy -- --branchfoo会把字面量--branchfoo转给{{ args }}——即使模板引用了{{ branch }}也一样。将{{ args }}转发给wt命令的别名——如co wt switch {{ args }}或cm wt step commit {{ 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别名没有 clap 风格的帮助页——模板本身就是文档wt alias --help会重定向到wt config alias show/dry-runwt --help与wt step --help会把已配置的别名列在内置命令旁边被内置命令遮蔽的别名会标注(shadowed by built-in)。别名相关输出通过 src/commands/alias.rs 的augment_help拼入 clap 渲染的帮助文本。多步骤流水线[[aliases.NAME]]定义流水线采用与钩子相同的[[block]]语义块按顺序执行、块内 key 并发、步骤失败中止剩余部分[[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不影响前面的步骤。从实现看src/commands/alias.rs 的run_alias别名的用户/项目主体分开保存AliasEntry { user, project }执行时用户优先、项目在后各按自身信任体制运行用户步骤跳过审批项目步骤需审批所有步骤通过共享的execute_pipeline_foreground以前台方式执行。改变目录wt switch、wt merge离开被移除的源 worktree 时与wt remove移除当前 worktree 时会改变父 shell 的工作目录——即使从别名内调用也一样Worktrunk 的 shell 集成会把目录变更传播出去。但其他 shell 状态不会持久别名在子 shell 中运行cd、export等只影响该子 shell。延迟展开到嵌套wt命令上文提到别名主体在分发时渲染一次。{% raw %}…{% endraw %}可延迟变量它作为字面量{{ branch }}存活过分发渲染随后由 for-each 在每个 worktree 中展开。对for-each有一个坑延迟的{{ branch }}含空格别名主体的sh -c会把它拆成{{、branch、}}三个 token导致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 中相同裸{{ default_branch }}在任何地方都已正确。配方一把每个 worktree rebase 到其上游[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先 fetch 所有远程再把每个 worktree 对齐到其上游无上游或已有 rebase 在进行则跳过有已修改/已暂存的跟踪文件则 fast-forward否则 rebase遇冲突则中止。它 rebase 到 git 原生的{u}而非{{ … }}模板因此由 git 解析每个 worktree 自己的上游无需延迟。扫描会命中你以任意状态留下的每个 worktree所以脚本主体是各种守卫git fetch --all在任一远程失败时退出非零。用时一个凭据过期的远程就足以跳过整个扫描连 refs 正常 fetch 的 worktree 也不会 rebase用;则基于已 fetch 的部分继续扫描fetch 错误照常打印git rebase拒绝在含已修改/已暂存跟踪文件的 worktree 中运行无论是否有东西可 rebase——你在编辑的 worktree 会拖垮整个扫描。git merge --ff-only是 git 在此仍会执行的 rebase 部分它推进单纯落后的分支否则不改变任何东西——分叉的分支或与你编辑冲突的传入文件会让 worktree 保持原样并打印原因。未跟踪文件既不触发拒绝也不触发git diff守卫因此只含新文件的 worktree 仍会 rebase——除非新文件与传入提交新增的文件同名git 拒绝覆盖rebasing区分git rebase失败的两种情形中途冲突会留下进行中的 rebase由 abort 回退拒绝启动跟踪文件被改、未跟踪文件挡路、pre-rebase钩子拒绝则没有可 abort 的东西、无需清理扫描继续——无条件git rebase --abort此时会回答fatal: no rebase in progress并以 128 退出顶掉 git 自己的消息两个分支都传--no-autostash因为全局rebase.autostash或merge.autostash会破坏两个分支各自依赖的机制带冲突的 autostash pop 会留下冲突标记却仍以 0 退出扫描会对刚陷入冲突的 worktree 报告成功且 autostash 让树短暂干净本应拒绝的 fast-forward 会通过碰撞落到 pop 上。因此该扫描只在留下需要人工处理的 worktreeabort 自身失败时退出非零。git 拒绝的事都是原子拒绝扫描带着 git 的原因继续处理下一个 worktree。这一点在别名作为钩子步骤时很关键——失败的步骤会停掉流水线其余部分。配方二把进行中的改动移动或复制到新 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 中 pop保持暂存/未暂存的分割。--之后的一切作为 argv 转发在 pop 之后于新 worktree 中运行。例如wt move-changes --tofeature-xyz -- claude会在那里打开 Claude。要复制而非移动在 push 后加git stash apply --index --quiet即可。配方三tail 某个具体的钩子日志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运行tail 当前分支上server钩子的日志。--kind选钩子类型分支经{{ branch }}从当前 worktree 取得。sanitize_hash把branch与name改写为带哈希后缀的文件系统安全形式与 Worktrunk 落盘时相同的变换保证任一含/等字符时也能解析到正确的日志。Custom subcommandswt-name即插即用标注为 experimental。PATH上任意名为wt-name的可执行文件都可用作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 的run_custom实现细节看参数、环境与工作目录透传后以Command::status()等待信号杀死的子进程以WorktrunkError::Interrupted呈现按 shell 惯例退出码为128 signal非零退出以AlreadyDisplayed呈现退出码原样转发不额外打印错误行——子命令已经报告了自己的失败wt只转发状态码子进程的环境会剥离 Worktrunk 内部的指令文件环境变量避免旧版父包装器在进程退出后把指令文件当 shell 代码 source。实际案例名称引用自官方文档可自行cargo installworktrunk-sync按 git 历史推断的依赖顺序 rebase 堆叠的 worktree 分支。cargo install worktrunk-sync后以wt sync运行workz为当前 worktree 提供无冲突的端口区间、独立数据库与 Docker Compose 项目合并进.env.local让并行 worktree 互不冲突。cargo install workz后把其wt-workz适配器放到PATH以wt workz运行。参考Hooks 与 Aliases 的接口差异除下表差异外Hooks 与 Aliases 行为一致。维度HooksAliases调用方式wt hook type [args...]嵌套在hook内置命令下wt name [args...]顶层裸位置参数过滤器名wt hook pre-merge test build只跑test与build转发给{{ args }}从位置参数到达{{ args }}必须用--wt hook pre-merge -- extra任意裸位置参数直接到达跳过审批的 flag支持子命令后--yes/-ywt hook pre-merge --yes仅全局形式wt -y alias别名后--yes落入{{ args }}来源区分user:/project:/user:name/project:name过滤语法用户先、项目后无过滤语法强制绑定转义--var KEYVALUE已废弃推荐--KEYVALUE但仍强制绑定无智能路由是唯一路径--helpwt hook --help列出钩子类型wt hook type --help显示该类型的 flag 与参数模板本体即文档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小结与选型建议三条扩展路径覆盖了从自动到手动、从Shell到任意语言的完整谱系Hooks适合把依赖安装、测试、构建、部署等固定动作自动挂接到 worktree 生命周期上——在 .config/wt.toml 中配置随仓库共享项目钩子经首次审批后对团队生效Aliases适合把高频手动操作压缩为短命令模板引擎 智能路由 [[aliases.NAME]]流水线足以支撑复杂的多步发布流程Custom subcommands适合语言无关的重型扩展——任何可执行文件放到PATH即成为wt的一等子命令参数透传、stdio 继承与退出码传播让集成无摩擦。无论选择哪种机制都建议先用wt config alias dry-run/wt hook type --dry-run预览渲染结果用-v检视模板变量——配置即代码预览即调试。更丰富的内置配方dev server per worktree、database per worktree、progressive validation 等可继续参阅 tips-patterns.md。【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考