Runtime scripts automatically execute before Claude runs or before opening a shell, allowing you to set up services, configure environments, and provide dynamic context to Claude.
- What are Runtime Scripts?
- Quick Start
- Script Discovery
- Execution Order
- Features
- Contributing Context to Claude
- Examples
- Debugging
Runtime scripts are bash scripts that run automatically before each Claude session or shell command. They allow you to:
- Start background services (databases, APIs, etc.)
- Set environment variables
- Initialize development environments
- Provide dynamic context to Claude
- Run health checks
- Seed databases
- Configure session-specific settings
Unlike setup scripts (which run once during template creation), runtime scripts run every time you start a session.
Create .claude-vm.runtime.sh in your project root:
#!/bin/bash
# .claude-vm.runtime.sh
# Start services
echo "Starting services..."
docker-compose up -d
# Wait for services
sleep 2
# Set environment
export API_KEY="dev-key"
export DEBUG=true
echo "✓ Environment ready"Now every time you run claude-vm or claude-vm shell, this script executes first.
No configuration needed - Claude VM automatically detects and runs:
./.claude-vm.runtime.sh # Project runtime scriptScript location:
- In a git worktree: searched at the worktree top directory
- In a git repository: searched at the repository root
- Outside git: searched in current directory
Add additional scripts in .claude-vm.toml:
[runtime]
scripts = [
"./.claude-vm.runtime.sh", # Auto-detected (optional to list)
"./scripts/start-services.sh", # Additional scripts
"~/scripts/dev-setup.sh", # Global scripts
]Pass scripts via CLI:
# Single script
claude-vm --runtime-script ./start-db.sh shell
# Multiple scripts
claude-vm --runtime-script ./setup.sh --runtime-script ./seed.sh shellScripts run in this order:
- Project runtime script -
./.claude-vm.runtime.sh(if exists) - Config runtime scripts - From
[runtime] scriptsin.claude-vm.toml - CLI runtime scripts - From
--runtime-scriptflags - Main command - Claude agent or shell
- Cleanup phases -
[[phase.cleanup]]phases (inside VM, after command completes) - VM stop - VM is stopped and deleted
All scripts and the main command run in a single shell invocation, sharing environment and processes.
Use [[phase.cleanup]] phases for cleanup operations that need VM filesystem access:
[[phase.cleanup]]
name = "save-logs"
script = """
# Runs after agent/shell completes, before VM stops
cp ~/.claude/logs/*.log /mounted-backup/ 2>/dev/null || true
"""See Configuration for details.
Runtime and cleanup scripts have access to $CLAUDE_VM_COMMAND to detect which command is running:
#!/bin/bash
# .claude-vm.runtime.sh
if [ "$CLAUDE_VM_COMMAND" = "agent" ]; then
echo "Running Claude Code agent"
# Agent-specific setup
elif [ "$CLAUDE_VM_COMMAND" = "shell" ]; then
echo "Running interactive shell"
# Shell-specific setup
fiAvailable in:
- Runtime phases (
[[phase.runtime]]) - Cleanup phases (
[[phase.cleanup]]) - Host runtime phases (
[[phase.host.before_runtime]],[[phase.host.after_runtime]]) - Host teardown phases (
[[phase.host.teardown]])
Values:
"agent"- when runningclaude-vm agentorclaude-vm(default)"shell"- when runningclaude-vm shell
All scripts and the main command share the same shell environment:
# script1.sh
export API_KEY="secret"
# script2.sh
echo "API_KEY is: $API_KEY" # Can access variable from script1
# Main command also has access
claude-vm --runtime-script script1.sh --runtime-script script2.sh shell
$ echo $API_KEY # "secret"If any script fails (exit code ≠ 0), execution stops:
#!/bin/bash
# .claude-vm.runtime.sh
docker-compose up -d || exit 1 # Stop if docker fails
npm run migrate || exit 1 # Stop if migration fails
echo "✓ Ready" # Only reached if above succeedsThe main command (Claude or shell) won't run if a runtime script fails.
Background processes started in runtime scripts continue running:
#!/bin/bash
# .claude-vm.runtime.sh
# Start services in background
docker-compose up -d
# Start development server (in background)
npm run dev &
# Continue with other setup
echo "Services started"Services remain running for the entire session.
Runtime scripts can prompt for input:
#!/bin/bash
# .claude-vm.runtime.sh
if [ -z "$API_KEY" ]; then
read -p "Enter API key: " API_KEY
export API_KEY
fi
read -p "Enable debug mode? (y/n): " enable_debug
if [ "$enable_debug" = "y" ]; then
export DEBUG=true
fiFull terminal support including colors and cursor control.
- Script paths are properly escaped
- Filenames are sanitized
- Unicode filenames supported
- No shell injection vulnerabilities
Runtime scripts can write dynamic context that Claude receives via ~/.claude/CLAUDE.md.
- Runtime script writes to
~/.claude-vm/context/<name>.txt - Content is automatically merged into
~/.claude/CLAUDE.md - Claude receives the context at session start
#!/bin/bash
# .claude-vm.runtime.sh
# Create context directory
mkdir -p ~/.claude-vm/context
# Write context
cat > ~/.claude-vm/context/services.txt <<EOF
Development services running:
- PostgreSQL: localhost:5432
- Redis: localhost:6379
- API: http://localhost:3000
EOFClaude will see this in its context as:
## Runtime Script Results
### services
Development services running:
- PostgreSQL: localhost:5432
- Redis: localhost:6379
- API: http://localhost:3000- Filename:
~/.claude-vm/context/<name>.txt - Section heading:
<name>(basename without .txt) - Multiple files: Each file becomes a separate section
- Ordering: Files are included in alphabetical order
#!/bin/bash
# .claude-vm.runtime.sh
# Start services
docker-compose up -d
# Wait for readiness
until curl -sf http://localhost:3000/health > /dev/null; do
sleep 1
done
# Write context
mkdir -p ~/.claude-vm/context
cat > ~/.claude-vm/context/services.txt <<EOF
Services Status:
- API: http://localhost:3000 (healthy)
- Database: postgresql://localhost:5432/myapp_dev
- Tables: users, posts, comments
- Test data: seeded
- Cache: redis://localhost:6379
Commands:
- View logs: docker-compose logs -f
- Reset DB: npm run db:reset
- API docs: http://localhost:3000/docs
EOF#!/bin/bash
# .claude-vm.runtime.sh
# Detect project type
PROJECT_TYPE="unknown"
if [ -f "package.json" ]; then
PROJECT_TYPE="Node.js $(node --version)"
elif [ -f "Cargo.toml" ]; then
PROJECT_TYPE="Rust $(rustc --version | cut -d' ' -f2)"
elif [ -f "requirements.txt" ]; then
PROJECT_TYPE="Python $(python3 --version | cut -d' ' -f2)"
fi
# Detect git info
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "none")
GIT_STATUS=$(git status --short 2>/dev/null | wc -l | tr -d ' ')
# Write context
mkdir -p ~/.claude-vm/context
cat > ~/.claude-vm/context/environment.txt <<EOF
Environment:
- Project: $PROJECT_TYPE
- Git branch: $GIT_BRANCH
- Uncommitted changes: $GIT_STATUS files
- Working directory: $(pwd)
Available tools:
- Docker: $(docker --version 2>/dev/null || echo "not installed")
- Make: $(make --version 2>/dev/null | head -n1 || echo "not installed")
EOFDifferent scripts can contribute different context files:
# .claude-vm.runtime.sh
cat > ~/.claude-vm/context/database.txt <<EOF
Database: PostgreSQL 15
Connection: localhost:5432/myapp
Status: healthy
EOF
# ./scripts/check-api.sh
cat > ~/.claude-vm/context/api.txt <<EOF
API: http://localhost:3000
Health: OK
Version: 1.2.3
EOFBoth appear in Claude's context as separate sections.
#!/bin/bash
# .claude-vm.runtime.sh
echo "Setting up database..."
# Start PostgreSQL
docker-compose up -d postgres
# Wait for readiness
until pg_isready -h localhost -p 5432 -U postgres; do
echo "Waiting for database..."
sleep 1
done
# Run migrations
npm run db:migrate
# Seed if empty
if [ "$(psql -h localhost -U postgres -d myapp -tAc "SELECT COUNT(*) FROM users")" -eq 0 ]; then
echo "Seeding database..."
npm run db:seed
fi
echo "✓ Database ready"#!/bin/bash
# .claude-vm.runtime.sh
echo "Starting services..."
# Start all services
docker-compose up -d
# Wait for each service
services=("postgres:5432" "redis:6379" "api:3000")
for service in "${services[@]}"; do
IFS=: read -r name port <<< "$service"
echo "Waiting for $name..."
until nc -z localhost "$port" 2>/dev/null; do
sleep 1
done
echo "✓ $name ready"
done
# Run health checks
curl -sf http://localhost:3000/health || {
echo "API health check failed"
exit 1
}
echo "✓ All services ready"#!/bin/bash
# .claude-vm.runtime.sh
# Load .env file if exists
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi
# Prompt for missing critical vars
if [ -z "$API_KEY" ]; then
read -p "Enter API key: " API_KEY
export API_KEY
fi
# Set development defaults
export NODE_ENV="${NODE_ENV:-development}"
export DEBUG="${DEBUG:-true}"
export LOG_LEVEL="${LOG_LEVEL:-debug}"
# Display configuration
echo "Environment:"
echo " NODE_ENV: $NODE_ENV"
echo " DEBUG: $DEBUG"
echo " API_KEY: ${API_KEY:0:10}..."#!/bin/bash
# .claude-vm.runtime.sh
# Only start services if not already running
if ! docker-compose ps | grep -q "Up"; then
echo "Starting services..."
docker-compose up -d
else
echo "Services already running"
fi
# Only run migrations if needed
PENDING=$(npm run db:migrate:status | grep -c "pending")
if [ "$PENDING" -gt 0 ]; then
echo "Running $PENDING pending migrations..."
npm run db:migrate
else
echo "Database up to date"
fiSee detailed script execution:
claude-vm --verbose shellOutput includes:
- Script copying progress (✓/✗)
- Lima VM startup logs
- Script execution output
- Error messages with context
When a script fails, you'll see:
Error: Runtime script failed: .claude-vm.runtime.sh
Exit code: 1
Output:
Starting services...
Error: docker-compose not found
Test scripts independently:
# Copy to VM and run manually
limactl shell <template-name> bash -c "$(cat .claude-vm.runtime.sh)"
# Or test locally (may not have all dependencies)
bash .claude-vm.runtime.shScript not found:
- Ensure script exists and path is correct
- Check script is executable:
chmod +x .claude-vm.runtime.sh
Environment variables not set:
- Variables only persist within the session
- Use
exportto make variables available to subsequent scripts
Services not starting:
- Check docker/docker-compose is installed in template
- Verify services are defined in docker-compose.yml
- Check for port conflicts
Scripts should be safe to run multiple times:
# Good: Check before starting
if ! docker-compose ps | grep -q "Up"; then
docker-compose up -d
fi
# Bad: Always starts (may fail if already running)
docker-compose up -dExit early on errors:
set -e # Exit on any error
docker-compose up -d
npm run migrate
npm run seedShow progress to the user:
echo "Starting services..."
docker-compose up -d
echo "✓ Services started"
echo "Running migrations..."
npm run db:migrate
echo "✓ Migrations complete"Runtime scripts run on every session - keep them quick:
# Good: Check if work is needed
if [ ! -f ".initialized" ]; then
npm run init
touch .initialized
fi
# Bad: Always runs expensive operation
npm run initProvide useful, actionable information:
# Good: Specific, actionable context
cat > ~/.claude-vm/context/services.txt <<EOF
API: http://localhost:3000
Test user: admin@example.com / password123
Docs: http://localhost:3000/api-docs
EOF
# Less useful: Vague information
cat > ~/.claude-vm/context/services.txt <<EOF
Everything is running.
EOF- Templates - Understand template VMs
- Custom Mounts - Mount additional directories
- Configuration - Configure runtime scripts
- Troubleshooting - Debug script issues