git worktree を fzf ベースの `wt` 系コマンドで管理し、各worktreeの目的を git config に記録する。Claude には `git worktree add` 直叩きを避け `wt new` 経由で作成させる動線を提供。

git worktree を fzf ベースの wt 系コマンドで管理するプラグインです。各 worktree の「何用か」を git config --worktree wt.description に記録し、git worktree list だけでは分からない用途を一覧・プレビューできます。Claude Code Agent View での並列セッション運用を前提に設計しています。
実装本体 (wt.zsh) は dotfiles リポジトリ側の1ファイルのみを実体とし、プラグインには複製を同梱しません。プラグインが提供するのは Claude 向けの SKILL・hook と、セッション内で使う /open コマンド (mod) だけです。
| 役割 | パス |
|---|---|
| zsh 関数の実体 (唯一) | ~/dotfiles/dotfiles/wt.zsh |
| テスト | ~/dotfiles/dotfiles/wt.test.zsh |
| Claude 向け動線 | skills/wt-manager/SKILL.md |
/open コマンド (mod) | hooks/register.tsx (テスト: claude plugin test plugins/wt-manager) |
前提: 本プラグインは dotfiles リポジトリが初期セットアップ済みであること (=
wt.zshが.zshrcから source 済み) を前提とします。wt.zshの複製を同梱しないため、dotfiles 未セットアップの環境ではwtコマンドが存在せず機能しません。プラグインインストールパス(キャッシュ位置)は変動するため、.zshrcから source するのは dotfiles 側の固定パスです。
wt.zsh を .zshrc と同じディレクトリに配置 (例: ~/dotfiles/dotfiles/wt.zsh).zshrc 末尾に相対パスで source 行を追加: source "${${(%):-%x}:A:h}/wt.zsh"
${${(%):-%x}:A:h} は「現在 source 中のファイル (= .zshrc) の解決済み絶対ディレクトリ」を返す zsh イディオム。:A で symlink (~/.zshrc → dotfiles 内) も解決されるため、同居している wt.zsh を確実に拾えます。
wt help で確認| コマンド | 用途 |
|---|---|
wt | fzf で worktree 選択 → エディタ (code/zed) 選択 → 起動 |
wt new -b <branch> <dir> [base] [-d desc] | 新規作成 + description 記録 (<dir> はディレクトリ名のみ、配置先は自動検出)。引数なしで叩くと対話フォーム (下記、tty のみ) |
wt ls [-p] | メタデータ付き一覧 (DIR / BRANCH / AGE / DESC)。-p で絶対 PATH 列を追加 |
wt set [<name>] ["<desc>"] | description 編集/設定。無引数 → fzf + $EDITOR、<name> "<desc>" で非対話 ("" でクリア) |
wt rm [<name>...] [-y] [-b] [-f] | worktree 削除。無引数 → fzf 複数選択 + 対話確認、-y で確認スキップ、-b でブランチも削除、-f で dirty な worktree も強制削除 (下記) |
wt claude [<name>] [-n <label>] | claude --bg で Agent View に idle 投入 (プロンプト無し)。name 指定で fzf スキップ。表示名 (-n) は既定で wt.description、無ければディレクトリ名。-n <label> で上書き |
wt cd [<name>] | worktree に cd (zsh 関数のため対話シェルでのみ機能) |
/open)Claude Code のセッション内で /open [code|zed] を打つと、セッションの cwd が属する worktree のルート (git rev-parse --show-toplevel、git 管理外なら cwd) をエディタで開きます。
code)wt claude で起動した background セッションでも使えるwt new)$HOME 直下のリポジトリ、basename が main 以外): ~/<repo>-worktrees/<dir>/<リポジトリの親ディレクトリ>/<dir>/wt new)引数の順番やフラグを覚えなくても、wt new を引数なしで叩けば対話フォームで作成できます。
/ 以降) を _ 区切りで前半から1段ずつ削った候補を番号付きで提示。番号で選択 / 空 Enter は 1) (フル) / それ以外の文字列は直接入力扱い。_ を含まなければ従来どおり [既定値] 入力。例: feature/077_TICKET-5_update-translate → 1) 077_TICKET-5_update-translate 2) TICKET-5_update-translate 3) update-translate(default) から選択 ((default) / Esc で wt.baseRef → HEAD)Create? (Y/n) で確認Bash、パイプ、CI) や引数付き呼び出しは従来どおりフラグ解析され、引数不足なら usage エラー。Claude に強制している wt new -b ... -d フローはフォームに落ちませんwt rm -f)git worktree remove は変更済み / 未追跡ファイル (ビルド生成物・tmp ファイル等) が残っていると fatal: ... contains modified or untracked files で失敗します。wt rm はこの失敗を検知したら、
git status --porcelain で一覧表示 (20 件超は ... and N more) したうえでForce delete <path>? (y/N) で確認-f 指定: 確認なしで git worktree remove --force-y のみ / 非 tty: force せず「wt rm '<絶対パス>' -y -f」を案内して終了 (終了コード 1)。案内は必ず絶対パスで出る (同名 worktree が複数あるとき、名前指定だと別の worktree を消してしまうため)--force でも消えない locked worktree の場合は git worktree unlock / remove --force --force を案内します (自動では実行しません)。削除できなかった worktree が1つでもあれば wt rm は非ゼロで終了します。
一覧に出るもの ≠ 消えるもの:
.gitignoreされたファイル (node_modules、.env、wt.postNewで配置した鍵など) は git の clean 判定の対象外なのでこの一覧には出ませんが、worktree を削除すればディレクトリごと消えます (force 不要)。復元が要るファイルは削除前に退避してください。
| キー | スコープ | 内容 |
|---|---|---|
wt.description | per-worktree (git config --worktree) | 「何用か」を自然文で。PR番号・状態などを含める |
wt.baseRef | per-repository | wt new の base 省略時のデフォルト ref (例: origin/develop) |
wt.postNew | per-repository | wt new 直後に実行する opt-in コマンド (下記参照) |
git config --worktree を使うため、初回書き込み時に extensions.worktreeConfig を自動で有効化します。
wt.postNew)wt new で worktree を作成した直後に、リポジトリ単位で設定したコマンドを実行できます。gitignore されていて新 worktree に複製されないファイル (例: .env の復号鍵 .env.keys) を自動で持ち込む用途を想定しています。
# リポジトリ内で 1 回設定 (共有 .git/config に保存され、全 worktree で有効)
git config wt.postNew 'cp "$WT_MAIN_WORKTREE/.env.keys" .env.keys'
wt.postNew 未設定のリポジトリでは何もしません| 変数 | 内容 |
|---|---|
WT_NEW_PATH | 新しい worktree の絶対パス |
WT_NEW_BRANCH | 新 worktree の branch 名 |
WT_MAIN_WORKTREE | main worktree のパス (コピー元に使う) |
WT_REPO_ROOT | wt new を呼んだリポジトリ root |
wt new 自体は成功扱い (worktree は既に作成済みのため)&& pnpm install のようにコマンドを連結すれば依存インストールまで自動化できますが、wt new が遅く・対話的になる点に注意zsh ~/dotfiles/dotfiles/wt.zsh # 本体 (source 用)
zsh ~/dotfiles/dotfiles/wt.test.zsh # テストスイート
fzf / claude --bg / $EDITOR 連動を除いた純粋ロジック + git 実操作を 71 アサーションで検証します (一時 git リポジトリを mktemp で作成し trap で削除)。internal ヘルパ (_wt_*) は wt() に内包されているため、テストは WT_KEEP_INTERNAL=1 でヘルパを展開してから直接検証します。
wt cd のシェル関数経由 cwd 変更、wt 引数なし時のエディタ選択)guard-worktree.sh の判定に使用)wt claude の Agent View 連携)skills/wt-manager/SKILL.md が、worktree 関連の依頼 (「worktree切って作業して」「並列で別タスク」「PR検証用に」など) を検知して以下のルールに乗せます:
git worktree add を直接叩かず wt new -d "<目的>" 経由で作成wt set <name> "<内容>" で description を更新wt claude で Agent View に投入EnterWorktree({ path }) (cwd を変える wt cd は Claude の Bash では効かないため)wt claude / claude --bg / Agent View) では hook が発火しないことがあるため、Claude 自身が wt new → EnterWorktree({ path }) で能動的に隔離 (SKILL の Rule 7)付属の PreToolUse hook (hooks/) が EnterWorktree の新規作成 (name) と ExitWorktree の remove を deny し、wt new / wt rm 動線へ誘導します。なお wt.zsh は internal ヘルパを wt() に内包しており、Claude の Bash(zsh) からも wt new 等が動作します。
background セッションでの前提: 上記 Rule 7 の運用には、ユーザーが
settings.jsonに"worktree": {"bgIsolation": "none"}を手動設定する必要があります (既定の"worktree"だと harness がwt規約を無視した worktree を.claude/worktrees/に自動生成するため)。bgIsolation: "none"は Edit/Write の隔離ブロックを無効化するだけで、wt new自体は値に関わらず動作します。
wt cd は非対話シェル / CI では機能しない (cwd 変更がサブプロセスに閉じるため)hooks/register.tsx 46 lines1import type { EngineInterface, Register } from 'claude-code'
2
3export const EDITORS = ['code', 'zed'] as const
4type Editor = (typeof EDITORS)[number]
5const LAST = 'lastEditor'
6
7const isEditor = (s: string): s is Editor => (EDITORS as readonly string[]).includes(s)
8
9// Worktree root rather than cwd so a session sitting in a subdirectory still opens the whole tree.
10const targetDir = async ($: EngineInterface) => {
11 const cwd = await $.session.cwd()
12 const r = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd })
13 return r.exitCode === 0 && r.stdout.trim() ? r.stdout.trim() : cwd
14}
15
16export const open = async ($: EngineInterface, args: string) => {
17 const arg = args.trim()
18 if (arg && !isEditor(arg)) return { text: `使い方: /open [${EDITORS.join('|')}]` }
19 const last = await $.store.get(LAST)
20 const editor: Editor = arg ? (arg as Editor) : typeof last === 'string' && isEditor(last) ? last : 'code'
21 const dir = await targetDir($)
22 try {
23 const r = await $.process.run([editor, dir], { timeoutMs: 20_000 })
24 if (r.exitCode !== 0) return { text: `${editor} の起動に失敗しました (exit ${r.exitCode}): ${r.stderr.trim()}` }
25 } catch (err) {
26 return { text: `${editor} を起動できません: ${err instanceof Error ? err.message : String(err)}` }
27 }
28 await $.store.set(LAST, editor)
29 $.ui.toast(`${editor} で ${dir} を開きました`)
30 return {}
31}
32
33export const register: Register = on => {
34 on('session.start', async ($, e, next) => {
35 await $.command.register({
36 name: 'open',
37 description: 'セッションの worktree を code / zed で開く (省略時は前回のエディタ)',
38 argumentHint: EDITORS.join('|'),
39 immediate: true,
40 })
41 return next(e)
42 })
43
44 on('command.run', { command: 'open' }, async ($, e) => open($, e.args))
45}
46