English · 简体中文 · 繁體中文 · 日本語 · 한국어
Per-task isolated worktrees for parallel Apple development with AI agents. Run multiple Claude / Codex / Copilot / Cursor sessions on the same Xcode project without
build.dblocks,DerivedDatathrash, or simulator collisions.
brew install maples7/tap/vchThen, in any Apple project:
vch new add-paywall # creates an isolated worktree + agent branch
vch add-paywall # drops you into a shell with isolation active
# → run xcodebuild / swift test as usual
vch test add-paywall --device "iPhone 16"
vch remove add-paywallThat's it. Every agent gets its own worktree, its own DerivedData, its
own simulator clone — and your ~/Library/Developer/ stays untouched.
Status: alpha. The CLI surface is settling but not yet frozen; on-disk
.vch/state.jsonmay gain fields. Pin a tag if you need stability.
Generic git-worktree managers stop at "isolate the source tree." Apple's
toolchain has at least seven more shared resources that, when contended
by parallel xcodebuild runs, cause non-deterministic failures:
| Resource | What goes wrong | VibeChard's answer |
|---|---|---|
DerivedData |
Module rebuild thrash, stale caches | -derivedDataPath <wt>/.agent-build/DerivedData |
ModuleCache.noindex |
Clang module corruption under concurrency | CLANG_MODULE_CACHE_PATH per worktree |
| SwiftPM global cache | Package.resolved write conflicts |
-clonedSourcePackagesDirPath per worktree |
xcresult bundles |
Last writer wins | -resultBundlePath per worktree |
| Simulator devices | Two tasks installing onto the same iPhone 16 | xcrun simctl clone per task |
xcodebuild PATH lookup by agents |
Agents bypass our flags | PATH shim that auto-injects flags |
| Source tree | Standard | git worktree + agent/<name> branch |
You bring your own AI agent — Claude, Codex, Copilot, Cursor, anything that speaks shell. VibeChard is not an AI vendor wrapper. No telemetry, no network calls, no SDK lock-in.
“Why not just git worktree + a 5-line shell wrapper?”
Reasonable instinct — that’s how I started. The tree is isolated, but every
xcodebuild invocation an agent fires from inside that tree still resolves
to these global locations:
~/Library/Developer/Xcode/DerivedData/MyApp-<hash>/(global default)~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/(global)~/Library/Caches/org.swift.swiftpm/(global)~/Library/Developer/CoreSimulator/Devices/<UDID>/(global)
As long as any of those are shared, xcodebuild is racy under concurrency.
There are exactly two ways out:
- Pass the right flags everywhere. Remember
-derivedDataPath,-clonedSourcePackagesDirPath, and-resultBundlePathon everyxcodebuildandswift test. Then teach Tuist, Fastlane, every custom test script, and anyPackage.swiftplugin that shells out to do the same. Then teach your AI agent not to forget. It will. - Put a PATH shim in front of
xcodebuildso those flags are guaranteed to be there no matter who or what invokes it.
VibeChard does (2). That’s the whole reason it’s a CLI instead of a
.zshrc snippet.
brew install maples7/tap/vchThe formula installs:
vchinto Homebrew'sbin/(onPATH)vch-xcodebuild-shimintolibexec/(intentionally not onPATH— it should only ever be reached by the symlinkvch execplants in the per-task.vch/bin/)- Bash, Zsh, and Fish completions
Requirements: macOS 13+, Xcode 15.3+ (Swift 5.10+).
git clone https://github.com/maples7/VibeChard.git
cd VibeChard
swift build -c release
ln -s "$PWD/.build/release/vch" /usr/local/bin/vch # or wherever you keep CLI binsFrom inside any git-tracked Apple project:
# 1. Spin up an isolated worktree on agent/add-paywall
vch new add-paywall
# 2. Open a shell inside it (PATH shim is active here)
vch add-paywall
# inside that shell:
# xcodebuild build ← gets -derivedDataPath injected automatically
# swift test ← isolated module cache + SwiftPM clone dir
# exit ← back to the host shell
# 3. Or run xcodebuild directly without entering the shell:
vch build add-paywall --scheme MyApp
vch test add-paywall --scheme MyApp --device "iPhone 16"
# 4. Driving an agent inside the worktree:
vch new fix-toast --exec "claude" # spawns claude inside the isolated worktree
vch new triage --copy-untracked # also bring over .env / .vscode / etc.
vch new --adopt-current # adopt this linked worktree, using its directory name
vch exec fix-toast -- npm run lint # one-shot command in the worktree
# 5. Inspect & clean up
vch list
vch path add-paywall # absolute path of the worktree
vch remove add-paywall # deletes worktree + branch + sim cloneIf an AI agent is driving the task, point it at the version-matched runbook:
vch runbookInside vch <name> / vch exec, the child also receives
VCH_AGENT_RUNBOOK_URL with the same tag-pinned link. The source copy
lives at docs/agent-runbook.md.
VibeChard's sweet spot isn't a single task — it's running many short tasks back-to-back (or in parallel), each in its own worktree, each landed before the next starts. A typical loop:
# Plan: A → B → C, each landed before the next starts.
# Task A — implement, test, review.
vch new task-a
cd "$(vch path task-a)"
# ...edit...
vch build task-a --scheme MyApp
vch test task-a --scheme MyApp --device "iPhone 16"
git commit -am "perf: task A"
vch open task-a # review in your IDE
# Once approved, merge from the main worktree:
cd /path/to/main-worktree
git merge --no-ff agent/task-a -m "Merge agent/task-a: <subject>"
vch remove task-a # worktree + branch + sim clone gone
# Task B starts from a clean develop, repeats the cycle.
vch new task-b
# ...Each vch new gets its own SwiftPM resolve cache, DerivedData, and
module cache under .vch/, so two in-flight tasks never block each
other on SPM lock contention or Xcode build cache invalidation. You
can run several vch test invocations concurrently from different
shells without a single Core Data store collision or simulator
clobber.
For scripted build/test loops, prefer vch build / vch test over
raw vch exec ... xcodebuild ...; the high-level commands keep
concise summaries, logs, and result bundles wired up:
vch test task-a --scheme MyApp --device 'iPhone 16' \
--only-testing MyAppTests/FooIf you need raw tools, prefer the stable vch state <name> --field <dotted> accessor over reading .vch/state.json by hand.
Patterns that aren't a built-in command but come up enough to write down — branching off mid-WIP work, running a test subset, keeping a long-running task current, preserving generated artifacts on land, warm-template fast path, resetting per-task sim state, the Booted template trap, and pruning merged tasks.
→ See docs/cookbook.md for the full set.
| Command | What it does |
|---|---|
vch new [<name>] |
Create worktree + agent/<name> branch, or adopt the current linked worktree (<name> may be omitted with --adopt-current; --exec "<cmd>", --copy-untracked, --seed-spm-from <task>, --cd). |
vch list |
List tasks in the workspace (--json, -v, --git-status). |
vch state <name> |
Print .vch/state.json for a task (--json, --field <dotted>). |
vch path <name> |
Print the absolute path of a task's worktree. |
vch open [<name>] |
Open the worktree in an IDE (--with xcode/code/cursor/…). |
vch <name> |
Drop into a shell inside the worktree with isolation active. |
vch exec <name> -- <cmd...> |
Run any command inside a task's worktree with isolation active. |
vch build <name> |
xcodebuild build with -derivedDataPath / -clonedSourcePackagesDirPath injected (--scheme, --runtime, --erase-clone, --shutdown-template, --verbose). |
vch test <name> |
xcodebuild test with -resultBundlePath injected; lazy sim clone (--device, --runtime, --only-testing, --skip-testing, --rerun, --rerun-failed, --erase-clone, --shutdown-template). |
vch run <name> |
Build, install, launch on the task's sim clone (--erase-clone, --shutdown-template, -- launch-args). |
vch logs <name> |
Print the most recent build/test xcodebuild log (--test/--build). |
vch sim {clone,erase,shutdown,info} <name> |
Manage the per-task simulator clone(s) explicitly; a task can own one clone per platform (e.g. iOS + watchOS) via repeated vch sim clone --device <name> (#99). |
vch sim warm-template {create,list,remove} |
Manage shared warm simulator templates (iOS/watchOS/tvOS/visionOS, #47/#58). |
vch land <name> |
Merge agent/<name> back into its base and clean up (--into, --no-ff/--ff-only/--squash, --keep, --push/--push-to, --dry-run). |
vch sync <name> |
Fetch the base's upstream and rebase the task branch onto it (--onto, --merge, --no-fetch, --dry-run). |
vch remove <name> |
Delete vch-created worktree, branch, and sim clone; adopted tasks are unregistered only (--allow-dirty, --force, --allow-unmerged, --keep-sim). |
vch prune |
List or remove tasks whose branch is fully merged into its base (--rm, --allow-dirty, --force, --keep-sim, --json). |
vch repair |
Re-sync .vch/state.json with what git worktree list actually shows. |
vch clean <name> |
Delete the task's DerivedData / ModuleCache (--swiftpm, --logs, --all, --dry-run). |
vch doctor |
Detect orphan sim clones, stale state, corrupt state.json (--clean, --bug-report, --json). |
vch shellenv |
Emit vch_cd / vch_new / vch_clean shell helpers (bash/zsh). |
vch completions install |
Install the shell completion script (--shell, --print, --force). |
vch runbook |
Print the version-pinned Agent runbook URL and installed Homebrew doc path hint (--json). |
vch version |
Print version + toolchain info (--json for machine-readable). |
All commands that take a <name> complete it from the current
workspace — install completions and hit <TAB>. For the full flag
reference see docs/commands.md.
Inside a task's worktree, <wt>/.vch/bin/ is prepended to PATH, and
contains symlinks xcodebuild, xcrun, swift → vch-xcodebuild-shim.
The shim reads three env vars (VCH_DERIVED_DATA_PATH,
VCH_SPM_CLONE_DIR, VCH_RESULT_BUNDLE_PATH), injects matching flags
into the xcodebuild argv if the user hasn't already passed them,
mkdir -p's the directories, then execv's the real binary resolved
via /usr/bin/xcrun -f xcodebuild (bypasses PATH, no recursion). For
xcrun and swift the shim is a transparent passthrough.
Result: any tool an agent might run — xcodebuild, swift test,
Tuist, custom scripts, anything that calls xcodebuild internally —
gets isolated automatically. No flag-passing required.
vch build, vch test, and vch run skip the PATH shim and call
xcodebuild directly with the same flags, since they know the args at
the call site.
vch <name> (= vch exec <name> -- $SHELL) and vch exec <name> -- <cmd> give the child a deterministic env layered on top of yours.
The same set drives vch build / vch test / vch run, so any
ad-hoc xcodebuild you type inside vch <name> behaves identically
to vch build. You rarely need a separate "drop me into the task's
env" command — vch <name> already is one:
| Variable | Set to |
|---|---|
VCH_TASK_NAME |
The task name (e.g. add-paywall). Useful for PS1 / window title. |
VCH_TASK_ROOT |
Absolute worktree path. |
VCH_AGENT_RUNBOOK_URL |
Version-pinned Agent runbook URL for this vch binary. |
VCH_DERIVED_DATA_PATH |
<wt>/.agent-build/DerivedData (read by the shim). |
VCH_SPM_CLONE_DIR |
<wt>/.agent-build/SourcePackages (read by the shim). |
VCH_RESULT_BUNDLE_PATH |
<wt>/.agent-build/Result.xcresult (read by the shim). |
VCH_RESULT_BUNDLE_DIR |
Parent dir of the result bundle. |
CLANG_MODULE_CACHE_PATH |
<wt>/.agent-build/ModuleCache (read by clang). |
SWIFTPM_CACHE_DIR |
<wt>/.agent-build/SourcePackages (read by SwiftPM). |
DEVELOPER_DIR |
Host's selected Xcode via xcode-select -p — only set if you didn't already export one. |
SIMCTL_CHILD_SIMULATOR_UDID |
The task's bound simulator clone — only set if a clone is bound. |
PATH |
Prepended with <wt>/.vch/bin so xcodebuild / xcrun / swift route through the shim. |
vch never clobbers a value you've already exported — set any of
these manually before vch exec and your value wins.
None. All per-task state lives at <worktree>/.vch/state.json. There
are no ~/.vchrc, no .vch.toml, no global config files. The only
runtime knobs are the VCH_* env vars listed above (typically set by
vch exec itself; you rarely set them by hand). For build commands,
vch also propagates the host's selected DEVELOPER_DIR (resolved via
xcode-select -p) into the child process — set the env var manually
to override.
If vch new printed a hint about eval "$(vch shellenv)", set
VCH_NEW_HINT=0 to silence it (or just install the shell helpers).
- Not an AI vendor wrapper. No SDK, no API key, no model abstraction. Use whatever agent you like — VibeChard just makes parallel sessions safe.
- Not cross-platform. Apple-only by design. The whole point is depth on the Xcode toolchain.
- Not a CI orchestrator. It runs locally, in your terminal, against worktrees on your disk. CI matrices are a different problem.
Does it work with Tuist / Fastlane / xcbeautify?
Yes. The PATH shim catches every xcodebuild invocation regardless of
who fires it. Tuist's generated runs, Fastlane's gym / scan,
xcbeautify's upstream pipe, and any custom test script that ends up in
xcodebuild all get the per-task -derivedDataPath /
-clonedSourcePackagesDirPath / -resultBundlePath injected
automatically. No flag plumbing on your side.
CocoaPods / Carthage?
Yes. Their dependency-fetching steps don't go through xcodebuild, so
there's nothing to isolate there. Their build steps eventually call
xcodebuild, which the shim catches. Pods/ and Carthage/
directories live inside the worktree alongside the source, so they're
isolated by git worktree itself.
SwiftPM-only project (no .xcodeproj)?
Works. swift build / swift test write to the per-worktree .build/
directory by default — already isolated for free, no shim flag injection
needed. The shim still wraps swift for transparency but doesn't modify
its argv.
What happens to uncommitted changes when I vch remove?
It refuses. vch remove aborts on a dirty worktree with a clear
message. Pass --allow-dirty to override (deletes uncommitted changes);
pass --allow-unmerged (or both flags together) to also allow removing
branches with unmerged commits. There is no silent destructive path.
Do I need an AI agent to use this?
No. Any “I want a parallel sandbox” use case works: try two competing
implementations of the same feature, run a long test suite while you
keep coding on the main worktree, etc. The CLI is agent-agnostic — the
so-called “agent integration” is just --exec "<your command>".
swift build -c release
./.build/release/vch version
swift test --parallelCI runs the same commands plus a shim smoke probe on every push: .github/workflows/ci.yml.
Apache-2.0. No CLA. No telemetry. No network calls.



