Personal Claude Code extensions: worktree lifecycle hooks and a status band mod above the prompt (CLI and desktop app alike).
Small-scale personal tooling, but every "gotcha I hit" is packaged as a reusable component. Fork it, tweak it, file issues.
License: MIT · 中文 README
Open Claude Code and paste this. Claude does the rest.
Install claude-utils:
- Run
git clone --depth 1 https://github.com/ericwu917/claude-utils.git ~/.claude/claude-utils- Run
~/.claude/claude-utils/install.sh --all- If the install script reports
Conflictorjq is required, read~/.claude/claude-utils/docs/SETTINGS_MERGE.mdand help me merge~/.claude/settings.jsonmanually (create a timestamped backup first).- When install is done, tell me to start a new Claude Code session for the hooks and the mod to take effect.
git clone --depth 1 https://github.com/ericwu917/claude-utils.git ~/.claude/claude-utils
~/.claude/claude-utils/install.sh --all # default: hooks + mods
~/.claude/claude-utils/install.sh --hooks # hooks only
~/.claude/claude-utils/install.sh --mods # statusband mod only
~/.claude/claude-utils/install.sh --statusline # legacy statusline.sh only (fallback, replaced by the mod)
~/.claude/claude-utils/install.sh --dry-run # show what would be copied and the settings diff, don't write- Requires
bash,jq,git; the mod needs a Claude Code build with hooks modules (mods). install.shcopies the runtime into~/.claude/(hooks/,mods/,statusline-refresh-caches.sh) and pointssettings.jsonat the copies, never at the repo, so the runtime and the working tree stay independent (switching branches can't break a live session). To upgrade,git pulland reruninstall.sh.- Before writing, the script backs up your existing settings to
settings.json.bak.<timestamp>. - Idempotent: reruns only refresh changed files and claude-utils' own entries. If a slot already holds a non-claude-utils entry, the script warns and skips — your config is never overwritten. Other folders already in
CLAUDE_CODE_PLUGIN_DIRSare kept; ours is appended.
Prefix-driven base branch selection, plus a date stamp:
Input name |
Branch | Base |
|---|---|---|
feat/<rest> |
feat/YYMMDD-<rest> |
origin/develop |
feature/<rest> |
feature/YYMMDD-<rest> |
origin/develop |
hotfix/<rest> |
hotfix/YYMMDD-<rest> |
origin/master |
| anything else | worktree-<name> |
origin/HEAD (fallback) |
Example: claude -w feat/kill-mutants-s2 → branch feat/260418-kill-mutants-s2, worktree at <repo>/.claude/worktrees/feat/260418-kill-mutants-s2/. If the expected base is missing (e.g. the repo has no origin/develop), the hook falls back to origin/HEAD — so it stays useful in projects that don't follow git-flow.
Local-scope MCP servers follow the worktree: local scope (claude mcp add's default) is keyed by directory in ~/.claude.json, so a worktree would otherwise start with none. The hook copies the parent repo's projects["<repo>"].mcpServers into the worktree's .mcp.json (merging into an existing untracked one; a tracked .mcp.json is left alone) and hides it via info/exclude. Project-scope servers need approval once per directory, i.e. per worktree — to skip that for servers you trust, list their names under enabledMcpjsonServers in ~/.claude/settings.json.
Paired cleanup. Runs git worktree remove (without --force, so dirty worktrees are preserved) + git branch -D (only if the branch's tip is already merged into develop / master / main or reachable from any remote ref) + empty-parent-directory cleanup. Unmerged, unpushed branches are kept — branch -D is force-delete, so dropping a branch whose commits live only there would lose work. Re-invoking claude -w <same-name> later reattaches a worktree via the create hook's reuse path. Because CC invokes this hook with cwd set to the worktree being removed, every destructive git op is routed through git -C "$MAIN_REPO" — git refuses to self-delete its cwd or a checked-out branch, so the hook does the work from the main repo instead.
A Claude Code mod (a plugin of function hooks) that draws an AbovePrompt band, in the CLI and the desktop app alike, each surface its own way:
- CLI: two lines matching the old statusline.sh —
[model vX↑] 📁 dir | 🔀 branch | N files +a -d | 💾 hit% ⏳expiry | $session/$today/$month, then context, 5h and 7d bars (a┃on 7d marks Fable's weekly usage). Bars areRasterrows: a solid track, a lighter same-hue elapsed-time band, a fill ending in a 1/8-width block (a 10-cell bar resolves 80 steps). Lines are fitted to the band's width without wrapping; a narrow terminal drops detail first (today/month, diff, countdowns…), never the bars. - Desktop app: only what the app doesn't already show (directory, git, cache hit + expiry, cost; then context, 5h and 7d bars), with Svg bars and line icons; the
↗after the directory opens it in Finder.
Cache-expiry reminder: ten minutes before a 1h prompt cache lapses, a toast (once per expiry, re-armed when the cache renews; 5m caches never qualify). If ~/.config/discord-webhook exists (holding a Discord webhook URL), a message goes there too. The context fill gets a toast too, each time it crosses 60% upward (dropping back under, e.g. after /compact, re-arms it). Both thresholds can be set in settings.json's env: STATUSBAND_CTX_WARN_PCT (default 60), STATUSBAND_CACHE_WARN_MIN (default 10).
The CLI bars' track follows CC's theme (/config → theme): light grey for light* themes, dark grey otherwise (including auto).
Color thresholds and the 7d work-hours pacing match the old statusline; work hours come from STATUSLINE_WORK_START / STATUSLINE_WORK_END (default 9–22), best set in settings.json's env — the desktop app doesn't read your shell rc files. Per-account data (5h/7d, Fable) always comes from the session's own account ($.session.usage(), $.session.authorize() + $.http.fetch), never the keychain — the CLI and the app may be signed in as different accounts. Today/month cost comes from ccusage over every local JSONL (i.e. all accounts on the machine), its cache refreshed through statusline-refresh-caches.sh ccusage.
After a change, run claude plugin validate mods/statusband and claude plugin test mods/statusband (render tests on the terminal and desktop surfaces).
Replaced by the statusband mod above; install.sh --statusline still installs it (with its companion hooks/last-reply.sh Stop hook, which timestamps replies for the ⏱ segment). The slow-data refreshers (ccusage cost, Fable usage) live in statusline/statusline-refresh-caches.sh, whose ccusage half the mod reuses. Full details: statusline/README.md.
| Location | Role |
|---|---|
| This repo | Source; install.sh copies the runtime out of it |
~/.claude/hooks/, ~/.claude/mods/statusband/, ~/.claude/statusline-refresh-caches.sh |
Runtime (copied by install.sh); settings.json references these |
~/.claude/settings.json |
CC's config; install.sh merges entries idempotently (hooks, env.CLAUDE_CODE_PLUGIN_DIRS) |
~/.claude/ccusage-cache.json |
today/month cost cache, shared by the mod and the legacy statusline |
~/.claude/worktree-hook.log |
stdin JSON of every worktree hook invocation — first stop when debugging |
claude-utils/
├── hooks/
│ ├── worktree-create.sh
│ ├── worktree-remove.sh
│ ├── worktree-lib.sh
│ ├── guard-worktree-edits.sh
│ └── last-reply.sh # legacy statusline companion (fallback)
├── mods/
│ └── statusband/ # status band mod (hooks/register.tsx, types/, tests/)
├── statusline/
│ ├── statusline.sh # retired, fallback
│ ├── statusline-refresh-caches.sh
│ └── README.md
├── docs/
│ └── SETTINGS_MERGE.md # manual-merge guide for conflict cases
├── install.sh
├── CHANGELOG.md
├── LICENSE
├── CLAUDE.md # repo notes for Claude Code instances
└── README.md
WorktreeCreatestdin: the worktree name is at top-level.name, not.tool_input.name.WorktreeRemovestdin: the path field is.worktree_path(snake_case), not.path/.worktreePath.WorktreeRemovecwd trap: CC invokes the hook from inside the worktree being removed.git worktree removeandgit branch -Dboth fail from that cwd because git refuses to self-delete its cwd or a checked-out branch. Usegit -C <main-repo>.- Pairing requirement: if you configure
WorktreeCreate, you must also configureWorktreeRemove. CC's built-in cleanup does not run on/exitonce a customWorktreeCreateis set — even a clean worktree won't auto-remove. Undocumented but reproducible.
When writing a mod:
- The CLI and the app can be different accounts: the keychain token and
~/.claude.jsonbelong to the CLI's. Fetch per-account data with the session's own credential (the handle from$.session.authorize(), passed to$.http.fetch), or the app shows the CLI account's numbers. - Mods sit behind a rollout switch (
tengu_plugin_hooks_modules), cached in~/.claude.jsonand refreshed only when the CLI starts with network access; ifclaude plugin testsays "hooks modules are turned off", startclaudeonce and retry. - Don't give a desktop
SvgisInteractive: it moves into an iframe with a white background at a default 300×150; givewidth/heightexplicitly too. - On desktop only a
Buttontakes clicks (SvgandBoxdon't), and it always draws as a native button. - The blank row between the band and the prompt is the engine's own spacing; a mod can't remove it.
-
install.sh+ paste-prompt one-shot install - English README
-
uninstall.sh/install.sh --update - shellcheck + shfmt CI
PRs and issues welcome.
MIT — see LICENSE.
