Übersicht der Git-Worktrees des aktuellen Repos: uncommittete Arbeit, Commits nicht in main, gepusht oder nicht

Four Claude Code mods (plugins that change the interface itself): a deploy status bar, a tasks pane, a RAM indicator and a Git worktree overview. The three that draw above the input field share one bar.
The UI texts of all mods are in German.
| Mod | What it shows | Works for you if |
|---|---|---|
worktrees | All Git worktrees with open work | you use Git (nothing else needed) |
dokploy-statusbar | Last Dokploy deploy per app | you deploy with Dokploy and have the private dokploy CLI |
todos | Open tasks of the current repo from a G-Brain | you have an MCP server claude.ai G-Brain with get_page |
ram | Used memory in the bar | you use macOS |
claude plugin marketplace add allorders1studio/claude-code-mods
claude plugin install worktrees@claude-code-mods
Replace worktrees with dokploy-statusbar, todos or ram as needed, then run /reload-plugins inside Claude Code.
The only mod that needs nothing but Git. /worktrees lists every worktree of the current repo and tells you which ones can be deleted.
╭──────────────────────────────────────────────────────────╮
│ feature-search feat/search │
│ 3 Dateien uncommittet · letzter Commit heute │
│ Pfad kopieren │
╰──────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ fix-login fix/login │
│ 2 Commits nicht in main, nicht gepusht · letzter Commit gestern │
╰──────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ old-experiment exp/cache │
│ nichts offen – kann weg · letzter Commit vor 12 Tagen │
╰──────────────────────────────────────────────────────────╯
Neu laden 3 Worktrees · 2 mit offener Arbeit · Stand 23:20:56
Status line while worktrees hold open work:
2 von 3 Worktrees mit offener Arbeit · /worktrees
origin/<default branch>)Command: /worktrees. Requirements: Git.
A bar above the input field with the last Dokploy deploy of every app belonging to the current repo.
● api-app ● web-app ● worker running 1:20 ● docs ausstehend 23:20
> _
running m:ss while a build is in progressgit push, polls every 5 s for 2 minutes instead of every 30 sCommand: /deploys (refresh now).
Requirements, honestly:
dokploy CLI from allorders1studio/dokploy-cli. That repository is currently private; it is not the npm package of the same name./opt/homebrew/bin/dokploy, /opt/homebrew/lib/node_modules/dokploy-cli and /opt/homebrew/bin/node are hard-coded in hooks/register.tsx and hooks/discover.cjs.hooks/discover.cjs asks Dokploy (through the CLI's API module) for all services and keeps those whose Git source matches git remote get-url origin. It also reads each service's watchPaths. No configuration per repo is needed. API keys stay inside the CLI; the mod never sees them.
/tickets opens a pane "Aufgaben" with the open tasks of the current repo, read from a G-Brain (GBrain) knowledge store through its MCP server. The bar above the input field shows Aufgaben N (parked tasks not counted): red if one is overdue, yellow if one is due today or has high priority. A click on it opens the pane.
╭──────────────────────────────────────────────────────────╮
│ Abo-Bestellungen per Tag kennzeichnen │
│ überfällig · gestern │
╰──────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ HubSpot-Workflow für die Ankündigungsmail │
│ seit 2026-10-14 · Prio hoch │
╰──────────────────────────────────────────────────────────╯
Neu laden 2 offen · Stand 23:20:56
Order: tasks with a due date first (by date), then high priority, then the rest in page order, parked tasks last and dimmed. A click on a task writes it, with its detail lines, into the input field; "Details ›" opens the detail view.
How it works: the mod reads the page todo, takes the list slugs from the lines - [[todo/<name>]] — … under ## Projekte, reads each list and keeps those whose Repos: line names the repo of git remote get-url origin. A repo may be on several lists; each is shown as its own section. Loaded at session start, every 5 minutes, on /tickets and on "Neu laden".
# To-do — My project
Repos: `owner/repo`, `owner/other-repo`
## Offen
- [ ] Task title · seit 2026-10-14 · fällig 2026-10-16 · Prio hoch
- any number of detail lines
- [ ] Waiting task · wartet auf the supplier
- [ ] Parked task · geparkt
## Erledigt
- [x] …
Parts after · in the title line are read as seit, fällig (both YYYY-MM-DD), wartet auf …, Prio hoch and geparkt; anything else stays in the text. [[a/b]] is shown as a/b.
Requirements, honestly: an MCP server whose tools include get_page, put_page and remember (the mod finds it by these tools, since its name differs between terminal and desktop app); get_page takes { slug, content_only } and answers with JSON containing compiled_truth. In auto mode a mod's MCP call is refused unless get_page is allowed in permissions.allow, e.g. "mcp__<server>__get_page". Without such a store this mod is of no use to you. No credentials are configured here; the connection is the session's own.
Command: /tickets.
Used memory in the bar, polled every 10 seconds from memory_pressure -Q (macOS only): RAM 66% is 100 minus the "free percentage". Grey below 80 %, yellow from 80 %, red from 90 %. Where the command is missing or fails, the entry is left out without a message.
dokploy-statusbar, todos and ram draw into the same band above the input field, which only one hook can fill. Each mod reads the tree of the hooks inside it (next(e)), adds its entry under a sorted key and rebuilds the bar, so the layout does not depend on plugin order. Apps on the left, the rest on the right, separated by · , the Dokploy fetch time last:
● api-app ● web-app ● worker running 1:20 Aufgaben 3 · RAM 66% · 23:20
> _
Without Dokploy only Aufgaben and RAM appear. hooks/bar.tsx is the same file in all three mods.
.claude-plugin/marketplace.json marketplace "claude-code-mods"
<plugin>/
.claude-plugin/plugin.json manifest (name, version)
hooks/register.tsx the mod: panes, bands, commands, atoms
hooks/*.ts pure logic (parsing, scanning) plus tests
types/index.d.ts state types per plugin
Mods use the claude-code mod API. Tests of the pure logic sit next to the code (*.test.ts); UI behavior can be tested with claude-code/testing.
MIT, see LICENSE.
hooks/register.tsx 81 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { SCRIPT, needsCare, parse, verdict } from './scan'
5
6const PANE = 'worktrees'
7const list = atom({ plugin: 'worktrees', key: 'list' } as const, [])
8const error = atom({ plugin: 'worktrees', key: 'error' } as const, '')
9const loadedAt = atom({ plugin: 'worktrees', key: 'loadedAt' } as const, '')
10
11const age = (sec: number) => {
12 const days = Math.floor((Date.now() / 1000 - sec) / 86400)
13 return !sec ? '' : days < 1 ? 'heute' : days === 1 ? 'gestern' : `vor ${days} Tagen`
14}
15
16async function scan($: EngineInterface) {
17 const r = await $.process.run(['sh', '-c', SCRIPT], { cwd: await $.session.cwd(), timeoutMs: 60000 })
18 if (r.exitCode !== 0) {
19 await update($, error, () => (r.stderr.trim().split('\n')[0] || 'kein Git-Repo'))
20 void $.ui.status(undefined)
21 return
22 }
23 const found = parse(r.stdout).sort((a, b) => verdict(a).rank - verdict(b).rank || b.committedAt - a.committedAt)
24 await update($, list, () => found)
25 await update($, error, () => '')
26 await update($, loadedAt, () => new Date().toLocaleTimeString('de-DE'))
27 const care = found.filter(needsCare).length
28 void $.ui.status(care ? `${care} von ${found.length} Worktrees mit offener Arbeit · /worktrees` : undefined)
29}
30
31export const register: Register = on => {
32 on('session.start', async ($, e, next) => {
33 await $.command.register({ name: 'worktrees', description: 'Worktrees dieses Repos mit offenem Stand zeigen' })
34 void scan($)
35 // ponytail: alle 10 min neu; Worktrees ändern sich selten, git-Aufrufe je Worktree kosten
36 $.clock.every(600000, () => scan($))
37 return next(e)
38 })
39
40 on('command.run', { command: 'worktrees' }, async $ => {
41 await $.ui.open({ id: PANE, title: 'Worktrees' })
42 await scan($)
43 return { text: 'Worktree-Übersicht geöffnet.' }
44 })
45
46 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
47 const { Box, Button, Text } = $.ui.resolve(e)
48 const all = await read($, list)
49 const err = await read($, error)
50 const at = await read($, loadedAt)
51 return (
52 <Box flexDirection="column">
53 {err && <Text color="red">{err}</Text>}
54 {!err && all.length === 0 && <Text dimColor>{at ? 'Keine Worktrees außer dem Hauptordner.' : 'Lade …'}</Text>}
55 {all.map(w => {
56 const v = verdict(w)
57 return (
58 <Box key={`wt:${w.path}`} flexDirection="column" borderStyle="round" borderColor={v.color} paddingX={1} marginBottom={1}>
59 <Text>
60 <Text bold>{w.path.split('/').pop()}</Text>
61 <Text dimColor> {w.branch || '(kein Branch)'}</Text>
62 </Text>
63 <Text>
64 <Text color={v.color}>{v.text}</Text>
65 <Text dimColor>{w.committedAt ? ` · letzter Commit ${age(w.committedAt)}` : ''}</Text>
66 </Text>
67 <Box>
68 <Button key={`copy:${w.path}`} plain label="Pfad kopieren" onPress={press => $.ui.copy({ text: w.path, surface: press.surface })} />
69 </Box>
70 </Box>
71 )
72 })}
73 <Box gap={1}>
74 <Button key="reload" label="Neu laden" onPress={() => scan($)} />
75 {at && <Text dimColor>{all.length} Worktrees · {all.filter(needsCare).length} mit offener Arbeit · Stand {at}</Text>}
76 </Box>
77 </Box>
78 )
79 })
80}
81hooks/scan.ts 54 lines1import type { Worktree } from '../types'
2
3// Eine Zeile je Worktree (ohne den Hauptordner), Tab-getrennt:
4// pfad, branch, uncommittete Dateien, Commits nicht in main (cherry-genau, -1 = Vergleich gescheitert), upstream, ungepushte Commits, Commit-Zeit, fehlt
5// Verglichen wird gegen origin/<default>, weil ein lokales main fehlen oder veraltet sein kann.
6export const SCRIPT = `
7main=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null); main=\${main:-origin/main}
8git rev-parse -q --verify "$main" >/dev/null || main=main
9git worktree list --porcelain | sed -n 's/^worktree //p' | tail -n +2 | while IFS= read -r w; do
10 if [ ! -d "$w" ]; then printf '%s\\t\\t0\\t0\\t\\t0\\t0\\t1\\n' "$w"; continue; fi
11 b=$(git -C "$w" branch --show-current 2>/dev/null)
12 d=$(git -C "$w" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
13 u=0; if [ -n "$b" ]; then if c=$(git cherry "$main" "$b" 2>/dev/null); then u=$(printf '%s\\n' "$c" | grep -c '^+'); else u=-1; fi; fi
14 up=$(git -C "$w" rev-parse --abbrev-ref '@{u}' 2>/dev/null)
15 a=0; [ -n "$up" ] && a=$(git -C "$w" rev-list --count '@{u}..HEAD' 2>/dev/null)
16 t=$(git -C "$w" log -1 --format=%ct 2>/dev/null)
17 printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t0\\n' "$w" "$b" "$d" "$u" "$up" "\${a:-0}" "\${t:-0}"
18done
19`
20
21export const parse = (out: string): Worktree[] =>
22 out
23 .split('\n')
24 .filter(Boolean)
25 .map(line => {
26 const [path, branch, dirty, unmerged, upstream, unpushed, committedAt, missing] = line.split('\t')
27 return {
28 path,
29 branch,
30 dirty: Number(dirty) || 0,
31 unmerged: Number(unmerged) || 0,
32 upstream,
33 unpushed: Number(unpushed) || 0,
34 committedAt: Number(committedAt) || 0,
35 missing: missing === '1',
36 }
37 })
38
39// Was mit dem Worktree los ist, schlimmstes zuerst: Farbe und ein Satz.
40export const verdict = (w: Worktree) =>
41 w.missing
42 ? { rank: 1, color: 'magenta', text: 'Ordner fehlt – git worktree prune' }
43 : w.dirty > 0
44 ? { rank: 0, color: 'red', text: `${w.dirty} Dateien uncommittet` }
45 : w.unmerged < 0
46 ? { rank: 2, color: 'yellow', text: 'Abgleich mit main gescheitert – Stand unklar' }
47 : w.unmerged > 0 && (!w.upstream || w.unpushed > 0)
48 ? { rank: 2, color: 'yellow', text: `${w.unmerged} Commits nicht in main, ${w.upstream ? `${w.unpushed} ungepusht` : 'nicht gepusht'}` }
49 : w.unmerged > 0
50 ? { rank: 3, color: 'cyan', text: `${w.unmerged} Commits nicht in main, gepusht` }
51 : { rank: 4, color: 'green', text: 'nichts offen – kann weg' }
52
53export const needsCare = (w: Worktree) => verdict(w).rank < 3
54types/index.d.ts 17 lines1export type Worktree = {
2 path: string
3 branch: string
4 dirty: number
5 unmerged: number
6 upstream: string
7 unpushed: number
8 committedAt: number
9 missing: boolean
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 worktrees: { list: Worktree[]; error: string; loadedAt: string }
15 }
16}
17