A Laravel Zero CLI that spins up ephemeral Ubuntu VMs via Tart for isolated Claude Code sessions against Laravel projects, with Herd Pro integration on the host.
- Scaffold Laravel Zero project, install database/dotenv components
-
SessionContextandServiceConfigDTOs -
OnExitenum (keep/merge/discard) withEnumHelperstrait -
TartManager— clone, run, stop, delete, ip, exists, list, set, randomizeMac, rename -
SshExecutor— run, interactive, tunnel, test (password auth via SSH_ASKPASS) -
ProvisionCommand+ProvisioningPipeline— build base image (PHP 8.4, nginx, Node 22, Claude Code) -
AuthManager+AuthCommand— API key + OAuth token support with local storage - Preflight pipeline:
ValidateProject → GetGitBranch → EnsureVmExists → CheckClaudeAuthentication → SaveSession - Session pipeline:
CloneRepo → CloneVm → BootVm → RunClaudeCode -
SessionTeardown— VM stop/delete, worktree prompt, Herd unproxy, tunnel kill, session record cleanup - Signal handling via
$this->trap()(SIGINT/SIGTERM) - Sessions SQLite table +
Sessionmodel -
SessionPipelinebase class with progress tracking via Laravel Prompts -
Stepinterface +ProgressAwareinterface +AcceptsProgresstrait - VirtioFS mount with retry logic and diagnostics
-
DiscoverGatewaystep — find NAT gateway IP inside VM -
BootstrapLaravelstep — symlink, .env patching, composer install, migrate -
CreateSshTunnelstep — port forwarding VM:80 → localhost:port -
ConfigureHerdProxystep —herd proxysetup/teardown - Port auto-assignment (scan 8081–8199)
- Host service connectivity check (warn if MySQL/Redis not reachable from VM)
-
SessionsCommand— list active sessions -
CleanupCommand— remove orphaned VMs -
ConfigCommand— manage per-user settings -
GitManagermerge flow verification (merge worktree back to base branch) -
--resumeflag for reconnecting to a running session - Error handling: composer failures, SSH timeouts, missing host services
- Build PHAR:
php clave app:build - Per-project
.clave.jsonconfig -
clave provision --update - In-VM services mode (
services.mode: "vm") -
clave execandclave sshfor running sessions
# From a Laravel project directory (must be a git repo):
clave
# That's it. Clave will:
# 1. Ensure a provisioned base VM image exists (first run only)
# 2. Create a git worktree for this session
# 3. Clone the base image to an ephemeral VM
# 4. Boot the VM with the worktree mounted via VirtioFS
# 5. Set up port forwarding so the VM's nginx is reachable from the host
# 6. Configure Herd Pro proxy (project.test → VM)
# 7. Bootstrap the Laravel app inside the VM (pointing at host MySQL/Redis)
# 8. Drop you into an interactive Claude Code session inside the VM
# 9. On exit: tear down proxy, stop VM, delete clone, prompt about worktreeMultiple simultaneous sessions work naturally — each clave invocation gets its own worktree, its own VM clone, and its own port. Run three terminals, run clave three times, get three isolated Claude Code agents working in parallel on different branches.
Terminal 1 Terminal 2 Terminal 3
clave clave clave
│ │ │
▼ ▼ ▼
worktree: .clave/wt/s-a1b2 .clave/wt/s-c3d4 .clave/wt/s-e5f6
vm: clave-a1b2 clave-c3d4 clave-e5f6
port: 8081 8082 8083
proxy: my-app-a1b2.test my-app-c3d4.test my-app-e5f6.test
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Ubuntu VM │ │ Ubuntu VM │ │ Ubuntu VM │
│ nginx + php-fpm │ │ nginx + php-fpm │ │ nginx + php-fpm │
│ Claude Code │ │ Claude Code │ │ Claude Code │
│ /srv/project │ │ /srv/project │ │ /srv/project │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │
│ DB_HOST=gateway │ │ DB_HOST=gateway │ │ DB_HOST=gateway │
└───────┬─────────┘ └───────┬─────────┘ └───────┬─────────┘
│ NAT gateway │ │
└────────────┬───────────────┘ │
▼ │
┌──────────────────────────────────────────────────────┐
│ Host (macOS) │
│ Herd Pro: MySQL (3306) · Redis (6379) · PHP · nginx │
│ Parked sites: ~/Herd/* │
└──────────────────────────────────────────────────────┘
For v0, MySQL and Redis run on the host via Herd Pro. Each VM connects to host services through the NAT gateway IP (typically 192.168.64.1). This means all sessions share the same database and Redis instance — which is fine for most development workflows and avoids duplicating heavy services in every ephemeral VM.
The VM runs only nginx and PHP-FPM — just enough to serve the Laravel app and provide Claude Code a realistic execution environment for running artisan commands, tests, etc.
Future: in-VM services. The architecture is designed so that a future version can optionally provision MySQL and Redis inside the VM for full session isolation. This is controlled by the
ServiceConfigDTO — swapping fromServiceConfig::hostServices()toServiceConfig::localServices()is the only change needed in the pipeline. The provisioning pipeline would gain additional steps for MySQL/Redis, gated by a config flag.
Herd Pro's MySQL and Redis must be reachable from the VM's network. By default:
- MySQL: Herd Pro binds to
127.0.0.1:3306. This needs to be changed to0.0.0.0:3306(or the gateway IP) for VMs to connect. Clave should detect this and warn on first run. - Redis: Same situation — Herd Pro's Redis binds to
127.0.0.1:6379and needs to accept connections from the VM subnet.
The NAT gateway IP is discovered by running ip route | grep default inside the VM.
A git repo is required. If the current directory is not inside a git repository, clave exits with an error.
When clave starts, it creates a worktree so each session has an isolated copy of the codebase. Claude Code in session A can be refactoring the auth system while session B rewrites the billing module — no conflicts.
my-app/ # main working copy (untouched)
my-app/.clave/wt/s-a1b2c3d4/ # session A's worktree
my-app/.clave/wt/s-e5f6g7h8/ # session B's worktree
Worktree lifecycle:
- Generate a random 8-character session ID via
Str::random(8) - Create a new branch:
clave/s-{id}from current HEAD git worktree add .clave/wt/s-{id} clave/s-{id}- Mount
.clave/wt/s-{id}into the VM via VirtioFS at/srv/project - On session exit, prompt (via Laravel Prompts
select()):- Keep (default) — leave the worktree and branch for manual review
- Merge — merge the session branch back to the original branch, remove worktree
- Discard — remove worktree and delete branch
.clave/ is automatically added to .gitignore on first use.
The session lifecycle is split into two pipelines, both extending a SessionPipeline base class that provides progress tracking via Laravel Prompts. A SessionContext DTO flows through each stage, accumulating state. Each stage implements the Step interface and is responsible for one concern.
abstract class SessionPipeline extends Pipeline
{
abstract public function label(): string;
abstract public function steps(): array;
public function run(SessionContext $context): SessionContext
{
// Creates a progress bar, sends context through steps,
// injects ProgressAware trait for steps that need it,
// handles exceptions with progress bar cleanup
}
}interface Step
{
public function handle(SessionContext $context, Closure $next): mixed;
}
interface ProgressAware
{
public function setProgress(Progress $progress): void;
}
trait AcceptsProgress
{
protected ?Progress $progress = null;
public function setProgress(Progress $progress): void { ... }
protected function hint(string $message): void { ... }
}class SessionContext
{
public function __construct(
// Determined at creation
public readonly string $session_id,
public readonly string $project_name,
public readonly string $project_dir,
public readonly ?OnExit $on_exit = null,
public readonly ?Command $command = null,
// Populated by pipeline stages
public ?string $base_branch = null,
public ?string $vm_name = null,
public ?string $vm_ip = null,
public ?string $clone_path = null,
public ?string $clone_branch = null,
public ?string $proxy_name = null,
public ?int $tunnel_port = null,
public ?InvokedProcess $tunnel_process = null,
public ?ServiceConfig $services = null,
public ?Session $session = null,
) {}
public function info(string $message): void { ... }
public function warn(string $message): void { ... }
public function error(string $message): void { ... }
public function abort(string $message): never { ... }
}enum OnExit: string
{
use EnumHelpers;
case Keep = 'keep';
case Merge = 'merge';
case Discard = 'discard';
}class ServiceConfig
{
public function __construct(
public readonly string $mysql_host,
public readonly int $mysql_port,
public readonly string $redis_host,
public readonly int $redis_port,
) {}
public static function hostServices(string $gateway_ip): static { ... }
public static function localServices(): static { ... }
}// In DefaultCommand::handle():
$context = $this->newContext();
// Phase 1: Validate project, check auth, ensure VM exists
$preflight->run($context);
// Register cleanup on interrupt
$this->trap([SIGINT, SIGTERM], function () use ($context, $teardown) {
$teardown->run($context);
});
// Phase 2: Create worktree, boot VM, run Claude
try {
$claude->run($context);
} finally {
$teardown->run($context);
}Label: "Setting up project..."
ValidateProject → GetGitBranch → EnsureVmExists → CheckClaudeAuthentication → SaveSession
- ValidateProject — checks for
artisanfile, aborts if not a Laravel project - GetGitBranch — verifies git repo, sets
context->base_branch - EnsureVmExists — checks for base VM, calls
ProvisionCommandif missing - CheckClaudeAuthentication — verifies auth via
AuthManager, attempts setup if missing - SaveSession — creates
Sessionmodel record in SQLite
Label: "Starting session..."
CloneRepo → CloneVm → BootVm → RunClaudeCode
Future stages to be inserted between BootVm and RunClaudeCode:
DiscoverGateway → CreateSshTunnel → ConfigureHerdProxy → BootstrapLaravel
class CloneRepo implements Step
{
use AcceptsProgress;
public function handle(SessionContext $context, Closure $next): mixed
{
$branch = "clave/s-{$context->session_id}";
$clone_path = "{$context->project_dir}/.clave/wt/s-{$context->session_id}";
$this->git->ensureIgnored($context->project_dir, '.clave/');
$this->git->cloneLocal($context->project_dir, $clone_path, $branch);
$context->clone_path = $clone_path;
$context->clone_branch = $branch;
return $next($context);
}
}class CloneVm implements Step
{
use AcceptsProgress;
public function handle(SessionContext $context, Closure $next): mixed
{
$vm_name = "clave-{$context->session_id}";
$this->tart->clone(config('clave.base_vm'), $vm_name);
$this->tart->randomizeMac($vm_name);
// Apply per-session overrides from config
$this->tart->set($vm_name,
cpus: config('clave.vm.cpus'),
memory: config('clave.vm.memory'),
display: config('clave.vm.display'),
);
$context->vm_name = $vm_name;
return $next($context);
}
}class BootVm implements Step
{
use AcceptsProgress;
public function handle(SessionContext $context, Closure $next): mixed
{
$mount_path = $context->clone_path ?? $context->project_dir;
$this->tart->runBackground($context->vm_name, [
'project' => $mount_path,
]);
// Wait for IP (timeout: 90s)
$context->vm_ip = $this->tart->ip($context->vm_name, timeout: 90);
$this->ssh->setHost($context->vm_ip);
// Wait for SSH (timeout: 90s)
$this->ssh->usePassword(config('clave.ssh.password'));
// Polls ssh->test() with retry
// Mount VirtioFS with retry logic (timeout: 30s)
// Falls back to detailed diagnostics on failure
$this->ssh->run('sudo mount -t virtiofs com.apple.virtio-fs.automount /srv/project');
return $next($context);
}
}class RunClaudeCode implements Step
{
use AcceptsProgress;
public function handle(SessionContext $context, Closure $next): mixed
{
// Resolve auth (API key or OAuth token) via AuthManager
// If OAuth, write credentials to ~/.claude/.credentials.json on VM
// Write settings to ~/.claude.json and ~/.claude/settings.json
// Finish progress bar before interactive session
$this->progress->finish();
$this->ssh->interactive(
'cd /srv/project && [ENV] claude --dangerously-skip-permissions'
);
return $next($context);
}
}class DiscoverGateway implements Step
{
public function handle(SessionContext $context, Closure $next): mixed
{
$result = $this->ssh->run("ip route | grep default | awk '{print \$3}'");
$context->gateway_ip = trim($result->output());
$context->services = ServiceConfig::hostServices($context->gateway_ip);
return $next($context);
}
}class CreateSshTunnel implements Step
{
public function handle(SessionContext $context, Closure $next): mixed
{
$host_port = $this->findAvailablePort(8081, 8199);
$context->tunnel_port = $host_port;
$context->tunnel_process = $this->ssh->tunnel($host_port, $context->vm_ip, 80);
return $next($context);
}
}class ConfigureHerdProxy implements Step
{
public function handle(SessionContext $context, Closure $next): mixed
{
$proxy_name = "{$context->project_name}-{$context->session_id}";
$context->proxy_name = $proxy_name;
$this->herd->proxy($proxy_name, "http://127.0.0.1:{$context->tunnel_port}", secure: true);
return $next($context);
}
}class BootstrapLaravel implements Step
{
public function handle(SessionContext $context, Closure $next): mixed
{
// Symlink VirtioFS mount to where nginx expects it
// Configure .env with host service connections
// composer install
// php artisan migrate --force
// Restart php-fpm + nginx
return $next($context);
}
}Cleanup runs after the pipeline completes (or on interrupt via signal handler). Each step is guarded by null checks and wrapped in rescue() so a failure in one doesn't prevent the others. A $completed flag prevents double execution.
class SessionTeardown
{
protected bool $completed = false;
public function run(SessionContext $context): void
{
if ($this->completed) {
return;
}
$this->completed = true;
$this->unproxy($context); // Remove Herd proxy if set
$this->killTunnel($context); // Stop SSH tunnel if running
$this->stopVm($context); // tart stop
$this->deleteVm($context); // tart delete
$this->handleClone($context); // Prompt: keep/merge/discard
$this->deleteSession($context); // Remove Session record
}
protected function handleClone(SessionContext $context): void
{
$action = $context->on_exit;
// If no pre-selected action, prompt user via Laravel Prompts
$action ??= OnExit::coerce(select(
label: 'What would you like to do with the worktree?',
options: OnExit::toSelectArray(),
default: OnExit::Keep->value,
));
match ($action) {
OnExit::Merge => $this->git->mergeAndCleanClone(...),
OnExit::Discard => $this->git->removeClone(...),
default => null, // keep
};
}
}class DefaultCommand extends Command
{
protected $signature = 'default {--on-exit= : Action on exit: keep, merge, discard}';
public function handle(
PreflightPipeline $preflight,
ClaudeCodePipeline $claude,
SessionTeardown $teardown,
): int {
clear();
$this->callSilently('migrate', ['--force' => true]);
$context = new SessionContext(
session_id: Str::random(8),
project_name: basename(getcwd()),
project_dir: getcwd(),
on_exit: OnExit::tryFrom($this->option('on-exit') ?? ''),
command: $this,
);
$preflight->run($context);
$this->trap([SIGINT, SIGTERM], function () use ($context, $teardown) {
$teardown->run($context);
});
try {
$claude->run($context);
} finally {
$teardown->run($context);
}
return self::SUCCESS;
}
}Builds or rebuilds the base VM image. Generates a provisioning bash script via ProvisioningPipeline::toScript(), mounts it into the VM via VirtioFS, and executes it.
class ProvisionCommand extends Command
{
protected $signature = 'provision
{--force : Re-provision}
{--image= : OCI image to pull}';
public function handle(TartManager $tart, SshExecutor $ssh): int
{
// Clone OCI image to temp VM name
// Configure VM (CPUs, memory, display)
// Write provisioning script to temp dir
// Mount script dir via VirtioFS
// Boot VM, wait for SSH
// Execute provisioning script
// Stop VM, rename to base VM name
}
}Manages Claude Code authentication (API key or OAuth token).
class AuthCommand extends Command
{
protected $signature = 'auth
{--status : Show auth method}
{--clear : Remove stored token}';
public function handle(AuthManager $auth): int
{
// --status: Show current auth method and source
// --clear: Remove stored token
// default: Run `claude setup-token` and store OAuth token
}
}class SessionsCommand extends Command
{
protected $signature = 'sessions';
public function handle(): int
{
$sessions = Session::all();
$this->table(
['ID', 'Project', 'Branch', 'VM', 'Started'],
$sessions->map(fn ($s) => [
$s->session_id,
$s->project_name,
$s->branch ?? '-',
$s->vm_name ?? '-',
$s->started_at?->diffForHumans() ?? '-',
]),
);
}
}class CleanupCommand extends Command
{
protected $signature = 'cleanup {--dry-run : Show what would be cleaned up}';
public function handle(TartManager $tart): int
{
// Find VMs named clave-* (excluding clave-base)
// Check if they have a matching active session
// Remove orphaned VMs and stale session records
}
}The base image provisioning installs everything needed so per-session boot is fast. The VM runs nginx and PHP-FPM only — no MySQL or Redis (v0 uses host services).
ProvisioningPipeline generates a self-contained bash script via toScript() which is mounted into the VM and executed. This is faster than step-by-step SSH execution.
Provisioned software:
- Base system packages (git, curl, wget, unzip)
- PHP 8.4 + extensions (curl, mbstring, xml, mysql, redis, sqlite3, bcmath, gd, intl)
- PHP-FPM pool configuration
- Composer
- Nginx with Laravel site config
- Node.js 22 (via NodeSource)
- Claude Code CLI (
@anthropic-ai/claude-code) - Laravel directories (
/srv/project) - VirtioFS fstab entry (
com.apple.virtio-fs.automount /srv/project virtiofs)
class TartManager
{
public function clone(string $source, string $name): void;
public function runBackground(string $name, array $dirs, bool $no_graphics = true): mixed;
public function stop(string $name): void;
public function delete(string $name): void;
public function ip(string $name, int $timeout = 0): ?string;
public function exists(string $name): bool;
public function list(): Collection;
public function set(string $name, ?int $cpus, ?int $memory, ?string $display): void;
public function randomizeMac(string $name): void;
public function rename(string $old_name, string $new_name): void;
public function waitForReady(string $name, SshExecutor $ssh, int $timeout): string;
}class GitManager
{
public function isRepo(string $path): bool;
public function currentBranch(string $path): string;
public function cloneLocal(string $repo_path, string $clone_path, string $branch): void;
public function removeClone(string $repo_path, string $clone_path): void;
public function mergeAndCleanClone(string $repo_path, string $clone_path, string $branch, string $target): void;
public function ensureIgnored(string $repo_path, string $pattern): void;
}Uses password auth via SSH_ASKPASS for all VM connections. VMs are ephemeral and local, so key management adds complexity with no security benefit.
class SshExecutor
{
public function setHost(string $host): self;
public function usePassword(string $password): self;
public function run(string $command, int $timeout = 60): mixed;
public function interactive(string $command): int;
public function tunnel(int $local_port, string $remote_host, int $remote_port): mixed;
public function test(): bool;
public function lastError(): ?string;
}Resolves Claude authentication from multiple sources with priority chain: ANTHROPIC_API_KEY env → CLAUDE_CODE_OAUTH_TOKEN env → stored token file.
class AuthManager
{
public function resolve(): ?array;
public function hasAuth(): bool;
public function setupToken(): bool;
public function clearToken(): void;
public function statusInfo(): array;
}class HerdManager
{
public function proxy(string $domain, string $target, bool $secure = true): void;
public function unproxy(string $domain): void;
}return [
'base_image' => env('CLAVE_BASE_IMAGE', 'ghcr.io/cirruslabs/ubuntu:latest'),
'base_vm' => env('CLAVE_BASE_VM', 'clave-base'),
'vm' => [
'cpus' => env('CLAVE_VM_CPUS', 4),
'memory' => env('CLAVE_VM_MEMORY', 8192),
'display' => env('CLAVE_VM_DISPLAY', 'none'),
],
'ssh' => [
'user' => env('CLAVE_SSH_USER', 'admin'),
'port' => env('CLAVE_SSH_PORT', 22),
'password' => env('CLAVE_SSH_PASSWORD', 'admin'),
'options' => [
'StrictHostKeyChecking' => 'no',
'UserKnownHostsFile' => '/dev/null',
'LogLevel' => 'ERROR',
'ConnectTimeout' => '5',
],
],
'anthropic_api_key' => env('ANTHROPIC_API_KEY'),
'oauth_token' => env('CLAUDE_CODE_OAUTH_TOKEN'),
'auth_file' => env('CLAVE_AUTH_FILE', '~/.config/clave/auth.json'),
];export ANTHROPIC_API_KEY=sk-ant-... # API key auth
export CLAUDE_CODE_OAUTH_TOKEN=... # OAuth token auth (alternative)
export CLAVE_VM_CPUS=4 # Optional override
export CLAVE_VM_MEMORY=8192 # Optional overrideclave/
├── app/
│ ├── Commands/
│ │ ├── DefaultCommand.php
│ │ ├── ProvisionCommand.php
│ │ ├── AuthCommand.php
│ │ └── LintCommand.php
│ ├── Dto/
│ │ ├── SessionContext.php
│ │ ├── ServiceConfig.php
│ │ └── OnExit.php
│ ├── Exceptions/
│ │ └── AbortedPipelineException.php
│ ├── Models/
│ │ └── Session.php
│ ├── Pipelines/
│ │ ├── Steps/
│ │ │ ├── Step.php (interface)
│ │ │ ├── ProgressAware.php (interface)
│ │ │ ├── AcceptsProgress.php (trait)
│ │ │ ├── ValidateProject.php
│ │ │ ├── GetGitBranch.php
│ │ │ ├── EnsureVmExists.php
│ │ │ ├── CheckClaudeAuthentication.php
│ │ │ ├── SaveSession.php
│ │ │ ├── CloneRepo.php
│ │ │ ├── CloneVm.php
│ │ │ ├── BootVm.php
│ │ │ └── RunClaudeCode.php
│ │ ├── SessionPipeline.php (abstract base)
│ │ ├── PreflightPipeline.php
│ │ └── ClaudeCodePipeline.php
│ ├── Support/
│ │ ├── TartManager.php
│ │ ├── GitManager.php
│ │ ├── SshExecutor.php
│ │ ├── AuthManager.php
│ │ ├── HerdManager.php
│ │ ├── SessionTeardown.php
│ │ ├── ProvisioningPipeline.php
│ │ └── EnumHelpers.php
│ └── Providers/
│ └── AppServiceProvider.php
├── config/
│ └── clave.php
├── database/
│ └── migrations/
│ └── 2026_02_23_000000_create_sessions_table.php
├── tests/
│ ├── Feature/
│ │ ├── Commands/
│ │ │ └── AuthCommandTest.php
│ │ └── Services/
│ │ ├── AuthManagerTest.php
│ │ └── TartManagerTest.php
│ └── Unit/
│ └── Dto/
│ ├── SessionContextTest.php
│ └── ServiceConfigTest.php
└── clave
Proves the core lifecycle: boot a VM, get a Claude Code session, tear it down.
- Scaffold Laravel Zero project, install database/dotenv components
SessionContext,ServiceConfig, andOnExitDTOsTartManager— clone, run, stop, delete, ip, exists, randomizeMac, rename, setSshExecutor— run, interactive, tunnel, test (password auth via SSH_ASKPASS)AuthManager+AuthCommand— API key + OAuth supportProvisionCommand+ProvisioningPipeline— build base image (PHP 8.4/nginx/Node 22/Claude Code)SessionPipelinebase class with progress tracking- Preflight pipeline:
ValidateProject → GetGitBranch → EnsureVmExists → CheckClaudeAuthentication → SaveSession - Session pipeline:
CloneRepo → CloneVm → BootVm → RunClaudeCode SessionTeardown— cleanup VM, worktree prompt, session record- Signal handling via
$this->trap()(SIGINT/SIGTERM) - Sessions SQLite table + model
Milestone: clave boots a VM from a Laravel project dir, you get a Claude Code prompt in a worktree, exiting shuts it all down.
Connect the VM to host services and expose the app.
DiscoverGatewaystep — find NAT gateway IPBootstrapLaravelstep — symlink, .env patching with host service IPs, composer install, migrateCreateSshTunnelstep — port forwarding VM:80 → localhost:portConfigureHerdProxystep —herd proxysetup/teardown- Port auto-assignment (scan 8081–8199)
- Host service connectivity check (warn if MySQL/Redis not reachable from VM)
Milestone: App accessible at https://project-a1b2.test, database works against host MySQL.
SessionsCommandandCleanupCommandConfigCommandGitManager— verify merge and discard worktree flows end-to-end--resumeflag- Error handling: composer failures, SSH timeouts, missing host services
Milestone: Handles crashes, parallel sessions, cleans up after itself.
- Build PHAR:
php clave app:build - Per-project
.clave.jsonconfig clave provision --update- In-VM services mode (
services.mode: "vm") clave execandclave sshfor running sessions
-
Host MySQL/Redis bind address. Herd Pro defaults to
127.0.0.1. VMs need services on0.0.0.0or the gateway interface. Clave should check reachability duringDiscoverGatewayand print actionable instructions if services aren't accessible. This is a one-time user configuration step. -
tart runprocess management. Need to verifyProcess::start()keeps the VM alive while the parent blocks on TTY. May neednohupwith PID file as fallback. -
Composer install on VirtioFS. v0 runs directly on the mount. If too slow, install on VM local disk and symlink
vendor/back.