Anvil 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
anvil work feature/new-feature
cd feature-new-feature
# Make changes, test, commit
go test ./...
anvil work another-feature # Create another if needed
# When done with a worktree
cd ..
anvil remove feature-new-featurebrew tap naoray/tap
brew install anvilUpgrade:
brew upgrade anvilDownload the latest release for your platform from the releases page.
go install github.com/naoray/anvil/cmd/anvil@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/naoray/anvil.git
cd anvil
# Build for your platform
go build -o anvil ./cmd/anvil
# 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 anvil ./cmd/anvilAfter installation, run the setup wizard to configure anvil:
anvil installThe wizard will:
- Check your PATH
- Detect Herd/Valet
- Install shell completions
- Set a default projects root
- Optionally install AI CLI skills (Codex CLI, Claude Code, etc.)
Shell completions are installed automatically by the wizard. To install or reinstall manually:
# Install completions for your current shell
anvil completion zsh # installs to zsh site-functions
anvil completion bash # installs to bash_completion.d
anvil completion fish # installs to fish completions dir
# Print completion script to stdout (for manual setup)
anvil completion zsh --printFor zsh with Homebrew, the completion file is written to $(brew --prefix)/share/zsh/site-functions/_anvil. For user-local installs, it goes to ~/.zsh/completions/_anvil. Add the directory to your fpath if needed:
# ~/.zshrc
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit# Check anvil version
anvil version
# Link an existing project (auto-detects name from git remote)
anvil link
# Create a feature worktree
anvil work feature/user-auth
# Create a worktree from a specific base branch
anvil work feature/user-auth -b develop
# Create a worktree without running scaffold steps
anvil work feature/user-auth --skip-scaffold
# Print path to a worktree
anvil info feature/user-auth
# Open a worktree in your IDE and browser
anvil open feature/user-auth
# Sync current worktree with upstream (defaults to main, uses rebase)
anvil sync
# Sync with a specific upstream branch
anvil sync --upstream develop
# Sync using merge instead of rebase
anvil sync --strategy merge
# Save sync settings to anvil.yaml for future use
anvil sync --upstream develop --strategy rebase --save
# List all worktrees with their status
anvil list
# Remove a worktree when done
anvil remove feature/user-auth
# Clean up merged worktrees
anvil prune
# Run scaffold steps on an existing worktree
anvil scaffold main
anvil scaffold feature/user-auth
# Copy anvil.yaml from default branch to project root
anvil pull-config
# Repair git configuration (fetch refspec, branch tracking)
anvil repair
# Setup global configuration and detect tools
anvil install
# Generate shell completion scripts
anvil completion zsh >> ~/.zshrc
# Unlink a project (stop managing it)
anvil unlinkSee 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, anvil 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)
anvil sync
# Sync with a specific upstream branch
anvil sync --upstream develop
anvil sync -u develop
# Sync using merge instead of rebase
anvil sync --strategy merge
anvil sync -s merge
# Use a specific remote
anvil sync --remote upstream
anvil sync -r upstream
# Disable auto-stashing (not recommended)
anvil sync --no-auto-stash
# Skip all confirmations
anvil sync --yes
anvil sync -y
# Save sync settings to anvil.yaml for future use
anvil sync --save
# Combination of options
anvil sync --upstream main --strategy rebase --saveConfiguration:
Sync settings can be persisted in anvil.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 (
anvil.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
Run scaffold steps for an existing worktree. This is useful when:
- 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
anvil scaffold main
anvil scaffold feature/user-auth
# When inside a worktree, scaffold current (prompts for confirmation)
anvil scaffold
# When at project root without args, interactively select worktree
anvil scaffoldOpen a worktree in your IDE and its Herd-linked site in the browser with a single command. Supports fuzzy matching by folder name, branch name, or partial match.
# Open in both IDE and browser
anvil open feature-auth
# Partial match
anvil open auth
# IDE only
anvil open auth --editor
# Browser only
anvil open auth --browser
# Use a specific editor command
anvil open auth --editor-cmd=zedURL Resolution:
The browser URL is determined by reading APP_URL from the worktree's .env file. If APP_URL is missing or points to localhost (a common .env.example default), it falls back to https://<folder-name>.test.
Editor Configuration:
The IDE command is resolved in this order:
--editor-cmdflag- Project config (
editor_cmdinanvil.yaml) - Global config (
editor_cmdin~/.config/anvil/anvil.yaml) - Default:
cursor
Create or checkout a feature worktree.
# Create a worktree for a new branch
anvil work feature/user-auth
# Create from a specific base branch
anvil work feature/user-auth -b develop
# Skip scaffold steps (run later with `anvil scaffold`)
anvil work feature/user-auth --skip-scaffold
# Skip remote tracking setup
anvil work feature/user-auth --no-track
# Interactive mode — select from available branches
anvil workCopy anvil.yaml from the default branch worktree to the project root. Useful for propagating team configuration changes from the main branch.
# Interactive — prompts before overwriting
anvil pull-config
# Force overwrite without prompt
anvil pull-config --force
# Preview what would happen
anvil pull-config --dry-runRepair git configuration for an existing anvil project. Fixes fetch refspec and branch tracking.
# Full repair (refspec + tracking)
anvil repair
# Preview changes
anvil repair --dry-run
# Only fix fetch refspec
anvil repair --refspec-only
# Only fix branch tracking
anvil repair --tracking-onlySetup global configuration and detect available tools (gh, herd, php, composer, npm). Creates ~/.config/anvil/anvil.yaml.
anvil installGenerate shell completion scripts for tab-completing worktree names in open, scaffold, info, and remove commands.
# Zsh
anvil completion zsh > "${fpath[1]}/_anvil"
# Bash
anvil completion bash > /etc/bash_completion.d/anvil
# Fish
anvil completion fish > ~/.config/fish/completions/anvil.fish
# PowerShell
anvil completion powershell > anvil.ps1Anvil uses a three-tier configuration system to separate team configuration from local state.
Located at the project root, this file contains:
- Scaffold steps and cleanup steps
- Preset selection
- Tool configurations
- Project-wide settings
This file can be committed to git for team sharing.
Example:
preset: laravel
editor_cmd: cursor # IDE for `anvil open` (optional)
sync:
upstream: main
strategy: rebase
scaffold:
steps:
- name: file.copy
from: .env.example
to: .envLocated inside each worktree, this file can contain worktree-specific overrides:
- Team default scaffold steps
- Shared cleanup steps
- Tool configurations
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 Anvil and should never be committed.
Example .gitignore entry:
.anvil.local
Example .anvil.local file:
db_suffix: "sunset"To share scaffold configuration with your team:
- Create
anvil.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 anvil.yaml
git commit -m "Add Anvil scaffold configuration"
git push- Team members link the project:
cd my-project
anvil linkThe config will be 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 |
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 |
{{ .DatabaseName }} |
Full database name (truncated to 63 chars) | myapp_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
.anvil.localfor cleanup
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
anvil remove
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: "{{ .DatabaseName }}"
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.jsonComplete 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: "{{ .DatabaseName }}"
# 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
anvil 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