Oct 1, 2026 · @Flavio
Uso Claude Code sul Mac, ma i miei progetti Delphi si compilano solo dentro una macchina virtuale Windows in Parallels (https://www.parallels.com). Per mesi il ciclo è stato questo: Claude Code modifica i sorgenti, io faccio commit e push, apro la VM, faccio pull, compilo, copio gli errori e li riporto nel terminale. Un agente che scrive codice ma non può compilarlo è come un collega a cui leggi gli errori al telefono.
L’obiettivo era semplice da dire: un comando sul Mac che compila nella VM e restituisce l’output, così che Claude Code possa leggere gli errori e correggerli da solo.
Si, avrei potuto pensarci prima e sinceramente c’ho pensato tanto tempo fa. E finalmente ho trovato qualche minuto e l’ho fatto.
In questo articolo non trovate la procedura passo per passo, ma le cinque linee guida che ne sono uscite. Valgono per Delphi, ma anche per qualunque toolchain Windows comandata da un agente che gira su macOS o Linux.
Il mio setup, per riferimento: MacBook con Parallels Desktop, una VM Windows 11 con Delphi 11 e 12, e i sorgenti in git.
Ah, dimenticavo: mi sono fatto ‘aiutare’ da Claude 😉
1. La build deve girare con il tuo utente
La prima idea è quella ovvia. Parallels ha un comando, prlctl exec, che esegue qualcosa dentro la VM direttamente dal terminale del Mac. Basta chiamare rsvars.bat e poi msbuild:
prlctl exec "Windows 11" cmd /c "call \"C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\rsvars.bat\" && msbuild C:\Dev\Zero\Zero.dproj"
Funziona finché il progetto usa solo la RTL e la VCL. Appena entra in gioco un package installato nell’IDE arriva questo:
warning : Expected configuration file missing - C:\WINDOWS\system32\config\systemprofile\...\EnvOptions.proj
MainForm.pas(7): error F2613: Unit 'MyLib.Graphics' not found.
Il warning è l’indizio. prlctl exec non esegue i comandi come te, ma come SYSTEM. E Delphi tiene quasi tutta la configurazione di build nel profilo dell’utente: il Library Path sta nel registro sotto HKCU, i percorsi dei pacchetti in %APPDATA%\Embarcadero\BDS\<versione>\EnvOptions.proj. SYSTEM ha un altro HKCU e un altro %APPDATA%, entrambi vuoti. Per lui quel package non è mai stato installato.
Si può rattoppare copiando EnvOptions.proj nel profilo di SYSTEM. Il registro però resta fuori portata, e il prossimo package installato vi riporta al punto di partenza.
La regola che ne ho ricavato vale per qualunque toolchain con configurazione per utente: il processo di build deve autenticarsi come l’utente che usa l’IDE.
Il modo più semplice per entrare nella VM come utente vero, da un terminale, è SSH.
2. SSH con una chiave dedicata, senza passphrase
Windows ha un server SSH nativo, mantenuto da Microsoft. È una funzionalità facoltativa: presente nel sistema ma non installata. Due righe di PowerShell da amministratore e il servizio sshd è attivo e parte da solo all’avvio:
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd; Set-Service sshd -StartupType Automatic
Lato Mac il consiglio è di non riusare la chiave personale. Generatene una dedicata alla VM, senza passphrase:
ssh-keygen -t ed25519 -f ~/.ssh/delphi_vm -N "" -C "delphi-vm"
Può sembrare una scelta poco prudente, ma il motivo è pratico: chi lancia la build è un agente, non una persona. Una passphrase è un prompt, e un prompt blocca lo script. A me è successo proprio questo: la mia chiave personale aveva una passphrase che non ricordavo più, e ogni tentativo finiva in tre richieste a vuoto. La chiave dedicata ha un perimetro piccolo, una sola VM locale, ed è facile da revocare. Se preferite comunque una passphrase, il Portachiavi di macOS (ssh-add --apple-use-keychain) la sblocca una volta per tutte, con UseKeychain yes nel config.
Un alias in ~/.ssh/config evita di ripetere IP, utente e chiave a ogni comando. L’ultima riga accetta da sola l’impronta della VM alla prima connessione: senza, uno script non interattivo si fermerebbe lì.
Host delphi-vm
HostName 10.211.55.3
User flavio
IdentityFile ~/.ssh/delphi_vm
StrictHostKeyChecking accept-new
Il test che conta è uno solo: ssh delphi-vm whoami deve rispondere con il vostro utente Windows, non con nt authority\system. Se è così, il problema della linea guida 1 è risolto.
La trappola degli amministratori
Qui si perde più tempo che altrove. Se il vostro utente Windows è amministratore (e quasi sempre lo è), sshd ignora il classico C:\Users\<utente>\.ssh\authorized_keys. Legge solo C:\ProgramData\ssh\administrators_authorized_keys, che deve essere accessibile esclusivamente a SYSTEM e Administrators. Se il file ha permessi più larghi, viene scartato senza un messaggio.
Per scriverlo senza combattere con il copia-incolla tra Mac e VM, ho usato proprio prlctl exec, l’unico caso in cui girare come SYSTEM è un vantaggio. Ho passato a PowerShell uno script codificato in Base64, così apici e backslash attraversano indenni zsh, prlctl e cmd. Per i permessi ho usato i SID dei gruppi al posto dei nomi, che cambiano con la lingua di Windows:
PUBKEY=$(cat ~/.ssh/delphi_vm.pub)
F='C:\ProgramData\ssh\administrators_authorized_keys'
PS="Add-Content -Path '$F' -Value '$PUBKEY' -Encoding ascii; icacls '$F' /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F'"
prlctl exec "Windows 11" powershell -NoProfile -EncodedCommand $(printf '%s' "$PS" | iconv -t UTF-16LE | base64)
Quando SSH continua a chiedere la password, non tirate a indovinare: il log di sshd dice esattamente perché ha rifiutato. Nel mio caso diceva Invalid user TuoUtente. Nel config era rimasto un segnaposto.
Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 20 | Format-List TimeCreated,Message
3. Uno script che fallisce presto e parla chiaro
Con SSH configurato, la build è una riga:
ssh delphi-vm 'call "C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\rsvars.bat" && msbuild "C:\Dev\Zero\Zero.dproj" /t:Build /p:Config=Debug /p:Platform=Win32 /v:minimal'
Vale comunque la pena avvolgerla in uno script, il mio si chiama delphi-build.sh. Il motivo per scriverlo con cura è uno: il suo lettore principale non sono io, è Claude Code. Un umano davanti a un terminale bloccato capisce che deve digitare una password, un agente aspetta e basta. Un umano riconosce un F2613 dovuto all’utente sbagliato, un agente prova a correggere gli uses di un file che non ha colpe.
Da qui quattro principi.
Mai un prompt interattivo. Lanciate ssh con -o BatchMode=yes. Se l’autenticazione non passa, il comando fallisce subito con un errore, invece di restare in attesa di una password che nessuno digiterà.
Controlli di sicurezza prima della build. Prima di compilare, lo script esegue whoami sulla VM e si ferma se la risposta è SYSTEM, con un messaggio esplicito. Meglio un errore che dice la causa vera che un F2613 da interpretare.
Exit code affidabile. L’agente decide se la build è andata dall’exit code, quindi deve essere quello di msbuild e non quello di un tee o di un echo finale. In bash vuol dire PIPESTATUS, ed evitare set -e: con set -e lo script esce prima di arrivare a stampare l’esito.
Parametri espliciti, default prevedibili. Io ho Delphi 11 e 12 nella stessa VM. Lo script rileva la versione più alta, ma per i progetti che restano su Delphi 11 la passo sempre con --delphi 22.0. Una build che cambia compilatore a seconda di cosa è installato non è riproducibile.
Già che c’ero, lo script accende anche la VM se è spenta (prlctl start) e aspetta che SSH risponda. A questo punto prlctl ha un ruolo preciso: gestisce il ciclo di vita della macchina, e la build passa sempre da SSH.
4. Git come ponte: la VM compila esattamente il commit del Mac
Resta da capire dove stanno i sorgenti. Le strade sono tre, e ognuna penalizza qualcuno:
| Dove stanno i sorgenti | Pro | Contro |
|---|---|---|
| Sul Mac, condivisi con la VM via cartella Parallels | Una sola copia, niente commit per compilare | La build attraversa la condivisione. Fragile se il repo sta su un disco esterno che va in standby |
| Sulla VM, letti dal Mac | Build sul disco locale, massima velocità | Claude Code legge e cerca su un volume di rete, lento su centinaia di unit |
| Due cloni, git in mezzo | Entrambi lavorano su disco locale | Si compila solo ciò che è stato pushato |
Ho scelto la terza. Claude Code lavora sul clone del Mac, la VM ha il suo clone e il remoto fa da arbitro. L’unico rischio è compilare codice diverso da quello appena scritto, ed è il più insidioso: la build passa, l’agente pensa di aver verificato la sua modifica, ma la VM aveva compilato il commit di prima.
Per questo lo script ha un’opzione --sync che rende l’allineamento una garanzia invece di un’abitudine:
- Legge dal repo del Mac il branch attivo e il commit.
- Si ferma se trova modifiche non committate, commit non pushati, o un branch che su origin non esiste ancora, e dice esattamente cosa manca.
- Sulla VM esegue
git fetch, poigit checkout <branch>egit pull --ff-only. - Confronta i commit di Mac e VM, e solo se coincidono parte la build.
Due dettagli fanno la differenza. Il fetch va prima del checkout, altrimenti un branch appena creato dall’agente non esiste ancora per la VM. E il pull dev’essere --ff-only, perché il clone della VM è in sola lettura: se diverge, è il segnale che qualcosa non va, non un conflitto da risolvere con un merge automatico che nessuno ha chiesto.
Un ultimo consiglio, che con due cloni su sistemi diversi evita diff fantasma: decidete una politica per i fine riga (core.autocrlf, oppure un .gitattributes con *.pas text eol=crlf).
5. Collegare la build a Claude Code
L’ultimo pezzo è il più breve. In ogni repository servono due file nella cartella .claude.
Il primo è un comando personalizzato, .claude/commands/build.md. Contiene il comando da eseguire e, soprattutto, cosa fare con il risultato:
Compila il progetto eseguendo:
```bash
~/bin/delphi-build.sh --delphi 22.0 --sync "C:\Dev\Zero\Zero.dproj" $ARGUMENTS
```
Se lo script si ferma per modifiche non committate o commit non pushati, chiedimi conferma e poi fai commit e push sul branch corrente prima di rilanciare.
Se la build fallisce, leggi gli errori e correggi i file .pas.
Da quel momento /build compila in Debug/Win32 e /build Release Win64 fa quello che vi aspettate. Le istruzioni in fondo al file contano quanto il comando: dicono all’agente come comportarsi quando lo script si ferma, così commit e push restano una decisione vostra.
Il secondo è .claude/settings.json, con un permesso che evita la conferma a ogni build:
{
"permissions": {
"allow": ["Bash(~/bin/delphi-build.sh:*)"]
}
}
Il permesso è volutamente stretto: autorizza solo lo script, non ssh in generale. Claude Code può compilare quando vuole, ma non può eseguire comandi arbitrari nella VM senza chiedervelo.
In sintesi
Se dovessi rifare tutto da capo, partirei da questa lista:
- La build gira con l’utente dell’IDE, mai come SYSTEM: niente
prlctl execper compilare. - SSH nativo di Windows, con una chiave dedicata e senza passphrase. Il test
ssh <vm> whoamideve rispondere con il vostro utente. - Se l’utente è amministratore, la chiave va in
administrators_authorized_keys, con i permessi ristretti a SYSTEM e Administrators. - Quando qualcosa non va, il log
OpenSSH/Operationalprima di qualsiasi ipotesi. - Uno script senza prompt, con controlli di sicurezza e un exit code affidabile.
- Due cloni git, e una verifica che la VM compili lo stesso commit del Mac.
- Un comando
/buildcon istruzioni chiare e un permesso limitato allo script.
Il risultato pratico è che l’agente chiude il ciclo da solo: modifica, compila, legge l’errore, corregge, ricompila. Io intervengo quando serve decidere, non per fare da tramite. Ma la lezione più utile non riguarda Delphi. Quando uno strumento lo usa un agente e non una persona, il messaggio d’errore fa parte dell’interfaccia: un errore che dice la causa vera vale più di dieci tentativi di correzione nel posto sbagliato.

Lascia un commento
Devi essere connesso per inviare un commento.