Arbor is a self-contained binary for managing git worktrees to assist with agentic development of applications. It is cross-project, cross-language, and cross-environment compatible.
All development occurs inside a worktree:
# Create a worktree for development
arbor work feature/new-feature
cd feature-new-feature
# Make changes, test, commit
go test ./...
arbor work another-feature # Create another if needed
# When done with a worktree
cd ..
arbor remove feature-new-featurebrew tap artisanexperiences/tap
brew install arborUpgrade:
brew upgrade arborDownload the latest release for your platform from the releases page.
go install github.com/artisanexperiences/arbor/cmd/arbor@latestNote: Installing via go install builds without version information. Use Homebrew or download releases for proper version metadata.
# Clone the repository
git clone https://github.com/artisanexperiences/arbor.git
cd arbor
# Build for your platform
go build -o arbor ./cmd/arbor
# Or build with version information
VERSION=$(git describe --tags --always)
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
go build -ldflags "-X main.Version=$VERSION -X main.Commit=$COMMIT -X main.BuildDate=$DATE" -o arbor ./cmd/arbor# Check arbor version
arbor version
# Initialise a new Laravel project (choose worktree or copy-on-write mode interactively)
arbor init [email protected]:user/my-laravel-app.git
# Initialise with a specific workspace mode
arbor init [email protected]:user/my-laravel-app.git --mode cow
arbor init [email protected]:user/my-laravel-app.git --mode worktree
# Create a feature workspace
arbor work feature/user-auth
# Create a workspace from a specific base branch
arbor work feature/user-auth -b develop
# Create a workspace without running scaffold steps
arbor work feature/user-auth --skip-scaffold
# Sync current workspace with upstream (defaults to main, uses rebase)
arbor sync
# Sync with a specific upstream branch
arbor sync --upstream develop
# Sync using merge instead of rebase
arbor sync --strategy merge
# Save sync settings to arbor.yaml for future use
arbor sync --upstream develop --strategy rebase --save
# List all workspaces with their status
arbor list
# Remove a workspace when done
arbor remove feature/user-auth
# Clean up merged workspaces
arbor prune
# Switch workspace mode (worktree ↔ copy-on-write)
arbor switch cow
arbor switch worktree
# Run scaffold steps on an existing workspace
arbor scaffold main
arbor scaffold feature/user-auth
# Destroy the entire project (removes all workspaces and project structure)
arbor destroy
# Pull updated config from the default branch workspace into the project root
arbor pull-configSee AGENTS.md for development guide.
- Command reference
- Configuration files
- Scaffold presets
- Testing strategy
Synchronizes the current worktree branch with an upstream branch by fetching the latest changes and rebasing or merging.
Auto-Stashing (Default):
By default, arbor sync automatically stashes changes before syncing, including:
- Tracked modifications
- Untracked files
Note: Ignored files (like node_modules, vendor, .env) are not stashed for performance reasons. This is safe because git does not modify ignored files during rebase/merge operations, and skipping them makes sync much faster on large projects.
After a successful sync, the stashed changes are automatically restored.
# Sync with default settings (upstream: main, strategy: rebase, auto-stash: on)
arbor sync
# Sync with a specific upstream branch
arbor sync --upstream develop
arbor sync -u develop
# Sync using merge instead of rebase
arbor sync --strategy merge
arbor sync -s merge
# Use a specific remote
arbor sync --remote upstream
arbor sync -r upstream
# Disable auto-stashing (not recommended)
arbor sync --no-auto-stash
# Skip all confirmations
arbor sync --yes
arbor sync -y
# Save sync settings to arbor.yaml for future use
arbor sync --save
# Combination of options
arbor sync --upstream main --strategy rebase --saveConfiguration:
Sync settings can be persisted in arbor.yaml:
sync:
upstream: main
strategy: rebase
remote: origin
auto_stash: true # Default: true, set to false to disableThe command resolves settings in this order:
- CLI flags (
--upstream,--strategy,--remote,--no-auto-stash) - Project config (
arbor.yaml) - Project
default_branch - Interactive selection (if in interactive mode)
Notes:
- Must be run from within a worktree (not project root)
- Fails if worktree is on detached HEAD
- Auto-stashes all changes by default (can be disabled with
--no-auto-stash) - If stash pop fails due to conflicts, the stash is preserved and instructions are provided
- Detects and blocks if rebase or merge is already in progress
- Provides guidance when conflicts occur
Converts the project between worktree and copy-on-write modes. Feature workspaces are removed during the switch (they can be recreated with arbor work <branch>).
# Switch a worktree-mode project to CoW
arbor switch cow
# Switch a CoW project back to worktree mode
arbor switch worktree
# Skip confirmation prompts
arbor switch cow --forceWhat happens during a switch:
- Lists any feature workspaces that will be removed
- Prompts for confirmation (skip with
--force) - Removes all feature workspaces
- Converts the project structure (
.bare↔.arbor) - Updates
arbor.yamlwith the newworkspace_mode
Run scaffold steps for an existing worktree. This is useful when:
- You used
arbor init --skip-scaffoldto clone without running scaffold - You want to re-run scaffold steps on an existing worktree
- You need to scaffold a worktree you're not currently in
# Scaffold a specific worktree by path
arbor scaffold main
arbor scaffold feature/user-auth
# When inside a worktree, scaffold current (prompts for confirmation)
arbor scaffold
# When at project root without args, interactively select worktree
arbor scaffold
# Skip confirmation prompts
arbor scaffold main --force
arbor scaffold main -fUpdates the project-level arbor.yaml (at the project root) with the one from the default branch worktree.
Use this when the repository arbor.yaml (committed in the default branch) has been updated by your team and you want to pull those changes into your local project config.
This replaces the project arbor.yaml entirely with the one from the default branch worktree.
# Pull config from the default branch worktree (prompts for confirmation)
arbor pull-config
# Skip confirmation prompt
arbor pull-config --force
arbor pull-config -f
# Preview what would happen without making changes
arbor pull-config --dry-run
# Show detailed output
arbor pull-config --verbose
arbor pull-config -v
# Suppress all non-essential output
arbor pull-config --quiet
arbor pull-config -qBoth arbor init and arbor work support --skip-scaffold to defer scaffold steps and run them manually later:
# Clone without scaffolding
arbor init [email protected]:user/repo.git --skip-scaffold
# Create a worktree without scaffolding
arbor work feature/my-feature --skip-scaffold
# Scaffold when ready
arbor scaffold main
arbor scaffold feature/my-featureArbor supports two ways to manage parallel feature workspaces. Choose at arbor init time, or switch later with arbor switch.
The traditional mode. Uses a single bare git repository (.bare/) with linked git worktrees for each branch. The object store is shared, so additional workspaces cost only the working-tree files.
my-project/
.bare/ # Shared bare git repo
arbor.yaml
main/ # Default branch worktree
feature-foo/ # Feature branch worktree
Each workspace is a full, independent git clone. On filesystems that support copy-on-write (APFS on macOS, Btrfs/XFS on Linux), new workspaces are created using filesystem-level cloning — they take zero additional disk space until files are actually modified.
my-project/
.arbor/ # CoW project marker
arbor.yaml
main/ # Normal git clone (default branch)
feature-foo/ # CoW clone of main
Benefits:
- Near-instant workspace creation (cloning is a filesystem operation)
- Shared disk blocks for unmodified files (
vendor/,node_modules/, source code) - Fully independent git repos — no shared
.barelock contention - Compatible with all git tools and IDEs (normal
.git/directories)
Requirements for full CoW benefits:
- macOS with APFS filesystem (default since macOS High Sierra)
- Linux with Btrfs or XFS (with reflink support enabled)
- Falls back to regular copy on unsupported filesystems (works, just without disk savings)
# Choose CoW at init time
arbor init [email protected]:user/repo.git --mode cow
# Or switch an existing project
arbor switch cow # worktree → CoW
arbor switch worktree # CoW → worktreeNote:
arbor switchremoves feature workspaces that cannot be migrated in-place. Recreate them witharbor work <branch>after switching.
The mode is stored in arbor.yaml at the project root:
workspace_mode: cow # or "worktree" (default when absent)Arbor uses a three-tier configuration system to separate team configuration from local state.
Located at the project root (alongside .bare/ or .arbor/), this file contains:
- Scaffold steps and cleanup steps
- Preset selection
- Tool configurations
- Project-wide settings
This file is not versioned (the project root is not a git repository).
During arbor init, if an arbor.yaml file is found in the repository, you'll be prompted to copy it to the project root.
Located inside each worktree and committed to git, this file contains:
- Team default scaffold steps
- Shared cleanup steps
- Tool configurations
This file serves as the source of truth for team configuration and is copied to the project root during arbor init.
Located inside each worktree and NOT versioned (should be in .gitignore), this file contains:
db_suffix- unique database suffix for the worktree- Other worktree-specific runtime state
This file is automatically created by Arbor and should never be committed.
Example .gitignore entry:
.arbor.local
Example .arbor.local file:
db_suffix: "sunset"To share scaffold configuration with your team:
- Create
arbor.yamlin your repository with scaffold steps:
preset: laravel
scaffold:
steps:
- name: file.copy
from: .env.example
to: .env
- name: db.create
- name: php.composer
args: ["install"]- Commit and push to git:
git add arbor.yaml
git commit -m "Add Arbor scaffold configuration"
git push- Team members run
arbor init:
arbor init user/repo
# → Found arbor.yaml in repository. Copy to project root for team config? [Y/n]
# → Press Enter to use team configThe config will be automatically copied to their project root and used for all worktrees.
Scaffold steps define actions to run when creating a new worktree. Each step can:
- Run commands (bash, binary, composer, npm, etc.)
- Manage databases (create/destroy)
- Read/write environment variables
- Copy files
- Execute Laravel Artisan commands
Pre-flight checks validate dependencies before any scaffold steps execute. This prevents worktrees from being left in a broken state due to missing requirements.
Configuration:
scaffold:
pre_flight:
condition:
# Check environment variables are set
env_exists:
- OP_VAULT
- OP_ITEM
# Check commands/binaries are installed
command_exists:
- op # 1Password CLI
- herd # Laravel Herd
- composer
# Check required files exist
file_exists:
- .env.op
- package.json
steps:
# Your scaffold steps hereSupported Conditions:
All condition types support both single values and arrays:
| Condition | Single Value | Array | Description |
|---|---|---|---|
env_exists |
env_exists: API_KEY |
env_exists: [API_KEY, API_SECRET] |
Check OS environment variables are set |
command_exists |
command_exists: docker |
command_exists: [docker, docker-compose] |
Check commands are available in PATH |
file_exists |
file_exists: .env |
file_exists: [.env, composer.json] |
Check files exist in worktree |
os |
os: darwin |
os: [darwin, linux] |
Check operating system |
context_var |
context_var: {key: skip_migrations, value: "true"} |
— | Check a runtime context variable set by a previous step |
You can combine multiple condition types:
pre_flight:
condition:
env_exists:
- OP_VAULT
- OP_ITEM
command_exists: op
file_exists: .env.op
os: darwinError Messages:
When pre-flight checks fail, you'll see a detailed breakdown:
âś— Running pre-flight checks
Pre-flight checks failed:
Missing environment variables:
- OP_VAULT
- OP_ITEM
Missing commands:
- op
Missing files:
- .env.op
Please resolve these issues and try again.
Example: 1Password Integration
scaffold:
pre_flight:
condition:
env_exists:
- OP_VAULT
- OP_ITEM
command_exists: op
file_exists: .env.op
steps:
- name: bash.run
command: "op inject -i .env.op -o .env"
- name: php.composer
args: ["install"]This ensures that before any steps run:
- The
opCLI is installed - Environment variables
OP_VAULTandOP_ITEMare set - The
.env.optemplate file exists
Notes:
- Pre-flight checks are skipped when using
--skip-scaffold - File paths in
file_existsare relative to the worktree (no template variables) - All checks must pass for scaffold to proceed
scaffold:
steps:
- name: step.name
enabled: true
args: ["--option"]
condition:
env_file_contains:
file: .env
key: DB_CONNECTION
cleanup:
steps:
- name: cleanup.stepAll steps support template variables that are replaced at runtime:
| Variable | Description | Example |
|---|---|---|
{{ .Path }} |
Worktree directory name | feature-auth |
{{ .RepoPath }} |
Project directory name | myapp |
{{ .RepoName }} |
Repository name | myapp |
{{ .SiteName }} |
Site/project name | myapp |
{{ .Branch }} |
Git branch name | feature-auth |
{{ .DbSuffix }} |
Database suffix (from db.create) | swift_runner |
{{ .VarName }} |
Custom variable from env.read or captured output | Custom values |
db.create - Create a database with unique name
- name: db.create
type: mysql # or pgsql, auto-detected from DB_CONNECTION if omitted
args: ["--prefix", "app"] # optional: customize database prefix- Generates unique name:
{prefix}_{adjective}_{noun}or{site_name}_{adjective}_{noun} - Suffix is generated once per
initorworkinvocation and shared across alldb.createsteps - Auto-detects engine from
DB_CONNECTIONin.env - Retries up to 5 times on collision
- Persists suffix to
.arbor.localfor cleanup
Interactive Features (MySQL/PostgreSQL):
In interactive mode, db.create offers database reuse and migration control:
-
Database Reuse: When other worktrees exist with databases, you can choose to reuse an existing database instead of creating a new one. This is useful for stacked PRs or related feature branches that share the same data.
- The suffix from the selected worktree is copied to your current worktree
- Both worktrees point to the same database
- Non-interactive mode (CI,
--no-interactive,--force) always creates new databases
-
Migration Prompt: After database creation/selection, you'll be asked whether to run
migrate:fresh --seed:- Confirm: Migrations run as part of the scaffold
- Decline: The migration step is skipped
- Non-interactive mode always runs migrations
Multiple databases with shared suffix:
scaffold:
steps:
- name: db.create
args: ["--prefix", "app"]
- name: db.create
args: ["--prefix", "quotes"]
- name: db.create
args: ["--prefix", "knowledge"]Result: Creates app_cool_engine, quotes_cool_engine, knowledge_cool_engine (same suffix, different prefixes)
db.destroy - Clean up databases matching suffix pattern
- name: db.destroy
type: mysql # matches db.create type- Drops all databases matching the suffix pattern
- Runs automatically during
arbor remove
Interactive Cleanup Confirmation:
In interactive mode, before dropping databases, you'll be shown a list of databases that will be affected and asked to confirm:
- Confirm: All databases matching the suffix are dropped
- Decline: Cleanup is skipped (databases are preserved)
- Non-interactive mode (CI,
--no-interactive,--force) drops databases without asking
This prevents accidental destruction of shared databases when working with stacked PRs.
env.read - Read from .env and store as variable
- name: env.read
key: DB_HOST
store_as: DbHost # optional, defaults to key name
file: .env # optional, defaults to .env- Stores value as
{{ .DbHost }}for later steps - Fails if key not found
env.write - Write to .env file
- name: env.write
key: DB_DATABASE
value: "{{ .SiteName }}_{{ .DbSuffix }}"
file: .env # optional, defaults to .env- Creates
.envif missing - Replaces existing values in-place
- Preserves comments, blank lines, and ordering
- Supports template variables
env.copy - Copy keys from another worktree's .env file
# Copy a single key
- name: env.copy
source: ../main # Source worktree path (relative or absolute)
key: API_KEY
# Copy multiple keys
- name: env.copy
source: ../main
keys:
- API_KEY
- API_SECRET
- STRIPE_KEY
source_file: .env # optional, defaults to .env
file: .env # optional target file, defaults to .env- Copies environment variables from a source worktree to the current worktree
- Useful for copying API keys, secrets, or other values from main to feature branches
- Creates target
.envif missing - Updates existing keys in-place
- Supports relative paths (resolved from worktree) or absolute paths
node.npm - npm package manager
- name: node.npm
args: ["install"]node.yarn - Yarn package manager
- name: node.yarn
args: ["install"]node.pnpm - pnpm package manager
- name: node.pnpm
args: ["install"]node.bun - Bun package manager
- name: node.bun
args: ["install"]php.composer - Composer dependency manager
- name: php.composer
args: ["install"]php.laravel - Laravel Artisan commands
- name: php.laravel
args: ["migrate:fresh", "--no-interaction"]Capture command output:
- name: php.laravel
args: ["--version"]
store_as: LaravelVersion
- name: env.write
key: APP_FRAMEWORK_VERSION
value: "{{ .LaravelVersion }}"herd.link - Laravel Herd link
- name: herd.linkbash.run - Run bash commands
- name: bash.run
command: echo "Setting up {{ .Path }}"Capture output for use in later steps:
- name: bash.run
command: "git rev-parse --short HEAD"
store_as: GitCommit
- name: env.write
key: BUILD_COMMIT
value: "{{ .GitCommit }}"file.copy - Copy files with template replacement
- name: file.copy
from: .env.example
to: .envcommand.run - Run any command
- name: command.run
command: npm
args: ["run", "build"]Capture output for use in later steps:
- name: command.run
command: "date +%Y-%m-%d"
store_as: BuildDate
- name: env.write
key: BUILD_DATE
value: "{{ .BuildDate }}"All steps support these configuration options:
| Option | Type | Description |
|---|---|---|
enabled |
boolean | Enable/disable step (default: true) |
condition |
object | Conditional execution rules |
args |
array | Arguments passed to the step (e.g., ["--prefix", "app"]) |
store_as |
string | Store command output as template variable (trimmed, on success only) |
Steps execute in the order they appear in the configuration file.
Steps can be conditionally executed based on environment. Conditions support both single values and arrays:
# Single value conditions
condition:
env_file_contains:
file: .env
key: DB_CONNECTION
# Array conditions - check multiple items at once
condition:
env_exists:
- API_KEY
- API_SECRET
command_exists:
- docker
- docker-compose
file_exists:
- .env
- composer.json
# Runtime context variable set by a previous step
condition:
context_var:
key: skip_migrations
value: "true"
# Negate a condition
condition:
not:
context_var:
key: skip_migrations
value: "true"Complete example for a Laravel project:
scaffold:
steps:
# Create database if DB_CONNECTION is set
- name: db.create
condition:
env_file_contains:
file: .env
key: DB_CONNECTION
# Write database name to .env
- name: env.write
key: DB_DATABASE
value: "{{ .SiteName }}_{{ .DbSuffix }}"
# Install dependencies
- name: php.composer
args: ["install"]
- name: node.npm
args: ["install"]
# Run migrations
- name: php.laravel
args: ["migrate:fresh", "--no-interaction"]
# Set domain based on worktree path
- name: env.write
key: APP_DOMAIN
value: "app.{{ .Path }}.test"
# Generate application key
- name: php.laravel
args: ["key:generate"]
cleanup:
steps:
# Clean up databases
- name: db.destroyExample: Multiple databases with shared suffix
For applications that need multiple databases (e.g., main app, quotes, knowledge):
scaffold:
steps:
# Create three databases with different prefixes but shared suffix
- name: db.create
args: ["--prefix", "app"]
- name: db.create
args: ["--prefix", "quotes"]
- name: db.create
args: ["--prefix", "knowledge"]
# Write the main database name to .env
- name: env.write
key: DB_DATABASE
value: "app_{{ .DbSuffix }}"
# Write other database names to .env (optional)
- name: env.write
key: DB_QUOTES_DATABASE
value: "quotes_{{ .DbSuffix }}"
- name: env.write
key: DB_KNOWLEDGE_DATABASE
value: "knowledge_{{ .DbSuffix }}"This creates: app_cool_engine, quotes_cool_engine, knowledge_cool_engine
Database Naming
- Automatically generates unique, readable database names
- Suffix is generated once per
initorworkinvocation - Format:
{prefix}_{adjective}_{noun}or{site_name}_{adjective}_{noun}(e.g.,myapp_swift_runner,app_cool_engine) - Multiple
db.createsteps share the same suffix, allowing consistent database naming - Handles collisions with automatic retries
- Enforces PostgreSQL/MySQL length limits
Database Cleanup
- Automatically drops databases when worktree is removed
- Uses pattern matching to find all databases with same suffix
- Integrates with
arbor removecommand
Template Variables
- All template syntax uses Go's
text/template - Handles whitespace variations:
{{ .Path }},{{ .Path }},{{ .Path }} - Fails fast on unknown variables with clear error messages
- Supports dynamic variables from previous steps
File Operations
- Atomic writes for environment files
- Preserves file permissions
- Maintains existing formatting (comments, blank lines, ordering)
- Creates directories as needed
Error Handling
- Graceful degradation where appropriate
- Clear error messages for configuration issues
- Non-fatal warnings for optional operations
MIT