<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Deland</title>
    <description>The latest articles on DEV Community by Deland (@deland).</description>
    <link>https://dev.to/deland</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081066%2F811177b7-309f-4537-80ce-ddfd3dbee741.png</url>
      <title>DEV Community: Deland</title>
      <link>https://dev.to/deland</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deland"/>
    <language>en</language>
    <item>
      <title>I gave my coding agent its own computer</title>
      <dc:creator>Deland</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:33:52 +0000</pubDate>
      <link>https://dev.to/deland/i-gave-my-coding-agent-its-own-computer-4l99</link>
      <guid>https://dev.to/deland/i-gave-my-coding-agent-its-own-computer-4l99</guid>
      <description>&lt;p&gt;I run coding agents with shell access all day, and for a long time there was a low-grade dread underneath it. The permission prompts get annoying enough that everyone eventually turns them off, and then you are one confidently wrong &lt;code&gt;rm&lt;/code&gt; away from a bad afternoon.&lt;/p&gt;

&lt;p&gt;What fixed it was not better prompting. It was giving the agent its own computer.&lt;/p&gt;

&lt;p&gt;The setup is a VM on the same Mac. The agent gets a filesystem with nothing of mine in it, credentials that only work on throwaway repos, and a snapshot taken before every session. If it does something destructive I do not debug it. I roll back. Thirty seconds and the mistake never happened.&lt;/p&gt;

&lt;p&gt;That part is easy to describe and, on Apple Silicon, surprisingly annoying to actually build. Here is what I learned doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Apple's Virtualization framework will actually boot
&lt;/h2&gt;

&lt;p&gt;This is the first thing that trips people up, so it is worth being blunt about it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Virtualization.framework&lt;/code&gt; boots two things: &lt;strong&gt;macOS on ARM&lt;/strong&gt; and &lt;strong&gt;Linux on ARM&lt;/strong&gt;. That is the entire list. There is no Windows on ARM, and no amount of configuration will produce one. It is not a licensing gate you can argue your way past, there is simply nothing to enable. Anything you read that claims otherwise is describing UTM's QEMU backend or VMware Fusion, both of which use their own hypervisor rather than Apple's.&lt;/p&gt;

&lt;p&gt;For the agent sandbox case this does not matter, because you want Linux anyway. Linux guests are smaller on disk, boot faster, and you are not burning one of your two permitted macOS guests on a machine that mostly runs &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The two guest types are configured differently in ways the docs do not really foreground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;macOS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Boots the macOS bootloader, and needs a separate&lt;/span&gt;
    &lt;span class="c1"&gt;// auxiliary storage file next to the disk image.&lt;/span&gt;
    &lt;span class="n"&gt;bootLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;macOS&lt;/span&gt;
    &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;auxiliaryStoragePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;auxPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Boots EFI, and needs a variable store that&lt;/span&gt;
    &lt;span class="c1"&gt;// persists across reboots. Lose this file and the&lt;/span&gt;
    &lt;span class="c1"&gt;// guest forgets where its bootloader is.&lt;/span&gt;
    &lt;span class="n"&gt;bootLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linuxEFI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;variableStorePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;efiVars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The EFI variable store is the one people lose. It is a small file that lives next to the disk image and holds the boot entries. Copy the disk image somewhere without it and you get a guest that boots to an EFI shell and looks broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The serial console will change your guest's behavior
&lt;/h2&gt;

&lt;p&gt;This one cost me an evening, and I have never seen it written down.&lt;/p&gt;

&lt;p&gt;Attaching a serial console to a Linux guest is the obvious move when the guest fails silently. You get &lt;code&gt;hvc0&lt;/code&gt; piped to a log file and you can finally see what the kernel is doing.&lt;/p&gt;

&lt;p&gt;Except Ubuntu's installer detects the serial port and decides that is where you want to be. Subiquity moves the installation UI off the graphical display and onto the serial console, in a reduced text mode, and your VM window sits there showing what looks like a hang. The guest is fine. You just moved its face.&lt;/p&gt;

&lt;p&gt;So the serial console has to be opt in, not always on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only attach the console when actively debugging.&lt;/span&gt;
&lt;span class="nv"&gt;consoleLogPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linux&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;UserDefaults&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Serial"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consoleLogURL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general lesson generalizes past this one bug: on this framework, adding a device is never free. Every device you attach is visible to the guest and the guest may make decisions about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting code in and out without defeating the point
&lt;/h2&gt;

&lt;p&gt;My first version of this mounted my real project directory into the VM through VirtioFS. It worked immediately and it was completely pointless, because an agent that can write to my actual project folder is just my main machine with extra steps.&lt;/p&gt;

&lt;p&gt;The version that works: copy in, work, copy the diff out. The share is read only, or there is no share at all and everything moves over SSH.&lt;/p&gt;

&lt;p&gt;If you do want a share, the tag matters and differs by guest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Linux guests: pick your own tag, then mount it:&lt;/span&gt;
&lt;span class="c1"&gt;//   mount -t virtiofs myshare /mnt/share&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"myshare"&lt;/span&gt;

&lt;span class="c1"&gt;// macOS guests: use Apple's automount tag and the&lt;/span&gt;
&lt;span class="c1"&gt;// share shows up at /Volumes/My Shared Files with&lt;/span&gt;
&lt;span class="c1"&gt;// no mount command at all.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;VZVirtioFileSystemDeviceConfiguration&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;macOSGuestAutomountTag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The macOS automount tag is genuinely nice and almost nobody knows it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rosetta inside a Linux guest
&lt;/h2&gt;

&lt;p&gt;If your toolchain has an x86-64 binary in it somewhere, and it usually does, you can share Rosetta into an ARM Linux guest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="kt"&gt;VZLinuxRosettaDirectoryShare&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;// attach the share&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;notInstalled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;// offer installRosetta { ... }&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;notSupported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;// hide the feature entirely&lt;/span&gt;
&lt;span class="kd"&gt;@unknown&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;// treat as unsupported&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount it in the guest with &lt;code&gt;mount -t virtiofs rosetta /mnt/rosetta&lt;/code&gt;, register it with &lt;code&gt;binfmt_misc&lt;/code&gt;, and x86-64 binaries start running.&lt;/p&gt;

&lt;p&gt;Two caveats before you build anything load bearing on this. Apple has signalled that Rosetta 2 is being wound down in future macOS releases, and has not committed to the Linux virtualization path surviving that. Check Apple's current guidance rather than mine. And handle &lt;code&gt;@unknown default&lt;/code&gt; as unsupported rather than crashing, because the day this enum grows a case is the day your app stops launching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Snapshot before, not after.&lt;/strong&gt; Obvious in hindsight. Cost me a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real credentials get used.&lt;/strong&gt; If the agent can reach it, treat it as in scope. Scope the tokens to throwaway repos and assume anything reachable from that VM is reachable by the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk images only grow.&lt;/strong&gt; The image expands toward whatever you provisioned and does not shrink when you free space inside the guest. Do not oversize "just in case". Keep one clean base image you never boot, clone off it, and delete the clone when you are done. One image that stays reasonable beats five that have all ballooned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two macOS guests per host, maximum.&lt;/strong&gt; That is Apple's license terms, not a technical limit. Nothing stops you, which means it is on you. Irrelevant for Linux guests, which is another reason to use them here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;I run with permissions fully open now. The blast radius is a disk image I can throw away, so the prompt that used to make me think twice does not anymore.&lt;/p&gt;

&lt;p&gt;The odd side effect is that I review the final diff more carefully than I used to. I am not spending attention on "is this &lt;code&gt;rm&lt;/code&gt; safe", so there is attention left over for whether the code is any good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Any of these will get you there. Apple's framework directly if you want to write the roughly two hundred lines yourself, and honestly it is a good weekend, the API is one of the better ones Apple ships. UTM if you want something free with a GUI and do not mind the setup. lume or Tart if you would rather script it.&lt;/p&gt;

&lt;p&gt;Disclosure: I got annoyed enough at the setup friction that I built a Mac app for this, called &lt;a href="https://kyvenza.com" rel="noopener noreferrer"&gt;Kyvenza&lt;/a&gt;. Every code sample above is from its engine. It is 49 dollars one time with a 7 day trial, and it will not run Windows either, for the reason in the second section. The approach is the point of the post though, and UTM does it for free if you do not mind the assembly.&lt;/p&gt;

&lt;p&gt;Still curious whether anyone has a cleaner pattern for handing credentials to a sandboxed agent. That is the part I like least.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>macos</category>
      <category>swift</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
