SLOPSHOPPER

wt-manager

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

newcommandtoastprocess
v1.11.0no licenseupdated 2026-10-07kuu13580/dotfiles/plugins/wt-manager
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · wt-manager
› fix the failing auth test and add an audit log call ╭─────────────────────────╮ │ wt-manager │ ⏺ Read(src/auth.ts) │ code で /work/app を開きました │ ⎿ Read 6 lines ╰─────────────────────────╯ ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /open ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

wt-manager

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 側の固定パスです。

セットアップ

  1. wt.zsh を .zshrc と同じディレクトリに配置 (例: ~/dotfiles/dotfiles/wt.zsh)
  2. .zshrc 末尾に相対パスで source 行を追加:
   source "${${(%):-%x}:A:h}/wt.zsh"

${${(%):-%x}:A:h} は「現在 source 中のファイル (= .zshrc) の解決済み絶対ディレクトリ」を返す zsh イディオム。:A で symlink (~/.zshrc → dotfiles 内) も解決されるため、同居している wt.zsh を確実に拾えます。

  1. 新しい zsh を起動し wt help で確認

コマンド

コマンド用途
wtfzf で 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)
  • モデルを呼ばずに実行され、応答中でもすぐ動く
  • mod (function hooks) のため terminal / Desktop の Code タブで動作。wt claude で起動した background セッションでも使える

配置自動検出 (wt new)

  • direct パターン ($HOME 直下のリポジトリ、basename が main 以外): ~/<repo>-worktrees/<dir>/
  • parent パターン (それ以外): <リポジトリの親ディレクトリ>/<dir>/

対話フォーム (引数なし wt new)

引数の順番やフラグを覚えなくても、wt new を引数なしで叩けば対話フォームで作成できます。

  • branch … プロンプト入力
  • dir … branch 名の末尾 (最後の / 以降) を _ 区切りで前半から1段ずつ削った候補を番号付きで提示。番号で選択 / 空 Enter は 1) (フル) / それ以外の文字列は直接入力扱い。_ を含まなければ従来どおり [既定値] 入力。例: feature/077_TICKET-5_update-translate → 1) 077_TICKET-5_update-translate 2) TICKET-5_update-translate 3) update-translate
  • description … プロンプト入力
  • base ref … fzf で既存ブランチ (ローカル / リモート) + (default) から選択 ((default) / Esc で wt.baseRef → HEAD)
  • 最後に内容サマリを表示し Create? (Y/n) で確認
  • 発動条件: 標準入力が tty のときのみ。非 tty (Claude の Bash、パイプ、CI) や引数付き呼び出しは従来どおりフラグ解析され、引数不足なら usage エラー。Claude に強制している wt new -b ... -d フローはフォームに落ちません

dirty な worktree の削除 (wt rm -f)

git worktree remove は変更済み / 未追跡ファイル (ビルド生成物・tmp ファイル等) が残っていると fatal: ... contains modified or untracked files で失敗します。wt rm はこの失敗を検知したら、

  1. git のエラーをそのまま表示し
  2. 残っているファイルを git status --porcelain で一覧表示 (20 件超は ... and N more) したうえで
  3. force 削除するかを判断します
  4. tty: worktree ごとに Force delete <path>? (y/N) で確認
  5. -f 指定: 確認なしで git worktree remove --force
  6. -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.descriptionper-worktree (git config --worktree)「何用か」を自然文で。PR番号・状態などを含める
wt.baseRefper-repositorywt new の base 省略時のデフォルト ref (例: origin/develop)
wt.postNewper-repositorywt new 直後に実行する opt-in コマンド (下記参照)

git config --worktree を使うため、初回書き込み時に extensions.worktreeConfig を自動で有効化します。

postNew フック (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'
  • opt-in: wt.postNew 未設定のリポジトリでは何もしません
  • 実行コンテキスト: cwd = 新しい worktree
  • 渡される環境変数:
変数内容
WT_NEW_PATH新しい worktree の絶対パス
WT_NEW_BRANCH新 worktree の branch 名
WT_MAIN_WORKTREEmain worktree のパス (コピー元に使う)
WT_REPO_ROOTwt 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 でヘルパを展開してから直接検証します。

前提環境

  • git ≥ 2.x (worktree per-config 対応)
  • fzf
  • zsh (wt cd のシェル関数経由 cwd 変更、wt 引数なし時のエディタ選択)
  • jq (PreToolUse hook guard-worktree.sh の判定に使用)
  • Claude Code v2.1.139+ (wt claude の Agent View 連携)

Claude からの利用 (SKILL)

skills/wt-manager/SKILL.md が、worktree 関連の依頼 (「worktree切って作業して」「並列で別タスク」「PR検証用に」など) を検知して以下のルールに乗せます:

  • git worktree add を直接叩かず wt new -d "<目的>" 経由で作成
  • PR 作成後は wt set <name> "<内容>" で description を更新
  • 並列実行は wt claude で Agent View に投入
  • Claude 自身が worktree を移動するときは EnterWorktree({ path }) (cwd を変える wt cd は Claude の Bash では効かないため)
  • background セッション (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 自体は値に関わらず動作します。

注意

  • 並列 N セッション = クォータ N 倍消費。Pro/Max プランでは多重起動に注意
  • 自動 cleanup / マージ済み worktree の自動削除・しきい値警告は行わない (手動運用)
  • wt cd は非対話シェル / CI では機能しない (cwd 変更がサブプロセスに閉じるため)
Source 1 files
hooks/register.tsx 46 lines
1import 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