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 Yerd/Herd/Valet and select a site driver
- 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 --printAfter installation, restart your shell to activate the completion. Existing zsh shells may need a manual refresh, and Anvil leaves zsh completion cache files untouched. For 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, then run autoload -Uz compinit && compinit:
# ~/.zshrc
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit# Check anvil version (same line as `anvil --version`; `--json` for agents)
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
# Preview a removal: never prompts, needs no --force, changes nothing
anvil remove feature-user-auth --dry-run
anvil remove feature-user-auth --dry-run --json # one anvil.agent/v1 plan document
# Legacy interactive cleanup (merge state and leases only; agents and scripts
# should use prune candidates/apply instead of --force)
anvil prune
# Preview every merged worktree without prompting or fetching
anvil prune --dry-run
# Register Anvil identity for worktrees created outside Anvil (no scaffolding)
anvil adopt --all
# Evidence-based bulk prune: list candidates, then apply after rechecking
anvil prune candidates --json --output candidates.json
anvil prune apply --from candidates.json --dry-run
anvil prune apply --from candidates.json --force
# 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 scaffold--force suppresses Anvil's interactive prompts. --no-interactive and a
nonempty CI environment variable also suppress prompts, including first-run
setup, even in a terminal. Run inside the desired worktree or supply WORKTREE
explicitly; missing selection returns an error. --quiet keeps prompts enabled.
--force does not authorize database resets or drops or bypass ownership checks.
Open a worktree in your IDE and its locally 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 workSelecting a remote branch with anvil work <remote>/<branch> or the interactive
picker creates local <branch> at that remote-tracking commit and tracks the
selected remote. Exact local names take precedence, including names beginning
with origin/. An unqualified name selects a unique remote match; ambiguous
matches require a remote-qualified name. Use refs/remotes/<remote>/<branch>
when a remote's short name collides with a literal local branch name.
Existing local branches retain their commit and upstream. For new branches,
--base overrides the starting commit while tracking still uses the selected
remote; --no-track disables new tracking, including Git's automatic tracking.
Anvil does not fetch or push automatically.
Worktrees have separate files, indexes and HEADs. Git refs and repository configuration remain shared across worktrees.
Opt in with a successfully scaffolded source in the same linked repository:
anvil work feature/next --warm-from feature/current
anvil work feature/next --warm-from feature/current --copy-mode copyUse an exact registered branch, folder name, or worktree path. The selected
target branch and --base remain authoritative. --warm-from requires scaffold
execution; --copy-mode requires --warm-from.
--copy-mode auto uses native filesystem cloning where supported, with ordinary
copy fallback for unsupported or cross-volume operations. --copy-mode copy
always makes ordinary copies. Each dependency install reports cloned, copied,
or cold. Copies never use hardlinks. No startup speedup or physical disk-space
savings are claimed; verification and tool probes have their own cost.
Only vendor and node_modules are eligible, independently. Reuse requires a
successful recorded locked install, matching manifests/lockfiles, configuration,
toolchain and platform, and unchanged installed contents, modes and links.
Missing, stale or incompatible provenance runs the normal cold installer.
Candidates are staged and checked before publication; unexpected copy failures
fail scaffolding. A warm hit skips its installer while later build and setup
steps still run.
Eligibility is deliberately narrow: plain npm ci with complete v2/v3 lockfile
integrity metadata, or scriptless composer install. Additional installer
arguments, install hooks, plugins, local/workspace dependencies, native node
addons, and nonstandard Composer paths stay cold by default (npm scripts and
addons can opt in, below). Any npm-shrinkwrap.json or
alternate manifest selected through COMPOSER stays cold. Normal Laravel
Composer scripts therefore keep Composer cold while npm may still warm.
Composer root versions and source refs must also match; compatible hits rebuild
target autoload files with plugins and scripts disabled.
Projects whose dependencies run install scripts or build native addons can opt
in for node_modules in the committed anvil.yaml:
warm:
node_modules:
trust_install_scripts: true
rebuild: npm rebuild # optional; runs in the destination after a warm hitWith the opt-in, root and dependency preinstall/install/postinstall/
prepare scripts and .node addons no longer force a cold install. Reuse
still requires the same lockfile, manifests, Node (process.versions) and npm
versions, platform and architecture, plus identical script identities: for the
root package.json and every installed lockfile dependency, Anvil records the
package path, script name and SHA-256 of the script text (a binding.gyp
without an install script counts as npm's implicit node-gyp rebuild). The
lockfile's hasInstallScript flags must agree with the installed packages.
Identities are checked on the source tree and again on the staged copy; any
difference, or changing or removing the warm section, reinstalls cold. Other
restrictions (workspaces, local dependencies, shrinkwrap, missing integrity)
still apply.
A warm hit never runs npm ci and therefore never re-runs the root project's
lifecycle scripts. When the root package.json declares any of them (for
example postinstall or prepare codegen outside node_modules), the project
is eligible only with a rebuild command, which must reproduce their effects
(for example npm rebuild && npm run postinstall); without one it stays cold.
Dependency scripts need no rebuild unless their output embeds paths (below).
A dependency binding.gyp counts as an implicit install unless the package
declares "gypfile": false.
Files that embed the source worktree path (for example node-gyp build/
directories) are reusable only when rebuild is configured. The rebuild runs
with the step's environment in the destination worktree; its output is shown
only on failure or with --verbose. Afterwards no file in the destination
node_modules may still reference the source worktree (including its
/private alias on macOS and Makefile-escaped spaces), no symlink may be
absolute or leave the destination tree, and the rebuilt tree must still pass
the same verification as a recorded install. If the rebuild fails or any check
fails, anvil work fails, the warmed node_modules is removed and
no provenance is recorded, so a retry installs cold. If Anvil is killed before
it can clean up, a retry with --warm-from reports
node_modules: cold (existing unverified tree) for the unstamped leftover and
runs the normal installer, which replaces it. Without rebuild, such
trees stay cold.
Trusted provenance in .anvil.local (dependency_installs.node_modules) adds
trust_install_scripts, trusted_scripts (package, script, sha256),
install_script_packages, rebuild and, for warm hits, the source worktree.
Script texts and secrets are never stored. Warm trees are always independent
copies or filesystem clones; writes in one worktree's node_modules, including
rebuilt addons, never affect another. Only enable trust for projects whose
install scripts are deterministic for the same inputs; scripts that download or
generate machine-specific state outside these inputs should keep the default.
Provenance lives in the target's .anvil.local, alongside its own database state.
Warm copies contain no Git metadata, environment files, ownership or borrowed
bindings, application caches, or live databases. Source and sibling writable
files and Git indexes remain independent; Git refs and configuration are shared
as with ordinary Git worktrees. Inspect the reported result rather than assuming
a requested source was eligible.
Create a worktree if needed, then prepare it idempotently. Designed for coding
agents and scripts: it never prompts and supports --json.
anvil ensure feature/auth # create (like `anvil work`) or resolve, then prepare
anvil ensure feature-auth --json # one versioned JSON document on stdout
anvil ensure # prepare the worktree containing the current directory
anvil ensure feature/auth --refresh # rerun every step, ignoring completion records
anvil ensure feature/auth --skip-prepareWORKTREE is matched exactly by folder name, branch, path or Anvil worktree
ID (wt_…); partial matches are never used. When nothing matches, it is treated
as a branch name and created exactly as anvil work would (--base, --no-track
apply). Concurrent ensure calls for the same branch converge on one worktree.
--dry-run is rejected before any worktree, identity or preparation changes.
Preparation runs the same steps as anvil scaffold (preset defaults plus
scaffold.steps) under a per-worktree lock and records each completed step in
.anvil.local as soon as it succeeds. On the next ensure, a step is skipped as
up_to_date when its definition is unchanged, its declared inputs hash to the
same digest and its declared outputs still exist. Steps without inputs run
once per definition. Database steps (db.create, db.migrate, db.destroy),
herd/yerd, env.read, store_as producers and always: true steps run every time under
their own safety policies. A failed or interrupted run resumes at the first
incomplete step. anvil scaffold and anvil work keep their run-everything
behavior but also record completions, so a later ensure builds on them.
The Laravel/PHP presets declare composer.json/composer.lock → vendor and
package.json/package-lock.json → node_modules, so ensure reinstalls only
when lockfiles change. Toolchain version changes are not detected; use
--refresh. Database flags (--db-action, --reset-database,
--borrow-app-db) behave exactly as on work and scaffold.
See docs/agent-api.md for the JSON contract and exit codes.
Register an Anvil worktree identity for existing Git worktrees of the linked project without scaffolding or preparing anything:
anvil adopt feature-x # one worktree (exact folder, branch, path or ID)
anvil adopt --all --json # every registered worktree on disk
anvil adopt feature-x --lease-owner codex # also acquire an advisory lease
anvil adopt --all --dry-run # preview; writes nothingRetrying is safe and keeps existing state exactly as anvil ensure would:
adopted (new ID), unchanged (identity already matches), updated (the
worktree moved: ID and lease kept, path updated) or reassigned (the state was
copied from another live worktree: new ID, the original's lease and process
records dropped from the copy). --all skips bare entries, missing
directories and the main checkout (adopt the main checkout by naming it).
Lease flags (--lease-owner, --lease-ttl, --session-id,
--takeover-stale) behave as on ensure: an active lease held by another
owner is never replaced (lease_held, exit 7).
An auditable alternative to anvil prune for one project. Legacy anvil prune --force only checks merge state and leases (no activity, open-file, unpushed
or ignored-file evidence), so agents and scripts should use these instead:
anvil prune candidates --json --output candidates.json # read-only
anvil prune apply --from candidates.json --dry-run # recheck, change nothing
anvil prune apply --from candidates.json --force --json # recheck, then removecandidates lists every worktree with candidate, reasons[] and
blockers[] plus the evidence: merge state against origin/<default> (never
fetched unless --fetch), GitHub PR state through gh pr list when available
(an open PR blocks; an unavailable lookup does not; only a PR merged into the
default branch counts as merged; --no-pr skips it), commits missing from
every remote, tracked changes, untracked files (node_modules excluded),
gitignored files, the lease (active blocks, stale warns), protected patterns,
recent activity and processes holding files open beneath the worktree. Any
evidence that is missing, failed or unsupported is a blocker. Git runs with
--no-optional-locks, so the read-only scan never rewrites an index. The main
checkout, bare, detached, locked and missing worktrees and the default branch
are never candidates, and worktrees without an Anvil identity are blocked
until adopted.
-
Protected patterns:
prune.protected(globs matched against the branch and the folder name) in the global~/.config/anvil/anvil.yamland in the projectanvil.yaml, plus--protect GLOB. The default branch is always protected. -
Ignored files: removing a worktree deletes its gitignored files too (
.env, SQLite databases,storage/, build output). Every ignored path outsidenode_modules,vendorand Anvil's.anvil.localblocks unless allowed by--allow-ignored GLOBorprune.allow_ignored(global and project, combined). A pattern matches the path relative to the worktree or one of its parent directories, sostorageallows everything below it. Typical:--allow-ignored .envfor scaffolded env files you can recreate.prune: protected: - release/* allow_ignored: - .env - storage
-
Recent activity (
--recent, default24h, minimum10m; it cannot be disabled): the newest of the HEAD reflog (a missing reflog only warns), tracked, untracked and ignored file modification times (excludingnode_modules,vendorand.git), the lease renewal, the latest check result and the preparation record. Running checks always block. -
Open files: on Linux every
/proc/<pid>is inspected forcwd,root,exe, open descriptors and memory maps beneath the worktree (a shell whose current directory is inside it counts); on macOSlsofis used; elsewhere the evidence is unsupported and blocks. Your own processes that cannot be inspected (for example non-dumpable agents such asssh-agent) block unless you vouch for them with--allow-uninspectable NAME. That exempts every process with that command name (the kernel'scomm: truncated to 15 characters, and any process of yours can set it) in every worktree, so name only processes that can never use one. -
Known gaps: other users' processes that cannot be inspected only warn by default (Linux counts them as
skipped_other_users; on macOS they are invisible tolsofand reported asnull). A root-owned container (Docker, Laravel Sail) or another account using the worktree is therefore not detected unless you pass--strict-processes, which makes them block (on macOS it always blocks, since they cannot be inspected there). The default is permissive here only because system daemons are always uninspectable for non-root users, so blocking would make pruning unusable. -
Mount namespaces: a process in another mount namespace (a container started as your user, snap or flatpak apps) sees namespace-local paths that cannot be mapped to the worktree. Your own such processes block ("runs in another mount namespace"); other users' follow the rule above. Kernel threads are ignored. Containers that share worktrees (Docker, Sail) should hold a lease (
anvil lease acquire); sandboxed desktop apps may need--allow-uninspectable NAME.
apply validates the file (schema anvil.prune-candidates/v1, the same Git
common directory as the current project) and, for each entry marked
candidate: true, that it is still a registered worktree at the same
canonical path with the same worktree ID, branch and HEAD (a file listing a
path or ID twice is refused). It then collects all
evidence again, and once more under the exclusive lifecycle gate immediately
before removal; any blocker skips the entry. Its own --recent, --protect
and process flags apply, only ever strengthened by the file's settings
(ignored paths may be deleted only if both the file and the current
invocation allow them), so a stale or edited file cannot override current
checks. Removal goes through the
same lifecycle as anvil remove and requires --force unless --dry-run
(never --ignore-lease; --keep-db
supported; branches are kept). A process that starts using the worktree outside
Anvil between the final recheck and git worktree remove is not detected.
Run a command in a worktree's configured environment without requiring a testing database:
anvil run feature-x -- cargo test
anvil run feature-x --component eval -- python score.py # component dir + its env (venv, node_modules/.bin)
anvil run -- npm test # current worktree and directoryExports ANVIL_* identity variables (ANVIL_WORKTREE_ID, ANVIL_INSTANCE, …),
recorded database names as ANVIL_DB_DATABASE/ANVIL_TEST_DB_DATABASE, and the
project env. It never sets DB_DATABASE; use anvil exec for the Laravel test
database. Child exit status passes through. COMMAND is resolved on the exported
PATH, so component-local tools win. Warns when preparation (including the
selected component's) is incomplete; --require-ready refuses instead.
Repositories with several stacks declare components; anvil ensure prepares
each one with lockfile-backed defaults from its capabilities:
components:
- name: desktop
path: apps/desktop
uses: [node, tauri] # pnpm install --frozen-lockfile; cargo fetch in src-tauri
- name: service
path: service
uses: [node] # npm ci
- name: helper
path: helpers/mac
uses: [swift] # macOS only by default; skipped elsewherePer-worktree runtime resources and managed processes:
resources:
ports: [{name: web, env: VITE_PORT}] # stable, collision-free ANVIL_PORT_WEB
directories: [index-out] # plus ANVIL_STATE_DIR/TMP/CACHE/LOG/ARTIFACTS
shared: [{name: models, path: ~/.cache/models, required: true, identity: {file: SHA256SUMS}}]
processes:
- {name: web, component: desktop, command: 'pnpm dev --port "$VITE_PORT" --strictPort'}anvil start feature-x --json # detached, logs in $ANVIL_LOG_DIR
anvil stop feature-x # verified PID + start-time identity only
anvil remove feature-x --dry-run # also lists processes, ports and runtime dirs (add --json for a plan)Named checks with trustworthy provenance:
anvil check fast --json # commit, dirty-state digest, toolchain, artifacts
anvil check --list --json # is the latest result still current? why not?
anvil check benchmark --allow-expensive --reuse # exclusive `locks`, conservative reuse
anvil check fast --reuse --json # reuse a matching author's pass in a review worktree
anvil check fast --reuse --reuse-exclude-role implementation # rerun author results for an independent review
anvil check e2e --allow-expensive --priority 5 # queue for a machine-wide slot, ahead of priority 0
anvil slots --json # slot capacity, load, holders and queue (read-only)Expensive checks (and checks declaring slots: N) share a machine-wide slot
semaphore across all projects and worktrees, so parallel agents never run more
heavy suites at once than the machine can take. Waiters queue in order
(higher --priority first, then arrival); --slot-wait 10m bounds the wait
(default 0 waits until interrupted, unlike --lock-wait 0). Slots are shared
per runtime root (per user, or per ANVIL_RUNTIME_ROOT); processes configured
with different capacities are capped at the largest, so keep it consistent.
A slot-gated check started from inside another one is refused unless it
passes --slot-wait, since it could wait forever for its parent's slot. A killed run frees its slots automatically.
Configure it in ~/.config/anvil/anvil.yaml:
checks:
slots: 2 # concurrent slot weight (default: max(2, CPUs/4); env ANVIL_CHECK_SLOTS)
max_load: 6.0 # also wait while the 1-minute load average exceeds this (optional)
load_wait: 2m # ...for at most this long, then proceed with a warning--reuse on a reusable: true check also accepts a clean passing result from
another worktree of the project at the same content identity: HEAD's tree,
check definition, inputs, toolchain, platform, declared shared resources and
declared environment (inherited variables such as agent session IDs are
ignored; a short list of toolchain variables such as PATH, GOFLAGS,
NODE_ENV and RUSTFLAGS and any named in identity_env count; worktree
paths, IDs, ports and database names are normalized). Content outside Git
(vendor/, node_modules/, generated .env) is not part of it: declare
lockfiles and manifests as inputs and add toolchain probes. Results record their producer (worktree, role, lease
owner); a reused result reports assessment.reused_from. Restrict producers
with --reuse-trust same-worktree|same-role|any or roles.<role>.reuse_trust
and reuse_exclude_roles (default: any).
A check that declares resources: [models] records that shared resource's
identity (a manifest file, a command's stdout, or a cached directory
digest); when it changes, the result is stale with
shared resource "models" changed. Undeclared resources never affect it.
Agent lifecycle — machine-readable, exact resolution, never prompts:
anvil ensure feature/x --json --lease-owner codex --task-id SOLO-7 # prepared + leased
anvil ensure --review <sha> --json # independent reviewer worktree pinned to a commit
anvil context feature-x --json # environment, components, resources, checks (read-only)
anvil status feature-x --json # readiness, lease, processes, check currency, cleanup preview
anvil status --all --json # every worktree: ready, prepare, lease, managed, role, dirty
anvil lease release feature-x # ANVIL_LEASE_TOKEN; remove refuses active leasescontext, status and each status --all entry report the same lease
object (lease.state), ready, a prepare object (prepare.status) and
worktree.managed (Anvil holds a valid identity for the path; the main
checkout and plain git worktree add worktrees are unmanaged until
anvil ensure). See docs/agent-api.md for the versioned
JSON contract.
Capabilities: node, rust, python (uv/poetry/pip), go, php, tauri,
swift. See docs/environments.md for the full schema and
examples/ for synthetic Rust/Python, Node/Rust/Tauri/Swift and
Laravel repositories.
Copy 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, yerd, 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.ps1Laravel scaffolding migrates and seeds a newly created, owned application
database. Existing application databases and borrowed/shared application data
skip migrations by default. The worktree's own testing database is migrated
(never seeded) on every scaffold or ensure; rebuilding it with migrate:fresh
is opt-in — see Test Database Isolation. --force,
CI, quiet mode and --no-interactive never authorize a reset.
anvil scaffold feature --db-action application=migrate
anvil scaffold feature --db-action application=skip --db-action testing=migrate
anvil scaffold feature --db-action testing=fresh
anvil scaffold feature --reset-database EXACT_OWNED_NAME
anvil work feature --borrow-app-db owner-worktree--db-action accepts application, testing or an exact database name, with
skip, migrate or fresh. fresh (migrate:fresh without --seed) is
accepted only for the worktree's owned, isolated testing database; any other
target is rejected before a command runs. The same selectors work in target
YAML:
scaffold:
database_actions:
application: skip
testing: migrateA CLI value replaces the same config selector. Exact-name spelling, including
case, is preserved. Selectors that overlap one database are rejected. Repeatable
--reset-database EXACT_NAME is the sole built-in authorization for
migrate:fresh --seed, and requires an existing exact owned database. Role names,
borrowed/shared targets and newly created databases cannot be reset. All selected
targets are checked before any migration/reset command. Each command uses its
own database environment and an uncached Laravel configuration path without
temporarily rewriting .env.
Borrowing is opt-in through --borrow-app-db WORKTREE on work or scaffold.
A registered source path or symlink alias can also identify it. The source must
own an available managed SQL application database at the same supported service
endpoint. Chains, self-borrowing, malformed state and engine/service/endpoint
mismatches fail; socket/URL overrides are rejected as ambiguous. An application
db.create step must apply the binding; an omitted/skipped step fails.
Credentials, suffixes and testing records are never copied.
Borrowing shares application data and schema: writes and migrations affect all
borrowers. Use isolated application databases for concurrent work that needs data
independence. A borrower retains its own suffix, testing database and historical
owned records. Re-scaffolding retains the binding and requires the source to
remain registered and available. anvil exec exports the borrower's own testing
database and the exact borrowed application identity without checking/starting a
service or requiring a still-registered owner merely to export recorded state.
New server ownership records retain the verified driver, service, host and port
under endpoint. Historical resources use that recorded identity even when the
current .env or selected site driver changes. Anvil verifies Yerd's actual
reported listener before availability checks and migrations. Unknown or
unverifiable identity cannot grant migration/reset or borrowing authority.
Legacy ownership records remain readable for safe skips and command exports;
remove them with --keep-db and inspect/manage those databases separately.
Removing a borrower drops only its owned resources. An owner with active borrowers
cannot be removed destructively, even with --force; --keep-db preserves its
databases and permits removal. Borrowers retain the binding and never adopt
ownership. Malformed or unreadable sibling state blocks destructive cleanup.
A registered sibling whose directory was deleted from disk cannot hold a
binding: it is skipped with a warning suggesting git worktree prune (or
git worktree repair if it was moved). An unlocked worktree on an
unmounted volume looks the same and is treated as deleted, so lock worktrees on
removable media with git worktree lock: a locked worktree that is absent
blocks destructive cleanup until it is reconnected, or unlocked and pruned. A
dangling symlink or any other uninspectable sibling path also blocks.
Prune and unlink --clean remove selected borrowers before selected owners.
unlink --clean uses guarded cleanup and preserves unregistered files.
The Laravel preset copies .env.example only when .env is missing. Its
env.write steps still update the resolved database name and an absent app key.
SQLite defaults to
database/database.sqlite; configured relative paths agree between creation and
DB_DATABASE. Missing files are created exclusively, preserving existing rows.
Absolute paths, traversal, symlinks, special files, URI forms and :memory: are
rejected. SQLite creates no SQL-server testing ownership record, and anvil exec
keeps its existing SQLite refusal. Live SQLite copying is not supported.
Custom scaffolds can use db.migrate; explicit database-action/reset flags append
that policy when an override omits it. Arbitrary shell commands remain user code:
this policy does not sandbox a custom migrate:fresh command.
Parallel worktrees used to share one test database: Laravel's phpunit.xml
commonly pins DB_DATABASE, and phpunit.xml values beat .env, so test runs
from two worktrees wiped each other's data. Anvil scaffolds a dedicated test
database per worktree and provides anvil exec to run commands with it
exported.
Scaffolding a worktree (Laravel preset) creates two databases and records
them in .anvil.local (generated, git-ignored, no credentials; unknown
fields are preserved on rewrite):
db_suffix: swift_runner
databases:
- name: myapp_swift_runner
engine: mysql
role: application
endpoint: {driver: yerd, service: mysql, host: 127.0.0.1, port: "3306"}
- name: myapp_swift_runner_test
engine: mysql
role: testing
endpoint: {driver: yerd, service: mysql, host: 127.0.0.1, port: "3306"}Anvil delegates server-backed database operations to the selected site driver.
With site_driver: yerd, Yerd owns service status plus database creation,
listing, and removal. With site_driver: herd, Anvil starts the selected Herd
service and uses the database binaries provided by Herd for logical database
creation, listing, and removal. Anvil has no embedded SQL clients; it still
owns worktree-safe naming, collision checks, and .anvil.local records.
role: application(the default) androle: testingeach use the first-created database for that role as the canonical record used byanvil exec. Additional distinct application or testing databases are retained as cleanup-onlyauxiliaryrecords, so every database remains eligible for exact cleanup.auxiliaryis an internal ownership role and is not valid in a user-configureddb.createstep.role: testingderives<site>_<suffix>_test(capped at 54 characters so Laravel's parallel-worker suffixes still fit MySQL/MariaDB's 64 and PostgreSQL's 63 identifier limits) and re-scaffolds idempotently.db.createitself only ensures the database exists; the followingdb.migratestep prepares its schema (below).- Custom scaffold steps can reference the name via
{{ .TestDatabaseName }}.
The Laravel preset's db.migrate step runs after both db.create steps and
prepares the testing database so anvil exec php artisan test finds a schema.
By default it runs a non-destructive artisan migrate (no --seed) on the
worktree's own testing database, whether this run created it (new worktree,
or dropped on the server since) or it already existed. db.migrate runs on
every anvil scaffold, anvil work and anvil ensure, so the default never
discards rows — preparation stays safe while anvil check or
anvil exec php artisan test use the database.
Each command gets the testing database through its own environment
(DB_DATABASE, DB_CONNECTION, host and port), never by editing .env. The
application database keeps its own policy: re-preparing never migrates or
wipes an existing one.
The testing target must establish ownership and isolation: it is this
worktree's canonical role: testing record in .anvil.local, with a
verified endpoint, named <site>_<suffix>_test for this worktree's
db_suffix, and ensured by db.create in the same run. Borrowing only ever
shares an application database, never a testing one. Anything else —
unrecorded databases with the same name, auxiliary records, borrowed or
shared application databases, legacy records without an endpoint, or a
testing record whose name does not match the worktree suffix — is never
targeted by default. laravel-shared-db has no testing database, so nothing
is migrated.
Rebuilding the testing database (for example after a branch switch that
rewrote migrations) is a one-off opt-in: --db-action testing=fresh on
anvil scaffold, work or ensure runs artisan migrate:fresh without
--seed, discarding its rows. It is accepted only as that flag:
database_actions in anvil.yaml rejects fresh with a configuration error,
because a declaration would rebuild the database on every ensure. fresh is
refused for any other target. Before the command it holds the repository
database lock and refuses when:
- a check of this worktree is running (
anvil checkmay be using the database). If the worktree's identity or runtime directory cannot be resolved, it refuses rather than guess; - another registered worktree of the repository records or borrows the same name (unreadable sibling state also refuses).
anvil exec sessions (for example a running anvil exec php artisan test)
hold no lock and cannot be detected, and a check started after the guard is
not blocked: don't run testing=fresh while tests use the database.
Overrides: database_actions: {testing: skip} (or --db-action testing=skip)
keeps the testing database untouched. --dry-run prints the planned line
(Database <name> role=testing ... action=migrate|fresh) and runs nothing. A
failed migration fails the step: anvil work/scaffold/ensure exit
non-zero, anvil status --json reports ready: false, and the error names
the database, role, action and corrective command, for example
database "myapp_swift_runner_test" role=testing action=migrate failed: exit status 1; fix the cause and rerun 'anvil scaffold' (or set database_actions: {testing: skip} to opt out), followed by the command's output.
Custom scaffolds that declare db.create with role: testing get the same
behavior only if they also list db.migrate after it.
Runs a command with the worktree's test database exported. Anvil reads
.anvil.local to resolve the database name; it does not create or modify
databases or contact the database server. The command it launches may contact
or modify a database, depending on what it runs.
With the Laravel preset, anvil work and anvil scaffold provision the
application and testing databases and migrate the testing database.
Supported project test commands run through anvil exec (for example
anvil exec php artisan test). A bare php artisan test does not use the
isolated database: it reads DB_DATABASE from phpunit.xml or .env, which
usually point at a shared test database or the worktree's application
database, so a bare run can clobber data that another worktree or your
local application relies on.
# Run the test suite against the worktree's test database
anvil exec -- ./scripts/git/run-tests.sh --parallel
# Works without -- too; child flags pass through
anvil exec php artisan test
# Inspect what the child sees
anvil exec -- php -r 'echo getenv("DB_DATABASE");'Environment exported to the child (inherited values are replaced, case-insensitively on Windows):
DB_DATABASE=<test database> # e.g. myapp_swift_runner_test
ANVIL_TEST_DB_DATABASE=<test database>
ANVIL_DB_DATABASE=<application database> # omitted if unknown
What anvil exec does when state is incomplete:
| Situation | Behavior | Corrective command |
|---|---|---|
No .anvil.local in the directory or any parent |
exits 1 before running the command |
anvil scaffold in the worktree (or anvil work <branch>) |
.anvil.local without a role: testing record |
exits 1 with the pre-1.8 guidance |
anvil work <branch> for a new worktree, or a scaffold override containing only db.create with role: testing |
| Recorded testing database missing on the server, or its schema is behind | runs the command with the recorded name; the test runner then reports the connection or schema error | For Laravel, anvil scaffold recreates a missing owned testing database and migrates it, without touching the application database; custom scaffolds need db.create and db.migrate steps |
| Testing schema incompatible with the branch (rewritten migrations) | as above | anvil scaffold --db-action testing=fresh |
Last scaffold failed (anvil status not ready) |
runs the command with the recorded name | fix the reported failure and rerun anvil scaffold |
Exit codes: the child's exit code is passed through (on Unix via execve,
on Windows via typed propagation). 127 means the command was not found,
126 means it was found but could not be executed, and 1 is anvil's own
resolution failure. Windows note: anvil remains the parent process; Ctrl+C
reaches the child via the shared console, but there is no Unix-style signal
forwarding or job-object management.
Anvil never installs or owns git hooks and never patches tracked
phpunit.xml. Wrap the outer script instead:
# Run the whole pre-push script with the test database exported
anvil exec -- ./scripts/git/pre-push.shLocal, untracked pre-push shim (.git/hooks/pre-push):
#!/bin/sh
exec anvil exec -- ./scripts/git/pre-push.sh "$@"exec replaces the shim process, so git's ref-list stdin reaches the script
unchanged.
anvil remove drops the exact owned databases recorded in .anvil.local
plus Laravel parallel-worker databases for both families
(<app>_test_<n> and <test>_test_<n>):
# Preview the exact drop set (live enumeration, zero mutation)
anvil remove feature-x --dry-run
# Remove the worktree but keep every database
anvil remove feature-x --keep-db--dry-run prints one Would drop database: <name> line per database and
never drops databases or removes the worktree. It needs neither confirmation
nor --force and leaves the worktree, branch, .anvil.local, runtime
directory, ports and processes untouched (it still takes the coordination lock
files). It never starts a database service: parallel-worker databases are
enumerated only when the service is already running; otherwise the exact
recorded names are listed with parallel-worker databases not enumerated: service not running.
anvil prune --dry-run previews every merged worktree without prompting and
without fetching origin; merge status reflects the current remote-tracking refs
(it fails if origin/<default> has never been fetched).
anvil unlink --clean --dry-run likewise never prompts. --keep-db prints the
preserved names (parallel-worker databases are kept too; drop them manually
when done). If ownership or sibling state is unreadable or invalid, destructive cleanup
stops before site unlink or worktree deletion. --force does not bypass borrower
protection; use --keep-db to retain databases when removing a worktree.
force="true"or<server>entries inphpunit.xmlbeat the exported environment and are not detected (a config scanner is deferred).- A cached Laravel config (
bootstrap/cache/config.php) beats the environment and is not detected. - Bare invocations that bypass
anvil execkeep today's shared behavior (they usephpunit.xml/.env, not the isolated testing database). env -i, containers without environment forwarding, andDB_URL-only connections are out of scope.- Laravel caches parallel-worker databases between runs; run
php artisan test --recreate-databasesafter schema-heavy branch switches. - Pre-1.8 worktrees have no recorded testing database: create a new
worktree, or configure a scaffold override containing ONLY a
db.createstep withrole: testingand run that (adddb.migrateafter it to migrate the new testing database; the policy never touches the unrecorded application database).anvil execnever creates or modifies databases or.anvil.local. --db-action testing=freshcannot detect runninganvil execsessions or checks that start after its guard; only use it while nothing tests against the database.- The opt-in
testing=freshisolation check sees only worktrees registered in this repository. A legacy worktree of another repository whose migrateddb_suffixand site name produce the same database name can adopt an existing database without an endpoint record; Anvil cannot detect that sharing. Ownership and the default non-destructivemigratelimit the impact, so only usefreshwhen you know the database is yours alone. - One database server connection per worktree (MySQL, MariaDB, or PostgreSQL).
SQLite worktrees are per-worktree files and already isolated, so
anvil execis not needed there.
Anvil uses a three-tier configuration system to separate team configuration from local state.
The local PHP site driver is selected globally in ~/.config/anvil/anvil.yaml:
site_driver: yerdSupported values are yerd and herd. When site_driver is omitted, Anvil
prefers Yerd when it is on PATH, falls back to Herd, and otherwise defaults
to Yerd. The setup wizard saves the detected selection. Both selections use
their manager's SQL services for server-backed database steps.
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
For work, scaffold, remove and prune, Anvil loads configuration from the
selected worktree's anvil.yaml. This includes a newly created worktree whose commit
differs from the invoking checkout. Target values override linked project
settings; omitted site name, preset, default branch, editor and cleanup settings
retain the linked defaults. If the target has no anvil.yaml, linked defaults
apply. Anvil does not substitute the invoking checkout's scaffold YAML.
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
- yerd # Yerd local PHP environment
- 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 |
{{ .TestDatabaseName }} |
Test database name (capped at 54 chars) | myapp_swift_runner_test |
{{ .VarName }} |
Custom variable from env.read or captured output | Custom values |
db.create - Create a database with unique name
- name: db.create
type: mysql # mysql, mariadb, or pgsql; auto-detected if omitted
args: ["--prefix", "app"] # optional: customize database prefix- Generates unique name:
{prefix}_{adjective}_{noun}or{site_name}_{adjective}_{noun} - Loads and reuses a persisted worktree suffix; otherwise generates a new suffix once, persists it, and shares it across all
db.createsteps in the run - Auto-detects engine from
DB_CONNECTIONin.env - With
site_driver: yerd, usesyerd service start,yerd db create, andyerd db list; direct host, port, username, and password overrides are rejected because Yerd owns the connection - With
site_driver: herd, usesherd services:startand the Herd-providedmysql,mariadb,createdb,psql, anddropdbbinaries. Connection values come from.envor explicit step arguments; passwords are passed as process environment values, not command arguments - Retries up to 5 times on collision
- Persists the suffix and every created database to
.anvil.localfor exact cleanup - Fails closed: when the engine is configured (explicit
typeor a MySQL, MariaDB or PostgreSQLDB_CONNECTION) and the database service cannot be reached, the step fails with a hint to start the service or check its connection settings.anvil work/scaffoldthen exit non-zero andanvil status --jsonreportsready: falsewith the failed step.typeis case-insensitive and also acceptspostgres/postgresql; an unsupportedtypeis rejected before any step runs. Only a project with neithertypenor a recognisedDB_CONNECTIONskips the step. SQLite creates a worktree-local file and skipsrole: testing - Only ensures the database exists; it never runs migrations. A later
db.migratestep prepares schemas (see Schema-ready testing database)
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 owned databases and their parallel-worker databases
- name: db.destroy
type: mysql # matches db.create type- Reads
.anvil.localownership records and drops every exact database, including cleanup-onlyauxiliaryrecords, then enumerates their Laravel parallel-worker databases - For pre-v1.8 worktrees without ownership records, falls back to suffix-based discovery
- Runs automatically during
anvil remove - See Test Database Isolation for the canonical ownership-first cleanup path and its fail-closed safety gate
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", "--no-interaction"]Capture command output:
- name: php.laravel
args: ["--version"]
store_as: LaravelVersion
- name: env.write
key: APP_FRAMEWORK_VERSION
value: "{{ .LaravelVersion }}"yerd - Yerd site management
- name: yerd
args: ["link", "{{ .SiteName }}"]
- name: yerd
args: ["secure", "{{ .SiteName }}"]The herd binary step remains available when site_driver: herd is configured.
Database steps also use Herd's service lifecycle and Herd-provided database
binaries in that mode.
bash.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) |
dir |
string | Worktree-relative working directory for command.run, bash.run and binary steps; file conditions are evaluated relative to it |
env |
map | Extra environment for command steps; ${VAR} expands against Anvil's worktree environment, then the inherited environment (never against other values of the same step) |
inputs |
array | Worktree-relative files, directories or globs that identify a completed step for anvil ensure |
outputs |
array | Paths that must still exist for a recorded step to count as complete |
always |
boolean | Rerun on every anvil ensure |
Steps execute in the order they appear in the configuration file.
Step directories, declared inputs/outputs and file conditions must remain inside the canonical worktree root, including through symlinks. Existing escaping links are rejected before preparation changes state or runs any step. Internal links are allowed. Step environment values all expand against the same worktree and inherited environment snapshot; another override in the same step does not change that snapshot.
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.jsonUse context_var to compare output saved by an earlier step with a literal string:
scaffold:
steps:
- name: command.run
command: "git rev-parse --is-inside-work-tree"
store_as: IsWorktree
- name: file.copy
from: .env.example
to: .env
condition:
context_var:
name: IsWorktree
value: "true"Conditions are checked when each step reaches execution, so later steps see the
latest store_as output. Quote boolean-looking values: value must be a string,
and comparisons are case-sensitive. A missing variable never matches, including
value: ""; a present variable with an empty string matches value: "".
Stored command output follows each step's existing trimming behavior.
Sibling conditions are ANDed. not negates its operand and still ANDs with its
siblings. A list under not is a conjunction before negation; nested not is
supported. Configured guards apply to every registered step and to cleanup,
alongside each step's intrinsic eligibility.
Anvil validates all effective condition names and shapes before scaffold state
changes or step execution, including disabled steps and cleanup steps excluded by
--keep-db. Unknown names, unsupported fields, non-string context_var values,
and malformed nested conditions fail with an error. Structural validation does
not require variables to exist yet. Preflight checks run before step producers,
so a context_var used in preflight must already be available then.
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: "{{ .DatabaseName }}"
# Install dependencies
- name: php.composer
args: ["install"]
- name: node.npm
args: ["install"]
# Apply the built-in database migration policy
- name: db.migrate
# 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
- Loads and reuses a persisted worktree suffix; otherwise generates a new suffix once, persists it, and shares it across all
db.createsteps in the run - 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/MariaDB length limits
Database Cleanup
- Uses
.anvil.localownership records to drop exact databases and enumerate their parallel-worker databases - Limits suffix matching to the pre-v1.8/no-record legacy fallback
- Integrates with
anvil remove; see Test Database Isolation for the canonical cleanup behavior and fail-closed ownership gate
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
- The Laravel preset clears PHPStan/Larastan's result cache after creating or updating
.envwhenvendor/bin/phpstanis available; projects without PHPStan are skipped
Error Handling
- Graceful degradation where appropriate
- Clear error messages for configuration issues
- Non-fatal warnings for optional operations
MIT