<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-03-13T14:25:22+00:00</updated><id>/feed.xml</id><title type="html">Amin’s Blog</title><subtitle>Blog posts on software development</subtitle><entry><title type="html">Python REPL in Production</title><link href="/devops/2026/03/13/python-repl-in-production.html" rel="alternate" type="text/html" title="Python REPL in Production" /><published>2026-03-13T00:00:00+00:00</published><updated>2026-03-13T00:00:00+00:00</updated><id>/devops/2026/03/13/python-repl-in-production</id><content type="html" xml:base="/devops/2026/03/13/python-repl-in-production.html"><![CDATA[<p>I found myself needing the <code class="language-plaintext highlighter-rouge">python</code> REPL in production, with access to the real config and objects of a running Flask application.
Here is a minimal approach using the builtin <code class="language-plaintext highlighter-rouge">code.InteractiveConsole</code> to expose the REPL over a Unix socket.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># repl_socket.py
</span><span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>

<span class="kn">import</span> <span class="nn">code</span>
<span class="kn">import</span> <span class="nn">io</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">socketserver</span>
<span class="kn">import</span> <span class="nn">threading</span>
<span class="kn">from</span> <span class="nn">contextlib</span> <span class="kn">import</span> <span class="n">redirect_stderr</span><span class="p">,</span> <span class="n">redirect_stdout</span><span class="p">,</span> <span class="n">suppress</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span>


<span class="k">def</span> <span class="nf">start_repl_socket</span><span class="p">(</span>
    <span class="n">app</span><span class="p">:</span> <span class="n">Any</span><span class="p">,</span>
    <span class="o">*</span><span class="p">,</span>
    <span class="n">namespace</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">],</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">class</span> <span class="nc">Handler</span><span class="p">(</span><span class="n">socketserver</span><span class="p">.</span><span class="n">StreamRequestHandler</span><span class="p">):</span>
        <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">locals_</span> <span class="o">=</span> <span class="p">{</span><span class="o">**</span><span class="n">namespace</span><span class="p">,</span> <span class="s">"app"</span><span class="p">:</span> <span class="n">app</span><span class="p">}</span>

            <span class="n">text_rfile</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="n">TextIOWrapper</span><span class="p">(</span>
                <span class="bp">self</span><span class="p">.</span><span class="n">rfile</span><span class="p">,</span>
                <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">,</span>
                <span class="n">errors</span><span class="o">=</span><span class="s">"replace"</span><span class="p">,</span>
                <span class="n">newline</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="n">text_wfile</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="n">TextIOWrapper</span><span class="p">(</span>
                <span class="bp">self</span><span class="p">.</span><span class="n">wfile</span><span class="p">,</span>
                <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">,</span>
                <span class="n">line_buffering</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
                <span class="n">write_through</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
            <span class="p">)</span>

            <span class="k">with</span> <span class="n">suppress</span><span class="p">(</span><span class="nb">Exception</span><span class="p">):</span>
                <span class="k">with</span> <span class="p">(</span>
                    <span class="n">app</span><span class="p">.</span><span class="n">app_context</span><span class="p">(),</span>
                    <span class="n">redirect_stdout</span><span class="p">(</span><span class="n">text_wfile</span><span class="p">),</span>
                    <span class="n">redirect_stderr</span><span class="p">(</span><span class="n">text_wfile</span><span class="p">),</span>
                <span class="p">):</span>
                    <span class="n">console</span> <span class="o">=</span> <span class="n">code</span><span class="p">.</span><span class="n">InteractiveConsole</span><span class="p">(</span><span class="nb">locals</span><span class="o">=</span><span class="n">locals_</span><span class="p">)</span>

                    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">app</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="s"> REPL (pid=</span><span class="si">{</span><span class="n">os</span><span class="p">.</span><span class="n">getpid</span><span class="p">()</span><span class="si">}</span><span class="s">)</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>

                    <span class="n">more</span> <span class="o">=</span> <span class="bp">False</span>
                    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
                        <span class="n">text_wfile</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">"... "</span> <span class="k">if</span> <span class="n">more</span> <span class="k">else</span> <span class="s">"&gt;&gt;&gt; "</span><span class="p">)</span>
                        <span class="n">text_wfile</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span>

                        <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">line</span> <span class="p">:</span><span class="o">=</span> <span class="n">text_rfile</span><span class="p">.</span><span class="n">readline</span><span class="p">()):</span>
                            <span class="k">break</span>

                        <span class="n">more</span> <span class="o">=</span> <span class="n">console</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s">"</span><span class="se">\r\n</span><span class="s">"</span><span class="p">))</span>

    <span class="n">socket_path</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"/tmp/api-repl.</span><span class="si">{</span><span class="n">os</span><span class="p">.</span><span class="n">getpid</span><span class="p">()</span><span class="si">}</span><span class="s">.sock"</span>
    <span class="n">server</span> <span class="o">=</span> <span class="n">socketserver</span><span class="p">.</span><span class="n">ThreadingUnixStreamServer</span><span class="p">(</span><span class="n">socket_path</span><span class="p">,</span> <span class="n">Handler</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">chmod</span><span class="p">(</span><span class="n">socket_path</span><span class="p">,</span> <span class="mo">0o600</span><span class="p">)</span>

    <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">server</span><span class="p">.</span><span class="n">serve_forever</span><span class="p">,</span> <span class="n">daemon</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="n">start</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">socket_path</span>
</code></pre></div></div>

<p>The nice thing about <code class="language-plaintext highlighter-rouge">console.push(...)</code> is that it behaves like a real Python REPL:</p>

<ul>
  <li>multi-line blocks work</li>
  <li>bare expressions print their values</li>
  <li>tracebacks show up correctly</li>
</ul>

<p>The rest is just plumbing:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">TextIOWrapper</code> turns the socket into normal text streams</li>
  <li><code class="language-plaintext highlighter-rouge">redirect_stdout(...)</code> and <code class="language-plaintext highlighter-rouge">redirect_stderr(...)</code> make <code class="language-plaintext highlighter-rouge">print()</code> and errors show up in the client</li>
  <li><code class="language-plaintext highlighter-rouge">app.app_context()</code> makes Flask globals such as <code class="language-plaintext highlighter-rouge">current_app</code> work immediately</li>
</ul>

<p>Then wire it into your app and expose whatever objects you want in the session:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># main.py
</span><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>

<span class="kn">from</span> <span class="nn">repl_socket</span> <span class="kn">import</span> <span class="n">start_repl_socket</span>


<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">healthcheck</span><span class="p">():</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"status"</span><span class="p">:</span> <span class="s">"ok"</span><span class="p">}</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">socket_path</span> <span class="o">=</span> <span class="n">start_repl_socket</span><span class="p">(</span>
        <span class="n">app</span><span class="p">,</span>
        <span class="n">namespace</span><span class="o">=</span><span class="p">{</span><span class="s">"config"</span><span class="p">:</span> <span class="n">app</span><span class="p">.</span><span class="n">config</span><span class="p">,</span> <span class="s">"db"</span><span class="p">:</span> <span class="bp">None</span><span class="p">},</span>
    <span class="p">)</span>
    <span class="k">if</span> <span class="n">socket_path</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"production REPL listening on </span><span class="si">{</span><span class="n">socket_path</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</code></pre></div></div>

<p>Start the app, then attach with <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run python main.py
nc <span class="nt">-U</span> /tmp/api-repl.&lt;pid&gt;.sock
</code></pre></div></div>

<p>An example session looks like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">main</span> <span class="n">REPL</span> <span class="p">(</span><span class="n">pid</span><span class="o">=</span><span class="mi">63503</span><span class="p">)</span>

<span class="o">&gt;&gt;&gt;</span> <span class="mi">1</span> <span class="o">+</span> <span class="mi">1</span>
<span class="mi">2</span>
<span class="o">&gt;&gt;&gt;</span> <span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">current_app</span>
<span class="o">&gt;&gt;&gt;</span> <span class="n">current_app</span><span class="p">.</span><span class="n">name</span>
<span class="s">'main'</span>
<span class="o">&gt;&gt;&gt;</span> <span class="k">with</span> <span class="n">app</span><span class="p">.</span><span class="n">test_request_context</span><span class="p">(</span><span class="s">"/"</span><span class="p">):</span>
<span class="p">...</span>     <span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">request</span>
<span class="p">...</span>     <span class="k">print</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">path</span><span class="p">)</span>
<span class="p">...</span>
<span class="o">/</span>
</code></pre></div></div>

<p>A few nice details:</p>

<ul>
  <li>the transport is local-only; no TCP debug port needed</li>
  <li><code class="language-plaintext highlighter-rouge">chmod 600</code> keeps the socket owner-only</li>
  <li>each worker process gets its own <code class="language-plaintext highlighter-rouge">/tmp/api-repl.&lt;pid&gt;.sock</code></li>
</ul>

<p>For request-bound objects such as <code class="language-plaintext highlighter-rouge">request</code> or <code class="language-plaintext highlighter-rouge">session</code>, create a request context manually:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">app</span><span class="p">.</span><span class="n">test_request_context</span><span class="p">(</span><span class="s">"/"</span><span class="p">):</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>That is it. No extra dependency, and no need to build your own <code class="language-plaintext highlighter-rouge">eval()</code> loop.</p>]]></content><author><name></name></author><category term="devops" /><summary type="html"><![CDATA[I found myself needing the python REPL in production, with access to the real config and objects of a running Flask application. Here is a minimal approach using the builtin code.InteractiveConsole to expose the REPL over a Unix socket.]]></summary></entry><entry><title type="html">Sandboxing opencode on macOS</title><link href="/tools/llm/security/macos/2026/02/25/sandboxing-opencode-on-macos.html" rel="alternate" type="text/html" title="Sandboxing opencode on macOS" /><published>2026-02-25T00:00:00+00:00</published><updated>2026-02-25T00:00:00+00:00</updated><id>/tools/llm/security/macos/2026/02/25/sandboxing-opencode-on-macos</id><content type="html" xml:base="/tools/llm/security/macos/2026/02/25/sandboxing-opencode-on-macos.html"><![CDATA[<p>This post shows how I run the <code class="language-plaintext highlighter-rouge">opencode</code> CLI inside a macOS sandbox using <a href="https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf">sandbox-exec</a>, with a tight file/process allowlist and a clean environment.</p>

<p>Repo (the exact files I use): <a href="https://github.com/aminroosta/opencode-sandbox">github.com/aminroosta/opencode-sandbox</a></p>
<ul>
  <li>deny home directory reads/writes by default.</li>
  <li>deny <code class="language-plaintext highlighter-rouge">process-exec</code> by default.</li>
  <li>start with an empty environment (<code class="language-plaintext highlighter-rouge">env -i</code>).</li>
</ul>

<p>There are three layers:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">opencode-sandbox</code> (a tiny wrapper script)</li>
  <li><code class="language-plaintext highlighter-rouge">opencode-dev-only.sb</code> (a sandbox profile)</li>
  <li><code class="language-plaintext highlighter-rouge">~/.config/opencode/opencode.json</code> (OpenCode config defaults)</li>
</ol>

<h2 id="the-wrapper-script">The Wrapper Script</h2>

<p>The wrapper does two important things:</p>

<ul>
  <li>runs OpenCode under <code class="language-plaintext highlighter-rouge">sandbox-exec -f opencode-dev-only.sb</code></li>
  <li>clears the environment with <code class="language-plaintext highlighter-rouge">/usr/bin/env -i</code> and opts out of some features via <code class="language-plaintext highlighter-rouge">OPENCODE_DISABLE_*</code></li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nv">SCRIPT_DIR</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">cd</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">dirname</span> <span class="si">$(</span><span class="nb">realpath</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">))</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span>
<span class="nv">PROFILE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$SCRIPT_DIR</span><span class="s2">/opencode-dev-only.sb"</span>

<span class="nv">OPENCODE_BIN</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">command</span> <span class="nt">-v</span> opencode <span class="o">||</span> <span class="nb">true</span><span class="si">)</span><span class="s2">"</span>

<span class="nb">exec</span> /usr/bin/env <span class="nt">-i</span> <span class="se">\</span>
  <span class="nv">HOME</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">HOME</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">PATH</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">EDITOR</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">EDITOR</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">VISUAL</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">VISUAL</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">TERM</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">TERM</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">TMPDIR</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">TMPDIR</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">COLORTERM</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">COLORTERM</span><span class="k">:-}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_AUTOCOMPACT</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_PRUNE</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_LSP_DOWNLOAD</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_DEFAULT_PLUGINS</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_MODELS_FETCH</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_SHARE</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_CLAUDE_CODE_PROMPT</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_CLAUDE_CODE_SKILLS</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_EXTERNAL_SKILLS</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_TERMINAL_TITLE</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  <span class="nv">OPENCODE_DISABLE_FILETIME_CHECK</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
  /usr/bin/sandbox-exec <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$PROFILE</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$OPENCODE_BIN</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>

  <span class="c"># If your OpenCode auth relies on environment variables (for example `OPENAI_API_KEY`), you need to explicitly forward them too.</span>
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">OPENCODE_DISABLE_*</code> list is intentional: fewer downloads and fewer background features means fewer sandbox allowances (and fewer surprises).</p>

<h2 id="the-sandbox-profile">The Sandbox Profile</h2>

<p>Two important rules:</p>

<ol>
  <li><strong>Deny writes globally</strong> (then allow writes only where needed)</li>
  <li><strong>Deny reads of your home directory</strong> (then allow reads only where needed)</li>
</ol>

<p>Key lines (trimmed):</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">allow</span> <span class="nv">default</span><span class="p">)</span>

<span class="c1">; Put broad denies first, then carve out explicit allows.</span>
<span class="p">(</span><span class="nv">deny</span> <span class="nv">file-write*</span><span class="p">)</span>
<span class="p">(</span><span class="nv">deny</span> <span class="nv">file-read*</span> <span class="p">(</span><span class="nv">subpath</span> <span class="s">"/Users/&lt;you&gt;"</span><span class="p">))</span>

<span class="c1">; Default-deny subprocess execution.</span>
<span class="p">(</span><span class="nv">deny</span> <span class="nv">process-exec</span><span class="p">)</span>

<span class="c1">; Allow system binaries + your toolchain.</span>
<span class="p">(</span><span class="nv">allow</span> <span class="nv">process-exec</span> <span class="p">(</span><span class="nv">subpath</span> <span class="s">"/bin"</span><span class="p">))</span>
<span class="p">(</span><span class="nv">allow</span> <span class="nv">process-exec</span> <span class="p">(</span><span class="nv">subpath</span> <span class="s">"/usr/bin"</span><span class="p">))</span>
<span class="p">(</span><span class="nv">allow</span> <span class="nv">process-exec</span> <span class="p">(</span><span class="nv">subpath</span> <span class="s">"/opt/homebrew"</span><span class="p">))</span>

<span class="c1">; Re-block high-risk launcher helpers.</span>
<span class="p">(</span><span class="nv">deny</span> <span class="nv">process-exec</span> <span class="p">(</span><span class="nv">literal</span> <span class="s">"/bin/launchctl"</span><span class="p">))</span>
<span class="p">(</span><span class="nv">deny</span> <span class="nv">process-exec</span> <span class="p">(</span><span class="nv">literal</span> <span class="s">"/usr/bin/open"</span><span class="p">))</span>
<span class="p">(</span><span class="nv">deny</span> <span class="nv">process-exec</span> <span class="p">(</span><span class="nv">literal</span> <span class="s">"/usr/bin/osascript"</span><span class="p">))</span>

<span class="c1">; Workspace: allow read/write only here.</span>
<span class="p">(</span><span class="nv">allow</span> <span class="nv">file-read*</span> <span class="p">(</span><span class="nv">subpath</span> <span class="s">"/Users/&lt;you&gt;/dev"</span><span class="p">))</span>
<span class="p">(</span><span class="nv">allow</span> <span class="nv">file-write*</span> <span class="p">(</span><span class="nv">subpath</span> <span class="s">"/Users/&lt;you&gt;/dev"</span><span class="p">))</span>
</code></pre></div></div>

<p>The full profile in the repo includes the practical stuff you tend to need in a real dev session:</p>

<ul>
  <li>temp dirs: <code class="language-plaintext highlighter-rouge">/private/tmp</code>, <code class="language-plaintext highlighter-rouge">/private/var/folders</code></li>
  <li>OpenCode state: <code class="language-plaintext highlighter-rouge">~/.config/opencode</code>, <code class="language-plaintext highlighter-rouge">~/.local/share/opencode</code>, <code class="language-plaintext highlighter-rouge">~/.cache/opencode</code>, etc.</li>
  <li>tool managers: <code class="language-plaintext highlighter-rouge">mise</code>, <code class="language-plaintext highlighter-rouge">uv</code>, language runtimes, homebrew</li>
  <li>minimal metadata reads (<code class="language-plaintext highlighter-rouge">file-read-metadata</code>) so path traversal works</li>
</ul>

<h2 id="recommended-opencode-settings">Recommended OpenCode Settings</h2>

<p>The OS sandbox is the hard boundary.<br />
OpenCode config is the “seatbelt” that still helps even when you forget to use the wrapper.</p>

<p>From <code class="language-plaintext highlighter-rouge">~/.config/opencode/opencode.json</code>, the settings I recommend for a sandboxed setup:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"$schema"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://opencode.ai/config.json"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"share"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"autoupdate"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"server"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"mdns"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="p">},</span><span class="w">

  </span><span class="nl">"lsp"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"formatter"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"snapshot"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"disabled_providers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"opencode"</span><span class="p">],</span><span class="w">

  </span><span class="nl">"permission"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"*"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ask"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"external_directory"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"*"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ask"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"$HOME/dev/*"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allow"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"/tmp/*"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allow"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">

  </span><span class="nl">"agent"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
     </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
       </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"primary"</span><span class="p">,</span><span class="w">
       </span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
         </span><span class="nl">"*"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
         </span><span class="nl">"apply_patch"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"batch"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"glob"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"grep"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"invalid"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"ls"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"read"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"task"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"webfetch"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"external_directory"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
         </span><span class="nl">"bash"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
       </span><span class="p">}</span><span class="w">
     </span><span class="p">},</span><span class="w">
   </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">share: disabled</code>: reduces accidental data leak.</li>
  <li><code class="language-plaintext highlighter-rouge">autoupdate: false</code>: avoids auto-downloading and executing new binaries.</li>
  <li><code class="language-plaintext highlighter-rouge">server.mdns: false</code>: avoids broadcasting a local service via mDNS.</li>
  <li><code class="language-plaintext highlighter-rouge">permission</code>: prompts before doing anything that touches the filesystem / shell / external dirs.</li>
  <li><code class="language-plaintext highlighter-rouge">tools</code>: enable only what you need.</li>
</ul>

<h2 id="limitations">Limitations</h2>

<ul>
  <li>This setup does <strong>not</strong> restrict outbound network by default.</li>
  <li>Sandboxing reduces blast radius; it does not make prompt injection “go away”.</li>
</ul>]]></content><author><name></name></author><category term="tools" /><category term="llm" /><category term="security" /><category term="macos" /><summary type="html"><![CDATA[This post shows how I run the opencode CLI inside a macOS sandbox using sandbox-exec, with a tight file/process allowlist and a clean environment.]]></summary></entry><entry><title type="html">Figma Chatbot</title><link href="/tools/llm/figma/2026/02/02/figma-chatbot.html" rel="alternate" type="text/html" title="Figma Chatbot" /><published>2026-02-02T00:00:00+00:00</published><updated>2026-02-02T00:00:00+00:00</updated><id>/tools/llm/figma/2026/02/02/figma-chatbot</id><content type="html" xml:base="/tools/llm/figma/2026/02/02/figma-chatbot.html"><![CDATA[<p>The <a href="https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server">Figma MCP server</a> is a decent <strong>read-only</strong> way to integrate Figma with Claude code. It’s good for frontend development, but it can’t make changes inside Figma.</p>

<p>I wanted something for small, repetitive chores: rename layers, nudge spacing normalize corner radius, etc. So I built <a href="https://github.com/aminroosta/figma-chatbot">figma-chatbot</a>: a local bridge that lets Claude execute JavaScript inside a running Figma Desktop document, using the Figma plugin API. That makes prompts like “change the button color to red” possible.</p>

<h2 id="how-it-works">How It Works</h2>

<p>There are two parts, both running locally:</p>

<ul>
  <li>a tiny Figma plugin window that stays open in Figma</li>
  <li>a localhost bridge that forwards requests from Claude to that plugin</li>
</ul>

<p>The bridge is only there to connect Claude to the Figma plugin runtime.</p>

<div class="jekyll-diagrams diagrams graphviz">
  <!-- Generated by graphviz version 2.43.0 (0)
 -->
<!-- Title: FigmaChatbot Pages: 1 -->
<svg width="640pt" height="230pt" viewBox="0.00 0.00 640.00 230.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 226)">
<title>FigmaChatbot</title>
<!-- you -->
<g id="node1" class="node">
<title>you</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M42,-221C42,-221 12,-221 12,-221 6,-221 0,-215 0,-209 0,-209 0,-197 0,-197 0,-191 6,-185 12,-185 12,-185 42,-185 42,-185 48,-185 54,-191 54,-197 54,-197 54,-209 54,-209 54,-215 48,-221 42,-221" />
<text text-anchor="middle" x="27" y="-200.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">You</text>
</g>
<!-- claude -->
<g id="node2" class="node">
<title>claude</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M243,-222C243,-222 179,-222 179,-222 173,-222 167,-216 167,-210 167,-210 167,-196 167,-196 167,-190 173,-184 179,-184 179,-184 243,-184 243,-184 249,-184 255,-190 255,-196 255,-196 255,-210 255,-210 255,-216 249,-222 243,-222" />
<text text-anchor="middle" x="211" y="-206.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Claude Code</text>
<text text-anchor="middle" x="211" y="-194.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">(/fig:go)</text>
</g>
<!-- you&#45;&gt;claude -->
<g id="edge7" class="edge">
<title>you&#45;&gt;claude</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M54.15,-203C81.87,-203 125.57,-203 159.81,-203" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="159.96,-205.45 166.96,-203 159.96,-200.55 159.96,-205.45" />
<text text-anchor="middle" x="110.5" y="-208.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">describe the change</text>
</g>
<!-- bridge -->
<g id="node3" class="node">
<title>bridge</title>
<defs>
<linearGradient id="l_0" gradientUnits="userSpaceOnUse" x1="147.5" y1="-111" x2="274.5" y2="-111">
<stop offset="0" style="stop-color:#a1c4fd;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#c2e9fb;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_0)" stroke="#0284c7" stroke-width="1.2" stroke-opacity="0.200000" d="M262.5,-130C262.5,-130 159.5,-130 159.5,-130 153.5,-130 147.5,-124 147.5,-118 147.5,-118 147.5,-104 147.5,-104 147.5,-98 153.5,-92 159.5,-92 159.5,-92 262.5,-92 262.5,-92 268.5,-92 274.5,-98 274.5,-104 274.5,-104 274.5,-118 274.5,-118 274.5,-124 268.5,-130 262.5,-130" />
<text text-anchor="middle" x="211" y="-114.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Local bridge</text>
<text text-anchor="middle" x="211" y="-102.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">(ws://127.0.0.1:7017)</text>
</g>
<!-- claude&#45;&gt;bridge -->
<g id="edge8" class="edge">
<title>claude&#45;&gt;bridge</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M186.49,-183.89C177.53,-174.94 170.55,-163.58 175,-152 177.21,-146.23 180.69,-140.78 184.62,-135.85" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="186.67,-137.23 189.4,-130.33 182.96,-134.02 186.67,-137.23" />
<text text-anchor="middle" x="192" y="-154.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">send JS</text>
</g>
<!-- bridge&#45;&gt;claude -->
<g id="edge9" class="edge">
<title>bridge&#45;&gt;claude</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M211.55,-130.32C211.73,-137.13 211.91,-144.92 212,-152 212.1,-160.11 212,-168.96 211.82,-176.93" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="209.37,-176.89 211.64,-183.95 214.27,-177.01 209.37,-176.89" />
<text text-anchor="middle" x="238" y="-154.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">logs + result</text>
</g>
<!-- ui -->
<g id="node4" class="node">
<title>ui</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M460.5,-130C460.5,-130 369.5,-130 369.5,-130 363.5,-130 357.5,-124 357.5,-118 357.5,-118 357.5,-104 357.5,-104 357.5,-98 363.5,-92 369.5,-92 369.5,-92 460.5,-92 460.5,-92 466.5,-92 472.5,-98 472.5,-104 472.5,-104 472.5,-118 472.5,-118 472.5,-124 466.5,-130 460.5,-130" />
<text text-anchor="middle" x="415" y="-114.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Figma plugin UI</text>
<text text-anchor="middle" x="415" y="-102.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">(WebSocket client)</text>
</g>
<!-- bridge&#45;&gt;ui -->
<g id="edge1" class="edge">
<title>bridge&#45;&gt;ui</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M274.71,-111C298.75,-111 326.12,-111 350.26,-111" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="350.31,-113.45 357.31,-111 350.31,-108.55 350.31,-113.45" />
<text text-anchor="middle" x="316" y="-116.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">eval_request</text>
</g>
<!-- ui&#45;&gt;bridge -->
<g id="edge6" class="edge">
<title>ui&#45;&gt;bridge</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M357.38,-100.45C350.04,-99.44 342.61,-98.58 335.5,-98 318.16,-96.59 299.49,-97.34 282.01,-99.02" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="281.31,-96.63 274.6,-99.8 281.82,-101.51 281.31,-96.63" />
<text text-anchor="middle" x="316" y="-100.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">response</text>
</g>
<!-- main -->
<g id="node5" class="node">
<title>main</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M466,-38C466,-38 362,-38 362,-38 356,-38 350,-32 350,-26 350,-26 350,-12 350,-12 350,-6 356,0 362,0 362,0 466,0 466,0 472,0 478,-6 478,-12 478,-12 478,-26 478,-26 478,-32 472,-38 466,-38" />
<text text-anchor="middle" x="414" y="-22.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Figma plugin main</text>
<text text-anchor="middle" x="414" y="-10.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">(runs JS with figma.*)</text>
</g>
<!-- ui&#45;&gt;main -->
<g id="edge2" class="edge">
<title>ui&#45;&gt;main</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M385.95,-91.99C378.74,-85.96 371.96,-78.56 368,-70 363.37,-59.98 368.27,-50.6 376.35,-42.74" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="378.07,-44.49 381.74,-38.05 374.85,-40.8 378.07,-44.49" />
<text text-anchor="middle" x="395.5" y="-62.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">postMessage</text>
</g>
<!-- main&#45;&gt;ui -->
<g id="edge5" class="edge">
<title>main&#45;&gt;ui</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M420.64,-38.07C422.8,-44.85 424.93,-52.68 426,-60 427.19,-68.08 426.1,-76.8 424.23,-84.66" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="421.82,-84.2 422.33,-91.59 426.54,-85.49 421.82,-84.2" />
<text text-anchor="middle" x="457.5" y="-62.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">eval_response</text>
</g>
<!-- doc -->
<g id="node6" class="node">
<title>doc</title>
<path fill="#c2e59c" stroke="#166534" stroke-width="1.2" stroke-opacity="0.200000" d="M620,-38C620,-38 548,-38 548,-38 542,-38 536,-32 536,-26 536,-26 536,-12 536,-12 536,-6 542,0 548,0 548,0 620,0 620,0 626,0 632,-6 632,-12 632,-12 632,-26 632,-26 632,-32 626,-38 620,-38" />
<text text-anchor="middle" x="584" y="-22.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Your Figma file</text>
<text text-anchor="middle" x="584" y="-10.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">(actual edits)</text>
</g>
<!-- main&#45;&gt;doc -->
<g id="edge3" class="edge">
<title>main&#45;&gt;doc</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M478.27,-19C494.82,-19 512.49,-19 528.55,-19" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="528.81,-21.45 535.81,-19 528.81,-16.55 528.81,-21.45" />
<text text-anchor="middle" x="507" y="-24.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">apply</text>
</g>
<!-- doc&#45;&gt;main -->
<g id="edge4" class="edge">
<title>doc&#45;&gt;main</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M535.87,-8.34C530.04,-7.37 524.15,-6.56 518.5,-6 507.75,-4.94 496.4,-5.07 485.3,-5.88" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="484.81,-3.46 478.05,-6.51 485.23,-8.35 484.81,-3.46" />
<text text-anchor="middle" x="507" y="-8.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">result</text>
</g>
</g>
</svg>

</div>

<p>Edits happen inside the Figma plugin, where the document is writable.</p>

<h2 id="setup">Setup</h2>

<p><code class="language-plaintext highlighter-rouge">figma-chatbot</code> is local-first by design: it runs on your machine and talks to your Figma Desktop app over localhost.</p>

<p>It uses <a href="https://bun.sh">Bun</a> as the runtime for the local bridge.</p>

<p>What the setup looks like:</p>

<p>1) Claude Code side (install + sanity check)</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/plugin marketplace add aminroosta/figma-chatbot
/plugin <span class="nb">install </span>fig@fig

/fig:setup
</code></pre></div></div>

<p>2) Figma Desktop side (import the dev plugin)</p>

<ul>
  <li>Plugins -&gt; Development -&gt; Import plugin from manifest</li>
  <li>select <code class="language-plaintext highlighter-rouge">~/.claude/plugins/marketplaces/fig/chatbot/</code> (it contains <code class="language-plaintext highlighter-rouge">manifest.json</code>)</li>
  <li>run the plugin (Figma command palette: <code class="language-plaintext highlighter-rouge">Cmd+/</code>, search “chatbot”) and keep its window open</li>
</ul>

<p>3) Back in Claude Code (start the bridge)</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/fig:go
<span class="c"># This uses the bridge under the hood to evaluate JS snippets.</span>
</code></pre></div></div>

<p>The plugin window shows connection state. When it says “Connected”, Claude can send edits.</p>

<p><img src="/assets/images/figma-chatbot-help.png" width="720" alt="figma-chatbot in action" /></p>

<h2 id="using-it">Using It</h2>

<p>Once the bridge is running, prompts like this become possible:</p>

<ul>
  <li>“Find every instance of Button, make the fill red, and align the padding across variants.”</li>
  <li>“Prefix everything on this page with Marketing/, except frames that already have a prefix.”</li>
  <li>“Rename these layers with a clean scheme, then center the viewport on them.”</li>
</ul>

<p><br />
If you want to try it, the repo is here: <a href="https://github.com/aminroosta/figma-chatbot">https://github.com/aminroosta/figma-chatbot</a></p>]]></content><author><name></name></author><category term="tools" /><category term="llm" /><category term="figma" /><summary type="html"><![CDATA[The Figma MCP server is a decent read-only way to integrate Figma with Claude code. It’s good for frontend development, but it can’t make changes inside Figma.]]></summary></entry><entry><title type="html">The Ralph Loop</title><link href="/tools/llm/2026/01/26/the-ralph-loop.html" rel="alternate" type="text/html" title="The Ralph Loop" /><published>2026-01-26T00:00:00+00:00</published><updated>2026-01-26T00:00:00+00:00</updated><id>/tools/llm/2026/01/26/the-ralph-loop</id><content type="html" xml:base="/tools/llm/2026/01/26/the-ralph-loop.html"><![CDATA[<p>The <a href="https://ghuntley.com/loop/">Ralph Loop</a> has completely changed how I write software. For over a decade, my workflow for implementing a feature looked like this:</p>

<ul>
  <li>Step 1: write a tech spec (maybe informal; maybe just a Markdown file in <a href="https://github.com/vimwiki/vimwiki">Vimwiki</a>).</li>
  <li>Step 2: add a few TODO tasks (obvious, low-hanging fruit that I should pick up next).</li>
</ul>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Implement 2FA with SMS</span>
<span class="p">
-</span> links to third-party SMS provider |
<span class="p">-</span> links to Figma designs            |&lt;- my understanding on day 1
<span class="p">-</span> functional requirement 1          |
<span class="p">-</span> functional requirement 2          |<span class="sb">


</span><span class="gh"># TODO</span>
<span class="p">-</span> [ ] Add a <span class="sb">`user_sms`</span> table + migration |
<span class="p">-</span> [ ] Add a <span class="sb">`POST /v1/sms/pin`</span> API       |&lt;- low-hanging fruit
</code></pre></div></div>

<ul>
  <li>Step 3: implement the <strong>TODO</strong> items.</li>
  <li>Step 4: think about what to do next.</li>
</ul>

<p>Go back to step 3 and repeat until the project is complete.</p>

<p>The way I use the Ralph Loop now isn’t too different from that approach; the key difference is that the <strong>TODO</strong>s are done by LLMs (GPT-5.2 (xhigh) or Opus 4.5 (max)).</p>

<div class="jekyll-diagrams diagrams graphviz">
  <!-- Generated by graphviz version 2.43.0 (0)
 -->
<!-- Title: RalphLoop Pages: 1 -->
<svg width="355pt" height="332pt" viewBox="0.00 0.00 355.00 332.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 328)">
<title>RalphLoop</title>
<!-- start -->
<g id="node1" class="node">
<title>start</title>
<ellipse fill="#c2e59c" stroke="#166534" stroke-width="1.2" cx="18" cy="-306" rx="18" ry="18" />
<text text-anchor="middle" x="18" y="-303.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Start</text>
</g>
<!-- prd -->
<g id="node2" class="node">
<title>prd</title>
<defs>
<linearGradient id="l_0" gradientUnits="userSpaceOnUse" x1="93.5" y1="-306" x2="170.5" y2="-306">
<stop offset="0" style="stop-color:#fdfbfb;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#ebedee;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_0)" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M158.5,-324C158.5,-324 105.5,-324 105.5,-324 99.5,-324 93.5,-318 93.5,-312 93.5,-312 93.5,-300 93.5,-300 93.5,-294 99.5,-288 105.5,-288 105.5,-288 158.5,-288 158.5,-288 164.5,-288 170.5,-294 170.5,-300 170.5,-300 170.5,-312 170.5,-312 170.5,-318 164.5,-324 158.5,-324" />
<text text-anchor="middle" x="132" y="-309.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#0f172a">Write PRD</text>
<text text-anchor="middle" x="132" y="-297.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#0f172a">(tech&#45;spec)</text>
</g>
<!-- start&#45;&gt;prd -->
<g id="edge1" class="edge">
<title>start&#45;&gt;prd</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M36.26,-306C52.95,-306 69.64,-306 86.33,-306" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="86.48,-308.45 93.48,-306 86.48,-303.55 86.48,-308.45" />
</g>
<!-- todos -->
<g id="node3" class="node">
<title>todos</title>
<defs>
<linearGradient id="l_1" gradientUnits="userSpaceOnUse" x1="77" y1="-225" x2="187" y2="-225">
<stop offset="0" style="stop-color:#fdfbfb;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#ebedee;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_1)" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M175,-243C175,-243 89,-243 89,-243 83,-243 77,-237 77,-231 77,-231 77,-219 77,-219 77,-213 83,-207 89,-207 89,-207 175,-207 175,-207 181,-207 187,-213 187,-219 187,-219 187,-231 187,-231 187,-237 181,-243 175,-243" />
<text text-anchor="middle" x="132" y="-228.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#0f172a">Add TODO tasks</text>
<text text-anchor="middle" x="132" y="-216.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#0f172a">(low&#45;hanging fruit)</text>
</g>
<!-- prd&#45;&gt;todos -->
<g id="edge2" class="edge">
<title>prd&#45;&gt;todos</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M132,-287.86C132,-276.88 132,-262.48 132,-250.33" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="134.45,-250.11 132,-243.11 129.55,-250.11 134.45,-250.11" />
</g>
<!-- llm -->
<g id="node4" class="node">
<title>llm</title>
<defs>
<linearGradient id="l_2" gradientUnits="userSpaceOnUse" x1="81.5" y1="-144" x2="182.5" y2="-144">
<stop offset="0" style="stop-color:#7b4397;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#dc2430;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_2)" stroke="#7b4397" stroke-width="2" stroke-opacity="0.333333" d="M170.5,-162C170.5,-162 93.5,-162 93.5,-162 87.5,-162 81.5,-156 81.5,-150 81.5,-150 81.5,-138 81.5,-138 81.5,-132 87.5,-126 93.5,-126 93.5,-126 170.5,-126 170.5,-126 176.5,-126 182.5,-132 182.5,-138 182.5,-138 182.5,-150 182.5,-150 182.5,-156 176.5,-162 170.5,-162" />
<text text-anchor="middle" x="132" y="-147.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="white">LLM implements</text>
<text text-anchor="middle" x="132" y="-135.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="white">TODO item</text>
</g>
<!-- todos&#45;&gt;llm -->
<g id="edge3" class="edge">
<title>todos&#45;&gt;llm</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M132,-206.86C132,-195.88 132,-181.48 132,-169.33" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="134.45,-169.11 132,-162.11 129.55,-169.11 134.45,-169.11" />
</g>
<!-- more -->
<g id="node5" class="node">
<title>more</title>
<polygon fill="#fde68a" stroke="#a16207" stroke-width="1.2" points="191,-72 137,-36 191,0 245,-36 191,-72" />
<text text-anchor="middle" x="191" y="-39.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#3b2f1a">More</text>
<text text-anchor="middle" x="191" y="-27.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#3b2f1a">steps?</text>
</g>
<!-- llm&#45;&gt;more -->
<g id="edge4" class="edge">
<title>llm&#45;&gt;more</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M141.48,-125.97C150.02,-110.62 162.81,-87.64 173.26,-68.88" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="175.43,-70.01 176.7,-62.7 171.15,-67.62 175.43,-70.01" />
</g>
<!-- think -->
<g id="node6" class="node">
<title>think</title>
<defs>
<linearGradient id="l_3" gradientUnits="userSpaceOnUse" x1="211.5" y1="-144" x2="290.5" y2="-144">
<stop offset="0" style="stop-color:#a1c4fd;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#c2e9fb;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_3)" stroke="#0284c7" stroke-width="1.2" stroke-opacity="0.200000" d="M278.5,-162C278.5,-162 223.5,-162 223.5,-162 217.5,-162 211.5,-156 211.5,-150 211.5,-150 211.5,-138 211.5,-138 211.5,-132 217.5,-126 223.5,-126 223.5,-126 278.5,-126 278.5,-126 284.5,-126 290.5,-132 290.5,-138 290.5,-138 290.5,-150 290.5,-150 290.5,-156 284.5,-162 278.5,-162" />
<text text-anchor="middle" x="251" y="-147.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#0f172a">Think about</text>
<text text-anchor="middle" x="251" y="-135.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00" fill="#0f172a">next steps</text>
</g>
<!-- more&#45;&gt;think -->
<g id="edge5" class="edge">
<title>more&#45;&gt;think</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M205.55,-62.7C215.36,-80.04 228.22,-102.76 237.75,-119.6" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="235.78,-121.08 241.36,-125.97 240.04,-118.67 235.78,-121.08" />
<text text-anchor="middle" x="235.5" y="-96.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">yes</text>
</g>
<!-- done -->
<g id="node7" class="node">
<title>done</title>
<ellipse fill="#c2e59c" stroke="#166534" stroke-width="1.2" cx="325" cy="-36" rx="18" ry="18" />
<ellipse fill="none" stroke="#166534" stroke-width="1.2" cx="325" cy="-36" rx="22" ry="22" />
<text text-anchor="middle" x="325" y="-33.2" font-family="Helvetica,Arial,sans-serif" font-size="11.00">Done</text>
</g>
<!-- more&#45;&gt;done -->
<g id="edge7" class="edge">
<title>more&#45;&gt;done</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M245.12,-36C262.31,-36 280.68,-36 295.47,-36" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="295.85,-38.45 302.85,-36 295.85,-33.55 295.85,-38.45" />
<text text-anchor="middle" x="274" y="-41.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#0b1220" fill-opacity="0.800000">no</text>
</g>
<!-- think&#45;&gt;todos -->
<g id="edge6" class="edge">
<title>think&#45;&gt;todos</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.400000" d="M225.23,-162.11C207.23,-174.06 183.08,-190.09 163.87,-202.84" />
<polygon fill="#0b1220" fill-opacity="0.400000" stroke="#0b1220" stroke-opacity="0.400000" points="162.3,-200.95 157.82,-206.86 165.01,-205.03 162.3,-200.95" />
</g>
</g>
</svg>

</div>

<p>I use <a href="https://opencode.ai/">OpenCode</a> with a script to automate most of step 3 (implementing the <strong>TODO</strong> items).</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ralph.ts    <span class="c"># it's somewhere on my PATH</span>
ralph.md    <span class="c"># the system prompt for the Ralph Loop</span>

<span class="c"># &lt;current project files&gt;</span>
.opencode/
├── opencode.json <span class="c"># OpenCode permission/settings</span>
├── log-sms.md    <span class="c"># append-only log for the LLM</span>
└── prd-sms.md    <span class="c"># the PRD file</span>
</code></pre></div></div>

<p>The setup is dead simple. I run <code class="language-plaintext highlighter-rouge">ralph.ts &lt;iterations&gt; .opencode/prd-&lt;name&gt;.md</code>. The script takes the <code class="language-plaintext highlighter-rouge">ralph.md</code> system prompt, inlines the paths to <code class="language-plaintext highlighter-rouge">.opencode/prd-&lt;name&gt;.md</code> and <code class="language-plaintext highlighter-rouge">.opencode/log-&lt;name&gt;.md</code>, and runs a new OpenCode session.
It’s basically a for loop. The system prompt (<code class="language-plaintext highlighter-rouge">ralph.md</code>) instructs the LLM to mark the TODO item as complete, append progress to <code class="language-plaintext highlighter-rouge">.opencode/log-&lt;name&gt;.md</code>, and commit the changes.</p>

<p>Here is the full prompt <code class="language-plaintext highlighter-rouge">ralph.md</code>:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Ralph Agent Instructions</span>

You are an autonomous coding agent responsible for executing tasks within a software project.

<span class="gu">## Your Task Pipeline</span>
<span class="p">
1.</span>  <span class="gs">**Contextual Review:**</span>
<span class="p">    *</span> Read the PRD at <span class="sb">`RALPH_PRD_PATH`</span>.
<span class="p">    *</span> Read the progress log at <span class="sb">`RALPH_LOG_PATH`</span>.
<span class="p">2.</span>  <span class="gs">**Incremental Planning:**</span>
<span class="p">    *</span> Decide on a single, granular step to move the project closer to the full PRD implementation.
<span class="p">    *</span> Steps need not be sequential, pick the one that feels right as the next step.
<span class="p">3.</span>  <span class="gs">**Execution:**</span>
<span class="p">    *</span> Implement that single step -- this is where you will spend most of your time.
<span class="p">4.</span>  <span class="gs">**Validation:**</span>
<span class="p">    *</span> Do everything that is available to verify that the changes are correct.
<span class="p">5.</span>  <span class="gs">**Version Control:**</span>
<span class="p">    *</span> Commit code changes with the message: <span class="sb">`feat: [Item Title]`</span>.
<span class="p">    *</span> <span class="ge">*Note: Skip `.opencode/` folder changes and any existing untracked files.*</span>

<span class="gu">## Progress Log Format</span>
<span class="ge">*Append-only. Information density is key.*</span>

<span class="se">\`\`\`</span>md
[short commit hash]
<span class="p">-</span> informative (not prescriptive) notes to future self;
  such as learnings, patterns, gotchas, and/or useful snippets.
<span class="se">\`\`\`</span>
<span class="gt">
&gt; **Note:** The "Learnings" section is vital for helping future iterations pick up the next steps effectively.</span>
<span class="p">
---
</span>
<span class="gu">## After Completing the Step</span>
<span class="p">
*</span> Mark the step as complete in the <span class="sb">`# Suggested Next Steps`</span> section of <span class="sb">`RALPH_PRD_PATH`</span>.
<span class="p">*</span> If there aren't enough steps left, add a new step to find more steps.
<span class="p">    *</span> The goal is to include a dedicated step where you act as a slow, deliberate, and analytical thinker: an experienced architect and a rigorous QA specialist, to identify future steps.
<span class="p">*</span> Improve existing steps, remove those that no longer apply (if any), and sort them by readiness to be picked up.

<span class="gu">## Core Principles</span>
<span class="p">
*</span> <span class="gs">**Atomic Progress:**</span> Work on <span class="gs">**ONE**</span> step at a time and commit frequently.
<span class="p">*</span> <span class="gs">**Excellence:**</span> Strive for high-quality implementation in every iteration.
</code></pre></div></div>

<div class="jekyll-diagrams diagrams graphviz">
  <!-- Generated by graphviz version 2.43.0 (0)
 -->
<!-- Title: RalphInternal Pages: 1 -->
<svg width="318pt" height="575pt" viewBox="0.00 0.00 318.00 575.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 571)">
<title>RalphInternal</title>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<path fill="transparent" stroke="#0f172a" stroke-dasharray="5,2" stroke-opacity="0.200000" d="M20,-244C20,-244 148,-244 148,-244 154,-244 160,-250 160,-256 160,-256 160,-307 160,-307 160,-313 154,-319 148,-319 148,-319 20,-319 20,-319 14,-319 8,-313 8,-307 8,-307 8,-256 8,-256 8,-250 14,-244 20,-244" />
<text text-anchor="middle" x="84" y="-303.8" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#0f172a" fill-opacity="0.600000">Inputs</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_iteration</title>
<path fill="#f8fafc" stroke="#0f172a" stroke-opacity="0.133333" d="M219,-244C219,-244 290,-244 290,-244 296,-244 302,-250 302,-256 302,-256 302,-547 302,-547 302,-553 296,-559 290,-559 290,-559 219,-559 219,-559 213,-559 207,-553 207,-547 207,-547 207,-256 207,-256 207,-250 213,-244 219,-244" />
<text text-anchor="middle" x="254.5" y="-543.8" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#0f172a" fill-opacity="0.600000">One Iteration</text>
</g>
<!-- prd -->
<g id="node1" class="node">
<title>prd</title>
<polygon fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="145.5,-288 96.5,-288 96.5,-252 151.5,-252 151.5,-282 145.5,-288" />
<polyline fill="none" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="145.5,-288 145.5,-282 " />
<polyline fill="none" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="151.5,-282 145.5,-282 " />
<text text-anchor="middle" x="124" y="-267.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00">prd&#45;*.md</text>
</g>
<!-- prompt -->
<g id="node3" class="node">
<title>prompt</title>
<defs>
<linearGradient id="l_0" gradientUnits="userSpaceOnUse" x1="94.5" y1="-197" x2="183.5" y2="-197">
<stop offset="0" style="stop-color:#a1c4fd;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#c2e9fb;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_0)" stroke="#0284c7" stroke-width="1.2" stroke-opacity="0.200000" d="M171.5,-215C171.5,-215 106.5,-215 106.5,-215 100.5,-215 94.5,-209 94.5,-203 94.5,-203 94.5,-191 94.5,-191 94.5,-185 100.5,-179 106.5,-179 106.5,-179 171.5,-179 171.5,-179 177.5,-179 183.5,-185 183.5,-191 183.5,-191 183.5,-203 183.5,-203 183.5,-209 177.5,-215 171.5,-215" />
<text text-anchor="middle" x="139" y="-200" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="#0f172a">ralph.md</text>
<text text-anchor="middle" x="139" y="-189" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="#0f172a">prompt template</text>
</g>
<!-- prd&#45;&gt;prompt -->
<g id="edge1" class="edge">
<title>prd&#45;&gt;prompt</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M127.63,-251.81C129.58,-242.57 132.02,-231.04 134.13,-221.03" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="136.22,-221.33 135.4,-215.03 132.11,-220.47 136.22,-221.33" />
</g>
<!-- log -->
<g id="node2" class="node">
<title>log</title>
<polygon fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="65.5,-288 16.5,-288 16.5,-252 71.5,-252 71.5,-282 65.5,-288" />
<polyline fill="none" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="65.5,-288 65.5,-282 " />
<polyline fill="none" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="71.5,-282 65.5,-282 " />
<text text-anchor="middle" x="44" y="-267.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00">log&#45;*.md</text>
</g>
<!-- log&#45;&gt;prompt -->
<g id="edge2" class="edge">
<title>log&#45;&gt;prompt</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M67,-251.81C80.3,-241.87 97.13,-229.29 111.18,-218.79" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="112.67,-220.3 116.21,-215.03 110.15,-216.94 112.67,-220.3" />
</g>
<!-- pick -->
<g id="node4" class="node">
<title>pick</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M276.5,-528C276.5,-528 231.5,-528 231.5,-528 225.5,-528 219.5,-522 219.5,-516 219.5,-516 219.5,-504 219.5,-504 219.5,-498 225.5,-492 231.5,-492 231.5,-492 276.5,-492 276.5,-492 282.5,-492 288.5,-498 288.5,-504 288.5,-504 288.5,-516 288.5,-516 288.5,-522 282.5,-528 276.5,-528" />
<text text-anchor="middle" x="254" y="-513" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Pick one</text>
<text text-anchor="middle" x="254" y="-502" font-family="Helvetica,Arial,sans-serif" font-size="10.00">TODO step</text>
</g>
<!-- prompt&#45;&gt;pick -->
<g id="edge3" class="edge">
<title>prompt&#45;&gt;pick</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M160.63,-215.16C174.53,-228.24 190,-247.6 190,-269 190,-438 190,-438 190,-438 190,-458.6 205.05,-475.92 220.56,-488.3" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="219.33,-490 225.37,-491.98 221.88,-486.66 219.33,-490" />
</g>
<!-- implement -->
<g id="node5" class="node">
<title>implement</title>
<defs>
<linearGradient id="l_1" gradientUnits="userSpaceOnUse" x1="228" y1="-437" x2="292" y2="-437">
<stop offset="0" style="stop-color:#7b4397;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#dc2430;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_1)" stroke="#7b4397" stroke-width="1.5" stroke-opacity="0.333333" d="M280,-455C280,-455 240,-455 240,-455 234,-455 228,-449 228,-443 228,-443 228,-431 228,-431 228,-425 234,-419 240,-419 240,-419 280,-419 280,-419 286,-419 292,-425 292,-431 292,-431 292,-443 292,-443 292,-449 286,-455 280,-455" />
<text text-anchor="middle" x="260" y="-434.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="white">Implement</text>
</g>
<!-- pick&#45;&gt;implement -->
<g id="edge4" class="edge">
<title>pick&#45;&gt;implement</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M255.45,-491.81C256.23,-482.57 257.21,-471.04 258.05,-461.03" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="260.15,-461.18 258.56,-455.03 255.96,-460.83 260.15,-461.18" />
</g>
<!-- validate -->
<g id="node6" class="node">
<title>validate</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M275,-382C275,-382 245,-382 245,-382 239,-382 233,-376 233,-370 233,-370 233,-358 233,-358 233,-352 239,-346 245,-346 245,-346 275,-346 275,-346 281,-346 287,-352 287,-358 287,-358 287,-370 287,-370 287,-376 281,-382 275,-382" />
<text text-anchor="middle" x="260" y="-361.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Validate</text>
</g>
<!-- implement&#45;&gt;validate -->
<g id="edge5" class="edge">
<title>implement&#45;&gt;validate</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M260,-418.81C260,-409.57 260,-398.04 260,-388.03" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="262.1,-388.03 260,-382.03 257.9,-388.03 262.1,-388.03" />
</g>
<!-- commit -->
<g id="node7" class="node">
<title>commit</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M280,-288C280,-288 240,-288 240,-288 234,-288 228,-282 228,-276 228,-276 228,-264 228,-264 228,-258 234,-252 240,-252 240,-252 280,-252 280,-252 286,-252 292,-258 292,-264 292,-264 292,-276 292,-276 292,-282 286,-288 280,-288" />
<text text-anchor="middle" x="260" y="-267.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00">git commit</text>
</g>
<!-- validate&#45;&gt;commit -->
<g id="edge6" class="edge">
<title>validate&#45;&gt;commit</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M260,-345.7C260,-331.17 260,-310.3 260,-294.31" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="262.1,-294.23 260,-288.23 257.9,-294.23 262.1,-294.23" />
</g>
<!-- update -->
<g id="node8" class="node">
<title>update</title>
<path fill="#fde68a" stroke="#a16207" stroke-width="1.2" stroke-opacity="0.333333" d="M287,-215C287,-215 227,-215 227,-215 221,-215 215,-209 215,-203 215,-203 215,-191 215,-191 215,-185 221,-179 227,-179 227,-179 287,-179 287,-179 293,-179 299,-185 299,-191 299,-191 299,-203 299,-203 299,-209 293,-215 287,-215" />
<text text-anchor="middle" x="257" y="-200" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Update PRD +</text>
<text text-anchor="middle" x="257" y="-189" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Progress Log</text>
</g>
<!-- commit&#45;&gt;update -->
<g id="edge7" class="edge">
<title>commit&#45;&gt;update</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M259.27,-251.81C258.88,-242.57 258.4,-231.04 257.97,-221.03" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="260.07,-220.93 257.72,-215.03 255.87,-221.11 260.07,-220.93" />
</g>
<!-- check -->
<g id="node9" class="node">
<title>check</title>
<polygon fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" points="194,-133 146.79,-115 194,-97 241.21,-115 194,-133" />
<text text-anchor="middle" x="194" y="-112.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00">All done?</text>
</g>
<!-- update&#45;&gt;check -->
<g id="edge8" class="edge">
<title>update&#45;&gt;check</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M243.33,-178.64C232.98,-165.5 218.8,-147.49 208.3,-134.15" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="209.69,-132.53 204.33,-129.11 206.39,-135.13 209.69,-132.53" />
</g>
<!-- check&#45;&gt;prompt -->
<g id="edge9" class="edge">
<title>check&#45;&gt;prompt</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M184.64,-129.62C176.36,-141.66 164.11,-159.47 154.39,-173.61" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="152.6,-172.51 150.93,-178.64 156.06,-174.89 152.6,-172.51" />
<text text-anchor="middle" x="174.5" y="-153.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00">no</text>
</g>
<!-- complete -->
<g id="node10" class="node">
<title>complete</title>
<ellipse fill="#c2e59c" stroke="#166534" stroke-width="1.2" cx="194" cy="-25.5" rx="21.5" ry="21.5" />
<ellipse fill="none" stroke="#166534" stroke-width="1.2" cx="194" cy="-25.5" rx="25.5" ry="25.5" />
<text text-anchor="middle" x="194" y="-23.3" font-family="Helvetica,Arial,sans-serif" font-size="9.00">COMPLETE</text>
</g>
<!-- check&#45;&gt;complete -->
<g id="edge10" class="edge">
<title>check&#45;&gt;complete</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M194,-96.71C194,-85.5 194,-70.57 194,-57.36" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="196.1,-57.04 194,-51.04 191.9,-57.04 196.1,-57.04" />
<text text-anchor="middle" x="201.5" y="-71.8" font-family="Helvetica,Arial,sans-serif" font-size="9.00">yes</text>
</g>
</g>
</svg>

</div>

<p>The <code class="language-plaintext highlighter-rouge">.opencode/prd-sms.md</code> might look something like this:</p>
<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code>We are building SMS authentication with provider XYZ.
You can read the docs at http://xyz.com/docs/sms/api

Use the Figma MCP to inspect these node IDs: 110-5501 110-5729 290-8616

<span class="gh"># Suggested Next Steps</span>
<span class="p">
-</span> [ ] Add a <span class="sb">`user_sms`</span> table + migration
<span class="p">-</span> [ ] Add a <span class="sb">`POST /v1/sms/pin`</span> API
<span class="p">-</span> [ ] Study what needs to be done next and add a few more suggested steps.
</code></pre></div></div>

<p>And the <code class="language-plaintext highlighter-rouge">ralph.ts</code> script is the glue that runs the loop, plus a few extra niceties like opening the OpenCode web UI so I can inspect progress more easily.</p>

<p>Here is the full <code class="language-plaintext highlighter-rouge">ralph.ts</code> <a href="https://github.com/aminroosta/aminroosta.github.io/raw/refs/heads/gh-pages/assets/download/ralph.ts?download=">click to download</a>.</p>

<div class="jekyll-diagrams diagrams graphviz">
  <!-- Generated by graphviz version 2.43.0 (0)
 -->
<!-- Title: RalphRunner Pages: 1 -->
<svg width="387pt" height="459pt" viewBox="0.00 0.00 387.00 459.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 455)">
<title>RalphRunner</title>
<g id="clust1" class="cluster">
<title>cluster_loop</title>
<path fill="#fffbeb" fill-opacity="0.133333" stroke="#a16207" stroke-dasharray="5,2" stroke-opacity="0.333333" d="M178.5,-149.5C178.5,-149.5 312.5,-149.5 312.5,-149.5 318.5,-149.5 324.5,-155.5 324.5,-161.5 324.5,-161.5 324.5,-431 324.5,-431 324.5,-437 318.5,-443 312.5,-443 312.5,-443 178.5,-443 178.5,-443 172.5,-443 166.5,-437 166.5,-431 166.5,-431 166.5,-161.5 166.5,-161.5 166.5,-155.5 172.5,-149.5 178.5,-149.5" />
<text text-anchor="middle" x="245.5" y="-427.8" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#a16207">Loop (max N iterations)</text>
</g>
<!-- start -->
<g id="node1" class="node">
<title>start</title>
<defs>
<linearGradient id="l_0" gradientUnits="userSpaceOnUse" x1="34" y1="-175.5" x2="77" y2="-175.5">
<stop offset="0" style="stop-color:#7b4397;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#dc2430;stop-opacity:1.;" />
</linearGradient>
</defs>
<ellipse fill="url(#l_0)" stroke="black" stroke-width="1.2" cx="55.5" cy="-175.5" rx="21.5" ry="21.5" />
<text text-anchor="middle" x="55.5" y="-173" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="blue">ralph.ts</text>
</g>
<!-- parse -->
<g id="node2" class="node">
<title>parse</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M99,-109C99,-109 12,-109 12,-109 6,-109 0,-103 0,-97 0,-97 0,-85 0,-85 0,-79 6,-73 12,-73 12,-73 99,-73 99,-73 105,-73 111,-79 111,-85 111,-85 111,-97 111,-97 111,-103 105,-109 99,-109" />
<text text-anchor="middle" x="55.5" y="-94" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Parse args</text>
<text text-anchor="middle" x="55.5" y="-83" font-family="Helvetica,Arial,sans-serif" font-size="10.00">(port, iterations, PRD)</text>
</g>
<!-- start&#45;&gt;parse -->
<g id="edge1" class="edge">
<title>start&#45;&gt;parse</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M55.5,-153.68C55.5,-141.97 55.5,-127.33 55.5,-115.25" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="57.6,-115.19 55.5,-109.19 53.4,-115.19 57.6,-115.19" />
</g>
<!-- fzf -->
<g id="node3" class="node">
<title>fzf</title>
<defs>
<linearGradient id="l_1" gradientUnits="userSpaceOnUse" x1="144" y1="-18" x2="249" y2="-18">
<stop offset="0" style="stop-color:#a1c4fd;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#c2e9fb;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_1)" stroke="#0284c7" stroke-width="1.2" stroke-opacity="0.200000" d="M237,-36C237,-36 156,-36 156,-36 150,-36 144,-30 144,-24 144,-24 144,-12 144,-12 144,-6 150,0 156,0 156,0 237,0 237,0 243,0 249,-6 249,-12 249,-12 249,-24 249,-24 249,-30 243,-36 237,-36" />
<text text-anchor="middle" x="196.5" y="-21" font-family="Helvetica,Arial,sans-serif" font-size="10.00">fzf: select model</text>
<text text-anchor="middle" x="196.5" y="-10" font-family="Helvetica,Arial,sans-serif" font-size="10.00">(GPT&#45;5.2 / Opus 4.5)</text>
</g>
<!-- parse&#45;&gt;fzf -->
<g id="edge2" class="edge">
<title>parse&#45;&gt;fzf</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M89.27,-72.99C109.62,-62.75 135.62,-49.66 156.92,-38.93" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="158.1,-40.69 162.51,-36.11 156.21,-36.94 158.1,-40.69" />
</g>
<!-- web -->
<g id="node4" class="node">
<title>web</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M367,-109C367,-109 310,-109 310,-109 304,-109 298,-103 298,-97 298,-97 298,-85 298,-85 298,-79 304,-73 310,-73 310,-73 367,-73 367,-73 373,-73 379,-79 379,-85 379,-85 379,-97 379,-97 379,-103 373,-109 367,-109" />
<text text-anchor="middle" x="338.5" y="-94" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Start opencode</text>
<text text-anchor="middle" x="338.5" y="-83" font-family="Helvetica,Arial,sans-serif" font-size="10.00">web server</text>
</g>
<!-- fzf&#45;&gt;web -->
<g id="edge3" class="edge">
<title>fzf&#45;&gt;web</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M230.73,-36.11C251.25,-46.38 277.44,-59.47 298.87,-70.18" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="298.18,-72.19 304.49,-72.99 300.06,-68.43 298.18,-72.19" />
</g>
<!-- session -->
<g id="node5" class="node">
<title>session</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M304.5,-412C304.5,-412 246.5,-412 246.5,-412 240.5,-412 234.5,-406 234.5,-400 234.5,-400 234.5,-388 234.5,-388 234.5,-382 240.5,-376 246.5,-376 246.5,-376 304.5,-376 304.5,-376 310.5,-376 316.5,-382 316.5,-388 316.5,-388 316.5,-400 316.5,-400 316.5,-406 310.5,-412 304.5,-412" />
<text text-anchor="middle" x="275.5" y="-397" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Create session</text>
<text text-anchor="middle" x="275.5" y="-386" font-family="Helvetica,Arial,sans-serif" font-size="10.00">+ open browser</text>
</g>
<!-- web&#45;&gt;session -->
<g id="edge4" class="edge">
<title>web&#45;&gt;session</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M338.5,-109.06C338.5,-125.75 338.5,-151.87 338.5,-174.5 338.5,-326 338.5,-326 338.5,-326 338.5,-344.94 324.98,-360.71 310.47,-372.18" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="309.05,-370.62 305.52,-375.9 311.58,-373.97 309.05,-370.62" />
</g>
<!-- inject -->
<g id="node6" class="node">
<title>inject</title>
<path fill="#fdfbfb" stroke="#0f172a" stroke-width="1.2" stroke-opacity="0.200000" d="M268.5,-343C268.5,-343 186.5,-343 186.5,-343 180.5,-343 174.5,-337 174.5,-331 174.5,-331 174.5,-319 174.5,-319 174.5,-313 180.5,-307 186.5,-307 186.5,-307 268.5,-307 268.5,-307 274.5,-307 280.5,-313 280.5,-319 280.5,-319 280.5,-331 280.5,-331 280.5,-337 274.5,-343 268.5,-343" />
<text text-anchor="middle" x="227.5" y="-328" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Inject PRD/log paths</text>
<text text-anchor="middle" x="227.5" y="-317" font-family="Helvetica,Arial,sans-serif" font-size="10.00">into ralph.md</text>
</g>
<!-- session&#45;&gt;inject -->
<g id="edge5" class="edge">
<title>session&#45;&gt;inject</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M263.14,-375.75C257.25,-367.53 250.13,-357.58 243.8,-348.76" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="245.19,-347.08 239.99,-343.43 241.77,-349.53 245.19,-347.08" />
</g>
<!-- run -->
<g id="node7" class="node">
<title>run</title>
<defs>
<linearGradient id="l_2" gradientUnits="userSpaceOnUse" x1="193" y1="-248" x2="266" y2="-248">
<stop offset="0" style="stop-color:#7b4397;stop-opacity:1.;" />
<stop offset="1" style="stop-color:#dc2430;stop-opacity:1.;" />
</linearGradient>
</defs>
<path fill="url(#l_2)" stroke="black" stroke-width="1.5" d="M254,-266C254,-266 205,-266 205,-266 199,-266 193,-260 193,-254 193,-254 193,-242 193,-242 193,-236 199,-230 205,-230 205,-230 254,-230 254,-230 260,-230 266,-236 266,-242 266,-242 266,-254 266,-254 266,-260 260,-266 254,-266" />
<text text-anchor="middle" x="229.5" y="-245.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="white">opencode run</text>
</g>
<!-- inject&#45;&gt;run -->
<g id="edge6" class="edge">
<title>inject&#45;&gt;run</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M227.95,-306.98C228.23,-296.7 228.58,-283.45 228.88,-272.21" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="230.98,-272.26 229.04,-266.21 226.78,-272.15 230.98,-272.26" />
</g>
<!-- check -->
<g id="node8" class="node">
<title>check</title>
<polygon fill="#fde68a" stroke="#a16207" stroke-width="1.2" points="233.5,-193.5 177.32,-175.5 233.5,-157.5 289.68,-175.5 233.5,-193.5" />
<text text-anchor="middle" x="233.5" y="-173.3" font-family="Helvetica,Arial,sans-serif" font-size="9.00">COMPLETE?</text>
</g>
<!-- run&#45;&gt;check -->
<g id="edge7" class="edge">
<title>run&#45;&gt;check</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M230.47,-229.93C230.98,-220.84 231.63,-209.53 232.19,-199.65" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="234.3,-199.52 232.54,-193.41 230.1,-199.28 234.3,-199.52" />
</g>
<!-- check&#45;&gt;session -->
<g id="edge9" class="edge">
<title>check&#45;&gt;session</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M247.21,-189.24C257.21,-199.32 270.22,-214.29 277.5,-230 298.78,-275.91 299.31,-293 291.5,-343 290.11,-351.9 287.41,-361.38 284.62,-369.66" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="282.49,-369.38 282.48,-375.73 286.45,-370.77 282.49,-369.38" />
<text text-anchor="middle" x="300.5" y="-284.6" font-family="Helvetica,Arial,sans-serif" font-size="8.00">no</text>
</g>
<!-- done -->
<g id="node9" class="node">
<title>done</title>
<ellipse fill="#c2e59c" stroke="#166534" stroke-width="1.2" cx="155.5" cy="-91" rx="18" ry="18" />
<ellipse fill="none" stroke="#166534" stroke-width="1.2" cx="155.5" cy="-91" rx="22" ry="22" />
<text text-anchor="middle" x="155.5" y="-88.5" font-family="Helvetica,Arial,sans-serif" font-size="10.00">Done</text>
</g>
<!-- check&#45;&gt;done -->
<g id="edge8" class="edge">
<title>check&#45;&gt;done</title>
<path fill="none" stroke="#0b1220" stroke-opacity="0.333333" d="M220.97,-161.24C208.4,-147.95 188.89,-127.32 174.44,-112.03" />
<polygon fill="#0b1220" fill-opacity="0.333333" stroke="#0b1220" stroke-opacity="0.333333" points="175.77,-110.39 170.13,-107.47 172.72,-113.27 175.77,-110.39" />
<text text-anchor="middle" x="203" y="-131.6" font-family="Helvetica,Arial,sans-serif" font-size="8.00">yes</text>
</g>
<!-- maxed -->
<g id="node10" class="node">
<title>maxed</title>
<path fill="#fecaca" stroke="#b91c1c" stroke-width="1.2" stroke-opacity="0.333333" d="M263.5,-109C263.5,-109 211.5,-109 211.5,-109 205.5,-109 199.5,-103 199.5,-97 199.5,-97 199.5,-85 199.5,-85 199.5,-79 205.5,-73 211.5,-73 211.5,-73 263.5,-73 263.5,-73 269.5,-73 275.5,-79 275.5,-85 275.5,-85 275.5,-97 275.5,-97 275.5,-103 269.5,-109 263.5,-109" />
<text text-anchor="middle" x="237.5" y="-94" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="#7f1d1d">Max iterations</text>
<text text-anchor="middle" x="237.5" y="-83" font-family="Helvetica,Arial,sans-serif" font-size="10.00" fill="#7f1d1d">reached</text>
</g>
<!-- check&#45;&gt;maxed -->
<g id="edge10" class="edge">
<title>check&#45;&gt;maxed</title>
<path fill="none" stroke="#b91c1c" stroke-dasharray="5,2" stroke-opacity="0.333333" d="M234.33,-157.41C234.92,-145.24 235.72,-128.74 236.37,-115.36" />
<polygon fill="#b91c1c" fill-opacity="0.333333" stroke="#b91c1c" stroke-opacity="0.333333" points="238.48,-115.17 236.67,-109.07 234.28,-114.96 238.48,-115.17" />
<text text-anchor="middle" x="242.5" y="-131.6" font-family="Helvetica,Arial,sans-serif" font-size="8.00">limit</text>
</g>
</g>
</svg>

</div>

<p>I hope you find this useful. This has significantly changed how I write code in 2026.<br />
The PRD becomes of utmost importance: I spend a lot of time manually editing it to make sure it’s solid. I gave a short example above, but usually instead of instructions like “read this URL” or “use the Figma MCP”, I do those ahead of time and put the materials somewhere on the filesystem so they’re easily accessible to the LLM. We want to avoid extra cognitive overhead for the LLM (this is called context rot).</p>

<p>Of course, not everything is sunshine and rainbows after the loops are done. I find myself adding a few more TODO items just to clean the slop; or, at worst, manually editing the code. I’d say slop cleanup takes about 60% of my time, but it’s still worth it because I’m 2x to 3x faster and much less mentally exhausted at the end of the day.</p>]]></content><author><name></name></author><category term="tools" /><category term="llm" /><summary type="html"><![CDATA[The Ralph Loop has completely changed how I write software. For over a decade, my workflow for implementing a feature looked like this:]]></summary></entry><entry><title type="html">Writing Effective LLM Skills</title><link href="/tools/llm/2026/01/11/writing-effective-llm-skills.html" rel="alternate" type="text/html" title="Writing Effective LLM Skills" /><published>2026-01-11T00:00:00+00:00</published><updated>2026-01-11T00:00:00+00:00</updated><id>/tools/llm/2026/01/11/writing-effective-llm-skills</id><content type="html" xml:base="/tools/llm/2026/01/11/writing-effective-llm-skills.html"><![CDATA[<p><img src="/assets/images/llm-skills-loading.svg" width="720" alt="How skills are loaded into the LLM context window" /></p>

<p>Years ago I started as an embedded developer. Back then, the limited resources were RAM and CPU, so I wrote C and sometimes even assembly!
Now, with LLM agents on the rise, the new limited resource is the <strong>context window</strong> and it’s a precious resource!</p>

<p>Do you remember prompt-engineering? The next evolution of that is called <strong>context engineering</strong> and <a href="https://code.claude.com/docs/en/skills">skills</a> are the latest approach to do that.</p>

<p>A skill is just a folder with a <code class="language-plaintext highlighter-rouge">SKILL.md</code> plus a few scripts:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;skill name&gt;/
├── SKILL.md   <span class="c"># includes frontmatter with "name" &amp; "description"</span>
└── scripts/
    ├── &lt;helper script&gt;.sh
    └── &lt;helper script 2&gt;.py
</code></pre></div></div>

<p>Only the skill name and description are loaded into the context window. The LLM agent loads skills on demand.</p>

<h2 id="heres-what-i-have-learned-while-writing-skills">Here’s what I have learned while writing skills:</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SKILL.md</code> should be as informative as possible: information density is the key.</li>
  <li>Be descriptive and avoid being prescriptive: describe how things work. Avoid “do X then Y” at all costs!</li>
  <li>Draft → then rewrite-from-scratch: once you have a messy first pass of <code class="language-plaintext highlighter-rouge">SKILL.md</code>, ask the LLM to rewrite it from scratch, emphasizing: <code class="language-plaintext highlighter-rouge">be informative, not prescriptive, information density is the key</code>. The rewrite is usually much smaller and easier to edit.</li>
  <li>Scripts matter: the <code class="language-plaintext highlighter-rouge">scripts/</code> directory lets the agent do complex, frequent tasks without polluting the context window.</li>
</ul>

<hr />

<h2 id="example-skill-openscad--bosl2">Example skill: OpenSCAD + BOSL2</h2>

<p><a href="https://openscad.org/">OpenSCAD</a> is the go-to CAD tool for software devs: you write <code class="language-plaintext highlighter-rouge">.scad</code> code, and it renders 3D geometry. <a href="https://github.com/BelfrySCAD/BOSL2/wiki">BOSL2</a> is a batteries-included OpenSCAD library.</p>

<p>My use-case is simple: design small 3D parts I can print.</p>

<p><strong><code class="language-plaintext highlighter-rouge">openscad/SKILL.md</code></strong>: this is what I mean by “information dense”.</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">openscad</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Design</span><span class="nv"> </span><span class="s">3D</span><span class="nv"> </span><span class="s">printable</span><span class="nv"> </span><span class="s">objects</span><span class="nv"> </span><span class="s">using</span><span class="nv"> </span><span class="s">OpenSCAD</span><span class="nv"> </span><span class="s">with</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">BOSL2</span><span class="nv"> </span><span class="s">library.</span><span class="nv"> </span><span class="s">Use</span><span class="nv"> </span><span class="s">when</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">user</span><span class="nv"> </span><span class="s">wants</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">create,</span><span class="nv"> </span><span class="s">modify,</span><span class="nv"> </span><span class="s">or</span><span class="nv"> </span><span class="s">render</span><span class="nv"> </span><span class="s">3D</span><span class="nv"> </span><span class="s">models</span><span class="nv"> </span><span class="s">(.scad</span><span class="nv"> </span><span class="s">files)."</span>
<span class="nn">---</span>

<span class="gh"># OpenSCAD BOSL2 Quick Reference</span>

<span class="gu">## 1. Transforms &amp; Constants</span>
<span class="gu">### Translation &amp; Scaling</span>
<span class="p">-</span> <span class="sb">`move([x,y,z])`</span>, <span class="sb">`up(z)`</span>, <span class="sb">`down(z)`</span>, <span class="sb">`fwd(y)`</span>, <span class="sb">`back(y)`</span>, <span class="sb">`left(x)`</span>, <span class="sb">`right(x)`</span>
<span class="p">-</span> <span class="sb">`scale([x,y,z])`</span>, <span class="sb">`scale(s)`</span>, <span class="sb">`xscale(s)`</span>, <span class="sb">`yscale(s)`</span>, <span class="sb">`zscale(s)`</span>
<span class="gu">### Rotation &amp; Mirroring</span>
<span class="p">-</span> <span class="sb">`rot([ax,ay,az])`</span>, <span class="sb">`rot(a, v=[x,y,z])`</span>, <span class="sb">`rot(from=V1, to=V2)`</span>
<span class="p">-</span> <span class="sb">`xrot(a)`</span>, <span class="sb">`yrot(a)`</span>, <span class="sb">`zrot(a)`</span>. All accept <span class="sb">`cp=[x,y,z]`</span> to rotate around a point.
<span class="p">-</span> <span class="sb">`mirror(v)`</span>, <span class="sb">`xflip()`</span>, <span class="sb">`yflip()`</span>, <span class="sb">`zflip()`</span>. Accept <span class="sb">`x|y|z=offset`</span> for plane selection.
<span class="gu">### Skewing</span>
<span class="p">-</span> <span class="sb">`skew(sxy, sxz, syx, syz, szx, szy)`</span>: <span class="sb">`sAB`</span> skews axis A as you move along axis B.
<span class="gu">### Direction Constants</span>
<span class="p">-</span> <span class="sb">`LEFT [-1,0,0]`</span>, <span class="sb">`RIGHT [1,0,0]`</span>, <span class="sb">`FWD [0,-1,0]`</span>, <span class="sb">`BACK [0,1,0]`</span>, <span class="sb">`DOWN [0,0,-1]`</span>, <span class="sb">`UP [0,0,1]`</span>, <span class="sb">`CENTER [0,0,0]`</span>

<span class="gu">## 2. Distributors</span>
<span class="p">-</span> <span class="sb">`xcopies(spacing|l=, n=2, sp=)`</span>, <span class="sb">`ycopies(...)`</span>, <span class="sb">`zcopies(...)`</span>
<span class="p">-</span> <span class="sb">`line_copies(spacing=V, n=, p1=, p2=)`</span>
<span class="p">-</span> <span class="sb">`grid_copies(spacing=, n=, size=, stagger=true|alt, inside=polygon)`</span>
<span class="p">-</span> <span class="sb">`xrot_copies(n=)`</span>, <span class="sb">`yrot_copies(n=)`</span>, <span class="sb">`zrot_copies(n=)`</span>, <span class="sb">`arc_copies(n, r, sa, ea)`</span>
<span class="p">-</span> <span class="sb">`xflip_copy()`</span>, <span class="sb">`yflip_copy()`</span>, <span class="sb">`zflip_copy()`</span>, <span class="sb">`mirror_copy(v)`</span>

<span class="gu">## 3. 3D Shapes (Enhanced Primitives)</span>
<span class="p">-</span> <span class="gs">**Common Args:**</span> <span class="sb">`anchor`</span>, <span class="sb">`spin`</span> (Z-rot), <span class="sb">`orient`</span> (tilt Z-axis to vector).
<span class="p">-</span> <span class="sb">`cuboid(size, rounding=, chamfer=, edges=, except_edges=)`</span>
<span class="p">-</span> <span class="sb">`cyl(l, r|d, rounding=, chamfer=, rounding1=, rounding2=)`</span>
<span class="p">-</span> <span class="sb">`spheroid(r|d, circum=true, style="aligned"|"icosa"|"octa")`</span>
<span class="p">-</span> <span class="sb">`tube(id=, od=, h=, wall=)`</span>
<span class="p">-</span> <span class="sb">`cube()`</span>, <span class="sb">`cylinder()`</span>, <span class="sb">`sphere()`</span> are overridden to support <span class="sb">`anchor/spin/orient`</span>.

<span class="gu">## 4. Positioning &amp; Attachments</span>
<span class="gu">### The Anchor System</span>
<span class="p">-</span> <span class="gs">**Definition:**</span> Anchors are vectors <span class="sb">`[x,y,z]`</span> (components <span class="sb">`-1`</span> to <span class="sb">`1`</span>) defining points on/in a shape.
<span class="p">-</span> <span class="gs">**Logic:**</span> <span class="sb">`anchor=A`</span> translates geometry so point <span class="sb">`A`</span> sits at the local origin <span class="sb">`[0,0,0]`</span>.
<span class="p">-</span> <span class="gs">**Standard Anchors:**</span> <span class="sb">`CENTER [0,0,0]`</span>, <span class="sb">`UP [0,0,1]`</span>, <span class="sb">`DOWN [0,0,-1]`</span>, <span class="sb">`LEFT [-1,0,0]`</span>, <span class="sb">`RIGHT [1,0,0]`</span>, <span class="sb">`FWD [0,-1,0]`</span>, <span class="sb">`BACK [0,1,0]`</span>.
<span class="p">-</span> <span class="gs">**Combinations:**</span> Sum vectors for corners/edges (e.g., <span class="sb">`UP+RIGHT`</span> is <span class="sb">`[1,0,1]`</span>).

<span class="gu">### Transform Order</span>
<span class="p">1.</span> <span class="gs">**Anchor:**</span> Translates selected point to origin.
<span class="p">2.</span> <span class="gs">**Spin:**</span> Z-axis rotation (in degrees) applied after anchoring.
<span class="p">3.</span> <span class="gs">**Orient:**</span> Tilts the Z-axis to align with a target vector.

<span class="gu">### Attachment Modules (Parent-Child)</span>
<span class="p">-</span> <span class="sb">`position(P_ANCHOR)`</span>: Moves child's origin to parent's <span class="sb">`P_ANCHOR`</span>. No rotation.
<span class="p">-</span> <span class="sb">`align(P_ANCHOR, C_ANCHOR)`</span>: Moves child so its <span class="sb">`C_ANCHOR`</span> is flush with parent's <span class="sb">`P_ANCHOR`</span>. No rotation.
<span class="p">-</span> <span class="sb">`attach(P_ANCHOR, C_ANCHOR)`</span>: Joins <span class="sb">`C_ANCHOR`</span> to <span class="sb">`P_ANCHOR`</span> and <span class="gs">**rotates**</span> child so its local <span class="sb">`UP`</span> matches parent's <span class="sb">`P_ANCHOR`</span> normal.
<span class="p">-</span> <span class="sb">`attach(P_ANCHOR)`</span>: Single-argument form. Child is attached to parent's face; child's own <span class="sb">`anchor`</span> and <span class="sb">`orient`</span> are respected.
<span class="p">-</span> <span class="sb">`attach_part(name)`</span>: Selects sub-geometry (e.g., <span class="sb">`"inside"`</span> of a <span class="sb">`tube()`</span>).
<span class="p">-</span> <span class="sb">`show_anchors(s=)`</span>: Visualizes anchors. Red flag = Y+ direction, Blue flag = Z+ direction.

<span class="gu">### Relative Transforms</span>
<span class="p">-</span> <span class="sb">`up()`</span>, <span class="sb">`down()`</span>, <span class="sb">`fwd()`</span>, <span class="sb">`back()`</span>, <span class="sb">`left()`</span>, <span class="sb">`right()`</span>: Directional translations.
<span class="p">-</span> <span class="sb">`zrot()`</span>, <span class="sb">`xrot()`</span>, <span class="sb">`yrot()`</span>: Axis-specific rotations.
<span class="p">-</span> <span class="sb">`recolor("color")`</span>: Applies color to VNFs and complex geometries.

<span class="gu">## 5. Tagged Operations &amp; Boolean Logic</span>
<span class="p">-</span> <span class="gs">**Tags:**</span> <span class="sb">`tag("name")`</span> labels geometry for boolean operations. <span class="sb">`tag("")`</span> clears tags.
<span class="p">-</span> <span class="gs">**Diff:**</span> <span class="sb">`diff(remove, keep)`</span> subtracts "remove" tagged children from the main geometry. "keep" objects are preserved but don't subtract.
<span class="p">-</span> <span class="gs">**Intersect:**</span> <span class="sb">`intersect(intersect, keep)`</span> keeps only the overlap with "intersect" tagged objects.
<span class="p">-</span> <span class="gs">**Hull:**</span> <span class="sb">`conv_hull(keep)`</span> computes the convex hull of children, excluding those tagged "keep".
<span class="p">-</span> <span class="gs">**Masking:**</span> <span class="sb">`edge_mask(edges)`</span> and <span class="sb">`corner_mask(corners)`</span> align standard 3D masks (auto-tagged "remove") to parent edges/corners.
<span class="p">-</span> <span class="gs">**Profiling:**</span> <span class="sb">`edge_profile(edges)`</span> extrudes 2D profiles along edges. <span class="sb">`corner_profile(corners, r)`</span> applies 3D profiles to corners.
<span class="p">-</span> <span class="gs">**Scope:**</span> Tagged operations work across the entire child hierarchy, allowing "holes" to be defined deep within nested modules.

<span class="gu">## 6. Making Custom Attachables</span>
<span class="p">```</span><span class="nl">openscad
</span><span class="sb">module my_shape(..., anchor=CENTER, spin=0, orient=UP) {
    attachable(anchor, spin, orient, size=[x,y,z], r=, l=, vnf=, anchors=) {
        geometry();
        children();
    }
}</span>
<span class="p">```</span>
<span class="p">-</span> <span class="gs">**Named Anchors:**</span> <span class="sb">`anchors=[named_anchor(name, pos, orient, spin), ...]`</span>
<span class="p">-</span> <span class="gs">**Overrides:**</span> <span class="sb">`override=[ [ANCHOR, [pos, dir, spin]], ... ]`</span>

<span class="gu">## 7. Common Functions</span>
<span class="p">-</span> <span class="gs">**Utility:**</span> <span class="sb">`typeof(x)`</span>, <span class="sb">`is_def(x)`</span>, <span class="sb">`any(l)`</span>, <span class="sb">`all(l)`</span>, <span class="sb">`default(val, dflt)`</span>, <span class="sb">`first_defined(list)`</span>.
<span class="p">-</span> <span class="gs">**Args:**</span> <span class="sb">`get_anchor(anchor, center)`</span>, <span class="sb">`get_radius(r1, d1, r, d)`</span>, <span class="sb">`scalar_vec3(v)`</span>.
<span class="p">-</span> <span class="gs">**Math:**</span> <span class="sb">`lerp(a, b, u)`</span>, <span class="sb">`sum(v)`</span>, <span class="sb">`mean(v)`</span>, <span class="sb">`quant(x, y)`</span>, <span class="sb">`constrain(v, min, max)`</span>, <span class="sb">`posmod(x, m)`</span>.
<span class="p">-</span> <span class="gs">**Lists:**</span> <span class="sb">`last(l)`</span>, <span class="sb">`idx(l)`</span>, <span class="sb">`reverse(l)`</span>, <span class="sb">`flatten(l)`</span>, <span class="sb">`select(l, start, end)`</span>.
<span class="p">-</span> <span class="gs">**Vectors:**</span> <span class="sb">`unit(v)`</span>, <span class="sb">`v_mul(v1, v2)`</span>, <span class="sb">`vector_angle(v1, v2)`</span>, <span class="sb">`path3d(pts)`</span>, <span class="sb">`path2d(pts)`</span>.
<span class="p">-</span> <span class="gs">**Matrices:**</span> <span class="sb">`move/rot/scale`</span> as functions return 4x4 matrices. <span class="sb">`apply(mat, pts)`</span> applies them.

<span class="gu">## 8. Tooling &amp; Scripts</span>
The following scripts are available at ./scripts/<span class="nt">&lt;script</span><span class="err">.</span><span class="na">sh</span><span class="nt">&gt;</span> relative to this SKILL.md file.

<span class="p">```</span><span class="nl">bash
</span><span class="c"># Installs the BOSL2 library in $HOME/Documents/OpenSCAD/libraries/BOSL2/</span>
bash ./scripts/ensure_bosl2.sh

<span class="c"># Validate a SCAD file</span>
./scripts/scad_tool.py validate <span class="nt">--in</span> path/to/file.scad

<span class="c"># Render an STL</span>
./scripts/scad_tool.py stl <span class="nt">--in</span> path/to/file.scad <span class="nt">--out</span> output.stl

<span class="c"># Take Screenshots:</span>
<span class="c"># Single isometric view</span>
openscad/scripts/scad_tool.py screenshots <span class="nt">--in</span> path/to/file.scad <span class="nt">--preset</span> single

<span class="c"># Standard views (front, back, left, right, top, bottom, iso)</span>
openscad/scripts/scad_tool.py screenshots <span class="nt">--in</span> path/to/file.scad <span class="nt">--preset</span> standard

<span class="c"># Custom angles (azimuth:elevation)</span>
openscad/scripts/scad_tool.py screenshots <span class="nt">--in</span> path/to/file.scad <span class="nt">--angles</span> 45:30,90:45

<span class="c"># Turntable (360 rotation with 10 degree steps)</span>
openscad/scripts/scad_tool.py screenshots <span class="nt">--in</span> path/to/file.scad <span class="nt">--turntable</span> 10
<span class="p">```</span>

<span class="gu">## 9. Resources &amp; Documentation</span>
<span class="p">-</span> <span class="gs">**Cheat Sheet:**</span> <span class="p">[</span><span class="nv">BOSL2 CheatSheet</span><span class="p">](</span><span class="sx">https://github.com/BelfrySCAD/BOSL2/wiki/CheatSheet</span><span class="p">)</span> - Quick syntax overview.
<span class="p">-</span> <span class="gs">**Tutorials:**</span> <span class="p">[</span><span class="nv">BOSL2 Tutorials</span><span class="p">](</span><span class="sx">https://github.com/BelfrySCAD/BOSL2/wiki/Tutorials</span><span class="p">)</span> - In-depth guides.
<span class="p">-</span> <span class="gs">**Alphabetical Index:**</span> <span class="p">[</span><span class="nv">AlphaIndex</span><span class="p">](</span><span class="sx">https://github.com/BelfrySCAD/BOSL2/wiki/AlphaIndex</span><span class="p">)</span> - Every function, module, and constant.

<span class="gu">## 10. Visual Debugging &amp; Inspection</span>
<span class="p">-</span> <span class="gs">**Modifiers:**</span> <span class="sb">`#`</span> (Debug: transparent red, shows <span class="sb">`diff`</span> removals), <span class="sb">`%`</span> (Background: transparent gray ghost), <span class="sb">`!`</span> (Root: isolate subtree), <span class="sb">`*`</span> (Disable: ignore subtree).
<span class="p">-</span> <span class="gs">**Coloring:**</span> <span class="sb">`color("name", alpha)`</span> supports SVG color names (e.g., "Tomato", "DodgerBlue"). Alpha &lt; 1.0 enables overlap inspection.
<span class="p">-</span> <span class="gs">**BOSL2 Tools:**</span> <span class="sb">`show_anchors(s=)`</span> (Red=Y+, Blue=Z+), <span class="sb">`recolor("color")`</span> for VNFs, <span class="sb">`half_of(DIR)`</span> for cross-sections.

</code></pre></div></div>

<hr />

<h2 id="the-scripts-are-the-hammer">The scripts are the hammer!</h2>

<p>They let the agent take screenshots, validate geometry, render STLs, and install BOSL2 without wasting the context window.</p>

<p><strong><code class="language-plaintext highlighter-rouge">openscad/scripts/ensure_bosl2.sh</code></strong>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># Install or verify BOSL2 library for OpenSCAD</span>
<span class="c"># ...</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">openscad/scripts/scad_tool.py</code></strong>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env -S uv run --script
</span><span class="s">"""OpenSCAD helper: validate, render STL, and take screenshots.

Mac-only version with preset screenshot modes and custom angle support.
"""</span>

<span class="c1"># ...
</span>
<span class="c1"># screenshots subcommand
# - preset: single/iso/ortho/standard
# - views: iso/front/top/etc.
# - angles: custom az:el
# - turntable: 360 rotation
</span></code></pre></div></div>

<hr />

<h2 id="using-the-skill-building-a-tray-step-by-step">Using the skill: building a tray step-by-step</h2>

<ol>
  <li>ask for a tiny change</li>
  <li>render a screenshot</li>
  <li>repeat</li>
</ol>

<p>Here are my prompt to build a part.</p>

<h3 id="prompt-1--using-openscad-skill-add-a-trayscad-file-having-a-cuboid-of-size-15-cm-x-5-cm-x-5mm">Prompt 1 — “using openscad skill, add a tray.scad file having a cuboid of size 15 cm x 5 cm x 5mm”</h3>

<p><img src="/assets/images/tray_step1_iso.png" width="640" alt="Step 1 - floor cuboid" /></p>

<h3 id="prompt-2--update-trayscad-to-have-snap_pin_socket-holes-on-the-15-cm-x-5mm-side-with-3-cm-spacing-between-the-holes">Prompt 2 — “update tray.scad to have snap_pin_socket() holes on the 15 cm x 5mm side with 3 cm spacing between the holes”</h3>

<h3 id="prompt-3--refactor-that-into-a-module-called-swall-short-for-socketted-wall-for-re-use">Prompt 3 — “refactor that into a module called swall() (short for socketted wall) for re-use”</h3>

<p><code class="language-plaintext highlighter-rouge">swall</code> = socketed wall. This is where BOSL2’s <code class="language-plaintext highlighter-rouge">attachable()</code> starts paying off.</p>

<p><img src="/assets/images/tray_step3_iso.png" width="640" alt="Step 3 - swall module" /></p>

<h3 id="prompt-4--add-two-perpendicular-swalls-to-the-floor-part-color-them-differently">Prompt 4 — “add two perpendicular swalls to the floor part, color them differently”</h3>

<p><img src="/assets/images/tray_step4_iso.png" width="640" alt="Step 4 - add side walls" /></p>

<h3 id="prompt-5--add-a-silver-cuboid-to-the-back-of-the-tray">Prompt 5 — “add a Silver cuboid to the back of the tray”</h3>

<p><img src="/assets/images/tray_step5_iso.png" width="640" alt="Step 5 - add back plate" /></p>

<h3 id="prompt-6--move-red-walls-up-to-sit-flush-with-the-blue-floor">Prompt 6 — “move red walls up to sit flush with the blue floor”</h3>

<p><img src="/assets/images/tray_step6_iso.png" width="640" alt="Step 6 - final tray" /></p>

<h2 id="summary">Summary</h2>

<ul>
  <li><strong>Information density wins</strong></li>
  <li><strong>Scripts are context-efficient</strong></li>
  <li><strong>Descriptive beats prescriptive</strong></li>
</ul>]]></content><author><name></name></author><category term="tools" /><category term="llm" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Containing Malware in a Container</title><link href="/2025/12/23/containerized-dev-environment.html" rel="alternate" type="text/html" title="Containing Malware in a Container" /><published>2025-12-23T00:00:00+00:00</published><updated>2025-12-23T00:00:00+00:00</updated><id>/2025/12/23/containerized-dev-environment</id><content type="html" xml:base="/2025/12/23/containerized-dev-environment.html"><![CDATA[<p>There is no escape from malware being constantly published to npm or pypi. Every <code class="language-plaintext highlighter-rouge">npm install</code> is a gamble.</p>

<p>Here is my attempt at moving my entire development setup to a container. This works well if you use tmux and neovim. The goal: isolate untrusted code from the host while keeping a comfortable dev experience.</p>

<h2 id="how-it-works">How It Works</h2>

<p>The setup uses three files that work together:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dev.sh (orchestrator)
   │
   ├─► Builds image from Dockerfile (base OS + tools)
   │
   ├─► Runs setup.sh inside container (configures dotfiles, mise, nvim plugins etc.)
   │
   ├─► Commits the result (so setup only runs once)
   │
   └─► Launches tmux in a fresh container from the committed image
</code></pre></div></div>

<ul>
  <li>Isolated: <code class="language-plaintext highlighter-rouge">~/.ssh</code>, browser data, credentials, everything outside the project</li>
  <li>Shared: the <code class="language-plaintext highlighter-rouge">~/dev</code> directory (mounted read-write)</li>
</ul>

<hr />

<h2 id="1-dockerfile-the-base-image">1. Dockerfile: The Base Image</h2>

<p>This builds a minimal Fedora image with core dev tools.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> fedora:latest</span>

<span class="c"># Install core tools</span>
<span class="k">RUN </span>dnf <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\
</span>    neovim <span class="se">\
</span>    tmux <span class="se">\
</span>    git <span class="se">\
</span>    curl <span class="se">\
</span>    wget <span class="se">\
</span>    gcc <span class="se">\
</span>    gcc-c++ <span class="se">\
</span>    make <span class="se">\
</span>    ripgrep <span class="se">\
</span>    fd-find <span class="se">\
</span>    fzf <span class="se">\
</span>    openssh-clients <span class="se">\
</span>    ca-certificates <span class="se">\
</span>    <span class="nb">sudo</span> <span class="se">\
</span>    glibc-langpack-en <span class="se">\
</span>    the_silver_searcher <span class="se">\
</span>    ncdu <span class="se">\
</span>    btop <span class="se">\
</span>    <span class="o">&amp;&amp;</span> dnf clean all

<span class="k">ENV</span><span class="s"> LANG=en_US.UTF-8</span>
<span class="k">ENV</span><span class="s"> LANGUAGE=en_US:en</span>
<span class="k">ENV</span><span class="s"> LC_ALL=en_US.UTF-8</span>
<span class="k">ENV</span><span class="s"> TERM=xterm-256color</span>
<span class="k">ENV</span><span class="s"> COLORTERM=truecolor</span>

<span class="k">ARG</span><span class="s"> USERNAME=amin</span>
<span class="k">ARG</span><span class="s"> USER_UID=1000</span>
<span class="k">ARG</span><span class="s"> USER_GID=1000</span>

<span class="k">RUN </span>groupadd <span class="nt">--gid</span> <span class="k">${</span><span class="nv">USER_GID</span><span class="k">}</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> useradd <span class="nt">--uid</span> <span class="k">${</span><span class="nv">USER_UID</span><span class="k">}</span> <span class="nt">--gid</span> <span class="k">${</span><span class="nv">USER_GID</span><span class="k">}</span> <span class="nt">-m</span> <span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span><span class="s2"> ALL=(ALL) NOPASSWD:ALL"</span> <span class="o">&gt;</span> /etc/sudoers.d/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> <span class="nb">chmod </span>0440 /etc/sudoers.d/<span class="k">${</span><span class="nv">USERNAME</span><span class="k">}</span>

<span class="k">COPY</span><span class="s"> setup.sh /tmp/setup.sh</span>

<span class="k">USER</span><span class="s"> ${USERNAME}</span>
<span class="k">WORKDIR</span><span class="s"> /home/${USERNAME}</span>
</code></pre></div></div>

<p><strong>Notes:</strong></p>
<ul>
  <li>The user creation matches the host UID/GID so mounted files have correct permissions</li>
  <li><code class="language-plaintext highlighter-rouge">sudo</code> is passwordless for convenience</li>
</ul>

<hr />

<h2 id="2-setupsh-personal-environment-setup">2. setup.sh: Personal Environment Setup</h2>

<p>This script runs once inside the container to set up the dotfiles and tools.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nb">readonly </span><span class="nv">GITHUB_REPO</span><span class="o">=</span><span class="s2">"your-username/dot-files-repo"</span>

<span class="c"># Clone dotfiles using SSH (agent forwarded from host)</span>
git clone <span class="se">\</span>
  <span class="nt">-c</span> core.sshCommand<span class="o">=</span><span class="s2">"ssh -o StrictHostKeyChecking=no"</span> <span class="se">\</span>
  git@github.com:<span class="k">${</span><span class="nv">GITHUB_REPO</span><span class="k">}</span>.git <span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nb">mv </span>dot-files/.git <span class="nb">.</span> <span class="se">\</span>
  <span class="o">&amp;&amp;</span> git checkout <span class="nt">--</span> <span class="nb">.</span> <span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> dot-files

<span class="c"># Install mise (manages node, python, etc.)</span>
curl https://mise.run | sh

<span class="c"># Install uv (fast Python package manager)</span>
curl <span class="nt">-LsSf</span> https://astral.sh/uv/install.sh | sh

<span class="nb">source</span> ~/.bashrc

<span class="c"># Install language runtimes via mise</span>
mise use <span class="nt">-g</span> node@latest
mise use <span class="nt">-g</span> bun@latest

<span class="c"># Install direnv for per-project env vars</span>
curl <span class="nt">-sfL</span> https://direnv.net/install.sh | bash

<span class="c"># Install neovim plugins</span>
nvim <span class="nt">--headless</span> <span class="s2">"+Lazy! sync"</span> +qa

git config <span class="nt">--global</span> user.email <span class="s2">"your-email@example.com"</span>
git config <span class="nt">--global</span> user.name <span class="s2">"your name"</span>
</code></pre></div></div>

<hr />

<h2 id="3-devsh-the-orchestrator">3. dev.sh: The Orchestrator</h2>

<p>It handles SSH agent forwarding (different on macOS vs Linux) and the build-setup-commit flow.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nb">readonly </span><span class="nv">IMAGE_NAME</span><span class="o">=</span><span class="s2">"dev"</span>
<span class="nb">readonly </span><span class="nv">VM_SOCKET</span><span class="o">=</span><span class="s2">"/tmp/ssh-agent.sock"</span>

<span class="nv">ssh_tunnel_pid</span><span class="o">=</span><span class="s2">""</span>

cleanup<span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$ssh_tunnel_pid</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">kill</span> <span class="s2">"</span><span class="nv">$ssh_tunnel_pid</span><span class="s2">"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true
    </span><span class="k">fi</span>
<span class="o">}</span>

<span class="c">### SSH Agent Forwarding</span>

<span class="c"># SSH keys stay on the host, but the container can use them.</span>
<span class="c"># macOS runs containers in a Linux VM, so we tunnel the socket through:</span>
setup_macos<span class="o">()</span> <span class="o">{</span>
    <span class="c"># Forward host's SSH agent socket into the Podman VM</span>
    podman machine ssh <span class="nt">--</span> <span class="nt">-R</span> <span class="s2">"</span><span class="k">${</span><span class="nv">VM_SOCKET</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">SSH_AUTH_SOCK</span><span class="k">}</span><span class="s2">"</span> <span class="nt">-N</span> &amp;
    <span class="nv">ssh_tunnel_pid</span><span class="o">=</span><span class="nv">$!</span>
    <span class="nb">sleep </span>1
    podman machine ssh <span class="nt">--</span> <span class="nb">chmod </span>777 <span class="s2">"</span><span class="nv">$VM_SOCKET</span><span class="s2">"</span>

    run_opts+<span class="o">=(</span>
        <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">VM_SOCKET</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">VM_SOCKET</span><span class="k">}</span><span class="s2">"</span>
        <span class="nt">-e</span> <span class="s2">"SSH_AUTH_SOCK=</span><span class="k">${</span><span class="nv">VM_SOCKET</span><span class="k">}</span><span class="s2">"</span>
    <span class="o">)</span>
<span class="o">}</span>

<span class="c"># Linux can mount the socket directly:</span>
setup_linux<span class="o">()</span> <span class="o">{</span>
    <span class="nb">local </span><span class="nv">socket_dir</span><span class="o">=</span><span class="s2">"/run/user/</span><span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span><span class="s2">"</span>

    run_opts+<span class="o">=(</span>
        <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">socket_dir</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">socket_dir</span><span class="k">}</span><span class="s2">"</span>
        <span class="nt">-e</span> <span class="s2">"SSH_AUTH_SOCK=</span><span class="k">${</span><span class="nv">SSH_AUTH_SOCK</span><span class="k">}</span><span class="s2">"</span>
        <span class="nt">--network</span><span class="o">=</span>host
        <span class="nt">--ulimit</span><span class="o">=</span>host
    <span class="o">)</span>
<span class="o">}</span>

<span class="c">### Build, Setup, and Commit</span>

main<span class="o">()</span> <span class="o">{</span>
    <span class="nb">trap </span>cleanup EXIT

    <span class="c"># Build the base image from Dockerfile</span>
    podman build <span class="nt">-t</span> <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">}</span><span class="s2">:latest"</span> <span class="nb">.</span>

    <span class="nb">declare</span> <span class="nt">-a</span> <span class="nv">run_opts</span><span class="o">=(</span>
        <span class="nt">--replace</span>
        <span class="nt">-it</span>
        <span class="nt">--detach-keys</span><span class="o">=</span><span class="s2">"ctrl-@"</span>
        <span class="nt">--name</span> <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">}</span><span class="s2">-temp"</span>
        <span class="nt">--userns</span><span class="o">=</span>keep-id        <span class="c"># Maps container UID to your host UID</span>
        <span class="nt">--security-opt</span> <span class="nv">label</span><span class="o">=</span>disable
    <span class="o">)</span>

    <span class="k">case</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">uname</span><span class="si">)</span><span class="s2">"</span> <span class="k">in
        </span>Darwin<span class="p">)</span> setup_macos <span class="p">;;</span>
        <span class="k">*</span><span class="p">)</span>      setup_linux <span class="p">;;</span>
    <span class="k">esac</span>

    <span class="c"># Run setup.sh and commit the result</span>
    podman run <span class="s2">"</span><span class="k">${</span><span class="nv">run_opts</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">}</span><span class="s2">:latest"</span> /tmp/setup.sh
    podman commit <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">}</span><span class="s2">-temp"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">}</span><span class="s2">:updated"</span>

    <span class="c">### Launch the Dev Environment</span>

    <span class="c"># Fresh options for the final container</span>
    <span class="nv">run_opts</span><span class="o">=(</span>
        <span class="nt">--rm</span>                    <span class="c"># Delete container on exit</span>
        <span class="nt">-it</span>
        <span class="nt">--detach-keys</span><span class="o">=</span><span class="s2">"ctrl-@"</span>
        <span class="nt">--userns</span><span class="o">=</span>keep-id
        <span class="nt">--security-opt</span> <span class="nv">label</span><span class="o">=</span>disable
    <span class="o">)</span>

    <span class="k">case</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">uname</span><span class="si">)</span><span class="s2">"</span> <span class="k">in
        </span>Darwin<span class="p">)</span> setup_macos <span class="p">;;</span>
        <span class="k">*</span><span class="p">)</span>      setup_linux <span class="p">;;</span>
    <span class="k">esac</span>

    <span class="c"># Mount ONLY the project(s) directory - nothing else</span>
    run_opts+<span class="o">=(</span><span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HOME</span><span class="k">}</span><span class="s2">/dev:/home/amin/dev"</span><span class="o">)</span>

    podman run <span class="s2">"</span><span class="k">${</span><span class="nv">run_opts</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_NAME</span><span class="k">}</span><span class="s2">:updated"</span> tmux
<span class="o">}</span>

main <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--userns=keep-id</code>: The host UID maps to the container user, so file permissions work correctly on mounted volumes</li>
  <li><code class="language-plaintext highlighter-rouge">--security-opt label=disable</code>: Disables SELinux labeling (avoids permission issues with mounts)</li>
  <li><code class="language-plaintext highlighter-rouge">--detach-keys="ctrl-@"</code>: Changes the detach sequence from <code class="language-plaintext highlighter-rouge">ctrl-p ctrl-q</code> (conflicts with tmux)</li>
</ul>

<hr />

<h2 id="why-podman">Why Podman?</h2>

<p>I’m using <a href="https://podman.io/">podman</a> instead of Docker because it runs rootless by default.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[There is no escape from malware being constantly published to npm or pypi. Every npm install is a gamble.]]></summary></entry><entry><title type="html">Animation-Free Workspaces on MacOS</title><link href="/tools/2025/10/19/animation-free-workspaces-on-macos.html" rel="alternate" type="text/html" title="Animation-Free Workspaces on MacOS" /><published>2025-10-19T10:00:00+00:00</published><updated>2025-10-19T10:00:00+00:00</updated><id>/tools/2025/10/19/animation-free-workspaces-on-macos</id><content type="html" xml:base="/tools/2025/10/19/animation-free-workspaces-on-macos.html"><![CDATA[<h3 id="the-problem-sluggish-workspace-switching">The Problem: Sluggish Workspace Switching</h3>

<p>MacOS native workspaces include transition animations that create noticeable delays when switching between them. Adjusting system settings cannot fully remove this lag. However <a href="https://github.com/nikitabobko/AeroSpace">Aerospace</a>, a macOS window manager inspired by i3, can be used to remove the animation delay.</p>

<h3 id="installing-aerospace-and-setting-up-the-config">Installing Aerospace and Setting Up the Config</h3>

<p>Aerospace can be installed via Homebrew (<code class="language-plaintext highlighter-rouge">brew install --cask nikitabobko/tap/aerospace</code>) and is configured through <code class="language-plaintext highlighter-rouge">~/.config/aerospace/aerospace.toml</code>.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Default layout settings</span>
<span class="py">default-root-container-layout</span> <span class="p">=</span> <span class="s">'accordion'</span>
<span class="py">default-root-container-orientation</span> <span class="p">=</span> <span class="s">'auto'</span>

<span class="c"># Zero out gaps for no wasted space</span>
<span class="nn">[gaps]</span>
<span class="py">inner.horizontal</span> <span class="p">=</span> <span class="mi">0</span>
<span class="py">inner.vertical</span> <span class="p">=</span> <span class="mi">0</span>
<span class="py">outer.left</span> <span class="p">=</span> <span class="mi">0</span>
<span class="py">outer.bottom</span> <span class="p">=</span> <span class="mi">0</span>
<span class="py">outer.top</span> <span class="p">=</span> <span class="mi">0</span>
<span class="py">outer.right</span> <span class="p">=</span> <span class="mi">0</span>

<span class="c"># Keybindings for workspace switching (we'll enhance this with a script)</span>
<span class="nn">[mode.main.binding]</span>
<span class="py">ctrl-h</span> <span class="p">=</span> <span class="s">'exec-and-forget source ~/.config/aerospace/aerospace.sh &amp;&amp; switch_workspace -1'</span>
<span class="py">ctrl-l</span> <span class="p">=</span> <span class="s">'exec-and-forget source ~/.config/aerospace/aerospace.sh &amp;&amp; switch_workspace +1'</span>
<span class="py">ctrl-shift-l</span> <span class="p">=</span> <span class="s">'exec-and-forget source ~/.config/aerospace/aerospace.sh &amp;&amp; move_to_workspace +1'</span>
<span class="py">ctrl-shift-h</span> <span class="p">=</span> <span class="s">'exec-and-forget source ~/.config/aerospace/aerospace.sh &amp;&amp; move_to_workspace -1'</span>

<span class="c"># Make all new windows floating by default</span>
<span class="nn">[[on-window-detected]]</span>
<span class="py">run</span> <span class="p">=</span> <span class="p">[</span><span class="s">'layout floating'</span><span class="p">]</span>
</code></pre></div></div>

<p>Every app window floats freely now—no more tiling.</p>

<h3 id="the-magic-trick-a-bash-script-for-navigation">The Magic Trick: A Bash Script for Navigation</h3>

<p>A bash script at <code class="language-plaintext highlighter-rouge">~/.config/aerospace/aerospace.sh</code> calculates next/previous workspaces.</p>

<p>It uses two functions:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">switch_workspace</code> for changing workspaces</li>
  <li><code class="language-plaintext highlighter-rouge">move_to_workspace</code> for shifting the active window with the switch</li>
</ul>

<p>These are bound to <code class="language-plaintext highlighter-rouge">ctrl+h/l</code> for navigation and <code class="language-plaintext highlighter-rouge">ctrl+shift+h/l</code> for moving windows.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>switch_workspace<span class="o">()</span> <span class="o">{</span>
  <span class="nv">offset</span><span class="o">=</span><span class="nv">$1</span>
  <span class="nv">workspaces</span><span class="o">=</span><span class="s2">"1 2 3 4"</span>
  <span class="nv">current_workspace</span><span class="o">=</span><span class="si">$(</span>aerospace list-workspaces <span class="nt">--monitor</span> focused <span class="nt">--visible</span><span class="si">)</span>
  <span class="nv">workspaces_array</span><span class="o">=(</span><span class="nv">$workspaces</span><span class="o">)</span>
  
  <span class="c"># Find current index</span>
  <span class="k">for </span>i <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="p">!workspaces_array[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">workspaces_array</span><span class="p">[i]</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"</span><span class="nv">$current_workspace</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nv">current_index</span><span class="o">=</span><span class="nv">$i</span>
      <span class="nb">break
    </span><span class="k">fi
  done</span>
  
  <span class="c"># Calculate target without wrapping</span>
  <span class="nv">target_index</span><span class="o">=</span><span class="k">$((</span>current_index <span class="o">+</span> offset<span class="k">))</span>
  <span class="k">if</span> <span class="o">((</span> target_index &amp;lt<span class="p">;</span> 0 <span class="o">))</span><span class="p">;</span> <span class="k">then
    </span><span class="nv">target_index</span><span class="o">=</span>0
  <span class="k">elif</span> <span class="o">((</span> target_index &amp;gt<span class="p">;</span><span class="o">=</span> <span class="k">${#</span><span class="nv">workspaces_array</span><span class="p">[@]</span><span class="k">}</span> <span class="o">))</span><span class="p">;</span> <span class="k">then
    </span><span class="nv">target_index</span><span class="o">=</span><span class="k">$((</span> <span class="k">${#</span><span class="nv">workspaces_array</span><span class="p">[@]</span><span class="k">}</span> <span class="o">-</span> <span class="m">1</span> <span class="k">))</span>
  <span class="k">fi</span>
  
  <span class="c"># Switch</span>
  aerospace workspace <span class="s2">"</span><span class="k">${</span><span class="nv">workspaces_array</span><span class="p">[target_index]</span><span class="k">}</span><span class="s2">"</span>
<span class="o">}</span>

move_to_workspace<span class="o">()</span> <span class="o">{</span>
  <span class="nv">offset</span><span class="o">=</span><span class="nv">$1</span>
  <span class="nv">workspaces</span><span class="o">=</span><span class="s2">"1 2 3 4"</span>
  <span class="nv">current_workspace</span><span class="o">=</span><span class="si">$(</span>aerospace list-workspaces <span class="nt">--monitor</span> focused <span class="nt">--visible</span><span class="si">)</span>
  <span class="nv">workspaces_array</span><span class="o">=(</span><span class="nv">$workspaces</span><span class="o">)</span>
  
  <span class="c"># Find current index</span>
  <span class="k">for </span>i <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="p">!workspaces_array[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">workspaces_array</span><span class="p">[i]</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"</span><span class="nv">$current_workspace</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nv">current_index</span><span class="o">=</span><span class="nv">$i</span>
      <span class="nb">break
    </span><span class="k">fi
  done</span>
  
  <span class="c"># Calculate target with wrapping for moving</span>
  <span class="nv">target_index</span><span class="o">=</span><span class="k">$((</span> <span class="o">(</span>current_index <span class="o">+</span> offset <span class="o">+</span> <span class="k">${#</span><span class="nv">workspaces_array</span><span class="p">[@]</span><span class="k">}</span><span class="o">)</span> <span class="o">%</span> <span class="k">${#</span><span class="nv">workspaces_array</span><span class="p">[@]</span><span class="k">}</span> <span class="k">))</span>
  <span class="nv">target_workspace</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">workspaces_array</span><span class="p">[target_index]</span><span class="k">}</span><span class="s2">"</span>
  
  <span class="c"># Move and switch</span>
  aerospace move-node-to-workspace <span class="s2">"</span><span class="nv">$target_workspace</span><span class="s2">"</span>
  aerospace workspace <span class="s2">"</span><span class="nv">$target_workspace</span><span class="s2">"</span>
<span class="o">}</span>
</code></pre></div></div>

<p>You can view my complete configs here: 
<a href="https://github.com/aminroosta/aminroosta.github.io/raw/refs/heads/gh-pages/assets/download/aerospace.toml?download=">aerospace.toml</a> 
and 
<a href="https://github.com/aminroosta/aminroosta.github.io/raw/refs/heads/gh-pages/assets/download/aerospace.sh?download=">aerospace.sh</a>.</p>]]></content><author><name></name></author><category term="tools" /><summary type="html"><![CDATA[The Problem: Sluggish Workspace Switching]]></summary></entry><entry><title type="html">Build Your Own VSCode</title><link href="/tools/2025/03/28/build-your-own-vscode.html" rel="alternate" type="text/html" title="Build Your Own VSCode" /><published>2025-03-28T10:00:00+00:00</published><updated>2025-03-28T10:00:00+00:00</updated><id>/tools/2025/03/28/build-your-own-vscode</id><content type="html" xml:base="/tools/2025/03/28/build-your-own-vscode.html"><![CDATA[<p>You can download a pre-built version of my customized version of VSCode <a href="https://github.com/aminroosta/aminroosta.github.io/raw/refs/heads/gh-pages/assets/download/VSCodium.zip?download=">here</a>, and then run:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># MacOS puts a quarantine on application files extracted from zip files</span>
xattr <span class="nt">-d</span> com.apple.quarantine /Applications/VSCodium.app
</code></pre></div></div>

<hr />

<p><a href="https://code.visualstudio.com/">VSCode</a> is a popular code editor that can be customized with <a href="https://marketplace.visualstudio.com/">community extensions</a>. These extensions can change how the editor works, but they’re limited. The deeper parts of VSCode, like its <a href="https://www.electronjs.org/">ElectronJS</a> window system or internal code, are locked away from extensions on purpose.</p>

<p>So, if you want to fully customize VSCode without limits, you’ll need to edit the <a href="https://github.com/microsoft/vscode">source code</a> yourself and build your own version. Here’s how to get started:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone the VSCode source code from GitHub</span>
git clone git@github.com:microsoft/vscode.git

<span class="c"># Move into the project folder</span>
<span class="nb">cd </span>vscode

<span class="c"># Install the required tools and dependencies</span>
npm <span class="nb">install</span>

<span class="c"># Build the code and watch for changes as you edit</span>
npm run watch

<span class="c"># Launch a debug version of your custom VSCode</span>
./scripts/code.sh
</code></pre></div></div>

<p>This method works well! For example, I submitted a <a href="https://github.com/microsoft/vscode/pull/244819/files">PR</a> to add floating window support on macOS. But not every change gets accepted by the VSCode team at Microsoft. They can’t maintain every new feature, which makes sense.</p>

<p>Sometimes, the changes you want to make don’t fit with how VSCode is designed. Let me show you an example to explain this better.</p>

<hr />

<p>By default, folding in VSCode looks like this:  <br />
<br />
<img src="/assets/images/build-your-own-vscode-fold-before.png" alt="Default Folding" /></p>

<p>Let’s say you want to tweak how folding works. Here’s a quick (but messy) fix I came up with:</p>

<div class="language-patch highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">--- a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts
</span><span class="gi">+++ b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts
</span><span class="p">@@ -20,6 +20,8 @@</span> const foldingContext: FoldingContext = {
 
 const ID_SYNTAX_PROVIDER = 'syntax';
 
<span class="gi">+export const FOLD_END_PATTERN = /^\s*\}|^\s*\]|^\s*\)|^\s*end/;
+
</span> export class SyntaxRangeProvider implements RangeProvider {
 
 	readonly id = ID_SYNTAX_PROVIDER;
<span class="p">@@ -73,6 +75,13 @@</span> function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextMode
 				}
 				const nLines = model.getLineCount();
 				for (const r of ranges) {
<span class="gi">+					if(r.end &lt; nLines) {
+						let endLineContent = model.getLineContent(r.end + 1);
+						if(FOLD_END_PATTERN.test(endLineContent)) {
+							r.end = r.end + 1;
+						}
+					}
+
</span> 					if (r.start &gt; 0 &amp;&amp; r.end &gt; r.start &amp;&amp; r.end &lt;= nLines) {
 						rangeData.push({ start: r.start, end: r.end, rank: i, kind: r.kind });
 					}
</code></pre></div></div>

<p>This patch fixes the folding issue, and the result looks like this:<br />
<br />
<img src="/assets/images/build-your-own-vscode-fold-after.png" alt="Modified Folding" /></p>

<p>But here’s the catch: this is a hack. The VSCode team probably wouldn’t accept it as a PR because it’s not a clean, long-term solution.</p>

<hr />

<p>The VSCode <a href="https://github.com/microsoft/vscode">source code</a> by default only lets you build a debug version called <code class="language-plaintext highlighter-rouge">Code - OSS</code>. This version is limited; it only runs on the computer where you built it.</p>

<p>For a fully working, shareable version, there’s a community project called <a href="https://github.com/VSCodium/vscodium">VSCodium</a>.<br />
<em>VSCodium takes the open-source VSCode code, removes Microsoft branding and telemetry, and adds patches to make it a complete, open-source alternative.</em></p>

<p>VSCodium uses a script (<code class="language-plaintext highlighter-rouge">dev/build.sh</code>) that grabs the VSCode source code, applies custom patches (from <code class="language-plaintext highlighter-rouge">patches/*.patch</code>), and builds the editor. Here’s how I built my own version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Clone the VSCodium project</span>
git clone git@github.com:VSCodium/vscodium.git

<span class="c"># Check the stable version’s commit hash</span>
<span class="nb">cat</span> ./vscodium/upstream/stable.json
<span class="c"># Note the commit hash—it’s the starting point for the patches</span>

<span class="c"># Clone the VSCode source code</span>
git clone git@github.com:microsoft/vscode.git

<span class="c"># Move into the VSCode folder</span>
<span class="nb">cd</span> ./vscode

<span class="c"># Switch to the commit hash from stable.json</span>
git checkout <span class="s2">"&lt;the commit hash&gt;"</span>

<span class="c"># Create a new branch for your changes</span>
git checkout <span class="nt">-b</span> your_branch

<span class="c"># Make your edits and test them here</span>

<span class="c"># Save your changes as a patch file</span>
git diff <span class="o">&gt;</span> ../vscodium/patches/your_patch.patch

<span class="c"># Move back to the VSCodium folder</span>
<span class="nb">cd</span> ../vscodium

<span class="c"># Install a required tool (if you’re on macOS)</span>
brew <span class="nb">install </span>cmake

<span class="c"># Build the editor (takes 5-10 minutes)</span>
./dev/build.sh

<span class="c"># Find your finished app</span>
<span class="nb">ls</span> ./VSCode-darwin-arm64/VSCodium.app
</code></pre></div></div>

<hr />

<h3 id="other-options">Other Options</h3>
<ul>
  <li>You can build <a href="https://github.com/aminroosta/vscodium">my VSCodium fork</a>, which includes the folding fix and floating window support.
    <ul>
      <li>To enable floating window, add this snippet to your <code class="language-plaintext highlighter-rouge">settings.json</code>:
        <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"window.workspacesOverlay"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"alwaysOnTop"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"hotKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Control+Enter"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"snapMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bottom"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>Or grab the pre-built version  <a href="https://github.com/aminroosta/aminroosta.github.io/raw/refs/heads/gh-pages/assets/download/VSCodium.zip?download=">here</a> and run:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c"># MacOS puts a quarantine on application files extracted from zip files</span>
  xattr <span class="nt">-d</span> com.apple.quarantine /Applications/VSCodium.app
</code></pre></div>    </div>
  </li>
</ul>]]></content><author><name></name></author><category term="tools" /><summary type="html"><![CDATA[You can download a pre-built version of my customized version of VSCode here, and then run: # MacOS puts a quarantine on application files extracted from zip files xattr -d com.apple.quarantine /Applications/VSCodium.app]]></summary></entry><entry><title type="html">NodeJS REPL in Production</title><link href="/devops/2025/03/09/nodejs-repl-in-production.html" rel="alternate" type="text/html" title="NodeJS REPL in Production" /><published>2025-03-09T05:00:00+00:00</published><updated>2025-03-09T05:00:00+00:00</updated><id>/devops/2025/03/09/nodejs-repl-in-production</id><content type="html" xml:base="/devops/2025/03/09/nodejs-repl-in-production.html"><![CDATA[<blockquote>
  <p>Updated Jan 27, 2025: Now supports Bun.</p>
</blockquote>

<p>The default REPL is hard to access for a detached NodeJS process.</p>

<p>Even if you gain access to it:</p>
<ul>
  <li>It can’t be accessed by multiple developers at once.</li>
  <li>Hitting <code class="language-plaintext highlighter-rouge">&lt;Ctrl+C&gt;</code> terminates the process; far from ideal!</li>
</ul>

<p>But there is a neat solution. We can leverage the built-in <code class="language-plaintext highlighter-rouge">node:repl</code> and <code class="language-plaintext highlighter-rouge">node:net</code> modules.</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">repl</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:repl</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">net</span><span class="p">,</span> <span class="p">{</span> <span class="nx">Socket</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:net</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">readline</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:readline</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">util</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:util</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">process</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:process</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">REPL_PORT</span> <span class="o">=</span> <span class="mi">5001</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">replContextAdditions</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">any</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">isBun</span> <span class="o">=</span> <span class="o">!!</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">versions</span> <span class="k">as</span> <span class="kr">any</span><span class="p">).</span><span class="nx">bun</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">net</span><span class="p">.</span><span class="nx">createServer</span><span class="p">((</span><span class="nx">socket</span><span class="p">:</span> <span class="nx">Socket</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">isBun</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">handleBunRepl</span><span class="p">(</span><span class="nx">socket</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nx">handleNodeRepl</span><span class="p">(</span><span class="nx">socket</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">});</span>

<span class="nx">server</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">:</span> <span class="nb">Error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Server error:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">server</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">REPL_PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`REPL server running on port </span><span class="p">${</span><span class="nx">REPL_PORT</span><span class="p">}</span><span class="s2"> (</span><span class="p">${</span><span class="nx">isBun</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">Bun</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">Node.js</span><span class="dl">"</span><span class="p">}</span><span class="s2">)`</span><span class="p">);</span>
<span class="p">});</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">addToRepl</span><span class="p">(</span><span class="nx">obj</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">any</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
  <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">replContextAdditions</span><span class="p">,</span> <span class="nx">obj</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">handleNodeRepl</span><span class="p">(</span><span class="nx">socket</span><span class="p">:</span> <span class="nx">Socket</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">replServer</span> <span class="o">=</span> <span class="nx">repl</span><span class="p">.</span><span class="nx">start</span><span class="p">({</span>
    <span class="na">prompt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&gt; </span><span class="dl">"</span><span class="p">,</span>
    <span class="na">input</span><span class="p">:</span> <span class="nx">socket</span><span class="p">,</span>
    <span class="na">output</span><span class="p">:</span> <span class="nx">socket</span><span class="p">,</span>
    <span class="na">terminal</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">preview</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="na">useColors</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">useGlobal</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
  <span class="p">});</span>

  <span class="nx">replServer</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">exit</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">socket</span><span class="p">.</span><span class="nx">end</span><span class="p">();</span>
  <span class="p">});</span>

  <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">replServer</span><span class="p">.</span><span class="nx">context</span><span class="p">,</span> <span class="nx">replContextAdditions</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">handleBunRepl</span><span class="p">(</span><span class="nx">socket</span><span class="p">:</span> <span class="nx">Socket</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">replContextAdditions</span> <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">rl</span> <span class="o">=</span> <span class="nx">readline</span><span class="p">.</span><span class="nx">createInterface</span><span class="p">({</span>
    <span class="na">input</span><span class="p">:</span> <span class="nx">socket</span><span class="p">,</span>
    <span class="na">output</span><span class="p">:</span> <span class="nx">socket</span><span class="p">,</span>
    <span class="na">terminal</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">prompt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">&gt; </span><span class="dl">"</span><span class="p">,</span>
    <span class="na">completer</span><span class="p">:</span> <span class="p">(</span><span class="na">line</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">compl</span><span class="p">(</span><span class="nx">line</span><span class="p">,</span> <span class="nx">context</span><span class="p">),</span>
  <span class="p">});</span>

  <span class="nx">rl</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">line</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">line</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">trimmed</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">trim</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">trimmed</span><span class="p">)</span> <span class="k">return</span> <span class="nx">rl</span><span class="p">.</span><span class="nx">prompt</span><span class="p">();</span>

    <span class="k">try</span> <span class="p">{</span>
      <span class="c1">// Basic eval implementation for Bun</span>
      <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Function</span><span class="p">(</span><span class="dl">"</span><span class="s2">context</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`with(context) { return (</span><span class="p">${</span><span class="nx">trimmed</span><span class="p">}</span><span class="s2">) }`</span><span class="p">)(</span><span class="nx">context</span><span class="p">);</span>
      <span class="nx">socket</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">util</span><span class="p">.</span><span class="nx">inspect</span><span class="p">(</span><span class="nx">result</span><span class="p">,</span> <span class="p">{</span> <span class="na">colors</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span> <span class="o">+</span> <span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">socket</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">util</span><span class="p">.</span><span class="nx">inspect</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="p">{</span> <span class="na">colors</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span> <span class="o">+</span> <span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nx">rl</span><span class="p">.</span><span class="nx">prompt</span><span class="p">();</span>
  <span class="p">});</span>

  <span class="nx">rl</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">close</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">socket</span><span class="p">.</span><span class="nx">end</span><span class="p">());</span>
  <span class="nx">socket</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">end</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">rl</span><span class="p">.</span><span class="nx">close</span><span class="p">());</span>

  <span class="nx">rl</span><span class="p">.</span><span class="nx">prompt</span><span class="p">();</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">compl</span><span class="p">(</span><span class="nx">line</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">context</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">globalThis</span><span class="p">,</span> <span class="p">...</span><span class="nx">context</span> <span class="p">};</span>
    <span class="kd">const</span> <span class="nx">match</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/^</span><span class="se">((?:</span><span class="sr">.*</span><span class="se">[</span><span class="sr"> .(</span><span class="se">\[])?)([^</span><span class="sr"> .(</span><span class="se">\[]</span><span class="sr">*</span><span class="se">)</span><span class="sr">$/</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">match</span><span class="p">)</span> <span class="k">return</span> <span class="p">[[],</span> <span class="nx">line</span><span class="p">];</span>

    <span class="kd">const</span> <span class="p">[,</span> <span class="nx">expr</span><span class="p">,</span> <span class="nx">last</span><span class="p">]</span> <span class="o">=</span> <span class="nx">match</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">expr</span><span class="p">.</span><span class="nx">trim</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">. </span><span class="se">]</span><span class="sr">+$/</span><span class="p">,</span> <span class="dl">""</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">query</span> <span class="p">?</span> <span class="k">new</span> <span class="nb">Function</span><span class="p">(</span><span class="dl">"</span><span class="s2">ctx</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`with(ctx) { return </span><span class="p">${</span><span class="nx">query</span><span class="p">}</span><span class="s2"> }`</span><span class="p">)(</span><span class="nx">ctx</span><span class="p">)</span> <span class="p">:</span> <span class="nx">ctx</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">target</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span> <span class="p">[[],</span> <span class="nx">line</span><span class="p">];</span>

    <span class="kd">const</span> <span class="nx">keys</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Set</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">();</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">o</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span> <span class="nx">o</span><span class="p">;</span> <span class="nx">o</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">getPrototypeOf</span><span class="p">(</span><span class="nx">o</span><span class="p">))</span> <span class="p">{</span>
      <span class="nb">Object</span><span class="p">.</span><span class="nx">getOwnPropertyNames</span><span class="p">(</span><span class="nx">o</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">k</span> <span class="o">=&gt;</span> <span class="nx">keys</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">k</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">hits</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">keys</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span><span class="nx">k</span> <span class="o">=&gt;</span> <span class="nx">k</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="nx">last</span><span class="p">));</span>
    <span class="k">return</span> <span class="p">[</span><span class="nx">hits</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">h</span> <span class="o">=&gt;</span> <span class="nx">expr</span> <span class="o">+</span> <span class="nx">h</span><span class="p">),</span> <span class="nx">line</span><span class="p">];</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">[[],</span> <span class="nx">line</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Run the <code class="language-plaintext highlighter-rouge">repl_server.ts</code> and manually add application objects to the context.</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// main.ts</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">addToRepl</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./repl_server.ts</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">let</span> <span class="nx">app</span> <span class="o">=</span> <span class="p">{</span> <span class="na">todo</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application</span><span class="dl">'</span> <span class="p">};</span>
<span class="nx">addToRepl</span><span class="p">({</span> <span class="nx">app</span> <span class="p">});</span>
</code></pre></div></div>

<p>To connect to the REPL, use the following script:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// repl_client.ts</span>
<span class="k">import</span> <span class="nx">net</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:net</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">process</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:process</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">let</span> <span class="nx">sock</span> <span class="o">=</span> <span class="nx">net</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span><span class="mi">5001</span><span class="p">)</span>

<span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">sock</span><span class="p">)</span>
<span class="nx">sock</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">stdout</span><span class="p">)</span>

<span class="nx">sock</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">connect</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">resume</span><span class="p">();</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">setRawMode</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
<span class="p">})</span>

<span class="nx">sock</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">close</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="nx">done</span> <span class="p">()</span> <span class="p">{</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">setRawMode</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">pause</span><span class="p">()</span>
  <span class="nx">sock</span><span class="p">.</span><span class="nx">removeListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">close</span><span class="dl">'</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span>
<span class="p">})</span>

<span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">end</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
  <span class="nx">sock</span><span class="p">.</span><span class="nx">destroy</span><span class="p">()</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">()</span>
<span class="p">})</span>

<span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">data</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="nx">b</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">===</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="dl">'</span><span class="s1">end</span><span class="dl">'</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">package.json</code> may look like this:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"module"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node src/main.ts"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"repl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node ./src/repl_client.ts"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>ta-da!</p>

<p><sub> Credits to <a href="https://gist.github.com/TooTallNate/2209310">gist.github.com/TooTallNate/2209310</a> </sub></p>]]></content><author><name></name></author><category term="devops" /><summary type="html"><![CDATA[Updated Jan 27, 2025: Now supports Bun.]]></summary></entry><entry><title type="html">HTTPS with Caddy</title><link href="/devops/2025/01/20/https-with-caddy.html" rel="alternate" type="text/html" title="HTTPS with Caddy" /><published>2025-01-20T21:30:00+00:00</published><updated>2025-01-20T21:30:00+00:00</updated><id>/devops/2025/01/20/https-with-caddy</id><content type="html" xml:base="/devops/2025/01/20/https-with-caddy.html"><![CDATA[<p>In 2021, I <a href="/devops/2021/10/25/https-setup-on-subdomains.html">wrote</a> about setting up HTTPS with <code class="language-plaintext highlighter-rouge">nginx</code> and <code class="language-plaintext highlighter-rouge">swag</code>.
But, I’ve found a much simpler approach recently using <code class="language-plaintext highlighter-rouge">Caddy</code>.</p>

<p><a href="https://github.com/caddyserver/caddy">Caddy</a> is an easy-to-configure reverse proxy which enables HTTPS by default.<br />
The TLS certificates are issued by <a href="https://letsencrypt.org/">Let’s Encrypt</a>, and caddy automatically renews them for you.</p>

<p>To install it, follow the <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">documentation</a>; here is the debian installation section:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> debian-keyring debian-archive-keyring apt-transport-https curl
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'</span> | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'</span> | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/caddy-stable.list
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>caddy
</code></pre></div></div>

<p>Caddy is installed as a new service, which continues to run even after you reboot your server.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use the service command to manage the "caddy" service.</span>
service caddy status 
service caddy restart

<span class="c"># Or systemctl, if you prefer that option.</span>
systemctl status caddy
</code></pre></div></div>

<p>The service, by default, points to the <code class="language-plaintext highlighter-rouge">/etc/caddy/Caddyfile</code>; create one if it doesn’t exist.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch</span> /etc/caddy/Caddyfile
</code></pre></div></div>

<p>You need to have a domain with an <code class="language-plaintext highlighter-rouge">A</code> record pointing to your server’s IP address.<br />
Here’s how to do that on <a href="https://chemicloud.com/kb/article/manage-dns-in-cloudflare/">Cloudflare</a>, <a href="https://www.namecheap.com/support/knowledgebase/article.aspx/319/2237/how-can-i-set-up-an-a-address-record-for-my-domain/">Namecheap</a>, or <a href="https://kinsta.com/knowledgebase/godaddy-a-record/">GoDaddy</a>.</p>

<p>Assuming your local HTTP based service is running on port <code class="language-plaintext highlighter-rouge">8080</code> …</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># For example, the comment section on this blog is handled by remark42.</span>
<span class="c1"># Here is the relevant portion of my docker-compose.yml</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">remark</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">umputun/remark42:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">remark"</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">127.0.0.1:8080:8080"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="c1"># redacted</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/lib/remark:/srv/var</span>
</code></pre></div></div>
<p>Update the <code class="language-plaintext highlighter-rouge">Caddyfile</code> and restart the service.</p>
<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/caddy/Caddyfile
</span><span class="n">yourdomain</span>.<span class="n">com</span> {
  <span class="n">reverse_proxy</span> * <span class="m">127</span>.<span class="m">0</span>.<span class="m">0</span>.<span class="m">1</span>:<span class="m">8080</span>
}
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>service caddy restart
</code></pre></div></div>
<p>After a minute or so, you should be able to open <code class="language-plaintext highlighter-rouge">https://yourdomain.com</code> using a web browser.</p>

<p>To see the service logs, use the <code class="language-plaintext highlighter-rouge">journalctl</code> command.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>journalctl <span class="nt">-f</span> <span class="nt">-u</span> caddy
</code></pre></div></div>

<p>Here is more a complex example.</p>
<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">myblog</span>.<span class="n">com</span> {
  <span class="c"># serve the html/css/js files from /root/app
</span>  <span class="n">root</span> * /<span class="n">root</span>/<span class="n">app</span>
  <span class="n">file_server</span>

  <span class="c"># Api calls go to 3001 on localhost
</span>  <span class="n">reverse_proxy</span> /<span class="n">api</span>/* <span class="m">127</span>.<span class="m">0</span>.<span class="m">0</span>.<span class="m">1</span>:<span class="m">3001</span>
}

<span class="c"># comment section on a subdomain
</span><span class="n">remark</span>.<span class="n">myblog</span>.<span class="n">com</span> {
  <span class="n">reverse_proxy</span> * <span class="m">127</span>.<span class="m">0</span>.<span class="m">0</span>.<span class="m">1</span>:<span class="m">8080</span>
}
</code></pre></div></div>
<p>See the <a href="https://caddyserver.com/docs/caddyfile/patterns"><em>Common Patterns</em></a> section of the docs for more advanced features.</p>]]></content><author><name></name></author><category term="devops" /><summary type="html"><![CDATA[In 2021, I wrote about setting up HTTPS with nginx and swag. But, I’ve found a much simpler approach recently using Caddy.]]></summary></entry></feed>