A worktree orchestrator for AI-assisted PR reviews and feature work with Claude Code.
AI-assisted development is changing how engineers work — more PRs to review, more context to juggle across parallel tasks. The old workflow — one IDE, one branch, one PR at a time — doesn't scale. You need many reviews and feature branches open in parallel, each with its own worktree, each with Claude Code ready to assist.
Zen manages this silently: a background daemon watches GitHub, creates worktrees, injects PR context into CLAUDE.local.md files (never touching the repo's own CLAUDE.md), retries git failures, and cleans up worktrees for merged PRs. For feature work, the same isolation — dedicated worktrees with persistent context. You just open a tab and start working with Claude.
- How It Works
- The Automated Loop
- Your Workflow
- Feature Work
- Dashboard
- Context Injection
- MCP Server
- Configuration
- Design
- Prerequisites
- Building
GitHub ──── zen watch ──── Worktrees Ready ──── You Review ──────── Cleanup
PRs (daemon) (silent prep) (zen review resume) (automatic)
There are two loops. The automated loop runs in the background: a daemon polls GitHub, creates worktrees for new PRs, injects context, and removes worktrees for merged PRs. The manual loop is yours: check what needs review, open a worktree in a new iTerm tab with Claude, and do the review.
The background daemon handles worktree lifecycle so you don't have to. It polls GitHub for PRs from configured authors, creates worktrees with context pre-loaded, sends a macOS notification when ready, and cleans up merged PRs after a configurable number of days.
Poll GitHub Setup Cleanup
┌──────────┐ ┌────────────────────┐ ┌─────────────────────────┐
│ New PR │───→│ git worktree add │───→│ Merged 5+ days ago? │
│ detected │ │ inject CLAUDE.local│ │ git worktree remove │
└──────────┘ │ cache PR meta │ └─────────────────────────┘
│ notify (macOS) │
└────────────────────┘
Each step is idempotent and retries on failure. The daemon does not open iTerm tabs — worktrees are prepared silently, you open reviews when ready with zen review resume.
zen watch start # Start background daemon
zen watch stop # Stop daemon
zen watch status # Show daemon status + last check
zen watch logs # Tail daemon log output
zen watch logs search 42 # Search logs for a PR, worktree, or keyword
Logs: ~/.zen/state/watch.log — automatically rotated at 10MB (previous log kept as watch.log.1). Search covers both files.
Once the daemon has prepared worktrees, your review flow looks like this:
zen inbox → zen review resume 42 → review in Claude → done
(what needs review) (open iTerm tab) (CLAUDE.md ready)
zen review 42 → review in Claude → done
(create + open) (CLAUDE.md ready)
zen inbox # PRs needing review (filtered by configured authors)
zen inbox --all # From all authors
zen inbox --path pkg/sts # PRs touching specific paths
zen inbox --repo other-repo # Different repo
Shows pending PR reviews that don't yet have a local worktree. Also shows your own approved-but-unmerged PRs and PRs touching watched paths.
Example output:
───────────────────────────────────────────────────────────────
Legend W = Worktree
* = local worktree exists
zen review resume <number> to open | zen review <number> to create
2 Pending PR Reviews — app
Authors: alice bob charlie dave
═══════════════════════════════════════════════════════════════
PR Author Title Link
────── ──────────────────── ────────────────────────────────────────── ────────────────────────
#1042 alice api: Add pagination to ListUsers endpoi... https://github.com/acme/app/pull/1042
#1038 bob fix(auth): Handle expired refresh tokens https://github.com/acme/app/pull/1038
3 Open PRs touching platform/ and agents/ — app
═══════════════════════════════════════════════════════════════
W PR Author Title Link
── ────── ──────────────────── ────────────────────────────────────────── ────────────────────────
#1045 eve fix(agents/result): handle reasoning ... https://github.com/acme/app/pull/1045
#1041 app/dependabot build(deps): bump the all-others grou... https://github.com/acme/app/pull/1041
* #1035 alice Surface a Tool for `format_config` https://github.com/acme/app/pull/1035
2 Other PRs Requesting Your Review — app
═══════════════════════════════════════════════════════════════
W PR Author Title Link
── ────── ──────────────────── ────────────────────────────────────────── ────────────────────────
#1039 app/dependabot build(deps): bump the anchore group a... https://github.com/acme/app/pull/1039
* #1036 alice Create a module for the metareconciler. https://github.com/acme/app/pull/1036
zen review 42 # Create worktree + open terminal tab (auto-detects repo)
zen review 42 --repo other # Specify repo explicitly
zen review 42 --no-terminal # Create worktree only, print command
zen review resume 42 # Open existing worktree in new terminal tab
zen review resume 42 --list # List available sessions
zen review resume 42 --session 2 # Resume specific session
Manually create a PR review worktree: fetches the PR branch, creates the worktree, injects CLAUDE.md context, and opens a terminal tab with Claude. When --repo is omitted, zen auto-detects the repo by querying GitHub — if the PR number exists in multiple repos, it prefers the one where you're a requested reviewer, or asks you to choose. Use this when the daemon hasn't picked up a PR yet or you want to start immediately. If the worktree already exists, use zen review resume instead. If you run zen review resume and no worktree exists, it offers to create one.
zen reviews # PR reviews from past 7 days
zen reviews --days 30 # Past 30 days
Lists PR review worktrees with titles from the PR cache and session status.
Not everything is a PR review. Create and manage feature branch worktrees for your own work:
zen work # List feature worktrees
zen work new <repo> <branch> # Create new feature worktree
zen work new app my-feature "initial prompt" # With Claude prompt
zen work resume <name> # Resume a feature session in new iTerm tab
zen work delete <name> # Delete a feature worktree
zen status
zen dashboard # Alias for zen status
Overview of all active work: worktree counts, PR reviews (with remote state and cleanup ETA), feature work, and daemon state.
zen search 42 # By PR number
zen search oidc # By branch/name
zen search --type pr <term> # Filter: pr, feature
Searches across worktrees. Shows active Claude session indicator.
zen agent status # Claude sessions across all worktrees
zen agent status --running # Only running sessions
zen agent status --full # Full token usage scan (slower)
Shows session ID, model, token usage, and last activity for each worktree.
zen cleanup # Find stale worktrees
zen cleanup --days 14 # Custom age threshold
zen cleanup --delete # Interactive deletion
Finds worktrees for merged/closed PRs or inactive branches. The watch daemon handles merged PR cleanup automatically (5+ days after merge), but this command is useful for manual cleanup and inactive feature branches.
The daemon writes a CLAUDE.local.md file into each PR worktree with the PR title, author, changed files, and review instructions. This keeps the repo's own CLAUDE.md untouched so there's no risk of accidental commits. To refresh it manually:
zen context inject <path> --pr 42 --repo app
zen mcp serve
Starts a Model Context Protocol server on stdio, exposing zen tools for Claude sessions to call directly:
zen_inbox— fetch pending PR reviewszen_worktree_list— list worktreeszen_pr_details/zen_pr_files— PR metadatazen_agent_status— session infozen_config_repos— configured repositories
To register with Claude Code:
claude mcp add --scope user zen -- zen mcp serve
This lets Claude call zen tools directly during sessions (e.g. list worktrees, check inbox, fetch PR details).
zen version # Show version and commit SHA
zen setup # Interactive first-time setup
--json JSON output (all commands)
--debug Debug logging
For Ghostty tab creation to work on macOS:
- Ghostty must be running - Open Ghostty manually before using Zen commands
- Accessibility Permissions - Grant Terminal access in System Preferences > Security & Privacy > Accessibility
- Automation Permissions - Grant Terminal access in System Preferences > Security & Privacy > Automation
- Ghostty Focus - Ghostty window should be focused for reliable tab creation
If these requirements aren't met, Zen will automatically fall back to opening new windows.
Config file: ~/.zen/config.yaml
repos:
app:
full_name: octo-sts/app
base_path: ~/git/repo-octo-sts-app
authors:
- mattmoor
- wlynch
poll_interval: "5m"
claude_bin: claude
terminal: iterm # or "ghostty" for Ghostty support
# Note: Ghostty on macOS attempts tab creation via UI scripting (requires Ghostty running + accessibility permissions)
# Falls back to new windows if tab creation fails
watch:
dispatch_interval: "10s" # How often to process queued work
cleanup_interval: "1h" # How often to scan for merged PRs
cleanup_after_days: 5 # Days after merge before removing worktree
concurrency: 2 # Parallel worktree setups
max_retries: 5 # Max retry attempts for git failuresEach repo key (e.g. app) is a short name you choose — it doesn't have to match the GitHub repo name. It's used for worktree naming (app-pr-42), queue keys (app:42), and display. The full_name is the actual owner/repo used for GitHub API calls. If two orgs have a repo with the same name, just pick different keys:
repos:
octo-app:
full_name: octo-sts/app
base_path: ~/git/repo-octo-sts-app
other-app:
full_name: other-org/app
base_path: ~/git/other/repo-appAll repos and authors must be configured — there are no hardcoded defaults.
The daemon re-reads config.yaml on every poll tick. Changes to poll_interval, authors, repos, and other settings take effect without restarting.
All state lives in ~/.zen/state/:
| File | Purpose |
|---|---|
watch.pid |
Daemon PID |
watch.log |
Daemon logs |
last_check.json |
Timestamp of last GitHub poll |
pr_cache.json |
PR titles/authors for display |
Under the hood, the daemon uses driftlessaf workqueues with two reconcilers — one for setup, one for cleanup:
┌─────────────────────────────────────────────────────────┐
│ watchDaemon() │
└────┬──────────────────┬──────────────────┬──────────────┘
│ │ │
poll_interval dispatch_interval cleanup_interval
│ │ │
v v v
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│reloadConfig │ │ dispatcher │ │ scanMerged │
│+ pollOnce() │ │ .Handle() │ │ PRs() │
└──────┬──────┘ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘
│ │ │
┌────────────────┼─────────┐ │ │
v v v v v
┌────────────────┐ ┌──────────┐ ┌───────────────┐ ┌───────────────┐
│ GitHub GraphQL │ │ macOS │ │ setupQueue │ │ cleanupQueue │
│ GetReview │ │ notify │ │ │ │ │
│ Requests() │ │ │ │ "app:42" │ │ "app:42" │
└────────┬───────┘ └──────────┘ │ "app:87" │ │ "app:35" │
│ └───────┬───────┘ └───────┬───────┘
│ new PRs from │ │
│ configured authors │ │
└──────────────────────────────┘ │
StorePRData() + │
Queue(key) │
│
┌──────────────────────────────────────┐ ┌──────────────────┴───────────────┐
│ SetupReconciler.Reconcile() │ │ CleanupReconciler.Reconcile() │
│ │ │ │
│ key ──→ ParsePRKey("app:42") │ │ key ──→ ParsePRKey("app:35") │
│ repo=app, pr=42 │ │ repo=app, pr=35 │
│ │ │ │
│ Step 1: ensureWorktree │ │ Step 1: removeWorktree │
│ ┌─────────────────────────────┐ │ │ ┌─────────────────────────────┐ │
│ │ if exists? skip │ │ │ │ if missing? skip │ │
│ │ git fetch origin pull/N/head│ │ │ │ git worktree remove --force │ │
│ │ git worktree add │ │ │ └─────────────────────────────┘ │
│ │ rm index.lock │ │ │ │ │
│ └─────────────────────────────┘ │ │ v on error: RETRY │
│ │ │ │ │
│ v on error: RETRY │ └───────────────────────────────────┘
│ │
│ Step 2: ensureContextInjected │
│ ┌─────────────────────────────┐ │ ┌──────────────────────────────────┐
│ │ if CLAUDE.local.md? skip │ │ │ Error Handling │
│ │ fetch PR details + files │ │ │ │
│ │ render CLAUDE.local.md │ │ │ Invalid key ──→ SKIP (permanent)
│ └─────────────────────────────┘ │ │ Unknown repo ──→ SKIP (permanent)
│ │ │ │ Git failure ──→ RETRY │
│ v on error: LOG, CONTINUE │ │ 30s → 60s → │
│ │ │ ... → 10m cap │
│ Step 3: cachePRMeta │ │ max 5 attempts│
│ ┌─────────────────────────────┐ │ │ Context/cache ──→ LOG, CONTINUE │
│ │ prcache.Set(repo, pr, │ │ │ │
│ │ title, author) │ │ └──────────────────────────────────┘
│ └─────────────────────────────┘ │
│ │
│ ✓ notify.WorktreeReady() │
│ │
└──────────────────────────────────────┘
Each step is idempotent — safe to re-run if interrupted. Git failures retry with exponential backoff (30s..10m, max 5 attempts). Context injection and PR cache writes are non-blocking — failures are logged but don't prevent the worktree from being created.
Git worktrees are the single source of truth. All inventory — which PRs have local worktrees, which feature branches exist, worktree paths and types — is derived from git worktree list via worktree.ListAll(). There is no external database or registry to drift out of sync.
PR metadata (titles, authors) is cached in a lightweight JSON file (~/.zen/state/pr_cache.json) written by the daemon during setup. This cache is purely for display — if it's missing or stale, commands still work (they just show PR numbers instead of titles).
| Type | Pattern | Example |
|---|---|---|
| PR review | <repo>-pr-<number> |
app-pr-42 |
| Feature | <repo>-<branch> |
app-add-oidc-claims |
zen
├── cmd/ # CLI commands (cobra)
├── commands/ # Claude Code commands (embedded in binary)
├── internal/
│ ├── config/ # YAML config (~/.zen/config.yaml)
│ ├── context/ # CLAUDE.md generation for PR reviews
│ ├── github/ # GitHub API (GraphQL + REST)
│ ├── iterm/ # iTerm2 tab management via AppleScript
│ ├── mcp/ # MCP server exposing zen tools
│ ├── notify/ # macOS notifications
│ ├── prcache/ # Lightweight PR metadata cache (JSON)
│ ├── reconciler/ # Workqueue-based PR setup + cleanup
│ ├── session/ # Claude session detection
│ ├── ui/ # Terminal formatting
│ └── worktree/ # Git worktree discovery + management
├── main.go
└── go.mod
After building, run the interactive setup to create your config:
zen setup
This walks you through configuring your repositories, GitHub usernames for PR filtering, and watch daemon settings. The config is written to ~/.zen/config.yaml.
| Requirement | Why |
|---|---|
| macOS | iTerm2/Ghostty tab management and notifications use AppleScript |
| Git | Worktree creation, fetching PR branches, cleanup |
GitHub CLI (gh) |
Authentication and GitHub API access — must be logged in (gh auth login) |
| iTerm2 or Ghostty | Opens review/work sessions in new tabs (iTerm2) or tabs/windows (Ghostty). Ghostty uses UI scripting for tab creation when possible, with fallback to windows (use --no-terminal to skip) |
Claude Code (claude) |
AI-assisted PR reviews and coding sessions |
| Go 1.24+ | Building from source |
make build # Includes version + commit SHA via ldflags
Or manually:
go build -o zen .
Check your build:
zen version # Shows version and commit SHA
make test
I was watching The Last Dance when naming this tool. Phil Jackson — the "Zen Master" — and his coaching philosophy resonated: orchestrate the system, trust the players, stay calm while everything moves around you. That's what this tool does: silently prepares worktrees, injects context, cleans up after itself, and lets you focus on the actual review when you're ready.
