<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
  <title>The Sprites Blog</title>
  <subtitle>Building, running, and shipping with Sprites on Fly.io</subtitle>
  <id>https://fly.io/sprites-blog/</id>
  <link href="https://fly.io/sprites-blog/"/>
  <link href="https://fly.io/sprites-blog/" rel="self"/>
  <updated>2026-04-01T00:00:00+00:00</updated>
  <author>
    <name>Fly</name>
  </author>
  <entry>
    <title>Run Paperclip on a Sprite</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/paperclip-on-a-sprite/"/>
    <id>https://fly.io/sprites-blog/paperclip-on-a-sprite/</id>
    <published>2026-04-01T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/paperclip-on-a-sprite/assets/paperclip-on-a-sprite.jpg"/>
    <content type="html">&lt;p&gt;Paperclip is extremely hot right now. I&amp;rsquo;m not going to explain what it is in any detail here because you have YouTube. But in short: it&amp;rsquo;s an open-source orchestration platform that lets you set up teams of AI agents that operate like a company. Org charts, goals, budgets, autonomous execution. It&amp;rsquo;s perfect for people who want a team of robots to do all the work that they issue from a dimly lit room in which they stroke a cat and sip from a crystal glass.&lt;/p&gt;

&lt;p&gt;It is genuinely cool and appears to have real utility. So it&amp;rsquo;s a safe bet that Mac Minis are sold out in every store in your city again.
Fortunately, Sprites people have already forgotten what it feels like to have something as trivial as hardware scarcity get in the way of building cool things.&lt;/p&gt;

&lt;p&gt;Let&amp;rsquo;s get a Linux computer to run our Paperclip on:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-9e6t02l7"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-9e6t02l7"&gt;sprite create paperclip
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Now we have a hardware-isolated Linux computer with 100GB of storage, its own URL with TLS, and a pause/resume lifecycle that means you only pay for it when it&amp;rsquo;s actually doing something.&lt;/p&gt;
&lt;h2 id='setup' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#setup' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Setup&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Node 22 is already in your Sprite. So this is the whole install:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-86wujfoi"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-86wujfoi"&gt;npx paperclipai onboard &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;-y&lt;/code&gt; flag accepts all defaults: embedded PostgreSQL, local file storage, port 3100. It creates your config, sets up the database, generates secrets. Done.&lt;/p&gt;
&lt;h3 id='make-it-reachable-and-register-the-service' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#make-it-reachable-and-register-the-service' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Make It Reachable and Register the Service&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Paperclip&amp;rsquo;s default config binds to &lt;code&gt;127.0.0.1&lt;/code&gt; in &lt;code&gt;local_trusted&lt;/code&gt; mode. You&amp;rsquo;re not sitting at this machine, so it needs to bind to &lt;code&gt;0.0.0.0&lt;/code&gt; in &lt;code&gt;authenticated&lt;/code&gt; mode, get told about your Sprite&amp;rsquo;s hostname, and be registered as a Sprite service. Tell Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In &lt;code&gt;~/.paperclip/instances/default/config.json&lt;/code&gt;, change &lt;code&gt;server.deploymentMode&lt;/code&gt; to &lt;code&gt;&amp;quot;authenticated&amp;quot;&lt;/code&gt; and &lt;code&gt;server.host&lt;/code&gt; to &lt;code&gt;&amp;quot;0.0.0.0&amp;quot;&lt;/code&gt;. Then run &lt;code&gt;npx paperclipai allowed-hostname&lt;/code&gt; with this Sprite&amp;rsquo;s hostname (get it from &lt;code&gt;sprite-env info&lt;/code&gt;). Then register Paperclip as a Sprite service called &amp;ldquo;paperclip&amp;rdquo; using &lt;code&gt;sprite-env services create&lt;/code&gt; with the command &lt;code&gt;npx&lt;/code&gt; (use &lt;code&gt;which npx&lt;/code&gt; to get the full path), args &lt;code&gt;paperclipai,run&lt;/code&gt;, and &lt;code&gt;--http-port 3100&lt;/code&gt;. Wait for it to start and verify the health endpoint at &lt;code&gt;http://localhost:3100/api/health&lt;/code&gt; returns ok. Then grep the service logs at &lt;code&gt;/.sprite/logs/services/paperclip.log&lt;/code&gt; for the &lt;code&gt;board-claim&lt;/code&gt; URL, replace &lt;code&gt;http://localhost:3100&lt;/code&gt; with this Sprite&amp;rsquo;s URL, and give me the link.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id='checkpoint' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoint' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoint&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;You&amp;rsquo;ve got a Paperclip instance with an embedded database, auth, and a live URL. Claude should checkpoint the sprite on its own, but in case it&amp;rsquo;s having a slow day:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-n12uuajx"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-n12uuajx"&gt;sprite checkpoint create &lt;span class="nt"&gt;--comment&lt;/span&gt; &lt;span class="s2"&gt;"Paperclip configured and running"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; paperclip
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='access-it-from-anywhere' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#access-it-from-anywhere' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Access It From Anywhere&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Your Sprite already has a URL. Check it:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-2u9h5w47"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-2u9h5w47"&gt;sprite url
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;That URL, with automatic TLS, is serving your Paperclip dashboard. Open it in a browser, create your account, and start hiring your AI workforce. From your phone if you want.&lt;/p&gt;

&lt;p&gt;The first time you hit the UI, Paperclip will ask you to claim the instance and create an admin account. This is the &lt;code&gt;authenticated&lt;/code&gt; mode doing its job. After that, anyone who accesses the URL needs to log in.&lt;/p&gt;

&lt;p&gt;Want to share it with a teammate?&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-irm9wgnn"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-irm9wgnn"&gt;sprite url update &lt;span class="nt"&gt;--auth&lt;/span&gt; public
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;That makes the Sprite URL reachable without Fly.io authentication. Paperclip&amp;rsquo;s own auth layer still protects the dashboard.&lt;/p&gt;
&lt;h2 id='why-a-sprite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#why-a-sprite' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Why a Sprite?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sprites are designed to be the ideal environment for running projects like Paperclip. These are some of the baked in features that make agents way more fun to use in a Sprite.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Checkpoints.&lt;/strong&gt; So you&amp;rsquo;ve achieved an inclusive and supportive corporate culture in your robot team, only to have it all go haywire thanks to an unruly new hire or a badly worded prompt? &lt;code&gt;sprite checkpoint restore &amp;lt;id&amp;gt;&lt;/code&gt; and the entire environment, database, config, agent state, everything, rolls back in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Pause/resume.&lt;/strong&gt; It&amp;rsquo;s conceivable that some people will build fully autonomous pipelines that run 24/7 in Paperclip, but the rest of us will be issuing instructions and coming back to check on progress when we get a chance. If our agents aren&amp;rsquo;t doing anything, we don&amp;rsquo;t want to be paying to run infra. Sprites automatically pause when work isn&amp;rsquo;t being done and cost next to nothing while asleep.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Isolation without ownership.&lt;/strong&gt; Each Sprite is hardware-isolated. This is entirely different to container isolation. You can sleep soundly knowing that even if your agents try the dumbest shit imaginable, it will never affect anything outside the Sprite env.&lt;/p&gt;
&lt;h2 id='network-policy' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#network-policy' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Network Policy&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Paperclip agents make HTTP requests. They call APIs, they fetch data, they do things on the internet. If you&amp;rsquo;re going to run something like this on your local machine, you need to be super careful that those agents don&amp;rsquo;t have unrestricted access to your entire home network. Every device, every NAS, every printer.&lt;/p&gt;

&lt;p&gt;In a Sprite, controlling exactly which domains the agents can reach is simple:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-zsgcfry7"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-zsgcfry7"&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /.sprite/policy/network.json
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;By default, Sprites allow common dev domains (GitHub, npm, Anthropic, etc). You can lock the policy down further from outside the Sprite so that even a compromised agent can&amp;rsquo;t phone home to somewhere unexpected. DNS resolution returns REFUSED for blocked domains. The agent doesn&amp;rsquo;t even know the domain exists.&lt;/p&gt;
&lt;h2 id='robots-running-side-hustles' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#robots-running-side-hustles' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Robots Running Side Hustles&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Right, I&amp;rsquo;m off to surround myself with robots smarter than me and see if that somehow results in me ever shipping a side project.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Put a (smol) Brain in Your Sprite</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/local-llm-on-a-sprite/"/>
    <id>https://fly.io/sprites-blog/local-llm-on-a-sprite/</id>
    <published>2026-03-16T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/local-llm-on-a-sprite/assets/local-llm-on-a-sprite.png"/>
    <content type="html">&lt;p&gt;There&amp;rsquo;s a wave of absurdly efficient local inference engines showing up. Models quantized so aggressively that they run on CPUs, no GPU required, at speeds that are actually usable. Microsoft&amp;rsquo;s &lt;a href='https://github.com/microsoft/BitNet' title=''&gt;BitNet&lt;/a&gt; is one of them: 1.58-bit quantization, ternary weights, optimized SIMD kernels. A 2.4 billion parameter model that fits in 1.1GB of RAM.&lt;/p&gt;

&lt;p&gt;So naturally, I needed to see if one of these little guys could achieve something approaching acceptable performance, running on a Sprite. And if it could, how much time could I waste wondering what I could do with a tiny LLM running on a hardware-isolated computer?&lt;/p&gt;
&lt;h2 id='why-though' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#why-though' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Why though?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sprites spin up with access to frontier models literally one CLI command away. Claude is right there. So why would I want to get a puny little 2B model running locally on a Sprite?&lt;/p&gt;

&lt;p&gt;Because a tiny local model on a Sprite is not the same as a tiny local model on your laptop. &lt;/p&gt;

&lt;p&gt;Every Sprite already has a URL with TLS. Stand up an HTTP server, register it as a service, and now any other Sprite on your account can call it. Your &lt;a href='https://sprites.dev/blog/agent-swarms-on-sprites' title=''&gt;agent swarm&lt;/a&gt; just gained a shared inference endpoint by adding one more Sprite to the fleet. No infrastructure changes, no new dependencies, just another URL.&lt;/p&gt;

&lt;p&gt;The Sprite sleeps when nobody&amp;rsquo;s calling it. A request arrives, the Sprite wakes, the model loads (~1.2 seconds), inference runs, response goes back, Sprite goes back to sleep. That lifecycle doesn&amp;rsquo;t exist on a VPS. You&amp;rsquo;re not paying for idle compute between bursts of classification requests at 3am.&lt;/p&gt;
&lt;h2 id='checkpoints-are-amazing-for-working-with-local-models' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoints-are-amazing-for-working-with-local-models' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoints are amazing for working with local models&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;With a local model, the environment &lt;em&gt;is&lt;/em&gt; the product. The compiled kernels, the model weights, the server code, the Python deps, it&amp;rsquo;s all files on disk. A checkpoint captures the entire thing atomically. I can hear you asking who gives a shit, but think about that for a minute. &lt;/p&gt;

&lt;p&gt;You have BitNet-2B running and serving requests. You want to try the 3B model. Or a Falcon variant. Or you want to retune the kernel parameters for better throughput. On a normal box, you&amp;rsquo;d be juggling git branches, backing up model files, praying your venv still works after you upgraded numpy. Here it&amp;rsquo;s &lt;code&gt;checkpoint create&lt;/code&gt;, fuck around, &lt;code&gt;checkpoint restore&lt;/code&gt; if it goes wrong. The whole stack, OS deps, compiled binaries, model weights, server code, rolls back together.&lt;/p&gt;

&lt;p&gt;Small models are wildly sensitive to prompt formatting. The difference between &lt;code&gt;User:&lt;/code&gt; and &lt;code&gt;[INST]&lt;/code&gt; can be the difference between coherent output and gibberish. Checkpoint before you start experimenting with chat templates. Try five different formats. Keep the one that works. Restore if you accidentally break the server while hacking on it.&lt;/p&gt;

&lt;p&gt;New llama.cpp commit drops with performance improvements? Checkpoint your working state, pull, rebuild. If it breaks, you&amp;rsquo;re one restore from a working service. This is how you gain the confidence to actually try cool new shit when it emerges, as opposed to living in fear that your whole service will drop dead if you even say update within earshot.&lt;/p&gt;
&lt;h2 id='try-it' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#try-it' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Try it&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Head to &lt;a href="https://bitnet-llm-beony.sprites.app/chat"&gt;https://bitnet-llm-beony.sprites.app/chat&lt;/a&gt; where you can chat with BitNet-b1.58-2B-4T with a ~200 line Python HTTP server in front of it. No frameworks. The server wraps &lt;code&gt;llama-cli&lt;/code&gt; as a subprocess and exposes an OpenAI-compatible API at &lt;code&gt;/v1/chat/completions&lt;/code&gt;, a status page at &lt;code&gt;/&lt;/code&gt;, and the self-serving client at &lt;code&gt;/client.py&lt;/code&gt;. Register it as a Sprite service on port 8080 and the Sprite URL does the rest.&lt;/p&gt;

&lt;p&gt;Yeah, the Sprite running the model serves its own client. Hit &lt;code&gt;/client.py&lt;/code&gt; and you get a single Python file with the Sprite&amp;rsquo;s URL baked in. No pip install, no API keys, no config. Any Sprite grabs it with one curl and immediately has &lt;code&gt;ask()&lt;/code&gt; and &lt;code&gt;classify()&lt;/code&gt; functions that work. The model distributes its own SDK.&lt;/p&gt;
&lt;h2 id='is-it-crap' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#is-it-crap' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Is it crap?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;It should come as no surprise that this thing is about as intelligent as a shovel and as well-informed as a child raised by wolves. Nonetheless, for what it is, it&amp;rsquo;s damn impressive. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Model size on disk:&lt;/strong&gt; 1.1GB
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;RAM usage:&lt;/strong&gt; ~1.3GB total (model + KV cache)
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Generation speed:&lt;/strong&gt; ~50 tokens/sec
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Request latency:&lt;/strong&gt; ~1.4 seconds end-to-end
&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;50 tokens per second on a CPU. That&amp;rsquo;s not a typo. 1.58-bit quantization is doing real work here.&lt;/p&gt;

&lt;p&gt;I grabbed the client from the Sprite and tested it:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative python"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-6diozwu8"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-6diozwu8"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;llm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;classify&lt;/span&gt;

&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What is a Sprite in computing?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# "A sprite in computing is a simple graphical object, such as a
#  character or icon, that is drawn in a single color and occupies
#  a single display area."
&lt;/span&gt;
&lt;span class="n"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the deploy failed with a timeout error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"bug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"feature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ops"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# "bug"
&lt;/span&gt;
&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Name three prime numbers under 20."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# "The prime numbers under 20 are 2, 3, 5, 7, 11, 13, and 17."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Coherent. Correct. ~1.4 seconds per response.&lt;/p&gt;
&lt;h2 id='build-your-own' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#build-your-own' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Build your own&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Tell Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a new Sprite. Clone &lt;a href="https://github.com/microsoft/BitNet"&gt;https://github.com/microsoft/BitNet&lt;/a&gt; with submodules. Install cmake and libomp-dev. Don&amp;rsquo;t bother with Conda, just pip install the requirements directly. Install sentencepiece 0.2.1 separately first since the pinned 0.2.0 won&amp;rsquo;t build on this compiler. Install the local gguf package from 3rdparty/llama.cpp/gguf-py with &amp;ndash;no-deps. Run the codegen and compile steps for x86&lt;em&gt;64 with i2&lt;/em&gt;s quantization. There&amp;rsquo;s a const-correctness bug in ggml-bitnet-mad.cpp around line 811 where a non-const pointer is assigned from a const source, fix it. Download the pre-quantized GGUF from microsoft/bitnet-b1.58-2B-4T-gguf instead of converting the model yourself. Verify inference works, then checkpoint.&lt;/p&gt;

&lt;p&gt;Then build a Python HTTP server that wraps llama-cli and serves: an OpenAI-compatible chat endpoint at /v1/chat/completions, a status page at / showing model info and usage instructions, a self-addressed Python client at /client.py that uses only stdlib and has the Sprite&amp;rsquo;s URL baked in, and a chat UI at /chat with a dark monospace theme that keeps conversation history in the browser. Register it as a Sprite service on port 8080. Checkpoint again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That&amp;rsquo;s the whole thing. Text classification, structured extraction, triage, routing, anything where a frontier model is overkill and a regex isn&amp;rsquo;t enough. If you want to get clever, use the local model for the fast cheap stuff and fall back to Claude for anything that needs real reasoning. The OpenAI-compatible API means swapping the endpoint is a one-line change.&lt;/p&gt;

&lt;p&gt;1.58 bits per weight. 50 tokens per second. On a CPU. LOL.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Context Engineering Is Just Writing Things Down</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/context-engineering-is-just-writing-things-down/"/>
    <id>https://fly.io/sprites-blog/context-engineering-is-just-writing-things-down/</id>
    <published>2026-02-23T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/context-engineering-is-just-writing-things-down/assets/context-engineering-is-just-writing-things-down.jpg"/>
    <content type="html">&lt;p&gt;&amp;ldquo;Context engineering&amp;rdquo; is the internet&amp;rsquo;s new shibboleth. Fine. We&amp;rsquo;ve had worse.
So here&amp;rsquo;s an argument starter: The only interesting thing about Openclaw is its context engineering. The way it manages and inserts memory into agent context. Yes, this is because I live in a world where everything you need from a computer is just a few prompts away. So no, I don&amp;rsquo;t think a Slack connector or a WhatsApp bridge is interesting. The memory system is cool though.&lt;/p&gt;

&lt;p&gt;People are having fun with SOUL.md because it lets you set a persistent personality and some behavioral traits. MEMORY.md is &amp;hellip; memories, and daily logs capture ongoing session context, but these are standard agent equipment. What&amp;rsquo;s worth a look is how each of these are managed. Daily logs are append-only and decay in relevance over time. They&amp;rsquo;re noise with signal in it. MEMORY.md doesn&amp;rsquo;t decay, it only changes when the agent or the user deliberately promotes something into it. One is a stream, the other is curated.&lt;/p&gt;

&lt;p&gt;Then there&amp;rsquo;s memory recall. A hybrid of keyword search (BM25) and vector search, weighted 70/30, with diversity re-ranking so you don&amp;rsquo;t get five variations of the same result. It finds both exact matches like error codes and conceptual matches like &amp;ldquo;that thing we discussed about auth.&amp;rdquo; All backed by local SQLite.&lt;/p&gt;

&lt;p&gt;Of course these aren&amp;rsquo;t revolutionary features, but they serve to make my point. The quality of an agent&amp;rsquo;s work is far more dependent on how much it knows about your project and its environment than it is about model capability.&lt;/p&gt;

&lt;p&gt;This is why we&amp;rsquo;re working our collective ass off to help Sprites understand how to operate themselves and other Sprites with built-in documentation. Just Tell Claude To Do It covered what comes in the box. But when it comes to your specific Sprite project, these should be considered a starting point.&lt;/p&gt;

&lt;p&gt;There are four different ways to make your Sprite better at working on your project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;CLAUDE.md&lt;/strong&gt; — project conventions, commands, patterns. Claude reads this on every session.
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Skills&lt;/strong&gt; — reusable multi-step workflows that activate by description.
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Hooks&lt;/strong&gt; — scripts that inject live environment state into Claude&amp;rsquo;s context.
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Checkpoints&lt;/strong&gt; — snapshots that make the whole investment durable.
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;Used together, these can turn a Sprite that everyone else has into a specialised coding agent environment that knows your project and your workflow inside out. Try this:&lt;/p&gt;
&lt;h2 id='the-project' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-project' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;The Project&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s build an excuse-as-a-service API called &lt;code&gt;Alibi&lt;/code&gt; in a Sprite. We want to be able to hit it with &amp;ldquo;late to standup&amp;rdquo;, &amp;ldquo;forgot to review the PR&amp;rdquo;, &amp;ldquo;haven&amp;rsquo;t replied in three days&amp;rdquo; situations and have it generate a plausible excuse. It&amp;rsquo;s a Bun + Hono backend with Postgres for storing excuse templates and usage stats. Yes, we&amp;rsquo;ll open source it.&lt;/p&gt;
&lt;h2 id='claude-md' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#claude-md' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;CLAUDE.md&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;See &lt;a href='https://alibi-beony.sprites.app/docs/claude-md' title=''&gt;Alibi&amp;rsquo;s CLAUDE.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is read from your project root at the start of every session. Write one before you ask Claude to do anything. Add what you know you want about the project; the stack, the commands, the conventions you care about. Then remember to grow it as you work. Any time Claude uses &lt;code&gt;console.log&lt;/code&gt; instead of your logger, or puts tests in the wrong directory, or forgets the auth middleware, tell it to fix it and update &lt;code&gt;CLAUDE.md&lt;/code&gt;. By the time Alibi soft launches, my &lt;code&gt;CLAUDE.md&lt;/code&gt; has the stack, the commands, the migration naming scheme, the error response format, the logging convention, and a note about where test files go.&lt;/p&gt;
&lt;h2 id='skills-for-repeated-workflows' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#skills-for-repeated-workflows' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Skills for Repeated Workflows&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;See &lt;a href='https://alibi-beony.sprites.app/docs/skills/preflight' title=''&gt;Alibi&amp;rsquo;s preflight skill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is not for multi-step processes you run repeatedly. That&amp;rsquo;s where I wanna use Skills.&lt;/p&gt;

&lt;p&gt;In preparation for the millions of users that Alibi will have, I want a robust preflight check before every PR. Tell Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write a Claude Code skill called &amp;ldquo;preflight&amp;rdquo; that runs tests, type checking, linting, and checks for leftover TODOs, in that order. Stop at the first failure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yeah I could just say this every time, but I&amp;rsquo;ll probably describe it differently every time and give Claude an excuse to screw it up. With a skill at &lt;code&gt;~/.claude/skills/preflight/SKILL.md&lt;/code&gt;, I just say run preflights. Done.&lt;/p&gt;
&lt;h2 id='hooks-for-things-you-forget' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#hooks-for-things-you-forget' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Hooks for Things You Forget&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;See &lt;a href='https://alibi-beony.sprites.app/docs/hooks/sprite-env-check' title=''&gt;Alibi&amp;rsquo;s environment hook&lt;/a&gt; and &lt;a href='https://alibi-beony.sprites.app/docs/hooks/git-state-check' title=''&gt;git state hook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use hooks to inject anything you want Claude to know about the state of the environment &lt;em&gt;right now&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Try a &lt;a href='https://sprites.dev/blog/claude-code-sprite-hooks' title=''&gt;Sprite environment hook&lt;/a&gt; that warns Claude about crashed services and stale checkpoints, and/or a &lt;a href='https://sprites.dev/blog/sprite-hooks-future-directions' title=''&gt;git state hook&lt;/a&gt; that feeds branch, uncommitted changes, and ahead/behind status into context. &lt;/p&gt;

&lt;p&gt;Hooks are how you stop Claude from building inside a windowless house. No more explaining environment errors it should already know about. No more soul-destroying moments where Claude says &amp;ldquo;Alibi is live!&amp;rdquo; when Postgres has been dead for ten minutes.&lt;/p&gt;
&lt;h2 id='checkpoint-the-investment' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoint-the-investment' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoint the Investment&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;See &lt;a href='https://alibi-beony.sprites.app/docs/settings' title=''&gt;Alibi&amp;rsquo;s hook config&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude should already be checkpointing automatically because the Sprite docs tell it to. If it&amp;rsquo;s not, tell it to. Sternly. A checkpointed Sprite is a safe place of work. Everything you&amp;rsquo;ve built; the CLAUDE.md, the skills, the hooks, the installed dependencies, the running Postgres service, the very project itself, is on the Sprite&amp;rsquo;s filesystem. &lt;/p&gt;

&lt;p&gt;This is the part you don&amp;rsquo;t get on your laptop. This stuff is scattered across your machine and easy to lose. In a Sprite, it&amp;rsquo;s all environment state. This is the quiet joy of the humble checkpoint.&lt;/p&gt;
&lt;h2 id='the-payoff' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-payoff' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;The Payoff&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;A week from now, a user from Yakutsk reports that excuse generation is returning duplicates when their category has fewer than ten templates. I open my Sprite, start Claude, and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There&amp;rsquo;s a bug where excuse generation returns duplicates when a category has fewer templates than the requested count. Investigate and fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude checks the git state hook: Clean working tree, on &lt;code&gt;main&lt;/code&gt;. It reads the service logs. It finds the query in &lt;code&gt;src/services/generator.ts&lt;/code&gt;, spots the issue (sampling with replacement instead of without), fixes it, writes a regression test next to the source file using &lt;code&gt;bun test&lt;/code&gt;, runs the suite, and checkpoints.&lt;/p&gt;

&lt;p&gt;It used the right test runner. It put the test in the right place. It logged with pino. It followed the error response format. It checkpointed. I said none of that. The context layer did.&lt;/p&gt;

&lt;p&gt;Look mom, I&amp;rsquo;m a context engineer! No mom, I didn&amp;rsquo;t even need a vector database or a RAG pipeline or some token-bingeing memory flush before compaction. Just files on disk, curated from real use, in an environment where they persist.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Agent Swarms on Sprites</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/agent-swarms-on-sprites/"/>
    <id>https://fly.io/sprites-blog/agent-swarms-on-sprites/</id>
    <published>2026-02-09T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/agent-swarms-on-sprites/assets/agent-swarms-on-sprites.jpg"/>
    <content type="html">&lt;p&gt;There&amp;rsquo;s a lot of noise right now about agent teams and swarms. The idea is simple: instead of one code agent grinding through a task list, set up a group of agents that can split work, delegate, and check each other&amp;rsquo;s output. It works. Multiple agents in parallel genuinely get more done.&lt;/p&gt;

&lt;p&gt;The part that nobody has a great answer for is the environment. Where do these agents actually run? Most setups put them all on the same machine; same filesystem, same network, same blast radius. Agent A installs Node 23, Agent B needs Node 20. Two agents write to the same file. One runs &lt;code&gt;pip install sketchy-package&lt;/code&gt; and now everyone&amp;rsquo;s environment is different.&lt;/p&gt;

&lt;p&gt;An agent swarm on Sprites comes with some handy capabilities. Full machine isolation per agent, checkpoints that let you roll back any agent independently, per-agent network policies, and persistence that survives you closing your laptop.&lt;/p&gt;
&lt;h2 id='one-agent-one-sprite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#one-agent-one-sprite' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;One Agent, One Sprite&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;When each agent lives in its own computer, agent teams work more like human teams. Everyone has their own desk. They can install whatever they need, make a mess, try things — without touching anyone else&amp;rsquo;s work. The orchestrator hands out assignments and collects results. No shared state to babysit.&lt;/p&gt;

&lt;p&gt;All you need is a Sprites API token (create one at &lt;code&gt;sprites.dev/account/{your-org}/tokens&lt;/code&gt;) and Claude Code on the orchestrator.&lt;/p&gt;

&lt;p&gt;Prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a new Sprite called &amp;ldquo;researcher&amp;rdquo;. When it&amp;rsquo;s ready, open a console and tell Claude to read the codebase at /home/sprite/myproject and write a summary of the architecture to /home/sprite/output/architecture.md. Use &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; so it can work autonomously.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The orchestrator Sprite creates the worker, gives it a task, and moves on. The worker does its thing independently. The orchestrator can check in later, read the output, or wait for the worker to finish.&lt;/p&gt;
&lt;h2 id='checkpoints-are-your-undo-button' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoints-are-your-undo-button' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoints Are Your Undo Button&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Every Sprite has checkpointing built in. Claude inside a worker Sprite can checkpoint its own environment before doing anything risky — and the orchestrator can tell workers to do this as part of the task prompt. No human needs to be in the loop.&lt;/p&gt;

&lt;p&gt;An agent refactors half the codebase, installs new dependencies, rewrites the database schema. If the result is garbage, restore the checkpoint. Three seconds later the Sprite is back to exactly where it was — not just the code, the whole environment.&lt;/p&gt;

&lt;p&gt;This makes speculative execution practical. Tell the orchestrator to spin up three workers, have each one checkpoint, then attempt a different approach to the same problem. Pick the winner, restore or destroy the rest.&lt;/p&gt;
&lt;h2 id='network-policy-per-agent' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#network-policy-per-agent' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Network Policy Per Agent&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Every Sprite has its own network policy. The orchestrator can lock down each worker to only the domains it actually needs. Denied domains return REFUSED at the DNS level — fast-fail, no silent failures, no data going somewhere you didn&amp;rsquo;t intend.&lt;/p&gt;
&lt;h2 id='communication-between-agents' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#communication-between-agents' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Communication Between Agents&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Workers are isolated, but they still need to share results. A few patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;HTTP endpoints.&lt;/strong&gt; Every Sprite has a URL. A worker can serve its results on port 8080 and the orchestrator fetches them. Good for structured output — JSON reports, test results, generated files.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Shared database.&lt;/strong&gt; Spin up a Postgres Sprite. Give every worker network access to it. Workers write results to tables. Orchestrator queries. Good when agents need to build on each other&amp;rsquo;s findings.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Orchestrator as relay.&lt;/strong&gt; Workers write output to a local file. Orchestrator opens a console to each worker and reads the file. Simple, no extra infrastructure. Good enough for most cases.&lt;/p&gt;
&lt;h2 id='persistence-for-free' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#persistence-for-free' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Persistence for Free&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sprites don&amp;rsquo;t die when you close your laptop. They pause when idle and wake on demand. The orchestrator, the workers, the checkpoints — all still there when you come back.&lt;/p&gt;

&lt;p&gt;Start the swarm, close your laptop, come back later. Everything is exactly where you left it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bonus level:&lt;/em&gt; Checkpoint your orchestrator Sprite after it&amp;rsquo;s set up with the API token and your standard prompts. Now you can restore to a ready-to-swarm state in three seconds whenever you need a fresh team.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Closing the Loop: The Missing Ingredient in Agentic Development</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/closing-the-loop/"/>
    <id>https://fly.io/sprites-blog/closing-the-loop/</id>
    <published>2026-02-05T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/closing-the-loop/assets/unicorn.jpg"/>
    <content type="html">&lt;p&gt;Remember when coding with AI meant copying code from a chat window, pasting it into your editor, running it, copying the error, pasting it back into the chat, etc. Yeah that sucked. We were middleware.  &lt;/p&gt;

&lt;p&gt;Now we have agents that can read and write inside our code bases, which makes them feel a lot more like legit tools. But answering Claude&amp;rsquo;s permission checks 150 times a session is the new copy/pasting from chat. It&amp;rsquo;s annoying. &lt;/p&gt;

&lt;p&gt;Working with a code agent in a Sprite feels like the next step. In a Sprite, the answer to &amp;ldquo;can I try this&amp;rdquo; is always yes. Install dependencies, start servers, hit endpoints, run browsers, just do it and let me know when it&amp;rsquo;s working.&lt;/p&gt;

&lt;p&gt;Thing is, if you want the joy of watching a code agent nail even complex builds regularly, it needs to be able to &amp;ldquo;see&amp;rdquo; what it has built, test its work and troubleshoot issues without needing your feedback. This is exactly why Sprites come with test and observability tooling preinstalled.     &lt;/p&gt;
&lt;h2 id='closing-the-loop' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#closing-the-loop' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Closing the Loop&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Code agents have gotten pretty good at testing their own output, trying different approaches and iterating. And with access to the right tools, a completely agent-driven build&amp;gt;test&amp;gt;ship pipeline is entirely possible. But it would be insane to prompt your agent, give it the &amp;ldquo;yes to all&amp;rdquo; freedom it needs and go to bed. If its running on your laptop that is. &lt;/p&gt;

&lt;p&gt;In a Sprite, the agent is working inside a fully hardware-isolated computer. It doesn&amp;rsquo;t need guardrails or baby proofing of any kind. But it also gains powers it could never have on your laptop. Service logs at known paths. Checkpoint-based rollback that the agent knows how to use. An HTTP URL that&amp;rsquo;s live the moment a server starts. &lt;/p&gt;

&lt;p&gt;The result is a tight loop: the agent writes code, starts a service, hits the endpoint, sees what&amp;rsquo;s wrong, fixes it, and tries again. No permission prompts, no asking you to check something, no guessing.&lt;/p&gt;
&lt;h2 id='the-tools-are-already-in-your-sprite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-tools-are-already-in-your-sprite' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;The Tools Are Already in Your Sprite&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Most of what your agent needs to operate like this is already installed in your Sprite. It also contains files like &lt;code&gt;/.sprite/llm.txt&lt;/code&gt; and &lt;code&gt;/.sprite/docs/agent-context.md&lt;/code&gt; that exist to help your agent understand its environment and the tools available to it. &lt;/p&gt;

&lt;p&gt;One super powerful tool that doesn&amp;rsquo;t (currently) come preinstalled is Playwright.&lt;/p&gt;
&lt;h3 id='browser-automation' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#browser-automation' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Browser Automation&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Installing Playwright on your Sprite is two commands; &lt;code&gt;npm install playwright&lt;/code&gt; and &lt;code&gt;npx playwright install chromium&lt;/code&gt;. It can write tests, run them, screenshot pages, and inspect what&amp;rsquo;s actually rendering instead of assuming its HTML is correct. This is the difference between an agent that writes a login form and one that writes a login form, tries to log in, and fixes the broken redirect.&lt;/p&gt;

&lt;p&gt;With Playwright installed, if your agent starts trying to reinvent the wheel tell it to stop being dense, read its knowledge base and make better choices. Here are some of the tools your agent already has in a Sprite, in case it starts acting the fool.  &lt;/p&gt;
&lt;h3 id='api-testing' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#api-testing' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;API Testing&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;curl, jq, and httpx are all preinstalled. The agent builds an endpoint, hits it, sees the actual response, and fixes what&amp;rsquo;s wrong. No relay through you.&lt;/p&gt;
&lt;h3 id='database-inspection' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#database-inspection' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Database Inspection&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;sqlite3 is preinstalled. The agent writes a migration, runs it, queries the result, sees the missing column, fixes it. If you need Postgres, it&amp;rsquo;s an &lt;code&gt;apt-get install&lt;/code&gt; away.&lt;/p&gt;
&lt;h3 id='test-runners' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#test-runners' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Test Runners&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Every language runtime in a Sprite ships with a built-in test runner — &lt;code&gt;node --test&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;go test&lt;/code&gt;, &lt;code&gt;cargo test&lt;/code&gt;. The agent writes code, runs tests, reads failures, fixes them. This is the most basic closed loop and it works out of the box.&lt;/p&gt;
&lt;h3 id='service-logs' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#service-logs' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Service Logs&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Sprite services write logs to &lt;code&gt;/.sprite/logs/services/&lt;/code&gt;. The agent starts a service, tails the logs, sees the stack trace, and knows exactly what to fix. No asking you &amp;ldquo;what does the console say?&amp;rdquo;&lt;/p&gt;
&lt;h3 id='network-debugging' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#network-debugging' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Network Debugging&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;dig&lt;/code&gt;, and &lt;code&gt;curl -v&lt;/code&gt; are preinstalled. When something isn&amp;rsquo;t connecting, the agent can check what&amp;rsquo;s listening, whether a domain resolves, and what&amp;rsquo;s actually happening on the wire.&lt;/p&gt;
&lt;h3 id='checkpoints' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoints' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoints&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;This is unique to Sprites. The agent can snapshot the entire environment before trying something risky, and roll back in seconds if it goes sideways. This makes aggressive experimentation cheap.&lt;/p&gt;
&lt;h2 id='sprite-on' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#sprite-on' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Sprite On&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You&amp;rsquo;re not middleware, you&amp;rsquo;re the idea guy. And you don&amp;rsquo;t need a smarter model. Just a smarter environment.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>I Build Everything In Sprites Now</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/i-build-everything-in-sprites-now/"/>
    <id>https://fly.io/sprites-blog/i-build-everything-in-sprites-now/</id>
    <published>2026-02-03T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/i-build-everything-in-sprites-now/assets/buildinsprites.png"/>
    <content type="html">&lt;p&gt;I just realised I build everything in Sprites now. It feels weird to say because Sprites are still really new (even for people who work at Fly.io). They&amp;rsquo;re obviously super exciting for people building agents, but when I finally &amp;ldquo;got&amp;rdquo; Sprites, it pretty much instantly felt weird to build anything anywhere else.&lt;/p&gt;

&lt;p&gt;If, like me, you have to flit between many projects every day - because breaking only one thing a day is lazy - you&amp;rsquo;ll know that local development is a joke.&lt;/p&gt;

&lt;p&gt;Let me pause what I&amp;rsquo;m working on to fix this tiny bug over on this other project! Wrong Node version. &lt;code&gt;asdf local nodejs 18.17.0&lt;/code&gt;. Oh wait, this project uses nvm. Fine. &lt;code&gt;nvm use&lt;/code&gt;. No &lt;code&gt;.nvmrc&lt;/code&gt; file. Check the README. Last updated in 1996. Check package.json engines field. Install that version. Now the wrong packages are installed. Ah, productivity.&lt;/p&gt;

&lt;p&gt;Oh of course, the wrong database is running. Start Postgres. Wait, this project uses MySQL. Stop Postgres, start MySQL. Except the other project&amp;rsquo;s MySQL data is still there. Where&amp;rsquo;s the docker-compose for this one?&lt;/p&gt;

&lt;p&gt;Ah I have screwy environment variables, let me just update the &amp;hellip; wait is it &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.development&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, &lt;code&gt;env.thisone&lt;/code&gt;? This project needs &lt;code&gt;API_KEY&lt;/code&gt; set but that collides with the other project&amp;rsquo;s &lt;code&gt;API_KEY&lt;/code&gt; which points somewhere else. Swear I have a &lt;code&gt;.env.projectname&lt;/code&gt; file somewhere. Maybe. Which shell session has the right exports?&lt;/p&gt;

&lt;p&gt;Yes! It&amp;rsquo;s running! Wait what was I doing again?&lt;/p&gt;
&lt;h2 id='no-just-no' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#no-just-no' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;No. Just No.&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now every project I&amp;rsquo;m working on is in its own Sprite. It feels like having a different computer for every project because that&amp;rsquo;s exactly what it is. Every Sprite is a persistent, fully hardware isolated linux computer with 100gb storage that only costs anything when it&amp;rsquo;s actually doing something. I can have 30 projects in 30 Sprites and I&amp;rsquo;m only ever paying for the one I&amp;rsquo;m currently working on.&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-luuaj4ps"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-luuaj4ps"&gt;sprite create dashboard
sprite create api-backend
sprite create billion-dollar-side-project
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Each one has its own versions, its own services, its own env vars, its own ports. Nothing collides because nothing is shared.&lt;/p&gt;
&lt;h2 id='waking-is-instant' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#waking-is-instant' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Waking Is Instant&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Impatient? Me too! Sprites wake in a few seconds.&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-kx4q1kdr"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-kx4q1kdr"&gt;sprite console &lt;span class="nt"&gt;-s&lt;/span&gt; api-backend
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If the api-backend Sprite is paused, it wakes up. Services come back up. I&amp;rsquo;m in the project, ready to work.&lt;/p&gt;

&lt;p&gt;Let me hop into &lt;code&gt;sprite console -s dashboard&lt;/code&gt; on another terminla quickly. Look at that, it just works. Prompt Claude in both Sprites, feel like Tony Stark&lt;/p&gt;

&lt;p&gt;I couldn&amp;rsquo;t care less if I forget about a Sprite I was working in. It puts itself to sleep when its inactive, but nothing about its state changes. Next time I open a console, it&amp;rsquo;s exactly where I left it.&lt;/p&gt;
&lt;h2 id='real-urls-not-localhost' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#real-urls-not-localhost' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Real URLs, Not Localhost&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Every Sprite has its own URL with automatic TLS. Run your dev server on port 8080 and it&amp;rsquo;s just&amp;hellip; there. On the internet. With HTTPS.&lt;/p&gt;

&lt;p&gt;No ngrok. No &amp;ldquo;can you see my screen?&amp;rdquo; No tunneling. OAuth callbacks just work. Webhooks just work. That API integration that refuses to talk to localhost? It just works.&lt;/p&gt;

&lt;p&gt;Need to show someone what you&amp;rsquo;re working on? Send them the URL. They see exactly what you see, in exactly the state it&amp;rsquo;s in. Checkpoint first if you want to freeze it there.&lt;/p&gt;
&lt;h2 id='checkpoints-are-the-whole-environment' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoints-are-the-whole-environment' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoints Are the Whole Environment&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Git is to checkpoints what World Series MLB is to the World Cup. Your code might be version controlled in Github, but what about &amp;hellip; everything else? A checkpoint in a Sprite is a snapshot of the entire environment. Node_modules, database state, running services, env vars, the whole shebang&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-1huweyzd"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-1huweyzd"&gt;sprite checkpoint create
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Wanna see if upgrading to Node 23 breaks everything? Do it! And laugh when it happens. Wait &amp;hellip; you did checkpoint right? &lt;/p&gt;

&lt;p&gt;Wanna tell Claude to go wild on an ambitious refactor just before you go to bed? Nighty night. &lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-wmaiwboy"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-wmaiwboy"&gt;sprite checkpoint restore abc123
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Git branches for experiments are so 2025. &lt;/p&gt;
&lt;h2 id='specialist-claudes-gt-one-very-confused-claude' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#specialist-claudes-gt-one-very-confused-claude' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Specialist Claudes &amp;gt; One Very Confused Claude&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I like to tell Claude to build a good knowledge base from project files and any other documentation I can find as a first step when I recruit it&amp;rsquo;s help on a project. A good claude.md and some project-specific skills completely change the game when it comes to how effective a coding agent can be in a given project. &lt;/p&gt;

&lt;p&gt;That&amp;rsquo;s why I want specialist Claudes. One per project. I don&amp;rsquo;t want the context polluted or diluted by work on any other project. &lt;/p&gt;

&lt;p&gt;What if I build a skill in one Sprite that I want in all my others? I tell Claude in that Sprite to use the Sprites API to propagate that skill to all my other project Sprites. &lt;/p&gt;

&lt;p&gt;OK Article done. &lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-4nibins9"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-4nibins9"&gt;sprite checkpoint create
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  <entry>
    <title>Run Any Code Fearlessly</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/running-untrusted-code/"/>
    <id>https://fly.io/sprites-blog/running-untrusted-code/</id>
    <published>2026-02-02T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/running-untrusted-code/assets/untrusted.jpg"/>
    <content type="html">&lt;p&gt;AI generates code faster than you can review it. Your agent wants to run a Python script. Your tool wants to execute user-submitted code. Your pipeline wants to test generated functions. All things you don&amp;rsquo;t want to be running locally or in production unless you&amp;rsquo;re just trying to add some drama to your life right now. &lt;/p&gt;

&lt;p&gt;If you want to walk the path of peace, you use a sandbox. But as any kindergartener will tell you: not all sandboxes are created equal.&lt;/p&gt;
&lt;h2 id='the-container-problem' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-container-problem' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;The Container Problem&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The default for the longest time has been &amp;ldquo;spin up a container.&amp;rdquo; This is safer than running dodgy code on your machine. It&amp;rsquo;s also not good enough.&lt;/p&gt;

&lt;p&gt;Containers share the host kernel. A kernel exploit in your untrusted code == emotional trauma. NIST SP 800-190 calls container escapes one of the most critical threats to container security. MITRE ATT&amp;amp;CK classifies it as technique T1611. The threat is real.&lt;/p&gt;

&lt;p&gt;Truly untrusted code needs hardware isolation. A dedicated kernel. A non-fictional boundary between the code and everything else.&lt;/p&gt;
&lt;h2 id='the-network-problem' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#the-network-problem' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;The Network Problem&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Filesystem isolation helps, but code that can&amp;rsquo;t touch your files can still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exfiltrate data to an attacker&amp;rsquo;s server
&lt;/li&gt;&lt;li&gt;Download additional payloads
&lt;/li&gt;&lt;li&gt;Call out to command-and-control infrastructure
&lt;/li&gt;&lt;li&gt;Use your compute. Which isn&amp;rsquo;t free
&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Network egress control should not be considered optional. The code needs to run in an environment where outbound connections are explicitly allowed, not implicitly permitted.&lt;/p&gt;
&lt;h2 id='what-you-really-need-from-a-sandbox' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-you-really-need-from-a-sandbox' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;What You Really Need From a Sandbox&lt;/span&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Hardware isolation&lt;/strong&gt;
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Network control&lt;/strong&gt; — allowlist-based, not denylist-based
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;External policy enforcement&lt;/strong&gt; — the code can&amp;rsquo;t modify its own permissions
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Rollback capability&lt;/strong&gt; — undo damage quickly when something goes wrong
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Multiple runtimes&lt;/strong&gt; — AI writes Python, JavaScript, Rust, Go, whatever
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;Spin up and configure your sandboxes with these features and you should be safe. &lt;/p&gt;

&lt;p&gt;Or just use Sprites. &lt;/p&gt;
&lt;h2 id='setting-up-a-code-runner-in-a-sprite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#setting-up-a-code-runner-in-a-sprite' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Setting Up a Code Runner in a Sprite&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Create a Sprite for running untrusted code. &lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-yhwgrj93"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-yhwgrj93"&gt;sprite create code-runner
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Then, lock down its network:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-nporkfo7"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-nporkfo7"&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.sprites.dev/v1/sprites/&lt;span class="o"&gt;{&lt;/span&gt;sprite-id&lt;span class="o"&gt;}&lt;/span&gt;/policy/network &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$SPRITES_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"rules": [{"domain": "*", "action": "deny"}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This is a deny-all policy. The code inside cannot make any outbound connections. DNS queries return &lt;code&gt;REFUSED&lt;/code&gt;. Raw IP connections are blocked.&lt;/p&gt;

&lt;p&gt;What&amp;rsquo;s super important is that this policy is set via the external API. Code running inside the Sprite cannot change it. Even if the code has root access, it cannot modify its own network policy.&lt;/p&gt;
&lt;h2 id='running-code' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#running-code' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Running Code&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Proceed to laugh in the face of danger:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative python"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-232flkfq"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-232flkfq"&gt;&lt;span class="c1"&gt;# runner.py - runs inside the Sprite
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_untrusted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""Execute code and capture output."""&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Add other languages as needed
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"stderr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"returncode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Expose this as an HTTP endpoint on port 8080. Another Sprite acting as an orchestrator, or any external service can call this with code to execute.&lt;/p&gt;
&lt;h2 id='selective-network-access' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#selective-network-access' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Selective Network Access&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes code needs &lt;em&gt;some&lt;/em&gt; network access. An AI agent writing code that calls an API. A script that needs to fetch data from a specific endpoint.&lt;/p&gt;

&lt;p&gt;Allow only what&amp;rsquo;s needed:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-l1r9zl2z"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-l1r9zl2z"&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.sprites.dev/v1/sprites/&lt;span class="o"&gt;{&lt;/span&gt;sprite-id&lt;span class="o"&gt;}&lt;/span&gt;/policy/network &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$SPRITES_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "rules": [
      {"domain": "api.openai.com", "action": "allow"},
      {"domain": "api.anthropic.com", "action": "allow"},
      {"domain": "*", "action": "deny"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Now the code can call OpenAI and Anthropic. Nothing else. Can&amp;rsquo;t exfiltrate to gimmeallyourdata.com. Can&amp;rsquo;t download from sketchy-cdn.net.&lt;/p&gt;
&lt;h2 id='when-code-breaks-the-sandbox' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#when-code-breaks-the-sandbox' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;When Code Breaks the Sandbox&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In the entirely unsurprising case that something does blow up; the code did something weird to the filesystem, filled the disk, corrupted something.&lt;/p&gt;

&lt;p&gt;Sprites are disposable. Just &lt;code&gt;sprite destroy&lt;/code&gt; it and spin up a fresh one. Problem solved.&lt;/p&gt;

&lt;p&gt;Or, if you&amp;rsquo;re running lots of code and want to reuse the same Sprite for efficiency, checkpoint your working state&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-68t893vr"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-68t893vr"&gt;sprite checkpoint create
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Then restore to that working state in about a second:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-violmaxm"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-violmaxm"&gt;sprite checkpoint restore &amp;lt;working-checkpoint-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='runtimes-are-already-there' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#runtimes-are-already-there' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Runtimes Are Already There&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sprites come with Python, Node, Ruby, Rust, Go, Java, Elixir, Bun, and Deno pre-installed. Version managers let you switch between versions. Your code runner doesn&amp;rsquo;t need to install anything. Just execute.&lt;/p&gt;

&lt;p&gt;Sprites sleep when idle and wake on demand. Your code runner costs nothing when it&amp;rsquo;s not running code.&lt;/p&gt;

&lt;p&gt;Run whatever you want. Dare it try any funny business.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Make Computers Make Computers</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/make-computers-make-computers/"/>
    <id>https://fly.io/sprites-blog/make-computers-make-computers/</id>
    <published>2026-01-28T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/make-computers-make-computers/assets/mcmc.png"/>
    <content type="html">&lt;p&gt;What&amp;rsquo;s better than one Sprite?. Sprites making other Sprites.&lt;/p&gt;

&lt;p&gt;Claude (running in a Sprite) already knows how to use the Sprites API. So you can ask it to create new Sprites, configure their network policies, hit their HTTP endpoints etc. &lt;/p&gt;

&lt;p&gt;To authorise one Sprite to control others, you&amp;rsquo;ll need to visit to &lt;code&gt;sprites.dev/account/{your-org}&lt;/code&gt; and create a token. Then tell Claude to use it to authorise it&amp;rsquo;s Sprite. &lt;/p&gt;

&lt;p&gt;Here are a few of ways that the Fly team is already using this capability on the daily:&lt;/p&gt;
&lt;h2 id='central-auth-for-everything' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#central-auth-for-everything' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Central Auth for Everything&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;A Sprite that handles OAuth flows for other Sprites.&lt;/p&gt;

&lt;p&gt;Say you have five Sprites that all need to talk to GitHub, Slack, or some other OAuth-protected service. Each one needs tokens. Managing that across five separate environments is annoying.&lt;/p&gt;

&lt;p&gt;Make an Auth Sprite do it for you instead. Workers call the Auth Sprite when they need a token. The Auth Sprite refreshes tokens, handles redirects and stores creds in one place.&lt;/p&gt;
&lt;h2 id='ci-that-doesnt-share-state' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#ci-that-doesnt-share-state' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;CI That Doesn&amp;rsquo;t Share State&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Traditional CI runs tests in containers that share&amp;hellip; something. A docker daemon. A cache directory. A network namespace.&lt;/p&gt;

&lt;p&gt;With Sprites, each test run gets its own actual computer. Tell your orchestrator Sprite to spin up a worker for each test file, lock down its network to just what it needs (pypi, github), run the test, collect results, done.&lt;/p&gt;

&lt;p&gt;Each test runs in full isolation. Nothing is shared. One flaky test can&amp;rsquo;t poison another.&lt;/p&gt;
&lt;h2 id='preview-environments-per-pr' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#preview-environments-per-pr' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Preview Environments Per PR&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Every pull request gets its own Sprite. Reviewers hit a real URL with a real running app.&lt;/p&gt;

&lt;p&gt;The orchestrator watches GitHub webhooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR opened → spin up a Sprite, deploy the branch, comment the URL
&lt;/li&gt;&lt;li&gt;PR updated → redeploy to the same Sprite
&lt;/li&gt;&lt;li&gt;PR merged/closed → destroy the Sprite
&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;No resource contention between PRs. No &amp;ldquo;sorry, all the preview slots are taken.&amp;rdquo; No shared database bleeding state between reviews.&lt;/p&gt;
&lt;h2 id='agent-delegation' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#agent-delegation' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Agent Delegation&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You have an AI agent that&amp;rsquo;s good at planning but you don&amp;rsquo;t want it directly touching production. Or you have specialized agents; one that&amp;rsquo;s great at frontend work, another that knows your database schema cold.&lt;/p&gt;

&lt;p&gt;The manager agent runs in one Sprite. It breaks down tasks and delegates to worker agents in their own Sprites:&lt;/p&gt;

&lt;p&gt;Each worker has network policy locked down to only what it needs. The React agent can hit npm but not your production database. The DB agent can hit Postgres but not the internet.&lt;/p&gt;
&lt;h2 id='running-untrusted-code' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#running-untrusted-code' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Running Untrusted Code&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Someone (or something) gives you code. You need to run it. Fear increases. &lt;/p&gt;

&lt;p&gt;Tell Claude in a Sprite to create a worker with a deny-all network policy. The worker can&amp;rsquo;t talk to anything. Run the untrusted code. Grab the output. &lt;code&gt;sprite destroy&lt;/code&gt; it if it does something scary. &lt;/p&gt;

&lt;p&gt;The code can&amp;rsquo;t phone home, can&amp;rsquo;t exfiltrate data, can&amp;rsquo;t crypto-mine on your dime.&lt;/p&gt;
&lt;h2 id='team-provisioning' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#team-provisioning' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Team Provisioning&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;New developer joins. They need a Sprite with your standard tooling, your repos cloned, your services configured.&lt;/p&gt;

&lt;p&gt;One &amp;ldquo;provisioner&amp;rdquo; Sprite has a script that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a new Sprite
&lt;/li&gt;&lt;li&gt;Configures its network policy for your internal services
&lt;/li&gt;&lt;li&gt;Clones your repos
&lt;/li&gt;&lt;li&gt;Installs your standard dependencies
&lt;/li&gt;&lt;li&gt;Creates a checkpoint (so they can always restore to &amp;ldquo;fresh&amp;rdquo;)
&lt;/li&gt;&lt;li&gt;Sends them the URL
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;Onboarding is one API call.&lt;/p&gt;
&lt;h2 id='bonus-level' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#bonus-level' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Bonus Level&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Add checkpoints to these workflows. Your controller can checkpoint a functioning worker, then restore to that checkpoint before each job. Immutable infrastructure, but fast.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Slack Bots on Sprites: OAuth is Easy Now</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/slack-bots-on-sprites/"/>
    <id>https://fly.io/sprites-blog/slack-bots-on-sprites/</id>
    <published>2026-01-27T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/slack-bots-on-sprites/assets/slack_bots.webp"/>
    <content type="html">&lt;p&gt;Slack bots are handy, but they typically require OAuth, which means you need a server with a public URL to handle callbacks. This is annoying. But with Sprites, you get an instant internet-accessible environment with a persistent filesystem that makes it trivial.&lt;/p&gt;
&lt;h2 id='oauth-pain' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#oauth-pain' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;OAuth Pain&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Getting your Slack bot working involves jumping through these OAuth hurdles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an endpoint to receive Slack&amp;rsquo;s authorization callback
&lt;/li&gt;&lt;li&gt;Get that endpoint online with a public URL
&lt;/li&gt;&lt;li&gt;Securely store tokens after the callback
&lt;/li&gt;&lt;li&gt;Maintain those tokens across restarts
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;This requires deploying something to a host, setting up Ngrok and implementing a storage solution for tokens.&lt;/p&gt;
&lt;h2 id='slack-bot-sprites' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#slack-bot-sprites' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Slack Bot Sprites&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Create a Sprite for your bot:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-wvr8yx7b"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-wvr8yx7b"&gt;sprite create slack-bot
sprite console &lt;span class="nt"&gt;-s&lt;/span&gt; slack-bot
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Install your dependencies:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-hafay8ig"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-hafay8ig"&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @slack/bolt express
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Build an OAuth Handler&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now we need a server that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Handles the OAuth callback from Slack
&lt;/li&gt;&lt;li&gt;Stores access tokens to the filesystem
&lt;/li&gt;&lt;li&gt;Loads those tokens when the server restarts
&lt;/li&gt;&lt;li&gt;Implements your bot&amp;rsquo;s functionality
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Ask your code assistant to:&lt;/strong&gt; &lt;em&gt;&amp;ldquo;Create a server.js file that handles Slack OAuth callbacks using the Bolt framework, stores tokens to the filesystem, and implements a simple slash command. The server should run on port 8080.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Register Your Slack Bot as a Sprite Service&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sprites don&amp;rsquo;t use systemd or traditional init systems. Instead, they have their own service manager that enables the Sprite&amp;rsquo;s automatic sleep/wake behavior.&lt;/p&gt;

&lt;p&gt;So you&amp;rsquo;ll want to register your bot as a service with the HTTP port flag:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-tm8sguzs"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-tm8sguzs"&gt;sprite-env services create &lt;span class="nt"&gt;--name&lt;/span&gt; slack-bot &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"node server.js"&lt;/span&gt; &lt;span class="nt"&gt;--http_port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This does three critical things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Auto-start on HTTP requests&lt;/strong&gt;: When a request hits your Sprite URL, the Sprite wakes from hibernation
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Auto-route requests:&lt;/strong&gt; The Sprite proxy routes incoming traffic to your specified port (8080)
&lt;/li&gt;&lt;li&gt;&lt;strong class='font-semibold text-navy-950'&gt;Persistent registration&lt;/strong&gt;: Your service registration survives Sprite restarts and hibernation
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;When idle, your Sprite hibernates to save costs and wakes in under a second when Slack sends events or users visit the install URL.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Get Your Public URL&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-mi51y2m8"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-mi51y2m8"&gt;sprite url update &lt;span class="nt"&gt;--auth&lt;/span&gt; public &lt;span class="nt"&gt;-s&lt;/span&gt; slack-bot
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Configure Your Slack App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rest is standard Slack stuff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://api.slack.com/apps"&gt;https://api.slack.com/apps&lt;/a&gt;
&lt;/li&gt;&lt;li&gt;Create a new app
&lt;/li&gt;&lt;li&gt;Under &amp;ldquo;OAuth &amp;amp; Permissions&amp;rdquo;, add your Sprite&amp;rsquo;s URL
&lt;/li&gt;&lt;li&gt;Copy your Client ID and Client Secret
&lt;/li&gt;&lt;li&gt;Set them as environment variables in your Sprite:
&lt;/li&gt;&lt;/ol&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-vfrcecpi"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-vfrcecpi"&gt;sprite-env &lt;span class="nb"&gt;env set &lt;/span&gt;&lt;span class="nv"&gt;SLACK_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_client_id
sprite-env &lt;span class="nb"&gt;env set &lt;/span&gt;&lt;span class="nv"&gt;SLACK_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_client_secret
sprite-env &lt;span class="nb"&gt;env set &lt;/span&gt;&lt;span class="nv"&gt;SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_signing_secret
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Install Your Bot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Visit: &lt;a href="https://your-slack-bot.sprites.app/slack/install"&gt;https://your-slack-bot.sprites.app/slack/install&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This triggers the OAuth flow. Slack redirects back to your Sprite, which handles the callback and stores the token in its filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Checkpoint Your Working Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once everything is working, create a checkpoint:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-qfnfrmm1"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-qfnfrmm1"&gt;sprite checkpoint create
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A Sprite checkpoint is a snapshot of the entire environment, not just your code. So if/when you break your bot, just:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-k8wy5ttw"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-k8wy5ttw"&gt;sprite restore &amp;lt;checkpoint-id&amp;gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='more-time-for-cheeky-slack-messages' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#more-time-for-cheeky-slack-messages' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;More Time for Cheeky Slack Messages&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Building a Slack bot in a Sprite means no longer having to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up and maintain the server
&lt;/li&gt;&lt;li&gt;Configure networking and SSL
&lt;/li&gt;&lt;li&gt;Implement your own hibernation/wake logic (or pay for idle time)
&lt;/li&gt;&lt;li&gt;Handle security updates and monitoring
&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Instead, write your bot, deploy it, forget about it. The Sprite wakes up when Slack calls, sleeps when it doesn&amp;rsquo;t, and you only pay for the moments it&amp;rsquo;s actually working.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Run Moltbot (Clawdbot) in a Sprite</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/moltbot-in-a-sprite/"/>
    <id>https://fly.io/sprites-blog/moltbot-in-a-sprite/</id>
    <published>2026-01-26T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/moltbot-in-a-sprite/assets/moltbot.webp"/>
    <content type="html">&lt;h2 id='running-moltbot-on-a-sprite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#running-moltbot-on-a-sprite' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Running Moltbot on a Sprite&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s zero chance this will be the first time you&amp;rsquo;re hearing about Clawdbot, recently renamed Moltbot. It&amp;rsquo;s an open-source AI assistant that connects to messaging platforms you&amp;rsquo;re probably already use: WhatsApp, Telegram, Slack, Discord, Signal, etc. It comes with a fast-growing list of connectors and Skills and self-iterating capabilities, right out of the box.&lt;/p&gt;

&lt;p&gt;If you really want to see what a project like Moltbot can do, you need to run it with generous permissions and give it access to its environment. That is inherently scary, which is why so many people have been snapping up Mac Minis to run their Moltbot in an isolated environment.&lt;/p&gt;

&lt;p&gt;You don&amp;rsquo;t need a Mac Mini. Instead, let&amp;rsquo;s learn how to run Moltbot in a Sprite. A stateful sandbox with automatic sleep/wake behaviour that keeps running costs low. Sprites are fully hardware isolated Linux computers, so you can give Moltbot total agency and not even have to think about building your own guardrails.&lt;/p&gt;
&lt;h2 id='create-a-sprite' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#create-a-sprite' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Create a Sprite&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Install the Sprites CLI and create a Sprite:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-k326c3v6"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-k326c3v6"&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://sprites.dev/install.sh | bash
sprite create moltbot
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Connect to the Sprite console:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-9qlkgkr"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-9qlkgkr"&gt;sprite console &lt;span class="nt"&gt;-s&lt;/span&gt; moltbot
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='install-and-configure-moltbot' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#install-and-configure-moltbot' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Install and Configure Moltbot&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Run the Moltbot install script:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-hg83kwlt"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-hg83kwlt"&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://molt.bot/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The wizard will configure your credentials, hooks, and messaging platforms. You&amp;rsquo;ll see two expected errors at the end:&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Systemd unavailable&lt;/strong&gt; — Sprites don&amp;rsquo;t use systemd&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Health check failed&lt;/strong&gt; — The Gateway isn&amp;rsquo;t running yet&lt;/p&gt;

&lt;p&gt;This is normal. The onboarding creates your config. We&amp;rsquo;ll start the Gateway next.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Important:&lt;/strong&gt; Copy the token shown in Moltbot&amp;rsquo;s output. It looks like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;9054a0cfca3f5be5325f988fb57ddf1f38e530d2883bbed7&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add moltbot to your PATH so it&amp;rsquo;s available in future sessions:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-jlm6262f"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-jlm6262f"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export PATH="$PATH:/.sprite/languages/node/nvm/versions/node/v22.20.0/bin"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='register-moltbot-as-a-sprite-service' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#register-moltbot-as-a-sprite-service' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Register Moltbot as a Sprite Service&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sprites have their own service manager that auto-starts services when HTTP requests arrive. Register the Gateway:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-tup0x1bp"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-tup0x1bp"&gt; sprite-env services create moltbot-gateway &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;which clawdbot&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--args&lt;/span&gt; &lt;span class="s2"&gt;"gateway"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--http-port&lt;/span&gt; 18789
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Note: The Moltbot CLI still installs as clawdbot, but its docs have already been updated to use &amp;quot;moltbot&amp;quot; commands. You can create a symlink after installing so that docs are easier to follow.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This registers the Gateway and routes your Sprite URL to port 18789. When requests hit your Sprite URL, the Sprite wakes and the Gateway starts automatically.&lt;/p&gt;
&lt;h2 id='make-the-url-public-optional' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#make-the-url-public-optional' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Make the URL Public (Optional)&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;By default, Sprite URLs require authentication. To make yours public and get the url:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-ldw6aiha"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-ldw6aiha"&gt;sprite url update &lt;span class="nt"&gt;--auth&lt;/span&gt; public &lt;span class="nt"&gt;-s&lt;/span&gt; moltbot
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='access-the-dashboard' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#access-the-dashboard' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Access the Dashboard&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;For initial setup, access your Sprite URL + the token from onboarding:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://yoursprite.sprites.app/?token=YOUR_TOKEN_HERE&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You&amp;rsquo;ll see a &amp;ldquo;pairing required&amp;rdquo; message. This is Moltbot&amp;rsquo;s security. It requires you to approve new devices before they can connect.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;## Approve Your Device&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Back in the Sprite console, list pending pairing requests:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-8wnjcoq6"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-8wnjcoq6"&gt;moltbot devices list
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Approve the pending request:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-6wmnjzdn"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-6wmnjzdn"&gt;moltbot devices approve REQUEST_ID
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Refresh your browser. You&amp;rsquo;re in.&lt;/p&gt;
&lt;h2 id='checkpoint-your-setup' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#checkpoint-your-setup' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Checkpoint Your Setup&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Checkpoint your working configuration:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-p6h0ggxh"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-p6h0ggxh"&gt;sprite checkpoint create
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you ever need to return to this state:&lt;/p&gt;
&lt;div class="highlight-wrapper group relative bash"&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-9 -mr-0.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-wrap-target="#code-bxcyiqso"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M9.912 8.037h2.732c1.277 0 2.315-.962 2.315-2.237a2.325 2.325 0 00-2.315-2.31H2.959m10.228 9.01H2.959M6.802 8H2.959" /&gt;&lt;path d="M11.081 6.466L9.533 8.037l1.548 1.571" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-9px] tail text-navy-950"&gt;
      Wrap text
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;button 
    type="button"
    class="bubble-wrap z-20 absolute right-1.5 top-1.5 text-transparent group-hover:text-gray-400 group-hover:hocus:text-white focus:text-white bg-transparent group-hover:bg-gray-900 group-hover:hocus:bg-gray-700 focus:bg-gray-700 transition-colors grid place-items-center w-7 h-7 rounded-lg outline-none focus:outline-none"
    data-copy-target="sibling"
  &gt;
    &lt;svg class="w-4 h-4 pointer-events-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.35"&gt;&lt;g buffered-rendering="static"&gt;&lt;path d="M10.576 7.239c0-.995-.82-1.815-1.815-1.815H3.315c-.995 0-1.815.82-1.815 1.815v5.446c0 .995.82 1.815 1.815 1.815h5.446c.995 0 1.815-.82 1.815-1.815V7.239z" /&gt;&lt;path d="M10.576 10.577h2.109A1.825 1.825 0 0014.5 8.761V3.315A1.826 1.826 0 0012.685 1.5H7.239c-.996 0-1.815.819-1.816 1.815v1.617" /&gt;&lt;/g&gt;&lt;/svg&gt;
    &lt;span class="bubble-sm bubble-tl [--offset-l:-6px] tail [--tail-x:calc(100%-30px)] text-navy-950"&gt;
      Copy to clipboard
    &lt;/span&gt;
  &lt;/button&gt;
  &lt;div class='highlight relative group'&gt;
    &lt;pre class='highlight '&gt;&lt;code id="code-bxcyiqso"&gt;sprite restore &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;h2 id='what-youve-built' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-youve-built' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;What You&amp;rsquo;ve Built&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Your Moltbot Gateway now lives on a Sprite that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sleeps when idle
&lt;/li&gt;&lt;li&gt;Wakes automatically when you hit the URL
&lt;/li&gt;&lt;li&gt;Restarts the Gateway service on wake
&lt;/li&gt;&lt;li&gt;Persists your config and pairing across restarts
&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Once paired, you can access the dashboard and start chatting with Moltbot. It&amp;rsquo;s not bad at understanding its own capabilities, so just ask for the integrations and channels you want to set up.&lt;/p&gt;
&lt;h2 id='bonus-level' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#bonus-level' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Bonus Level:&lt;/span&gt;&lt;/h2&gt;&lt;hr class='-mt-4 mb-10'&gt;
&lt;p&gt;Tell Moltbot to build a Sprites Skill for itself, so it knows how to manage its own environment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a Sprite skill by reading the documentation at /.sprite/llm.txt and /.sprite/docs/agent-context.md. The skill should teach you how to manage Sprite services (since there&amp;rsquo;s no systemd), create checkpoints, restart the moltbot-gateway, and check network policy. Save it to ~/.moltbot/skills/sprite/SKILL.md.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  <entry>
    <title>How Sprites Accidentally Solved Mobile Development</title>
    <link rel="alternate" href="https://fly.io/sprites-blog/how-sprites-accidentally-solved-mobile-development/"/>
    <id>https://fly.io/sprites-blog/how-sprites-accidentally-solved-mobile-development/</id>
    <published>2026-01-21T00:00:00+00:00</published>
    <updated>2026-07-07T11:34:54+00:00</updated>
    <media:thumbnail url="https://fly.io/sprites-blog/how-sprites-accidentally-solved-mobile-development/assets/sprite-mobile.webp"/>
    <content type="html">&lt;p&gt;A few days ago, &lt;a href='https://news.ycombinator.com/item?id=46517458' title=''&gt;this post &lt;/a&gt;hit HN&amp;rsquo;s front page and generated over 400 comments. The author is talking about the setup he uses to code from his phone using Tailscale, an always-on Mac at home, SSH via Termius, and Claude Code in the terminal.&lt;/p&gt;

&lt;p&gt;If you&amp;rsquo;re asking why anyone would want to code from their phone, you can safely stop reading now and return to a peaceful existence, where ideas only come to you when you&amp;rsquo;re sitting behind a computer. For those familiar with the need to try an idea right now, wherever you are, whatever you&amp;rsquo;re doing, you&amp;rsquo;re in the right place.&lt;/p&gt;
&lt;h2 id='what-was-and-need-no-longer-be' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#what-was-and-need-no-longer-be' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;What Was and Need No Longer Be&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The HN post&amp;rsquo;s comments section showed off some genuinely impressive mobile coding setups that range from the creative to the absurdly over-engineered. Tmux sessions, VPS boxes, all the SSH clients, and a million ways to access these env&amp;rsquo;s from your phone.&lt;/p&gt;

&lt;p&gt;For a lot of people, designing setups like this is fun. But if managing always-on servers and VPN configurations is less exciting to you than building an app in real time while brainstorming it on a hiking trail, there&amp;rsquo;s a better way.  We call &amp;lsquo;em Sprites.&lt;/p&gt;
&lt;h2 id='whats-a-sprite-again' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#whats-a-sprite-again' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;What&amp;rsquo;s a Sprite Again?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The correct answer is: Whatever you want it to be. But that isn&amp;rsquo;t super helpful in this context. So let&amp;rsquo;s start with the idea of a Sprite as the server in your mobile coding setup. Instead of running a machine 24/7 or starting and stopping a VM, use a Sprite. It&amp;rsquo;s a full Linux computer that spins up in under a second and runs only when you need it.&lt;/p&gt;

&lt;p&gt;Your code, installed packages, and project state persist between sessions, even when the Sprite is suspended. You (or Claude) can checkpoint your Sprite at any time, which creates a restorable snapshot of the entire environment, not just your code. And since it&amp;rsquo;s a hardware-isolated computer rather than a shared container, you can run literally any code and experiment aggressively, while genuinely just not caring about breaking anything important.&lt;/p&gt;

&lt;p&gt;If you want to unleash your code assistant, give it full file system access and tell it to build that genius app idea that occurs to you at 3am without getting out of bed, you wanna do that in a Sprite.&lt;/p&gt;

&lt;p&gt;But wait, how are you doing this from your phone?&lt;/p&gt;
&lt;h2 id='sleep-is-for-the-weak' class='group flex items-start whitespace-pre-wrap relative mt-14 sm:mt-16 mb-4 text-navy-950 font-heading'&gt;&lt;a class='inline-block align-text-top relative top-[.15em] w-6 h-6 -ml-6 after:hash opacity-0 group-hover:opacity-100 transition-all' href='#sleep-is-for-the-weak' aria-label='Anchor'&gt;&lt;/a&gt;&lt;span class='plain-code'&gt;Sleep Is For the Weak&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We are blessed with multiple really smart guys called Chris at Fly.io. And one of them, a Christopher actually, has built a - dare I say - elegant way for you to issue orders to Claude in a Sprite from your phone. The aptly named &lt;a href='https://github.com/clouvet/sprite-mobile' title=''&gt;sprite-mobile&lt;/a&gt;, a chat UI for accessing Claude Code running in YOLO mode on a sprite.&lt;/p&gt;

&lt;p&gt;&lt;strong class='font-semibold text-navy-950'&gt;Here&amp;rsquo;s how to set it up:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Sprite
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;sprite create cheese_sandwich&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download and run the Sprite-mobile install script
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;curl -fsSL https://raw.githubusercontent.com/clouvet/sprite-mobile/refs/heads/main/scripts/sprite-setup.sh -o sprite-setup.sh &amp;amp;&amp;amp; chmod +x sprite-setup.sh &amp;amp;&amp;amp; ./sprite-setup.sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I shit you not. That is it.&lt;/p&gt;

&lt;p&gt;The script will speedrun you through everything needed to get up and running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure your sprite and git user
&lt;/li&gt;&lt;li&gt;Authenticate Claude CLI, GitHub CLI, Fly.io CLI, Sprites CLI
&lt;/li&gt;&lt;li&gt;Install and configure Tailscale
&lt;/li&gt;&lt;li&gt;Clone and run sprite-mobile and configure a Tailscale-based Sprite network (if you want automatic discovery of other sprites in your org)
&lt;/li&gt;&lt;li&gt;Set up Tailscale Serve (HTTPS for PWA support)
&lt;/li&gt;&lt;li&gt;Start the Tailnet Gate (public entry point to wake up the sprite if it&amp;rsquo;s suspended)
&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;To access this Claude-powered dev sandbox you just spun up in the same time it takes to make a midnight cheese sandwich, you&amp;rsquo;ll want to get its public url:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sprite url update --auth public -s cheese_sandwich&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Drop that in your Tailscale-connected phone&amp;rsquo;s browser and you&amp;rsquo;re cooking.&lt;/p&gt;

&lt;p&gt;Your Sprites automatically go to sleep after 30s of inactivity and wake up faster than a cat when you open a can of tuna a mile away (sub 1s). And you only pay for the resources your Sprites are actively using. Got 30 projects in 30 Sprites waiting for you to decide when you&amp;rsquo;re interested in them again? Cool. No charge.&lt;/p&gt;

&lt;p&gt;Why are you still here? Check out the full details in the &lt;a href='https://github.com/clouvet/sprite-mobile' title=''&gt;sprite-mobile repo&lt;/a&gt; and read more about Sprites at &lt;a href='https://sprites.dev/' title=''&gt;sprites.dev&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
</feed>
