Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ agent-vm --offline --readonly claude # Both

## Customization

### Extra host mounts: `~/.agent-vm/volumes`

List host files or directories to mount **read-only** inside every VM. One path per line, `~` is expanded, `#` starts a comment. Uses Docker Compose-style `source:destination` syntax:

```bash
# ~/.agent-vm/volumes

# Mount at the same path in the VM
~/.gitconfig
~/.gitignore

# Mount at a different path in the VM
~/.claude:/home/$USER.linux/.claude
```

When no destination is specified, the path is mounted at the same location inside the VM. Non-existent paths are skipped with a warning. Changes to this file take effect on new VMs (use `--reset` to re-apply to existing ones).

Lima does not allow to mount files, only folders are supported as mount point.

### Per-user setup: `~/.agent-vm/setup.sh`

Create this file to install extra tools into the base VM template. It runs once during `agent-vm setup`, as the default VM user (with sudo available):
Expand Down
44 changes: 39 additions & 5 deletions agent-vm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,39 @@ _agent_vm_ensure_running() {
esac
done

# Build mounts JSON: project dir (writable) + extra mounts from ~/.agent-vm/volumes (read-only)
local mounts_json="[{\"location\": \"${host_dir}\", \"writable\": true}"
local mounts_file="$AGENT_VM_STATE_DIR/volumes"
if [[ -f "$mounts_file" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}" # strip comments
line="${line#"${line%%[![:space:]]*}"}" # trim leading whitespace
line="${line%"${line##*[![:space:]]}"}" # trim trailing whitespace
[[ -z "$line" ]] && continue
# Parse source:destination syntax (like docker compose volumes)
local src="$line" dst=""
if [[ "$line" == *:* ]]; then
src="${line%%:*}"
dst="${line#*:}"
fi
src="${src/#\~/$HOME}" # expand ~
if [[ ! -e "$src" ]]; then
echo "Warning: Mount path '${src}' (from ~/.agent-vm/volumes) does not exist, skipping." >&2
continue
fi
if [[ ! -d "$src" ]]; then
echo "Warning: Mount path '${src}' (from ~/.agent-vm/volumes) is not a directory, skipping. Lima only supports directory mounts." >&2
continue
fi
if [[ -n "$dst" ]]; then
mounts_json+=", {\"location\": \"${src}\", \"mountPoint\": \"${dst}\", \"writable\": false}"
else
mounts_json+=", {\"location\": \"${src}\", \"writable\": false}"
fi
done < "$mounts_file"
fi
mounts_json+="]"

if ! limactl list -q 2>/dev/null | grep -q "^${AGENT_VM_TEMPLATE}$"; then
echo "Error: Base VM not found. Run 'agent-vm setup' first." >&2
return 1
Expand All @@ -97,7 +130,7 @@ _agent_vm_ensure_running() {
# Mount and memory/cpus are applied separately from disk, because
# Lima rejects the entire edit if disk shrinking is attempted.
local edit_args=()
edit_args+=(--set ".mounts = [{\"location\": \"${host_dir}\", \"writable\": true}]")
edit_args+=(--set ".mounts = ${mounts_json}")
[[ -n "$memory" ]] && edit_args+=(--memory "$memory")
[[ -n "$cpus" ]] && edit_args+=(--cpus "$cpus")
(cd /tmp && limactl edit "$vm_name" "${edit_args[@]}") &>/dev/null
Expand Down Expand Up @@ -128,7 +161,7 @@ _agent_vm_ensure_running() {
fi
echo "Updating VM resources..."
local edit_args=()
edit_args+=(--set ".mounts = [{\"location\": \"${host_dir}\", \"writable\": true}]")
edit_args+=(--set ".mounts = ${mounts_json}")
[[ -n "$memory" ]] && edit_args+=(--memory "$memory")
[[ -n "$cpus" ]] && edit_args+=(--cpus "$cpus")
local edit_output
Expand Down Expand Up @@ -319,6 +352,7 @@ VMs are persistent and unique per directory. Running "agent-vm shell" or
"agent-vm claude" in the same directory will reuse the same VM.

Customization:
~/.agent-vm/volumes Extra host paths to mount read-only in VMs (one per line)
~/.agent-vm/setup.sh Per-user setup (runs during "agent-vm setup")
~/.agent-vm/runtime.sh Per-user runtime (runs on each VM start)
<project>/.agent-vm.runtime.sh Per-project runtime (runs on each VM start)
Expand Down Expand Up @@ -456,7 +490,7 @@ _agent_vm_claude() {
_agent_vm_print_resources "$vm_name"

local exit_code=0
limactl shell --workdir "$host_dir" "$vm_name" claude --dangerously-skip-permissions "${args[@]}"
limactl shell --tty --workdir "$host_dir" "$vm_name" claude --dangerously-skip-permissions "${args[@]}"
exit_code=$?
[[ -n "$rm" ]] && { echo "Removing VM..."; _agent_vm_destroy; }
return $exit_code
Expand Down Expand Up @@ -522,7 +556,7 @@ _agent_vm_codex() {
_agent_vm_print_resources "$vm_name"

local exit_code=0
limactl shell --workdir "$host_dir" "$vm_name" codex --full-auto "${args[@]}"
limactl shell --tty --workdir "$host_dir" "$vm_name" codex --full-auto "${args[@]}"
exit_code=$?
[[ -n "$rm" ]] && { echo "Removing VM..."; _agent_vm_destroy; }
return $exit_code
Expand Down Expand Up @@ -559,7 +593,7 @@ _agent_vm_shell() {
echo "Type 'exit' to leave (VM keeps running). Use 'agent-vm stop' to stop it."
fi
local exit_code=0
limactl shell --workdir "$host_dir" "$vm_name" zsh -l
limactl shell --tty --workdir "$host_dir" "$vm_name" zsh -l
exit_code=$?
[[ -n "$rm" ]] && { echo "Removing VM..."; _agent_vm_destroy; }
return $exit_code
Expand Down